Merge master into wallet-bridge (the wallet's line meets master for the landing of the page bridge)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> # Conflicts: # packaging/ota/publish-manifest.sh # site/index.html # site/wallet.html
This commit is contained in:
commit
7a1fb95cb5
1826 changed files with 338891 additions and 6338 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
26
.forgejo/issue_template/grant.md
Normal file
26
.forgejo/issue_template/grant.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: Grant application
|
||||
about: Apply for an Igneum grant: tooling, a reference app, infrastructure or research, paid in IGN on delivery
|
||||
title: "Grant: "
|
||||
labels: grant
|
||||
---
|
||||
|
||||
<!-- Read igneum.network/grants first. A short application is fine; the definition of done is the part that matters. -->
|
||||
|
||||
## What you will build
|
||||
|
||||
<!-- One paragraph. Name the tier: tooling, reference app, infrastructure, research. -->
|
||||
|
||||
## What done looks like
|
||||
|
||||
<!-- What runs where on the public testnet, and what a reviewer can click or call to see it working. -->
|
||||
|
||||
## Your public work
|
||||
|
||||
<!-- A repository, a package, a paper, a deployed thing. Links. -->
|
||||
|
||||
## How to reach you
|
||||
|
||||
<!-- An email, a Discord handle, or this issue. -->
|
||||
|
||||
<!-- Devnet and testnet coins have no value. No amount, price or date is promised. Not legal advice. -->
|
||||
48
.github/workflows/ci-red.yml
vendored
Normal file
48
.github/workflows/ci-red.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# The red watcher as its own workflow, on workflow_run, so the copy on master watches EVERY branch's ci run whatever
|
||||
# ci.yml that branch carries: GitHub runs a workflow_run workflow from the default branch only, and the branch's own
|
||||
# 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, 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
|
||||
# release: it reads the run, writes one line, and ends.
|
||||
name: ci-red
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ci]
|
||||
types: [completed]
|
||||
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)
|
||||
# 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
|
||||
runs-on: [self-hosted, linux, x64, ci-red]
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: read # the failed run's jobs API (the first real red run, 21:19Z on 6 October: the default token answered 403 and the line carried no step)
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci
|
||||
- name: record the failed run (one line, the branch, the commit, the failed jobs and their first failed step from the run's own API, the pushing author)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
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 }}
|
||||
RED_WATCH_URL: ${{ github.event.workflow_run.html_url }}
|
||||
RED_WATCH_ACTOR: ${{ github.event.workflow_run.actor.login }}
|
||||
RED_WATCH_TITLE: ${{ github.event.workflow_run.head_commit.message }}
|
||||
RED_WATCH_AUTHOR: ${{ github.event.workflow_run.head_commit.author.name }}
|
||||
run: node tools/ci/red-watch.mjs record --file /srv/ci-red/red.jsonl
|
||||
116
.github/workflows/ci.yml
vendored
116
.github/workflows/ci.yml
vendored
|
|
@ -1,10 +1,23 @@
|
|||
# CI on every push and pull request (private repository, free runner minutes).
|
||||
#
|
||||
# What runs: the lottery-hash crate's tests (igneum-pow, release profile), the census tool's build, the two Python
|
||||
# simulators' --quick modes (each under two minutes), the site build with an internal link check, the gh-free
|
||||
# identity grep of the public export list (tools/ci/forbidden-strings.txt), and the no-secrets check of the tree
|
||||
# (tools/ci/no-secrets-check.sh: no file named like a key of ~/.config/igneum, no 64-hex value assigned to a
|
||||
# token/key/secret name outside tests and the allowlist; docs/security/keys.md).
|
||||
# simulators' --quick modes (each under two minutes), and the tree gate: every fast check in ONE script,
|
||||
# tools/ci/pre-push.sh (site build and link check, the ledger sentences, the identity grep of the public export list
|
||||
# and the served site, Windows-valid paths, workflow shell syntax, the copied-sources and playbook classes, the unit
|
||||
# tests, the no-secrets check). The pre-push hook runs the SAME script before a push to master or release-*, so the
|
||||
# local gate and CI cannot drift (6 October 2026: 131 red `ci` runs in three days, 92 of them on master, every one a
|
||||
# tree check that would have failed on the pushing machine in under 25 s; docs/analysis/ci-failures-2026-10-06.md).
|
||||
#
|
||||
# Where it runs: `pow` and `sims` go to the self-hosted pool (label igneum-build-1: the runners on igneum-build-1 and, since
|
||||
# 7 October 2026, igneum-build-2, which carries that label too; rustc pinned, sccache read-only) and only when the push
|
||||
# touched code (the `changes` job; a docs-only push skips them)
|
||||
# when the repository variable IGNEUM_CI_RUNNER is `box`, else to ubuntu-latest (docs/plans/ci-self-hosted.md; GitHub
|
||||
# has no fallback in runs-on, the variable is the switch). The `site` job stays on GitHub's machines. The red watcher
|
||||
# is its own workflow, .github/workflows/ci-red.yml (workflow_run, so the copy on master watches every branch's run
|
||||
# whatever ci.yml that branch carries): one line per failed run, naming the branch, the commit, the red check and the
|
||||
# pushing author, to the hidden updates channel and to /srv/ci-red/red.jsonl (tools/ci/red-watch.mjs;
|
||||
# infra/build-server/ci-red), so nobody opens the Actions page to learn a branch is red (the inline `red` job here
|
||||
# watched master and release-* only until 7 October 2026, when eight red runs on ca3-v4-node went unseen).
|
||||
#
|
||||
# What does not run, on purpose: the node fork (vendor/igneum-node*, a rusty-kaspa fork of about 500 crates with
|
||||
# rocksdb, blst and the execution layer) is gitignored here and too big for the free runners today (a cold build is
|
||||
|
|
@ -15,9 +28,42 @@ on:
|
|||
push:
|
||||
pull_request:
|
||||
jobs:
|
||||
changes:
|
||||
# What the push touched (tools/ci/docs-only-check.sh): a push of documents only (docs/, site/, *.md) skips the two
|
||||
# compile-or-compute jobs below, which read none of those paths, so the self-hosted queue carries only runs that can
|
||||
# change their result (7 October 2026: 31 runs queued on one runner, most of them status-document pushes). The tree
|
||||
# gate (the `site` job) runs on ubuntu-latest for every push. A pull request, a new branch or a force push answers
|
||||
# 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:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci
|
||||
- id: classify
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BEFORE: ${{ github.event.before }}
|
||||
AFTER: ${{ github.sha }}
|
||||
REPO: ${{ github.repository }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
run: |
|
||||
if [ "$EVENT" != push ] || [ -z "$BEFORE" ] || [ "$BEFORE" = 0000000000000000000000000000000000000000 ]; then
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"; echo "no base to compare from ($EVENT): the compile jobs run"; exit 0
|
||||
fi
|
||||
files="$(gh api "repos/$REPO/compare/$BEFORE...$AFTER" --paginate --jq '.files[].filename' 2>/dev/null || true)"
|
||||
line="$(printf '%s\n' "$files" | bash tools/ci/docs-only-check.sh)"
|
||||
echo "$line" >> "$GITHUB_OUTPUT"
|
||||
echo "$line: $(printf '%s\n' "$files" | grep -c .) changed path(s) between ${BEFORE:0:8} and ${AFTER:0:8}"
|
||||
pow:
|
||||
name: igneum-pow tests, igneum-census build
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
|
|
@ -32,13 +78,20 @@ jobs:
|
|||
run: cargo build --release
|
||||
sims:
|
||||
name: simulators, quick modes
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# master and release-* pushes, and pull requests into them, only (main, 7 October 2026: every code push cost two box jobs and the
|
||||
# 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
|
||||
if: vars.IGNEUM_CI_RUNNER != 'box' # the box has python3 and numpy from provision.sh
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: python3 -m pip install --quiet numpy
|
||||
if: vars.IGNEUM_CI_RUNNER != 'box'
|
||||
- name: finality_v2.py --quick (under two minutes)
|
||||
working-directory: sim
|
||||
run: time timeout 120 python3 finality_v2.py --quick > finality_quick.md
|
||||
|
|
@ -54,49 +107,22 @@ 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
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: site build
|
||||
run: node site/build.mjs
|
||||
- name: internal link check of site/*.html
|
||||
run: node tools/ci/link-check.mjs
|
||||
- name: identity grep of the public export list
|
||||
run: bash tools/ci/identity-check.sh
|
||||
- name: no conflict markers in tracked files
|
||||
run: bash tools/ci/no-conflict-markers.sh
|
||||
- name: copied sources are re-stamped before a build
|
||||
run: bash tools/ci/copied-sources-check.sh
|
||||
- name: second-engine playbooks log to a file and end their tree (C35)
|
||||
run: bash tools/ci/second-engine-check.sh
|
||||
- name: no playbook quits, pauses or resumes the installed app (self-test first, then the tree)
|
||||
run: bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh
|
||||
- name: the signer is never piped into head
|
||||
run: bash tools/ci/signer-pipe-check.sh
|
||||
- name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree)
|
||||
run: bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh
|
||||
- name: run jobs test their fetched kit before use, the wiped-jobs-folder class (self-test first, then the tree)
|
||||
run: bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh
|
||||
- name: every Windows spawn of the app runs with a hidden console (self-test first, then the tree)
|
||||
run: node tools/ci/windows-spawn-check.mjs --self-test && node tools/ci/windows-spawn-check.mjs
|
||||
- name: pinned guest programs match their manifest and are built only by pin-guests.sh
|
||||
run: bash tools/ci/pinned-guests-check.sh
|
||||
- name: root prover playbooks kill the GPU server and unlink its socket (the root-socket class, 5 October 2026)
|
||||
run: bash tools/ci/prover-socket-check.sh
|
||||
- name: no secret file names and no 64-hex secrets in the tree (self-test first, then the tree)
|
||||
run: bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh
|
||||
- name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors)
|
||||
run: node --test site/api/faucet.test.mjs
|
||||
- name: explorer and public stats unit tests (search router, formatters, emission rule against the node's own test values, the documented API fields from a fixture)
|
||||
run: node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/api/public-stats.test.mjs
|
||||
- name: a headless Chromium for the text-overlap sweep (Playwright outside the tree; the gate finds it through IGNEUM_PLAYWRIGHT_DIR)
|
||||
run: |
|
||||
mkdir -p /tmp/pw && cd /tmp/pw && npm init -y >/dev/null && npm i --no-audit --no-fund playwright@1.56 | tail -1
|
||||
npx playwright install --with-deps chromium | tail -1
|
||||
echo "IGNEUM_PLAYWRIGHT_DIR=/tmp/pw" >> "$GITHUB_ENV"
|
||||
- name: the tree gate, tools/ci/pre-push.sh --ci (the same script the pre-push hook runs; one line per check, a red check prints its output)
|
||||
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
|
||||
- name: ship tool self-test (version bump, the dl-both and public manifest helpers)
|
||||
run: node tools/ship-app.mjs --self-test
|
||||
- name: relay unit tests (parsers, secret compare, the wake endpoint)
|
||||
run: node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
||||
- name: miner app notice strip and update card (ordering, keys, wording, timers, when the card shows)
|
||||
run: node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs
|
||||
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
|
||||
|
|
|
|||
29
.github/workflows/windows.yml
vendored
29
.github/workflows/windows.yml
vendored
|
|
@ -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)" }
|
||||
|
|
@ -293,3 +299,24 @@ jobs:
|
|||
path: build/inputs-artifact/
|
||||
retention-days: 90
|
||||
if-no-files-found: error
|
||||
|
||||
red:
|
||||
# The red watcher for the Windows pipeline (see ci.yml `red`): one line per failed master or release-* run to the
|
||||
# hidden updates channel and /srv/ci-red/red.jsonl on the box, recorded by the box's own runner.
|
||||
name: red watcher (master and release-* only; one line per failed run to the updates channel and the box file)
|
||||
needs: [parse, build]
|
||||
if: ${{ failure() && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release-')) }}
|
||||
runs-on: [self-hosted, linux, x64, igneum-build-1]
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: read # the run's jobs API (the first real red run, 21:19Z: the default token answered 403 and the line carried no step)
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: tools/ci
|
||||
- name: record this run (one line, the failed jobs and their first failed step, from the run's own API)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
RED_WATCH_TITLE: ${{ github.event.head_commit.message }}
|
||||
run: node tools/ci/red-watch.mjs record --file /srv/ci-red/red.jsonl
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -31,3 +31,8 @@ vendor/igneum-node-ship/
|
|||
# Trademark instruction packs name the director and the applicant company; never in the repository
|
||||
brand/trademark/pbip-pack/
|
||||
brand/trademark/*.zip
|
||||
|
||||
# the Discord bot and reddit bot dry-run renders
|
||||
tools/community/out/
|
||||
# the GPU workers built on igneum-build-1 (tools/workers-remote.sh); binaries, never committed
|
||||
infra/cross/out-workers-box/
|
||||
|
|
|
|||
2
app/igneum-app/Cargo.lock
generated
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
edition = "2021"
|
||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// 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 <winver.h>
|
||||
|
||||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,3,13,0
|
||||
PRODUCTVERSION 0,3,13,0
|
||||
FILEVERSION 0,3,14,0
|
||||
PRODUCTVERSION 0,3,14,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Miner engine"
|
||||
VALUE "FileVersion", "0.3.13"
|
||||
VALUE "FileVersion", "0.3.14"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.13"
|
||||
VALUE "ProductVersion", "0.3.14"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ pub struct CardPref {
|
|||
pub sweep_class: String,
|
||||
#[serde(default)]
|
||||
pub sweep_source: String,
|
||||
/// Ember 2: the memory clock the last tune chose (0 = the driver's default)
|
||||
#[serde(default)]
|
||||
pub sweep_mem_mhz: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
|
|
@ -84,12 +87,21 @@ 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.
|
||||
#[serde(default)]
|
||||
pub power_control: bool,
|
||||
/// Ember 2 (6 October 2026): the tune's goal ("efficiency" = most MH per watt within 10% of the top rate,
|
||||
/// "balanced" = within 1%, "rate" = the fastest point), the electricity price in pence per kWh for the £/day
|
||||
/// reading on each card, and the hill-climb switch (memory up, core down from the fleet prior; off = the ladders).
|
||||
#[serde(default = "balanced")]
|
||||
pub tune_goal: String,
|
||||
#[serde(default)]
|
||||
pub power_price_pence: f64,
|
||||
#[serde(default)]
|
||||
pub tune_climb: bool,
|
||||
/// When this install first ran (unix s), for the "first hour after install" sweep.
|
||||
#[serde(default)]
|
||||
pub installed_at: u64,
|
||||
|
|
@ -113,13 +125,16 @@ pub struct Settings {
|
|||
fn one() -> u32 {
|
||||
1
|
||||
}
|
||||
fn balanced() -> String {
|
||||
"balanced".into()
|
||||
}
|
||||
fn yes() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Settings {
|
||||
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false }
|
||||
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), power_price_pence: 0.0, tune_climb: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -330,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,
|
||||
}
|
||||
|
||||
|
|
@ -342,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![],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,9 +20,18 @@ use std::time::{Duration, Instant};
|
|||
/// The power steps, percent of the card's default limit, highest first (the same ladder as src/sweep.rs).
|
||||
pub const POWER_STEPS_PCT: [u32; 6] = [100, 90, 80, 70, 60, 50];
|
||||
/// The clock steps, percent of the card's maximum core clock, highest first; 100 = unlocked (the card's own boost).
|
||||
pub const CLOCK_STEPS_PCT: [u32; 5] = [100, 90, 80, 70, 60];
|
||||
/// A card's clock floor when the vendor reports none: this share of its maximum core clock.
|
||||
pub const CLOCK_FLOOR_PCT: u32 = 60;
|
||||
/// 6 October 2026, run 6: the 5090's best MH/W sat on the 60% floor (1,854 MHz: 0.563 MH/W, the rate within 0.15%),
|
||||
/// so the ladder and the floor go to 45% of the maximum; the 1% rate tolerance is the guard below that
|
||||
pub const CLOCK_STEPS_PCT: [u32; 7] = [100, 90, 80, 70, 60, 50, 45];
|
||||
/// A card's clock floor when the vendor reports none: this share of its maximum core clock. 7 October 2026, the PC 1
|
||||
/// efficiency passes (docs/bench-log.md): the 5090's best MH per watt sat at 1,200 to 1,300 MHz (39 to 42 percent of
|
||||
/// 3,090) and the rate fell past 5 percent only at 1,200 (class v3) and 1,100 (class v4), under the old 45 percent
|
||||
/// floor; so the ladder continues below 45 percent in [`CLOCK_FINE_STEP_MHZ`] steps down to 20 percent of the maximum
|
||||
/// (618 MHz on the 5090) and the stop rule, not the floor, ends the search.
|
||||
pub const CLOCK_FLOOR_PCT: u32 = 20;
|
||||
/// Below the percent ladder's last rung (45 percent) the clock ladder descends in steps of this many MHz until the
|
||||
/// rate falls more than the tolerance under the cap point's rate (the knee), a step faults, or the floor is reached.
|
||||
pub const CLOCK_FINE_STEP_MHZ: u32 = 100;
|
||||
/// A point may lose this much rate against the fastest point and still win on MH per watt (the manifest can change it).
|
||||
pub const RATE_TOLERANCE_PCT: f64 = 1.0;
|
||||
/// A step whose hottest GPU reading reaches this is marked hot and cannot win (the engine aborts at 90).
|
||||
|
|
@ -49,6 +58,10 @@ pub struct Limits {
|
|||
pub clock_max_mhz: u32,
|
||||
/// the lowest cap the vendor allows (the ADLX gmax_range floor); 0 = CLOCK_FLOOR_PCT of the maximum
|
||||
pub clock_min_mhz: u32,
|
||||
/// Ember 2: the memory clock the card runs at by default and the vendor's maximum (nvidia-smi clocks.mem and
|
||||
/// clocks.max.mem); 0 = no memory knob (AMD through ADLX on RDNA 4 exposes none)
|
||||
pub mem_default_mhz: u32,
|
||||
pub mem_max_mhz: u32,
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
|
|
@ -66,6 +79,13 @@ impl Limits {
|
|||
}
|
||||
mhz.clamp(self.clock_floor(), self.clock_max_mhz)
|
||||
}
|
||||
/// A memory clock inside the vendor's range; 0 stays 0 (the default).
|
||||
pub fn clamp_mem(&self, mhz: u32) -> u32 {
|
||||
if mhz == 0 || self.mem_max_mhz == 0 || self.mem_default_mhz == 0 {
|
||||
return 0;
|
||||
}
|
||||
mhz.clamp(self.mem_default_mhz, self.mem_max_mhz)
|
||||
}
|
||||
/// The watts a power percent asks for, inside the card's min and max, rounded to a watt.
|
||||
pub fn watts_for(&self, pct: u32) -> f64 {
|
||||
let mut w = self.power_default_w * pct as f64 / 100.0;
|
||||
|
|
@ -79,13 +99,15 @@ impl Limits {
|
|||
}
|
||||
}
|
||||
|
||||
/// One setting of the two knobs.
|
||||
/// One setting of the knobs (Ember 2 adds the memory clock: 0 = the driver's default).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Point {
|
||||
/// the core clock cap in MHz; 0 = unlocked
|
||||
pub clock_mhz: u32,
|
||||
/// the power limit, percent of the default
|
||||
pub power_pct: u32,
|
||||
/// the memory clock in MHz (NVIDIA `-lmc`, locked to one value); 0 = the driver's default
|
||||
pub mem_mhz: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
|
@ -96,6 +118,8 @@ pub enum Kind {
|
|||
Clock,
|
||||
/// the fleet prior and one neighbour
|
||||
Confirm,
|
||||
/// Ember 2: a hill-climb probe
|
||||
Climb,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
@ -106,11 +130,53 @@ pub struct Step {
|
|||
pub kind: Kind,
|
||||
}
|
||||
|
||||
/// What the tune is for (Settings > Ember Tune > goal). The rate floor is the share of the best rate seen a point
|
||||
/// must keep to win on MH per watt: efficiency keeps 90%, balanced 99% (the 1% rule of lever 3), maximum rate
|
||||
/// takes the fastest point and uses MH per watt only to break ties.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Goal {
|
||||
Efficiency,
|
||||
Balanced,
|
||||
MaxRate,
|
||||
}
|
||||
|
||||
impl Goal {
|
||||
pub fn parse(s: &str) -> Goal {
|
||||
match s {
|
||||
"efficiency" | "eff" => Goal::Efficiency,
|
||||
"rate" | "max_rate" | "maximum" => Goal::MaxRate,
|
||||
_ => Goal::Balanced,
|
||||
}
|
||||
}
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Goal::Efficiency => "efficiency",
|
||||
Goal::Balanced => "balanced",
|
||||
Goal::MaxRate => "rate",
|
||||
}
|
||||
}
|
||||
/// The rate tolerance the choice rule uses, percent under the best rate.
|
||||
pub fn tolerance_pct(&self, manifest_default: f64) -> f64 {
|
||||
match self {
|
||||
Goal::Efficiency => 10.0,
|
||||
Goal::Balanced => manifest_default,
|
||||
Goal::MaxRate => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pounds a day for a draw at a price in pence per kWh: watts × 24 h / 1000 × price / 100.
|
||||
pub fn pounds_per_day(watts: f64, pence_per_kwh: f64) -> f64 {
|
||||
watts * 24.0 / 1000.0 * pence_per_kwh / 100.0
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PlanKind {
|
||||
Full,
|
||||
Confirm,
|
||||
Baseline,
|
||||
/// Ember 2: the hill-climb over memory up and core down from the start point
|
||||
Climb,
|
||||
}
|
||||
|
||||
impl PlanKind {
|
||||
|
|
@ -119,6 +185,7 @@ impl PlanKind {
|
|||
PlanKind::Full => "full",
|
||||
PlanKind::Confirm => "confirm",
|
||||
PlanKind::Baseline => "baseline",
|
||||
PlanKind::Climb => "climb",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,10 +200,86 @@ pub struct Plan {
|
|||
pub tolerance_pct: f64,
|
||||
power: Vec<Step>,
|
||||
clock_pcts: Vec<u32>,
|
||||
/// the clock ladder below the percent rungs: MHz values from the last rung minus one fine step down to the floor
|
||||
/// (the core-clock knob of 7 October 2026; empty when the card has no readable maximum clock)
|
||||
clock_fine: Vec<u32>,
|
||||
fixed: Vec<Step>,
|
||||
/// Ember 2 (Climb): the start point, the step sizes and the step budget
|
||||
climb: Option<Climb>,
|
||||
}
|
||||
|
||||
/// The hill-climb's shape: from `start`, each probe moves the memory clock up by `mem_step` or the core clock down
|
||||
/// by `core_step` (or both), keeps the move when the goal's score improves, else turns to the other knob; a
|
||||
/// refused step (a fault on it) backs that knob off for good. At most `budget` steps including the start.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Climb {
|
||||
pub start: Point,
|
||||
pub mem_step: u32,
|
||||
pub core_step: u32,
|
||||
pub budget: usize,
|
||||
pub goal: Goal,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
/// Ember 2: the hill-climb. The start is the fleet prior (or the card's current point); a probe step is 5% of
|
||||
/// the memory range above the default (0 when the card has no memory knob) and 5% of the maximum core clock;
|
||||
/// five steps of 60 s converge in under 10 minutes.
|
||||
pub fn climb(limits: &Limits, start: Point, goal: Goal, tolerance_pct: f64) -> Plan {
|
||||
let mem_step = if limits.mem_max_mhz > limits.mem_default_mhz { ((limits.mem_max_mhz - limits.mem_default_mhz) / 20).max(25) } else { 0 };
|
||||
let core_step = if limits.clock_max_mhz > 0 { (limits.clock_max_mhz / 20).max(25) } else { 0 };
|
||||
let start = Point { clock_mhz: limits.clamp_clock(start.clock_mhz), power_pct: start.power_pct.clamp(50, 100), mem_mhz: limits.clamp_mem(start.mem_mhz) };
|
||||
Plan { kind: PlanKind::Climb, limits: limits.clone(), before: start, tolerance_pct: goal.tolerance_pct(tolerance_pct), power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed: Vec::new(), climb: Some(Climb { start, mem_step, core_step, budget: 5, goal }) }
|
||||
}
|
||||
|
||||
/// The goal's score of a row: MH per watt for efficiency and balanced, the rate for maximum rate.
|
||||
pub fn score(&self, r: &Row) -> f64 {
|
||||
match self.climb.as_ref().map(|c| c.goal) {
|
||||
Some(Goal::MaxRate) => r.mhs,
|
||||
_ => r.eff,
|
||||
}
|
||||
}
|
||||
|
||||
/// The climb's next probe from the rows so far: None when the budget is spent or no move is left.
|
||||
fn climb_next(&self, rows: &[Row]) -> Option<Step> {
|
||||
let c = self.climb.as_ref()?;
|
||||
let step = |p: Point| Step { point: p, watts: self.limits.watts_for(p.power_pct), kind: Kind::Climb };
|
||||
if rows.is_empty() {
|
||||
return Some(step(c.start));
|
||||
}
|
||||
if rows.len() >= c.budget {
|
||||
return None;
|
||||
}
|
||||
// the best usable row so far is the hill's top; a knob that produced a marked (refused) row is backed off
|
||||
let best = rows.iter().filter(|r| r.usable()).max_by(|a, b| self.score(a).partial_cmp(&self.score(b)).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||
let refused_mem = rows.iter().any(|r| !r.usable() && r.point.mem_mhz > best.point.mem_mhz);
|
||||
let refused_core = rows.iter().any(|r| !r.usable() && r.point.clock_mhz != 0 && (best.point.clock_mhz == 0 || r.point.clock_mhz < best.point.clock_mhz));
|
||||
let last = rows.last()?;
|
||||
let mem_up = |p: Point| -> Option<Point> {
|
||||
if c.mem_step == 0 || refused_mem { return None; }
|
||||
let base = if p.mem_mhz == 0 { self.limits.mem_default_mhz } else { p.mem_mhz };
|
||||
let m = self.limits.clamp_mem(base + c.mem_step);
|
||||
(m > 0 && m != p.mem_mhz && m != base).then_some(Point { mem_mhz: m, ..p })
|
||||
};
|
||||
let core_down = |p: Point| -> Option<Point> {
|
||||
if c.core_step == 0 || refused_core { return None; }
|
||||
let base = if p.clock_mhz == 0 { self.limits.clock_max_mhz } else { p.clock_mhz };
|
||||
let k = self.limits.clamp_clock(base.saturating_sub(c.core_step));
|
||||
(k > 0 && k != p.clock_mhz).then_some(Point { clock_mhz: k, ..p })
|
||||
};
|
||||
let tried = |p: Point| rows.iter().any(|r| r.point == p);
|
||||
// the last move improved: keep going the same way from the top; else turn: memory first, then core, then both
|
||||
let last_improved = last.usable() && last.point == best.point && rows.len() > 1;
|
||||
let last_was_mem = rows.len() > 1 && last.point.mem_mhz != rows[rows.len() - 2].point.mem_mhz;
|
||||
let candidates: Vec<Option<Point>> = if last_improved && last_was_mem {
|
||||
vec![mem_up(best.point), core_down(best.point)]
|
||||
} else if last_improved {
|
||||
vec![core_down(best.point), mem_up(best.point)]
|
||||
} else {
|
||||
vec![mem_up(best.point), core_down(best.point), mem_up(best.point).and_then(core_down)]
|
||||
};
|
||||
candidates.into_iter().flatten().find(|p| !tried(*p)).map(step)
|
||||
}
|
||||
|
||||
/// Power 100% to 50% at the unlocked clock (duplicate watts dropped, as the card's floor clamps them), then the
|
||||
/// clock ladder 90% to the floor of the maximum core clock at the chosen power. A card without a readable
|
||||
/// maximum clock gets the power ladder only; a card without a default limit gets the clock ladder only.
|
||||
|
|
@ -148,42 +291,95 @@ impl Plan {
|
|||
if power.last().map(|s: &Step| (s.watts - w).abs() < 0.5).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
power.push(Step { point: Point { clock_mhz: 0, power_pct: pct }, watts: w, kind: Kind::Power });
|
||||
power.push(Step { point: Point { clock_mhz: 0, power_pct: pct, mem_mhz: 0 }, watts: w, kind: Kind::Power });
|
||||
}
|
||||
}
|
||||
let clock_pcts = if limits.clock_max_mhz > 0 { CLOCK_STEPS_PCT[1..].to_vec() } else { Vec::new() };
|
||||
Plan { kind: PlanKind::Full, limits: limits.clone(), before, tolerance_pct, power, clock_pcts, fixed: Vec::new() }
|
||||
// the fine ladder: from the last percent rung down to the floor in CLOCK_FINE_STEP_MHZ steps (the knob of
|
||||
// 7 October 2026; the stop rule in `next` ends it at the knee)
|
||||
let mut clock_fine = Vec::new();
|
||||
if limits.clock_max_mhz > 0 {
|
||||
let last_pct = limits.clamp_clock(limits.clock_max_mhz * CLOCK_STEPS_PCT[CLOCK_STEPS_PCT.len() - 1] / 100);
|
||||
let floor = limits.clock_floor();
|
||||
let mut m = (last_pct / CLOCK_FINE_STEP_MHZ) * CLOCK_FINE_STEP_MHZ;
|
||||
if m >= last_pct {
|
||||
m = m.saturating_sub(CLOCK_FINE_STEP_MHZ);
|
||||
}
|
||||
while m >= floor && m > 0 {
|
||||
clock_fine.push(m);
|
||||
m = m.saturating_sub(CLOCK_FINE_STEP_MHZ);
|
||||
}
|
||||
}
|
||||
Plan { kind: PlanKind::Full, limits: limits.clone(), before, tolerance_pct, power, clock_pcts, clock_fine, fixed: Vec::new(), climb: None }
|
||||
}
|
||||
|
||||
/// The prior's point, then one neighbour: the next clock step up when the prior caps the clock (is the cap
|
||||
/// costing rate?), else one power step down (is there efficiency left?).
|
||||
pub fn confirm(limits: &Limits, prior: Point, before: Point, tolerance_pct: f64) -> Plan {
|
||||
let p = Point { clock_mhz: limits.clamp_clock(prior.clock_mhz), power_pct: prior.power_pct.clamp(50, 100) };
|
||||
let p = Point { clock_mhz: limits.clamp_clock(prior.clock_mhz), power_pct: prior.power_pct.clamp(50, 100), mem_mhz: limits.clamp_mem(prior.mem_mhz) };
|
||||
let first = Step { point: p, watts: limits.watts_for(p.power_pct), kind: Kind::Confirm };
|
||||
let neighbour = if p.clock_mhz > 0 && limits.clock_max_mhz > 0 {
|
||||
let up = p.clock_mhz + limits.clock_max_mhz / 10;
|
||||
let clock = if up >= limits.clock_max_mhz { 0 } else { limits.clamp_clock(up) };
|
||||
Point { clock_mhz: clock, power_pct: p.power_pct }
|
||||
Point { clock_mhz: clock, power_pct: p.power_pct, mem_mhz: p.mem_mhz }
|
||||
} else {
|
||||
Point { clock_mhz: p.clock_mhz, power_pct: (p.power_pct.saturating_sub(10)).max(50) }
|
||||
Point { clock_mhz: p.clock_mhz, power_pct: (p.power_pct.saturating_sub(10)).max(50), mem_mhz: p.mem_mhz }
|
||||
};
|
||||
let mut fixed = vec![first];
|
||||
if neighbour != p {
|
||||
fixed.push(Step { point: neighbour, watts: limits.watts_for(neighbour.power_pct), kind: Kind::Confirm });
|
||||
}
|
||||
Plan { kind: PlanKind::Confirm, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), fixed }
|
||||
Plan { kind: PlanKind::Confirm, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed, climb: None }
|
||||
}
|
||||
|
||||
/// One step at the card's current point: the before number, and all a measure-only card (Apple, or NVIDIA
|
||||
/// with Power control off) reports.
|
||||
pub fn baseline(limits: &Limits, before: Point, tolerance_pct: f64) -> Plan {
|
||||
let fixed = vec![Step { point: before, watts: limits.watts_for(before.power_pct), kind: Kind::Baseline }];
|
||||
Plan { kind: PlanKind::Baseline, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), fixed }
|
||||
Plan { kind: PlanKind::Baseline, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed, climb: None }
|
||||
}
|
||||
|
||||
/// How many steps the plan has at most (the clock ladder counts whether or not it runs).
|
||||
pub fn len(&self) -> usize {
|
||||
self.fixed.len() + self.power.len() + self.clock_pcts.len()
|
||||
if let Some(c) = &self.climb {
|
||||
return c.budget;
|
||||
}
|
||||
self.fixed.len() + self.power.len() + self.clock_pcts.len() + self.clock_fine.len()
|
||||
}
|
||||
|
||||
/// The cap point's row: the power ladder's choice (the row the clock search is read against), else the first row.
|
||||
pub fn cap_row(&self, rows: &[Row]) -> Option<Row> {
|
||||
if self.power.is_empty() {
|
||||
rows.first().cloned()
|
||||
} else {
|
||||
choose(&rows[..self.power.len().min(rows.len())], self.tolerance_pct).or_else(|| rows.first().cloned())
|
||||
}
|
||||
}
|
||||
|
||||
/// Why the clock search ended after `rows`, in words, or None while it runs: a faulted clock row (a rejected or
|
||||
/// mismatched hash during the hold: the fingerprint check), the knee (the rate under the cap point's by more than
|
||||
/// the tolerance), or the floor.
|
||||
pub fn clock_stop_reason(&self, rows: &[Row]) -> Option<String> {
|
||||
let last = rows.last()?;
|
||||
if last.point.clock_mhz == 0 || !matches!(self.kind, PlanKind::Full) {
|
||||
return None;
|
||||
}
|
||||
let clock_rows = rows.len().saturating_sub(self.power.len());
|
||||
if clock_rows == 0 {
|
||||
return None;
|
||||
}
|
||||
if last.mark == Some(Mark::Faulted) {
|
||||
return Some(format!("fingerprint mismatch at {} MHz, clocks reset", last.point.clock_mhz));
|
||||
}
|
||||
if let Some(cap) = self.cap_row(rows) {
|
||||
if last.usable() && cap.usable() && cap.mhs > 0.0 && last.mhs < cap.mhs * (1.0 - self.tolerance_pct.max(0.0) / 100.0) {
|
||||
return Some(format!("rate fell {:.1} percent at {} MHz", 100.0 * (cap.mhs - last.mhs) / cap.mhs, last.point.clock_mhz));
|
||||
}
|
||||
}
|
||||
if clock_rows >= self.clock_pcts.len() + self.clock_fine.len() {
|
||||
return Some(format!("the floor at {} MHz", last.point.clock_mhz));
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
|
|
@ -191,6 +387,9 @@ impl Plan {
|
|||
|
||||
/// The next step after `rows` (one row per step done so far), or None when the plan is complete.
|
||||
pub fn next(&self, rows: &[Row]) -> Option<Step> {
|
||||
if self.climb.is_some() {
|
||||
return self.climb_next(rows);
|
||||
}
|
||||
let i = rows.len();
|
||||
if !self.fixed.is_empty() {
|
||||
return self.fixed.get(i).cloned();
|
||||
|
|
@ -199,15 +398,27 @@ impl Plan {
|
|||
return Some(self.power[i].clone());
|
||||
}
|
||||
let k = i - self.power.len();
|
||||
let pct = *self.clock_pcts.get(k)?;
|
||||
// the stop rule (7 October 2026): a faulted clock row or the knee ends the search; the choice is made among
|
||||
// the rows so far
|
||||
if k > 0 {
|
||||
if let Some(reason) = self.clock_stop_reason(rows) {
|
||||
if !reason.starts_with("the floor") {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let clock = if k < self.clock_pcts.len() {
|
||||
self.limits.clamp_clock(self.limits.clock_max_mhz * self.clock_pcts[k] / 100)
|
||||
} else {
|
||||
*self.clock_fine.get(k - self.clock_pcts.len())?
|
||||
};
|
||||
// the clock ladder rides the power point the power ladder chose (the before point when nothing won)
|
||||
let power_pct = if self.power.is_empty() { self.before.power_pct } else { choose(&rows[..self.power.len()], self.tolerance_pct).map(|r| r.point.power_pct).unwrap_or(self.before.power_pct) };
|
||||
let clock = self.limits.clamp_clock(self.limits.clock_max_mhz * pct / 100);
|
||||
// a step whose clamp lands on the previous step's clock is dropped (the floor was reached)
|
||||
if rows.last().map(|r| r.point.clock_mhz == clock).unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
Some(Step { point: Point { clock_mhz: clock, power_pct }, watts: self.limits.watts_for(power_pct), kind: Kind::Clock })
|
||||
Some(Step { point: Point { clock_mhz: clock, power_pct, mem_mhz: 0 }, watts: self.limits.watts_for(power_pct), kind: Kind::Clock })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,9 +516,10 @@ impl Row {
|
|||
/// `TUNE card=<label> step=<i> clock=<MHz> cap=<pct> limit=<W> watts=<W> mhs=<x> eff=<MH/W> gclk=<MHz> mclk=<MHz> tmax=<C> mark=<m>`
|
||||
pub fn line(&self, card: &str, i: usize) -> String {
|
||||
format!(
|
||||
"TUNE card={card} step={i} clock={} cap={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} gclk={:.0} mclk={:.0} tmax={:.0} mark={}{}",
|
||||
"TUNE card={card} step={i} clock={} cap={} mem={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} gclk={:.0} mclk={:.0} tmax={:.0} mark={}{}",
|
||||
self.point.clock_mhz,
|
||||
self.point.power_pct,
|
||||
self.point.mem_mhz,
|
||||
self.limit,
|
||||
self.watts,
|
||||
self.mhs,
|
||||
|
|
@ -320,7 +532,7 @@ impl Row {
|
|||
)
|
||||
}
|
||||
pub fn json(&self) -> serde_json::Value {
|
||||
serde_json::json!({ "clock_mhz": self.point.clock_mhz, "power_pct": self.point.power_pct, "limit_w": self.limit, "watts": r1(self.watts), "mhs": r2(self.mhs), "eff": r4(self.eff), "gclk": self.gclk.round(), "mclk": self.mclk.round(), "tmax": self.tmax.round(), "faults": self.faults, "mark": self.mark.unwrap_or(Mark::NoReadings).name() })
|
||||
serde_json::json!({ "clock_mhz": self.point.clock_mhz, "power_pct": self.point.power_pct, "mem_mhz": self.point.mem_mhz, "limit_w": self.limit, "watts": r1(self.watts), "mhs": r2(self.mhs), "eff": r4(self.eff), "gclk": self.gclk.round(), "mclk": self.mclk.round(), "tmax": self.tmax.round(), "faults": self.faults, "mark": self.mark.unwrap_or(Mark::NoReadings).name() })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -454,7 +666,7 @@ pub fn prior_of(tuning: Option<&serde_json::Value>, key: &str, min_samples: u32)
|
|||
if samples < min_samples.max(1) {
|
||||
return None;
|
||||
}
|
||||
let point = Point { clock_mhz: n("clock_mhz") as u32, power_pct: (n("power_pct") as u32).clamp(50, 100) };
|
||||
let point = Point { clock_mhz: n("clock_mhz") as u32, power_pct: (n("power_pct") as u32).clamp(50, 100), mem_mhz: n("mem_mhz") as u32 };
|
||||
if point.power_pct == 0 && point.clock_mhz == 0 {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -463,7 +675,7 @@ pub fn prior_of(tuning: Option<&serde_json::Value>, key: &str, min_samples: u32)
|
|||
|
||||
/// The fleet record: one `TUNE {json}` line. `machine` is a hash of the install id (never the id, never an address).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn record_json(ts: f64, machine_hash: &str, app: &str, os: &str, card: &str, vendor: &str, driver: &str, class: &str, plan: PlanKind, rows: &[Row], chosen: Option<&Row>, before: Option<&Row>) -> serde_json::Value {
|
||||
pub fn record_json(ts: f64, machine_hash: &str, app: &str, os: &str, card: &str, vendor: &str, driver: &str, class: &str, plan: PlanKind, rows: &[Row], chosen: Option<&Row>, before: Option<&Row>, floor: bool) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"ts": ts.round(),
|
||||
"machine": machine_hash,
|
||||
|
|
@ -482,6 +694,8 @@ pub fn record_json(ts: f64, machine_hash: &str, app: &str, os: &str, card: &str,
|
|||
"eff": chosen.map(|r| r4(r.eff)).unwrap_or(0.0),
|
||||
"mhs": chosen.map(|r| r2(r.mhs)).unwrap_or(0.0),
|
||||
"watts": chosen.map(|r| r1(r.watts)).unwrap_or(0.0),
|
||||
"floor": floor,
|
||||
"note": if floor { "floor, not optimum" } else { "" },
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -591,9 +805,28 @@ impl Run {
|
|||
self.rows.first().map(|r| r.mclk).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Seconds left in the plan from here: the rest of this step plus (settle + hold + a 10 s apply) per step to come.
|
||||
pub fn eta_s(&self, now: Instant) -> i64 {
|
||||
let per = (self.timing.settle + self.timing.hold).as_secs() as i64 + 10;
|
||||
let this = match &self.phase {
|
||||
Phase::Applying { .. } => per,
|
||||
Phase::Settling { since } => per - 10 - now.duration_since(*since).as_secs().min(per as u64) as i64,
|
||||
Phase::Holding { since } => (self.timing.hold.as_secs() as i64 - now.duration_since(*since).as_secs() as i64).max(0),
|
||||
Phase::Finishing { .. } | Phase::Done => 0,
|
||||
};
|
||||
let left = self.plan.len().saturating_sub(self.rows.len() + 1) as i64;
|
||||
this.max(0) + left * per
|
||||
}
|
||||
|
||||
/// The phase in words for the card row.
|
||||
pub fn words(&self, now: Instant) -> String {
|
||||
let what = |p: &Point| if p.clock_mhz > 0 { format!("{} MHz · {}%", p.clock_mhz, p.power_pct) } else { format!("{}%", p.power_pct) };
|
||||
let what = |p: &Point| {
|
||||
let mut w = if p.clock_mhz > 0 { format!("{} MHz · {}%", p.clock_mhz, p.power_pct) } else { format!("{}%", p.power_pct) };
|
||||
if p.mem_mhz > 0 {
|
||||
w.push_str(&format!(" · mem {}", p.mem_mhz));
|
||||
}
|
||||
w
|
||||
};
|
||||
let cur = self.current.as_ref().map(|s| what(&s.point)).unwrap_or_default();
|
||||
let n = self.rows.len() + 1;
|
||||
let of = self.plan.len();
|
||||
|
|
@ -611,10 +844,15 @@ impl Run {
|
|||
fn applied(step: &Step, rb: Readback, before_w: f64) -> bool {
|
||||
let power_ok = rb.limit_w <= 0.0 || (rb.limit_w - step.watts).abs() < 1.5;
|
||||
let power_changed = (step.watts - before_w).abs() >= 1.5;
|
||||
if step.point.clock_mhz > 0 || !power_changed {
|
||||
if step.point.clock_mhz > 0 || step.point.mem_mhz > 0 || !power_changed {
|
||||
rb.acked && power_ok
|
||||
} else if rb.limit_w <= 0.0 {
|
||||
// a card with no limit readback at all (AMD through ADLX reports an offset, never watts; run 6 on
|
||||
// 6 October 2026 aborted the 9070 XT's ladder at its first step on "card reports 0 W"): the
|
||||
// acknowledgement is the proof
|
||||
rb.acked
|
||||
} else {
|
||||
rb.limit_w > 0.0 && power_ok
|
||||
power_ok
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -719,6 +957,13 @@ impl Run {
|
|||
Verdict::NoReadings => None,
|
||||
},
|
||||
PlanKind::Full => choose(&self.rows, self.plan.tolerance_pct),
|
||||
PlanKind::Climb => {
|
||||
if self.plan.climb.as_ref().map(|c| c.goal) == Some(Goal::MaxRate) {
|
||||
self.rows.iter().filter(|r| r.usable()).max_by(|a, b| a.mhs.partial_cmp(&b.mhs).unwrap_or(std::cmp::Ordering::Equal)).cloned()
|
||||
} else {
|
||||
choose(&self.rows, self.plan.tolerance_pct)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -733,6 +978,53 @@ pub fn tuned_line(mhs: f64, watts: f64, eff: f64) -> String {
|
|||
format!("Tuned: {mhs:.1} MH/s at {watts:.0} W ({eff:.3} MH/W)")
|
||||
}
|
||||
|
||||
/// The row's line for a baseline (measure-only) result: the card was measured as it runs, nothing was set.
|
||||
/// (6 October 2026: PC 1's rows read "Tuned: 114.2 MH/s at 305 W" for a baseline, which is not a tune.)
|
||||
pub fn measured_line(mhs: f64, watts: f64, eff: f64) -> String {
|
||||
format!("Measured: {mhs:.1} MH/s at {watts:.0} W ({eff:.3} MH/W)")
|
||||
}
|
||||
|
||||
/// The row's line for a plan's result.
|
||||
pub fn result_line(kind: PlanKind, mhs: f64, watts: f64, eff: f64) -> String {
|
||||
if kind == PlanKind::Baseline {
|
||||
measured_line(mhs, watts, eff)
|
||||
} else {
|
||||
tuned_line(mhs, watts, eff)
|
||||
}
|
||||
}
|
||||
|
||||
/// The core-clock knob's result on a card (7 October 2026; the UI lane's field shape): the chosen lock against the cap
|
||||
/// point's unlocked row, and the stop reason in words.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct LockResult {
|
||||
/// the chosen core clock cap (0 = unlocked)
|
||||
pub lock_mhz: u32,
|
||||
pub lock_mhs: f64,
|
||||
pub lock_w: f64,
|
||||
pub lock_mhw: f64,
|
||||
/// the cap point's row (clock 0): the rate and draw the lock is read against
|
||||
pub unlocked_mhs: f64,
|
||||
pub unlocked_w: f64,
|
||||
/// "rate fell 5.1 percent at 1,200 MHz", "fingerprint mismatch at 1,400 MHz, clocks reset", "the floor at 618 MHz",
|
||||
/// "no lever" (a card without a clock cap), "" while nothing ran
|
||||
pub lock_note: String,
|
||||
}
|
||||
|
||||
/// The knob's result from a finished plan's rows and its chosen row.
|
||||
pub fn lock_result(plan: &Plan, rows: &[Row], chosen: &Row) -> LockResult {
|
||||
let cap = plan.cap_row(rows);
|
||||
let (unlocked_mhs, unlocked_w) = cap.as_ref().map(|c| (c.mhs, c.watts)).unwrap_or((0.0, 0.0));
|
||||
let ran_clocks = rows.iter().any(|r| r.point.clock_mhz > 0);
|
||||
let note = if plan.limits.clock_max_mhz == 0 {
|
||||
"no lever".to_string()
|
||||
} else if !ran_clocks {
|
||||
String::new()
|
||||
} else {
|
||||
plan.clock_stop_reason(rows).unwrap_or_else(|| format!("stopped at {} MHz", rows.last().map(|r| r.point.clock_mhz).unwrap_or(0)))
|
||||
};
|
||||
LockResult { lock_mhz: chosen.point.clock_mhz, lock_mhs: chosen.mhs, lock_w: chosen.watts, lock_mhw: chosen.eff, unlocked_mhs, unlocked_w, lock_note: note }
|
||||
}
|
||||
|
||||
/// Why a card cannot be tuned beyond measuring, or None when both knobs are available.
|
||||
pub fn control_reason(vendor: &str, limits: &Limits, device: &str, power_control: bool, amd_helper: bool) -> Option<String> {
|
||||
match vendor {
|
||||
|
|
@ -754,7 +1046,7 @@ mod tests {
|
|||
/// PC 1's RTX 5090 (nvidia-smi, 4 and 5 October 2026): default 575 W, min 400 W, max 600 W; clocks.max.gr is
|
||||
/// read at the first tune (3,090 MHz is the shape used here, not a measurement).
|
||||
fn l5090() -> Limits {
|
||||
Limits { power_default_w: 575.0, power_min_w: 400.0, power_max_w: 600.0, clock_max_mhz: 3090, clock_min_mhz: 0 }
|
||||
Limits { power_default_w: 575.0, power_min_w: 400.0, power_max_w: 600.0, clock_max_mhz: 3090, clock_min_mhz: 0, mem_default_mhz: 13801, mem_max_mhz: 14001 }
|
||||
}
|
||||
|
||||
fn row_at(p: Point, watts: f64, mhs: f64) -> Row {
|
||||
|
|
@ -763,10 +1055,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn the_full_plan_is_the_power_ladder_then_the_clock_ladder_at_the_chosen_power() {
|
||||
let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 80 }, 1.0);
|
||||
assert_eq!(plan.len(), 5 + 4, "five power steps (60% and 50% clamp to 400 W; one kept) and four clock steps");
|
||||
let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 80, mem_mhz: 0 }, 1.0);
|
||||
// five power steps (60% and 50% clamp to 400 W; one kept), six percent rungs (90% down to 45% = 1,390) and the
|
||||
// fine ladder 1,300 down to the 20% floor (618): 1,300, 1,200, ..., 700 = 7 steps
|
||||
assert_eq!(plan.len(), 5 + 6 + 7);
|
||||
let first = plan.next(&[]).unwrap();
|
||||
assert_eq!((first.point, first.watts, first.kind), (Point { clock_mhz: 0, power_pct: 100 }, 575.0, Kind::Power));
|
||||
assert_eq!((first.point, first.watts, first.kind), (Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 575.0, Kind::Power));
|
||||
// the power ladder: 575, 518, 460, 403, 400
|
||||
let mut rows = Vec::new();
|
||||
let mut watts_seen = Vec::new();
|
||||
|
|
@ -781,35 +1075,57 @@ mod tests {
|
|||
// the clock ladder rides the chosen power point: a flat ladder ties on efficiency and rate, the lowest draw wins (100%)
|
||||
let s = plan.next(&rows).unwrap();
|
||||
assert_eq!(s.kind, Kind::Clock);
|
||||
assert_eq!(s.point, Point { clock_mhz: 2781, power_pct: 100 });
|
||||
assert_eq!(s.point, Point { clock_mhz: 2781, power_pct: 100, mem_mhz: 0 });
|
||||
rows.push(row_at(s.point, 250.0, 123.8));
|
||||
let s = plan.next(&rows).unwrap();
|
||||
assert_eq!(s.point.clock_mhz, 2472);
|
||||
rows.push(row_at(s.point, 220.0, 123.5));
|
||||
rows.push(row_at(plan.next(&rows).unwrap().point, 200.0, 118.0));
|
||||
let s = plan.next(&rows).unwrap();
|
||||
assert_eq!(s.point.clock_mhz, 1854, "60% of 3,090 is the floor");
|
||||
rows.push(row_at(s.point, 180.0, 100.0));
|
||||
assert_eq!(s.point.clock_mhz, 2163, "70%");
|
||||
rows.push(row_at(s.point, 200.0, 118.0));
|
||||
// the stop rule (7 October 2026): 118 is 4.8% under the cap point's 124, past the 1% tolerance, so the search
|
||||
// ends here (the 6 October ladder went on to 1,854, 1,545 and 1,390)
|
||||
assert_eq!(plan.next(&rows), None);
|
||||
assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("rate fell 4.8 percent at 2163 MHz"));
|
||||
// the choice: 2,472 MHz keeps 99.6% of the top rate at 220 W = 0.561 MH/W; 2,163 MHz (118 MH/s) is outside the 1% tolerance
|
||||
let best = choose(&rows, 1.0).unwrap();
|
||||
assert_eq!(best.point, Point { clock_mhz: 2472, power_pct: 100 });
|
||||
assert_eq!(best.point, Point { clock_mhz: 2472, power_pct: 100, mem_mhz: 0 });
|
||||
// a wider tolerance lets the 2,163 MHz step (0.590 MH/W, 4.8% slower) win
|
||||
assert_eq!(choose(&rows, 5.0).unwrap().point.clock_mhz, 2163);
|
||||
// no power limits, clocks only; no clocks, power only; nothing, empty
|
||||
assert_eq!(Plan::full(&Limits { clock_max_mhz: 2000, ..Default::default() }, Point::default(), 1.0).len(), 4);
|
||||
// clocks only: six percent rungs (1,800 .. 900) then the fine ladder 800 .. 400 (the 20% floor) = 5 more
|
||||
assert_eq!(Plan::full(&Limits { clock_max_mhz: 2000, ..Default::default() }, Point::default(), 1.0).len(), 6 + 5);
|
||||
assert_eq!(Plan::full(&Limits { power_default_w: 300.0, ..Default::default() }, Point::default(), 1.0).len(), 6);
|
||||
assert!(Plan::full(&Limits::default(), Point::default(), 1.0).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_power_step_on_a_card_without_a_limit_readback_is_applied_on_the_acknowledgement() {
|
||||
// AMD through ADLX: the limit reads back as an offset, never watts (run 6, 6 October 2026)
|
||||
let step = Step { point: Point { clock_mhz: 0, power_pct: 90, mem_mhz: 0 }, watts: 90.0, kind: Kind::Power };
|
||||
assert!(Run::applied(&step, Readback { limit_w: 0.0, acked: true }, 100.0));
|
||||
assert!(!Run::applied(&step, Readback { limit_w: 0.0, acked: false }, 100.0));
|
||||
// NVIDIA: the watts must read back
|
||||
assert!(!Run::applied(&step, Readback { limit_w: 100.0, acked: true }, 100.0));
|
||||
assert!(Run::applied(&step, Readback { limit_w: 90.0, acked: false }, 100.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baseline_result_reads_measured_and_a_tune_reads_tuned() {
|
||||
assert_eq!(result_line(PlanKind::Baseline, 114.2, 305.0, 0.3744), "Measured: 114.2 MH/s at 305 W (0.374 MH/W)");
|
||||
assert_eq!(result_line(PlanKind::Full, 127.71, 226.8, 0.5632), "Tuned: 127.7 MH/s at 227 W (0.563 MH/W)");
|
||||
assert_eq!(result_line(PlanKind::Confirm, 127.71, 226.8, 0.5632), "Tuned: 127.7 MH/s at 227 W (0.563 MH/W)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_never_exceed_the_vendor_or_undercut_the_floor() {
|
||||
let l = l5090();
|
||||
assert_eq!(l.clock_floor(), 1854);
|
||||
assert_eq!(l.clamp_clock(1000), 1854);
|
||||
assert_eq!(l.clock_floor(), 618, "20% of 3,090 (7 October 2026; the 45% floor of 6 October sat on the 5090's knee)");
|
||||
assert_eq!(l.clamp_clock(1000), 1000);
|
||||
assert_eq!(l.clamp_clock(500), 618);
|
||||
assert_eq!(l.clamp_clock(5000), 3090);
|
||||
assert_eq!(l.clamp_clock(0), 0, "unlocked stays unlocked");
|
||||
assert_eq!(Limits { clock_max_mhz: 3000, clock_min_mhz: 2100, ..Default::default() }.clamp_clock(1500), 2100, "the vendor's floor wins over the 60% rule");
|
||||
assert_eq!(Limits { clock_max_mhz: 3000, clock_min_mhz: 2100, ..Default::default() }.clamp_clock(1500), 2100, "the vendor's floor wins over the 20% rule");
|
||||
assert_eq!(l.watts_for(100), 575.0);
|
||||
assert_eq!(l.watts_for(50), 400.0);
|
||||
assert_eq!(Limits { power_default_w: 300.0, power_max_w: 250.0, ..Default::default() }.watts_for(100), 250.0);
|
||||
|
|
@ -817,7 +1133,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn the_choice_keeps_the_best_mh_per_watt_within_the_rate_tolerance() {
|
||||
let p = |c: u32, pct: u32| Point { clock_mhz: c, power_pct: pct };
|
||||
let p = |c: u32, pct: u32| Point { clock_mhz: c, power_pct: pct, mem_mhz: 0 };
|
||||
// a card that gets more efficient as the cap drops until it collapses: 70% wins (0.280 MH/W), 60% is 35% slower
|
||||
let rows = vec![row_at(p(0, 100), 560.0, 124.0), row_at(p(0, 90), 510.0, 123.5), row_at(p(0, 80), 455.0, 123.2), row_at(p(0, 70), 400.0, 122.9), row_at(p(0, 60), 345.0, 80.0)];
|
||||
assert_eq!(choose(&rows, 1.0).unwrap().point, p(0, 70));
|
||||
|
|
@ -844,7 +1160,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn the_guards_mark_a_step_so_it_cannot_win() {
|
||||
let step = Step { point: Point { clock_mhz: 2000, power_pct: 100 }, watts: 575.0, kind: Kind::Clock };
|
||||
let step = Step { point: Point { clock_mhz: 2000, power_pct: 100, mem_mhz: 0 }, watts: 575.0, kind: Kind::Clock };
|
||||
let good = Samples { draws: vec![250.0, 251.0, 249.0], rates: vec![123.0], gclks: vec![1998.0], mclks: vec![2505.0], tmax: 70.0, faults: 0, unapplied: false };
|
||||
assert_eq!(Row::from_samples(&step, &good, 2505.0).mark, Some(Mark::Ok));
|
||||
// one rejected or mismatched hash: faulted
|
||||
|
|
@ -862,15 +1178,15 @@ mod tests {
|
|||
assert!(!r.usable());
|
||||
assert!(r.line("c", 3).contains("mark=no_readings"), "{}", r.line("c", 3));
|
||||
let r = Row::from_samples(&step, &good, 0.0);
|
||||
assert_eq!(r.line("nvidia-ae432dc7-1", 7), "TUNE card=nvidia-ae432dc7-1 step=7 clock=2000 cap=100 limit=575 watts=250.0 mhs=123.00 eff=0.4920 gclk=1998 mclk=2505 tmax=70 mark=ok");
|
||||
assert_eq!(r.line("nvidia-ae432dc7-1", 7), "TUNE card=nvidia-ae432dc7-1 step=7 clock=2000 cap=100 mem=0 limit=575 watts=250.0 mhs=123.00 eff=0.4920 gclk=1998 mclk=2505 tmax=70 mark=ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fault_during_a_step_reverts_it_and_the_run_goes_on() {
|
||||
let t0 = Instant::now();
|
||||
let timing = Timing { settle: Duration::from_secs(2), hold: Duration::from_secs(4), apply: Duration::from_secs(30) };
|
||||
let limits = Limits { power_default_w: 300.0, power_min_w: 150.0, power_max_w: 300.0, clock_max_mhz: 0, clock_min_mhz: 0 };
|
||||
let plan = Plan::full(&limits, Point { clock_mhz: 0, power_pct: 80 }, 1.0);
|
||||
let limits = Limits { power_default_w: 300.0, power_min_w: 150.0, power_max_w: 300.0, clock_max_mhz: 0, clock_min_mhz: 0, mem_default_mhz: 0, mem_max_mhz: 0 };
|
||||
let plan = Plan::full(&limits, Point { clock_mhz: 0, power_pct: 80, mem_mhz: 0 }, 1.0);
|
||||
let mut run = Run::new(0, "0", "c", plan, 240.0, false, timing, t0);
|
||||
let mut t = t0;
|
||||
let mut limit = 240.0;
|
||||
|
|
@ -919,13 +1235,13 @@ mod tests {
|
|||
#[test]
|
||||
fn the_confirm_plan_checks_the_prior_and_its_neighbour() {
|
||||
let l = l5090();
|
||||
let prior = Point { clock_mhz: 2472, power_pct: 100 };
|
||||
let plan = Plan::confirm(&l, prior, Point { clock_mhz: 0, power_pct: 80 }, 1.0);
|
||||
let prior = Point { clock_mhz: 2472, power_pct: 100, mem_mhz: 0 };
|
||||
let plan = Plan::confirm(&l, prior, Point { clock_mhz: 0, power_pct: 80, mem_mhz: 0 }, 1.0);
|
||||
assert_eq!(plan.len(), 2);
|
||||
let a = plan.next(&[]).unwrap();
|
||||
assert_eq!((a.point, a.kind), (prior, Kind::Confirm));
|
||||
let b = plan.next(&[row_at(prior, 220.0, 123.5)]).unwrap();
|
||||
assert_eq!(b.point, Point { clock_mhz: 2781, power_pct: 100 }, "one clock step up");
|
||||
assert_eq!(b.point, Point { clock_mhz: 2781, power_pct: 100, mem_mhz: 0 }, "one clock step up");
|
||||
// the neighbour within 1%: the prior stands
|
||||
let rows = vec![row_at(prior, 220.0, 123.5), row_at(b.point, 250.0, 123.8)];
|
||||
assert!(matches!(confirm_verdict(&rows, 1.0), Verdict::Keep(r) if r.point == prior));
|
||||
|
|
@ -934,20 +1250,20 @@ mod tests {
|
|||
assert!(matches!(confirm_verdict(&rows, 1.0), Verdict::FullDue { .. }));
|
||||
assert_eq!(confirm_verdict(&[], 1.0), Verdict::NoReadings);
|
||||
// an unlocked prior at 100%: the neighbour is one power step down; at the top clock the neighbour is unlocked
|
||||
let plan = Plan::confirm(&l, Point { clock_mhz: 0, power_pct: 100 }, Point::default(), 1.0);
|
||||
assert_eq!(plan.next(&[row_at(Point { clock_mhz: 0, power_pct: 100 }, 1.0, 1.0)]).unwrap().point, Point { clock_mhz: 0, power_pct: 90 });
|
||||
let plan = Plan::confirm(&l, Point { clock_mhz: 2900, power_pct: 100 }, Point::default(), 1.0);
|
||||
assert_eq!(plan.next(&[row_at(Point { clock_mhz: 2900, power_pct: 100 }, 1.0, 1.0)]).unwrap().point.clock_mhz, 0);
|
||||
let plan = Plan::confirm(&l, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, Point::default(), 1.0);
|
||||
assert_eq!(plan.next(&[row_at(Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0, 1.0)]).unwrap().point, Point { clock_mhz: 0, power_pct: 90, mem_mhz: 0 });
|
||||
let plan = Plan::confirm(&l, Point { clock_mhz: 2900, power_pct: 100, mem_mhz: 0 }, Point::default(), 1.0);
|
||||
assert_eq!(plan.next(&[row_at(Point { clock_mhz: 2900, power_pct: 100, mem_mhz: 0 }, 1.0, 1.0)]).unwrap().point.clock_mhz, 0);
|
||||
// a prior outside the vendor's range is clamped, never applied as is
|
||||
let plan = Plan::confirm(&l, Point { clock_mhz: 9000, power_pct: 30 }, Point::default(), 1.0);
|
||||
assert_eq!(plan.next(&[]).unwrap().point, Point { clock_mhz: 3090, power_pct: 50 });
|
||||
let plan = Plan::confirm(&l, Point { clock_mhz: 9000, power_pct: 30, mem_mhz: 0 }, Point::default(), 1.0);
|
||||
assert_eq!(plan.next(&[]).unwrap().point, Point { clock_mhz: 3090, power_pct: 50, mem_mhz: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baseline_plan_measures_the_card_as_it_runs() {
|
||||
let t0 = Instant::now();
|
||||
let timing = Timing { settle: Duration::from_secs(1), hold: Duration::from_secs(2), apply: Duration::from_secs(3) };
|
||||
let before = Point { clock_mhz: 0, power_pct: 80 };
|
||||
let before = Point { clock_mhz: 0, power_pct: 80, mem_mhz: 0 };
|
||||
let mut run = Run::new(0, "0", "c", Plan::baseline(&l5090(), before, 1.0), 460.0, false, timing, t0);
|
||||
// nothing acknowledges the request (Power control is off): the apply window passes and the hold starts anyway
|
||||
let mut t = t0;
|
||||
|
|
@ -979,8 +1295,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn the_record_and_the_prior_round_trip_through_the_manifest_shape() {
|
||||
let chosen = row_at(Point { clock_mhz: 2472, power_pct: 100 }, 220.0, 123.5);
|
||||
let rec = record_json(1_791_230_000.0, "8f3a2c1d", "0.3.10", "windows", "NVIDIA GeForce RTX 5090", "nvidia", "581.57", "l128w16", PlanKind::Full, &[chosen.clone()], Some(&chosen), None);
|
||||
let chosen = row_at(Point { clock_mhz: 2472, power_pct: 100, mem_mhz: 0 }, 220.0, 123.5);
|
||||
let rec = record_json(1_791_230_000.0, "8f3a2c1d", "0.3.10", "windows", "NVIDIA GeForce RTX 5090", "nvidia", "581.57", "l128w16", PlanKind::Full, &[chosen.clone()], Some(&chosen), None, false);
|
||||
assert_eq!(rec["floor"], false);
|
||||
assert_eq!(rec["card"], "NVIDIA_GeForce_RTX_5090");
|
||||
assert_eq!(rec["key"], "NVIDIA_GeForce_RTX_5090|581|l128w16");
|
||||
assert_eq!(rec["driver_major"], "581");
|
||||
|
|
@ -999,7 +1316,7 @@ mod tests {
|
|||
let s = settings_of(Some(&tuning));
|
||||
assert_eq!(s, Settings { enabled: true, min_samples: 5, tolerance_pct: 1.0, period_s: PERIOD_S });
|
||||
let p = prior_of(Some(&tuning), "NVIDIA_GeForce_RTX_5090|581|l128w16", s.min_samples).unwrap();
|
||||
assert_eq!(p.point, Point { clock_mhz: 2472, power_pct: 100 });
|
||||
assert_eq!(p.point, Point { clock_mhz: 2472, power_pct: 100, mem_mhz: 0 });
|
||||
assert_eq!(p.samples, 7);
|
||||
assert!(prior_of(Some(&tuning), "AMD_Radeon_RX_9070_XT|32|l128w16", 5).is_none(), "3 samples are under the floor");
|
||||
assert!(prior_of(Some(&tuning), "AMD_Radeon_RX_9070_XT|32|l128w16", 3).is_some());
|
||||
|
|
@ -1016,6 +1333,84 @@ mod tests {
|
|||
assert_eq!(program_class(0, 0), "v2");
|
||||
}
|
||||
|
||||
/// Ember 2: a synthetic memory-latency-bound card. Rate rises 1% per 100 MHz of memory above the default and
|
||||
/// falls only 0.3% per 100 MHz of core below the maximum; draw falls 8 W per 100 MHz of core and rises 2 W per
|
||||
/// 100 MHz of memory. The climb must walk memory up and core down and converge in under five probes.
|
||||
fn synthetic(p: Point) -> Row {
|
||||
let mem = if p.mem_mhz == 0 { 13801.0 } else { p.mem_mhz as f64 };
|
||||
let core = if p.clock_mhz == 0 { 3090.0 } else { p.clock_mhz as f64 };
|
||||
let mhs = 127.0 * (1.0 + 0.015 * (mem - 13801.0) / 100.0) * (1.0 - 0.003 * (3090.0 - core) / 100.0);
|
||||
let watts = 310.0 - 8.0 * (3090.0 - core) / 100.0 + 2.0 * (mem - 13801.0) / 100.0;
|
||||
Row { point: p, limit: 575.0, watts, mhs, eff: mhs / watts, draws: 12, rates: 6, gclk: core, mclk: mem, tmax: 65.0, faults: 0, mark: Some(Mark::Ok) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_climb_walks_memory_up_and_core_down_and_converges_in_five_probes() {
|
||||
// a 2 GHz memory range above the default (the headroom a 5090 has in practice; PC 1's driver reports 14,001)
|
||||
let l = Limits { mem_max_mhz: 15801, ..l5090() };
|
||||
let plan = Plan::climb(&l, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, Goal::Balanced, 1.0);
|
||||
assert_eq!(plan.len(), 5);
|
||||
let mut rows = Vec::new();
|
||||
while let Some(step) = plan.next(&rows) {
|
||||
assert_eq!(step.kind, Kind::Climb);
|
||||
rows.push(synthetic(step.point));
|
||||
}
|
||||
assert_eq!(rows.len(), 5, "the budget");
|
||||
assert_eq!(rows[0].point, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, "it starts at the start point");
|
||||
// every probe moved one knob the right way: memory never down, core never up, and both inside the vendor's range
|
||||
for w in rows.windows(2) {
|
||||
let (a, b) = (w[0].point, w[1].point);
|
||||
assert!(b.mem_mhz >= a.mem_mhz || b.mem_mhz == 0, "{a:?} -> {b:?}");
|
||||
assert!(b.mem_mhz <= 15801 && (b.clock_mhz == 0 || b.clock_mhz >= 1854), "{b:?}");
|
||||
}
|
||||
let best = choose(&rows, plan.tolerance_pct).unwrap();
|
||||
assert!(best.eff > rows[0].eff, "the chosen point beats the start: {:.4} > {:.4}", best.eff, rows[0].eff);
|
||||
assert!(best.point.mem_mhz > 13801 || (best.point.clock_mhz > 0 && best.point.clock_mhz < 3090), "it moved a knob");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_refused_probe_backs_that_knob_off_for_good() {
|
||||
let l = Limits { mem_max_mhz: 15801, ..l5090() };
|
||||
let plan = Plan::climb(&l, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, Goal::Balanced, 1.0);
|
||||
let mut rows = vec![synthetic(plan.next(&[]).unwrap().point)];
|
||||
// the first probe (memory up) draws a mismatched hash: refused
|
||||
let probe = plan.next(&rows).unwrap();
|
||||
assert!(probe.point.mem_mhz > 13801, "memory first: {:?}", probe.point);
|
||||
let mut bad = synthetic(probe.point);
|
||||
bad.faults = 1;
|
||||
bad.mark = Some(Mark::Faulted);
|
||||
rows.push(bad);
|
||||
// from here every probe leaves the memory clock alone
|
||||
while let Some(step) = plan.next(&rows) {
|
||||
assert_eq!(step.point.mem_mhz, 0, "memory backed off: {:?}", step.point);
|
||||
rows.push(synthetic(step.point));
|
||||
}
|
||||
assert!(rows.len() >= 3 && rows.len() <= 5);
|
||||
assert!(choose(&rows, 1.0).unwrap().point.mem_mhz == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_goal_picks_its_point() {
|
||||
let l = l5090();
|
||||
let p = |c: u32, m: u32| Point { clock_mhz: c, power_pct: 100, mem_mhz: m };
|
||||
// three points: the fastest (slightly less efficient), the most efficient (4% slower), and the start
|
||||
let rows = vec![synthetic(p(0, 0)), synthetic(p(0, 14001)), synthetic(p(1854, 0))];
|
||||
let eff_plan = Plan::climb(&l, p(0, 0), Goal::Efficiency, 1.0);
|
||||
let bal_plan = Plan::climb(&l, p(0, 0), Goal::Balanced, 1.0);
|
||||
let rate_plan = Plan::climb(&l, p(0, 0), Goal::MaxRate, 1.0);
|
||||
assert_eq!(eff_plan.tolerance_pct, 10.0);
|
||||
assert_eq!(bal_plan.tolerance_pct, 1.0);
|
||||
assert_eq!(rate_plan.tolerance_pct, 0.0);
|
||||
let by = |plan: &Plan| rows.iter().filter(|r| r.usable() && r.mhs >= rows.iter().map(|x| x.mhs).fold(0.0, f64::max) * (1.0 - plan.tolerance_pct / 100.0)).max_by(|a, b| plan.score(a).partial_cmp(&plan.score(b)).unwrap()).unwrap().point;
|
||||
assert_eq!(by(&rate_plan), p(0, 14001), "maximum rate takes the fastest point");
|
||||
assert_eq!(by(&eff_plan), p(1854, 0), "efficiency takes the most MH/W inside 10%");
|
||||
assert_eq!(by(&bal_plan), p(0, 14001), "balanced keeps within 1% of the top rate");
|
||||
assert_eq!(Goal::parse("efficiency"), Goal::Efficiency);
|
||||
assert_eq!(Goal::parse("rate"), Goal::MaxRate);
|
||||
assert_eq!(Goal::parse("anything"), Goal::Balanced);
|
||||
assert!((pounds_per_day(310.0, 28.5) - 2.1204).abs() < 1e-3, "310 W a day at 28.5 p/kWh = £2.12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_reasons_per_vendor() {
|
||||
let l = l5090();
|
||||
|
|
@ -1030,4 +1425,138 @@ mod tests {
|
|||
assert!(control_reason("amd", &Limits::default(), "1", false, true).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// The core-clock knob (7 October 2026, the PC 1 efficiency passes): a flat ladder walks below the old 45 percent
|
||||
/// floor in 100 MHz steps to the 20 percent floor, and the result names the floor.
|
||||
#[test]
|
||||
fn the_clock_ladder_continues_below_45_percent_in_100_mhz_steps_to_the_floor() {
|
||||
let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0);
|
||||
let mut rows = Vec::new();
|
||||
for _ in 0..5 {
|
||||
let s = plan.next(&rows).unwrap();
|
||||
rows.push(row_at(s.point, 300.0, 136.8));
|
||||
}
|
||||
let mut clocks = Vec::new();
|
||||
while let Some(s) = plan.next(&rows) {
|
||||
assert_eq!(s.kind, Kind::Clock);
|
||||
clocks.push(s.point.clock_mhz);
|
||||
// the rate holds (memory-bound): the draw falls with the clock
|
||||
rows.push(row_at(s.point, 300.0 - clocks.len() as f64 * 10.0, 136.0));
|
||||
}
|
||||
assert_eq!(clocks, vec![2781, 2472, 2163, 1854, 1545, 1390, 1300, 1200, 1100, 1000, 900, 800, 700]);
|
||||
assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("the floor at 700 MHz"));
|
||||
let chosen = choose(&rows, 1.0).unwrap();
|
||||
assert_eq!(chosen.point.clock_mhz, 700, "flat rate: the lowest draw wins");
|
||||
let r = lock_result(&plan, &rows, &chosen);
|
||||
assert_eq!((r.lock_mhz, r.lock_w, r.unlocked_mhs, r.unlocked_w), (700, 170.0, 136.8, 300.0));
|
||||
assert_eq!(r.lock_note, "the floor at 700 MHz");
|
||||
}
|
||||
|
||||
/// The stop rule on PC 1's RTX 5090 rows of 7 October 2026 (class v3, the card alone): the first clock row more
|
||||
/// than the tolerance under the cap point's rate ends the search and the best MH per watt among the rows within
|
||||
/// tolerance is chosen. At the 1 percent tolerance the 5090's rate (136.6 at 2,781) is 1.24 percent down at
|
||||
/// 1,545 MHz, so the search ends there and 1,854 MHz (135.6 MH/s at 239.6 W) is the point; at 1.5 percent it runs
|
||||
/// on to 1,200 (5.2 percent down) and 1,300 MHz is the point. The tolerance is the manifest's.
|
||||
#[test]
|
||||
fn the_clock_search_stops_at_the_knee_and_names_it() {
|
||||
let measured: Vec<(u32, f64, f64)> = vec![(2781, 317.9, 136.6), (2472, 276.0, 136.4), (2163, 252.4, 136.1), (1854, 239.6, 135.6), (1545, 232.1, 134.9), (1390, 229.0, 134.85), (1300, 223.3, 134.6), (1200, 215.7, 129.5)];
|
||||
let walk = |tolerance: f64| -> (Plan, Vec<Row>) {
|
||||
let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, tolerance);
|
||||
let mut rows = vec![];
|
||||
for (mhz, w, mhs) in &measured {
|
||||
let Some(s) = plan.next(&rows) else { break };
|
||||
assert_eq!(s.point.clock_mhz, *mhz);
|
||||
rows.push(row_at(s.point, *w, *mhs));
|
||||
}
|
||||
(plan, rows)
|
||||
};
|
||||
let (plan, rows) = walk(1.0);
|
||||
assert_eq!(rows.last().unwrap().point.clock_mhz, 1545, "the search ends on the first row over 1 percent under the cap row");
|
||||
assert_eq!(plan.next(&rows), None);
|
||||
let reason = plan.clock_stop_reason(&rows).unwrap();
|
||||
assert!(reason.starts_with("rate fell 1.2 percent at 1545 MHz"), "{reason}");
|
||||
let chosen = choose(&rows, 1.0).unwrap();
|
||||
assert_eq!(chosen.point.clock_mhz, 1854, "the best MH per watt within 1 percent of the fastest row");
|
||||
let r = lock_result(&plan, &rows, &chosen);
|
||||
assert_eq!((r.lock_mhz, r.unlocked_mhs), (1854, 136.6));
|
||||
assert!((r.lock_mhw - 135.6 / 239.6).abs() < 1e-6);
|
||||
let (plan, rows) = walk(1.5);
|
||||
assert_eq!(rows.last().unwrap().point.clock_mhz, 1200);
|
||||
assert_eq!(plan.next(&rows), None);
|
||||
assert!(plan.clock_stop_reason(&rows).unwrap().starts_with("rate fell 5.2 percent at 1200 MHz"));
|
||||
assert_eq!(choose(&rows, 1.5).unwrap().point.clock_mhz, 1300);
|
||||
}
|
||||
|
||||
/// The fingerprint rule: a clock row marked Faulted (a rejected or mismatched hash during the hold) ends the search
|
||||
/// at once; the choice is made among the usable rows and the note says why.
|
||||
#[test]
|
||||
fn a_faulted_clock_row_ends_the_search_and_the_note_says_so() {
|
||||
let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0);
|
||||
let mut rows = vec![];
|
||||
for (mhz, w, mhs) in [(2781, 317.9, 136.6), (2472, 276.0, 136.4)] {
|
||||
let s = plan.next(&rows).unwrap();
|
||||
assert_eq!(s.point.clock_mhz, mhz);
|
||||
rows.push(row_at(s.point, w, mhs));
|
||||
}
|
||||
let s = plan.next(&rows).unwrap();
|
||||
assert_eq!(s.point.clock_mhz, 2163);
|
||||
let mut bad = row_at(s.point, 252.4, 136.1);
|
||||
bad.faults = 1;
|
||||
bad.mark = Some(Mark::Faulted);
|
||||
rows.push(bad);
|
||||
assert_eq!(plan.next(&rows), None, "the search ends on the faulted row");
|
||||
assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("fingerprint mismatch at 2163 MHz, clocks reset"));
|
||||
let chosen = choose(&rows, 1.0).unwrap();
|
||||
assert_eq!(chosen.point.clock_mhz, 2472, "the faulted row never wins");
|
||||
assert_eq!(lock_result(&plan, &rows, &chosen).lock_note, "fingerprint mismatch at 2163 MHz, clocks reset");
|
||||
}
|
||||
|
||||
/// The same through the state machine with a fake helper (the known-failed case first: a mismatch mid-search must
|
||||
/// reset and abort): the run applies 2,781 and 2,472, a fault lands during 2,163's hold, the row comes out Faulted,
|
||||
/// the next step is none, and the run's final Apply is the chosen 2,472 point (the reset), then Finished.
|
||||
#[test]
|
||||
fn a_mismatch_mid_search_resets_to_the_chosen_point_and_finishes() {
|
||||
let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0);
|
||||
let timing = Timing { settle: Duration::from_secs(1), hold: Duration::from_secs(2), apply: Duration::from_secs(3) };
|
||||
let t0 = Instant::now();
|
||||
let mut run = Run::new(0, "0", "card-0", plan, 300.0, false, timing, t0);
|
||||
let mut t = t0;
|
||||
let mut applied: Vec<Step> = Vec::new();
|
||||
let mut finished: Option<Row> = None;
|
||||
let watts_for = |mhz: u32| -> f64 { match mhz { 2781 => 317.9, 2472 => 276.0, _ => 252.4 } };
|
||||
for _ in 0..200 {
|
||||
t += Duration::from_millis(500);
|
||||
let acked = true;
|
||||
let limit = run.current.as_ref().map(|s| s.watts).unwrap_or(0.0);
|
||||
// the fake helper: every setting takes; during 2,163's hold the worker reports a mismatched hash
|
||||
if let Some(cur) = run.current.clone() {
|
||||
if matches!(run.phase, Phase::Holding { .. }) {
|
||||
run.sample_rate(136.4);
|
||||
run.sample_telemetry(watts_for(cur.point.clock_mhz), cur.point.clock_mhz as f64, 13801.0, 60.0);
|
||||
if cur.point.clock_mhz == 2163 {
|
||||
run.sample_fault();
|
||||
}
|
||||
}
|
||||
}
|
||||
for o in run.tick(t, Readback { limit_w: limit, acked }) {
|
||||
match o {
|
||||
Out::Apply(s) => applied.push(s),
|
||||
Out::Finished(r) => finished = Some(r),
|
||||
Out::Failed(e) => panic!("the run failed: {e}"),
|
||||
Out::Row(_) => {}
|
||||
}
|
||||
}
|
||||
if finished.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let clocks: Vec<u32> = applied.iter().map(|s| s.point.clock_mhz).collect();
|
||||
assert_eq!(clocks, vec![2781, 2472, 2163, 2472], "2,781, 2,472, the faulted 2,163, then the reset to the chosen 2,472");
|
||||
assert_eq!(applied.last().unwrap().kind, Kind::Confirm);
|
||||
let f = finished.expect("finished");
|
||||
assert_eq!(f.point.clock_mhz, 2472);
|
||||
assert_eq!(run.rows.len(), 3);
|
||||
assert_eq!(run.rows[2].mark, Some(Mark::Faulted));
|
||||
assert_eq!(run.plan.clock_stop_reason(&run.rows).as_deref(), Some("fingerprint mismatch at 2163 MHz, clocks reset"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use std::time::{Duration, Instant};
|
|||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const POW_EPOCH_BLOCKS: u64 = 3_600;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CardChoice {
|
||||
pub key: String,
|
||||
pub enabled: bool,
|
||||
|
|
@ -73,6 +73,10 @@ pub enum Cmd {
|
|||
SweepHelperDone(Result<(), String>),
|
||||
/// a tune request (by number) was carried out: what the vendor tool printed, or why it refused
|
||||
TuneSet(u64, Result<String, String>),
|
||||
/// POST /api/tune-progress from a measurement engine beside this app: the card's live tune state
|
||||
TuneProgress(Value),
|
||||
/// Ember 2: Settings > goal, electricity price, the hill-climb switch
|
||||
TuneGoal(Option<String>, Option<f64>, Option<bool>),
|
||||
/// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the
|
||||
/// prover found a host that was not there when the node started
|
||||
RestartNode(String),
|
||||
|
|
@ -119,7 +123,7 @@ impl Shared {
|
|||
st.mining.accepted_total = settings.accepted_total;
|
||||
st.mining.fee_total = settings.fee_total;
|
||||
st.address = address_state(&settings, &wallet_path);
|
||||
st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust };
|
||||
st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), tune_goal: settings.tune_goal.clone(), power_price_pence: settings.power_price_pence, tune_climb: settings.tune_climb, tune_period_s: crate::ember::PERIOD_S, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust };
|
||||
st.dev_fee = crate::state::DevFeeState { on: settings.dev_fee, percent: if settings.dev_fee { 1 } else { 0 }, address: String::new(), line: String::new() };
|
||||
st.live_page = packaged.live_page.clone();
|
||||
st.finality.message = "waiting for the miner".into();
|
||||
|
|
@ -1142,6 +1146,29 @@ impl Engine {
|
|||
self.shared.log("sweep helper: exited");
|
||||
}
|
||||
}
|
||||
Cmd::TuneProgress(v) => self.tune_progress(&v),
|
||||
Cmd::TuneGoal(goal, price, climb) => {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
if let Some(g) = &goal {
|
||||
s.tune_goal = g.clone();
|
||||
}
|
||||
if let Some(p) = price {
|
||||
s.power_price_pence = p;
|
||||
}
|
||||
if let Some(c) = climb {
|
||||
s.tune_climb = c;
|
||||
}
|
||||
}
|
||||
self.shared.save_settings();
|
||||
let s = self.shared.settings.lock().unwrap().clone();
|
||||
let mut st = self.st();
|
||||
st.settings.tune_goal = s.tune_goal.clone();
|
||||
st.settings.power_price_pence = s.power_price_pence;
|
||||
st.settings.tune_climb = s.tune_climb;
|
||||
drop(st);
|
||||
self.shared.event("info", &format!("tune goal: {}{}{}", s.tune_goal, if s.power_price_pence > 0.0 { format!(", electricity {:.1} p/kWh", s.power_price_pence) } else { String::new() }, if s.tune_climb { ", hill-climb on" } else { "" }));
|
||||
}
|
||||
Cmd::TuneSet(seq, r) => match r {
|
||||
Ok(text) => {
|
||||
self.tune_acked = Some(seq);
|
||||
|
|
@ -1790,7 +1817,7 @@ impl Engine {
|
|||
// once, ever (src/powertask.rs): a registered task sets the caps with no prompt; the readback judges it
|
||||
if cfg!(windows) && self.power_task_registered() {
|
||||
let pairs: Vec<(String, u64)> = self.st().mining.cards.iter().filter(|c| c.vendor == "nvidia" && c.enabled && c.present() && c.power_default_w > 0.0 && !c.power_applied).map(|c| (c.device.clone(), requested_watts(c).round() as u64)).collect();
|
||||
let dir = self.sweep_dir();
|
||||
let dir = crate::powertask::helper_dir();
|
||||
let shared = self.shared.clone();
|
||||
self.shared.log("power cap: through the Igneum Power Helper task (no prompt)");
|
||||
std::thread::spawn(move || {
|
||||
|
|
@ -1818,7 +1845,8 @@ impl Engine {
|
|||
let dir = self.sweep_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let script = dir.join("register-power-task.ps1");
|
||||
match std::env::current_exe().map(|exe| std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), crate::powertask::register_script(&exe).as_bytes()].concat())) {
|
||||
let installed: Vec<PathBuf> = crate::powertask::install_candidates();
|
||||
match std::env::current_exe().map(|exe| std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), crate::powertask::register_script(&crate::powertask::task_exe(&exe, &installed)).as_bytes()].concat())) {
|
||||
Ok(Ok(())) => format!("{} & \"{}\" -NoProfile -ExecutionPolicy Bypass -File \"{}\"", cmds.join(" & "), crate::platform::tool("powershell").display(), script.display()),
|
||||
_ => cmds.join(" & "),
|
||||
}
|
||||
|
|
@ -1908,8 +1936,9 @@ impl Engine {
|
|||
}
|
||||
if cfg!(windows) && self.power_task_registered() {
|
||||
// the kill switch: the task unregisters itself (elevated) and exits; nothing is left behind
|
||||
let dir = self.sweep_dir();
|
||||
let _ = std::fs::write(dir.join("cmd.txt"), "remove\n");
|
||||
let hdir = crate::powertask::helper_dir();
|
||||
let _ = std::fs::create_dir_all(&hdir);
|
||||
let _ = std::fs::write(hdir.join("cmd.txt"), "remove\n");
|
||||
match crate::powertask::start() {
|
||||
Ok(()) => self.shared.log("power control off: the Igneum Power Helper task removes itself"),
|
||||
Err(e) => self.shared.log(&format!("power control off: the task could not be started to remove itself ({e}); remove it in Task Scheduler")),
|
||||
|
|
@ -2114,6 +2143,16 @@ impl Engine {
|
|||
self.shared.runtime.app_dir.join("sweep")
|
||||
}
|
||||
|
||||
/// Where this engine's helper reads its commands: the task's own folder when the task is the helper, else this
|
||||
/// engine's sweep folder (the prompted helper script was handed that path on its command line).
|
||||
fn helper_cmd_dir(&self) -> PathBuf {
|
||||
if self.sweep_helper_is_task {
|
||||
crate::powertask::helper_dir()
|
||||
} else {
|
||||
self.sweep_dir()
|
||||
}
|
||||
}
|
||||
|
||||
/// The manifest's tuning object (<app data>/tuning.json, written by the updater from the signed manifest), or
|
||||
/// None: the Ember settings (kill switch, thresholds) and the fleet priors live under it.
|
||||
fn tuning_object(&self) -> Option<Value> {
|
||||
|
|
@ -2148,6 +2187,7 @@ impl Engine {
|
|||
let mut st = self.st();
|
||||
st.settings.tuning_off = !ember.enabled;
|
||||
st.settings.tuning_note = if ember.enabled { String::new() } else { "tuning paused fleet-wide by the signed manifest".into() };
|
||||
st.settings.tune_period_s = ember.period_s;
|
||||
}
|
||||
if paused || !ember.enabled {
|
||||
return;
|
||||
|
|
@ -2235,16 +2275,19 @@ impl Engine {
|
|||
let current = if c.power_limit_w > 0.0 { c.power_limit_w } else { c.power_default_w }.round() as u64;
|
||||
let known_direct = self.sweep_direct;
|
||||
std::thread::spawn(move || {
|
||||
let q = crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "--query-gpu=clocks.max.gr,driver_version", "--format=csv,noheader,nounits"]), None, Duration::from_secs(10)).unwrap_or_default();
|
||||
let q = crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "--query-gpu=clocks.max.gr,driver_version,clocks.mem,clocks.max.mem", "--format=csv,noheader,nounits"]), None, Duration::from_secs(10)).unwrap_or_default();
|
||||
let p: Vec<&str> = q.trim().split(',').map(|s| s.trim()).collect();
|
||||
let clock_max = p.first().and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0) as u32;
|
||||
let driver = p.get(1).map(|s| s.to_string()).unwrap_or_default();
|
||||
// Ember 2: the memory clock now (the default under load) and the vendor's maximum
|
||||
let mem_default = p.get(2).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0) as u32;
|
||||
let mem_max = p.get(3).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0) as u32;
|
||||
let direct = match known_direct {
|
||||
Some(d) => Some(d),
|
||||
None if allowed || current > 0 => crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-pl", ¤t.to_string()]), None, Duration::from_secs(20)).map(|out| out.contains("All done")),
|
||||
None => Some(false),
|
||||
};
|
||||
shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: clock_max, clock_min_mhz: 0, driver, direct: direct.unwrap_or(false), amd_ordinal: -1, ..Default::default() })));
|
||||
shared.send(Cmd::TuneProbe(idx, Ok(TuneProbe { clock_max_mhz: clock_max, clock_min_mhz: 0, driver, direct: direct.unwrap_or(false), amd_ordinal: -1, mem_default_mhz: mem_default, mem_max_mhz: mem_max, ..Default::default() })));
|
||||
});
|
||||
}
|
||||
"amd" => {
|
||||
|
|
@ -2263,7 +2306,7 @@ impl Engine {
|
|||
// the stock clock, not MHz; a range with a negative floor is an offset range and the clock
|
||||
// knob stays closed until the stock clock is known (the power limit is the AMD lever), and
|
||||
// `plimit_range -30 10` bounds the power ladder (the percent scale rides power_* below)
|
||||
Some(t) if t.ok => TuneProbe { clock_max_mhz: if t.gmax_min >= 0.0 && t.gmax_max > 0.0 { t.gmax_max as u32 } else { 0 }, clock_min_mhz: if t.gmax_min > 0.0 { t.gmax_min as u32 } else { 0 }, driver, direct: true, amd_ordinal: t.ordinal as i64, plimit_min: t.plimit_min, plimit_max: t.plimit_max },
|
||||
Some(t) if t.ok => TuneProbe { clock_max_mhz: if t.gmax_min >= 0.0 && t.gmax_max > 0.0 { t.gmax_max as u32 } else { 0 }, clock_min_mhz: if t.gmax_min > 0.0 { t.gmax_min as u32 } else { 0 }, driver, direct: true, amd_ordinal: t.ordinal as i64, plimit_min: t.plimit_min, plimit_max: t.plimit_max, mem_default_mhz: 0, mem_max_mhz: 0 },
|
||||
Some(t) => TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver, direct: false, amd_ordinal: t.ordinal as i64, ..Default::default() },
|
||||
None => TuneProbe { clock_max_mhz: 0, clock_min_mhz: 0, driver, direct: false, amd_ordinal: -1, ..Default::default() },
|
||||
})));
|
||||
|
|
@ -2295,13 +2338,15 @@ impl Engine {
|
|||
};
|
||||
// NVIDIA control: this process is elevated (direct), or Power control is on so the one-prompt helper may run.
|
||||
// The --sweep job alone never counts: it must not raise a prompt on a PC with nobody there (5 October 2026).
|
||||
let power_control = probe.direct || self.shared.settings.lock().unwrap().power_control;
|
||||
// a registered Igneum Power Helper task (src/powertask.rs) is permission too: it sets limits with no prompt,
|
||||
// so an unelevated measurement engine (Power control forced off) still controls NVIDIA through it
|
||||
let power_control = probe.direct || self.shared.settings.lock().unwrap().power_control || (cfg!(windows) && self.power_task_registered());
|
||||
// AMD's power limit is a percent offset from the default (ADLX): the plan's watts scale becomes a percent
|
||||
// scale (default 100, floor 100 + plimit_min, ceiling 100 + plimit_max), tune_apply sends pct - 100
|
||||
let limits = if c.vendor == "amd" && probe.direct && probe.plimit_max >= probe.plimit_min && probe.plimit_min > -100.0 {
|
||||
crate::ember::Limits { power_default_w: 100.0, power_min_w: 100.0 + probe.plimit_min, power_max_w: 100.0 + probe.plimit_max, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz }
|
||||
crate::ember::Limits { power_default_w: 100.0, power_min_w: 100.0 + probe.plimit_min, power_max_w: 100.0 + probe.plimit_max, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz, mem_default_mhz: 0, mem_max_mhz: 0 }
|
||||
} else {
|
||||
crate::ember::Limits { power_default_w: c.power_default_w, power_min_w: c.power_min_w, power_max_w: c.power_max_w, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz }
|
||||
crate::ember::Limits { power_default_w: c.power_default_w, power_min_w: c.power_min_w, power_max_w: c.power_max_w, clock_max_mhz: probe.clock_max_mhz, clock_min_mhz: probe.clock_min_mhz, mem_default_mhz: probe.mem_default_mhz, mem_max_mhz: probe.mem_max_mhz }
|
||||
};
|
||||
let control = match c.vendor.as_str() {
|
||||
"nvidia" => crate::ember::control_reason("nvidia", &limits, &c.device, power_control, false),
|
||||
|
|
@ -2310,6 +2355,26 @@ impl Engine {
|
|||
};
|
||||
if c.vendor == "nvidia" {
|
||||
self.sweep_direct = Some(probe.direct);
|
||||
// the approved step that made this engine elevated registers the Igneum Power Helper here too (run 5,
|
||||
// 6 October 2026: a --sweep engine skipped the cap path where the registration lived, so the one
|
||||
// approved click registered nothing); no prompt: this process already holds the rights
|
||||
if probe.direct && cfg!(windows) && !self.power_task_registered() {
|
||||
let dir = self.sweep_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let script = dir.join("register-power-task.ps1");
|
||||
let installed: Vec<PathBuf> = crate::powertask::install_candidates();
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
let target = crate::powertask::task_exe(&exe, &installed);
|
||||
if std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), crate::powertask::register_script(&target).as_bytes()].concat()).is_ok() {
|
||||
let mut p = std::process::Command::new(crate::platform::tool("powershell"));
|
||||
p.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
||||
crate::platform::quiet(&mut p);
|
||||
let ok = p.status().map(|s| s.success()).unwrap_or(false);
|
||||
self.power_task = None;
|
||||
self.sweep_say(&format!("TUNE helper registered={} action={}", ok, target.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut st = self.st();
|
||||
|
|
@ -2327,16 +2392,24 @@ impl Engine {
|
|||
}
|
||||
let tuning = self.tuning_object();
|
||||
let ember = crate::ember::settings_of(tuning.as_ref());
|
||||
let before = crate::ember::Point { clock_mhz: c.clock_cap_mhz, power_pct: if c.power_pct == 0 { 80 } else { c.power_pct } };
|
||||
let before = crate::ember::Point { clock_mhz: c.clock_cap_mhz, power_pct: if c.power_pct == 0 { 80 } else { c.power_pct }, mem_mhz: c.mem_cap_mhz };
|
||||
let before_w = if c.power_limit_w > 0.0 { c.power_limit_w } else { requested_watts(&c) };
|
||||
let key = crate::ember::prior_key(&c.name, &probe.driver, &c.program_class);
|
||||
let prior = crate::ember::prior_of(tuning.as_ref(), &key, ember.min_samples);
|
||||
let full_due = self.tune_full_due.remove(&idx);
|
||||
let (goal, climb_on) = {
|
||||
let s = self.shared.settings.lock().unwrap();
|
||||
(crate::ember::Goal::parse(&s.tune_goal), s.tune_climb)
|
||||
};
|
||||
let plan = if let Some(why) = control.as_ref() {
|
||||
if let Some(cc) = self.st().mining.cards.get_mut(idx) {
|
||||
cc.sweep_note = why.clone();
|
||||
}
|
||||
crate::ember::Plan::baseline(&limits, before, ember.tolerance_pct)
|
||||
} else if climb_on || prior.as_ref().map(|p| p.point.mem_mhz > 0).unwrap_or(false) {
|
||||
// Ember 2: the hill-climb from the fleet prior (or the card's point) toward the goal
|
||||
let start = prior.as_ref().map(|p| p.point).unwrap_or(before);
|
||||
crate::ember::Plan::climb(&limits, start, goal, ember.tolerance_pct)
|
||||
} else if let (Some(p), false, 0) = (prior.as_ref(), full_due, c.sweep_pct) {
|
||||
crate::ember::Plan::confirm(&limits, p.point, before, ember.tolerance_pct)
|
||||
} else {
|
||||
|
|
@ -2380,6 +2453,7 @@ impl Engine {
|
|||
crate::ember::PlanKind::Full => format!("{steps} steps over the power limit and the core clock, {} s each on the live program", (run.timing.settle + run.timing.hold).as_secs()),
|
||||
crate::ember::PlanKind::Confirm => format!("the fleet prior ({} samples) and one neighbour, {} s each", prior.as_ref().map(|p| p.samples).unwrap_or(0), (run.timing.settle + run.timing.hold).as_secs()),
|
||||
crate::ember::PlanKind::Baseline => format!("measuring the card as it runs ({})", control.clone().unwrap_or_default()),
|
||||
crate::ember::PlanKind::Climb => format!("the hill-climb: memory up, core down, up to {steps} probes of {} s", (run.timing.settle + run.timing.hold).as_secs()),
|
||||
};
|
||||
self.shared.event("info", &format!("{}: tuning started: {what}", c.name));
|
||||
self.sweep = Some(run);
|
||||
|
|
@ -2388,7 +2462,8 @@ impl Engine {
|
|||
|
||||
/// The elevated helper (src/sweep.rs helper_script_*): one administrator prompt; it polls <app>/sweep/cmd.txt.
|
||||
fn sweep_helper_start(&mut self, c: &CardState) -> Result<(), String> {
|
||||
if !self.shared.settings.lock().unwrap().power_control {
|
||||
let task = cfg!(windows) && self.power_task_registered();
|
||||
if !task && !self.shared.settings.lock().unwrap().power_control {
|
||||
return Err("Power control is off in Settings".into());
|
||||
}
|
||||
let dir = self.sweep_dir();
|
||||
|
|
@ -2406,8 +2481,11 @@ impl Engine {
|
|||
format!("sh \"{}\" \"{}\" \"{}\" {} {}", script.display(), dir.display(), smi, c.device, restore)
|
||||
};
|
||||
if cfg!(windows) && self.power_task_registered() {
|
||||
// once, ever: the registered task is the helper; it reads the same command file, no prompt
|
||||
let _ = std::fs::write(dir.join("cmd.txt"), format!("{} dev {}\n", crate::platform::unix_now() % 1_000_000, c.device));
|
||||
// once, ever: the registered task is the helper; it reads ITS command file (powertask::helper_dir, never
|
||||
// under a scratch IGNEUM_APP_DATA), no prompt
|
||||
let hdir = crate::powertask::helper_dir();
|
||||
let _ = std::fs::create_dir_all(&hdir);
|
||||
let _ = std::fs::write(hdir.join("cmd.txt"), format!("{} dev {}\n", crate::platform::unix_now() % 1_000_000, c.device));
|
||||
crate::powertask::start()?;
|
||||
self.shared.log("tune helper: the Igneum Power Helper task (no prompt)");
|
||||
self.sweep_helper = true;
|
||||
|
|
@ -2427,7 +2505,7 @@ impl Engine {
|
|||
|
||||
fn sweep_helper_quit(&mut self) {
|
||||
if self.sweep_helper {
|
||||
let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), "quit\n");
|
||||
let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), "quit\n");
|
||||
if self.sweep_helper_is_task {
|
||||
// the task exits on quit and reports nothing back; the next tune starts it again
|
||||
self.sweep_helper = false;
|
||||
|
|
@ -2446,6 +2524,7 @@ impl Engine {
|
|||
let Some(c) = card else { return };
|
||||
let w = step.watts.round() as u64;
|
||||
let clock = step.point.clock_mhz;
|
||||
let mem = step.point.mem_mhz;
|
||||
let shared = self.shared.clone();
|
||||
self.tune_acked = None;
|
||||
match c.vendor.as_str() {
|
||||
|
|
@ -2469,6 +2548,15 @@ impl Engine {
|
|||
ok &= out.contains("All done") || out.to_ascii_lowercase().contains("clocks set") || out.to_ascii_lowercase().contains("reset");
|
||||
text.push(' ');
|
||||
text.push_str(out.trim());
|
||||
// Ember 2: the memory clock, locked to one value (`-lmc m,m`) or reset (`-rmc`)
|
||||
let out = if mem > 0 {
|
||||
crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-lmc", &format!("{mem},{mem}")]), None, Duration::from_secs(20)).unwrap_or_else(|| "nvidia-smi did not answer".into())
|
||||
} else {
|
||||
crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-rmc"]), None, Duration::from_secs(20)).unwrap_or_else(|| "nvidia-smi did not answer".into())
|
||||
};
|
||||
ok &= out.contains("All done") || out.to_ascii_lowercase().contains("clocks set") || out.to_ascii_lowercase().contains("reset") || out.to_ascii_lowercase().contains("not supported");
|
||||
text.push(' ');
|
||||
text.push_str(out.trim());
|
||||
shared.send(Cmd::TuneSet(seq, if ok { Ok(text) } else { Err(text) }));
|
||||
});
|
||||
}
|
||||
|
|
@ -2478,8 +2566,8 @@ impl Engine {
|
|||
return;
|
||||
}
|
||||
let dev = device.to_string();
|
||||
let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() });
|
||||
let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), cmd);
|
||||
let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n{seq}3 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }, if mem > 0 { format!("lmc {mem}") } else { "rmc".to_string() });
|
||||
let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), cmd);
|
||||
// the helper polls twice a second and nvidia-smi answers within a second or two
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_secs(4));
|
||||
|
|
@ -2520,7 +2608,50 @@ impl Engine {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
self.shared.log(&format!("tune: {} MHz, {}% ({w} W) requested on device {device} (request {seq})", if clock > 0 { clock.to_string() } else { "unlocked".into() }, step.point.power_pct));
|
||||
self.shared.log(&format!("tune: {} MHz, {}% ({w} W), memory {} requested on device {device} (request {seq})", if clock > 0 { clock.to_string() } else { "unlocked".into() }, step.point.power_pct, if mem > 0 { format!("{mem} MHz") } else { "default".into() }));
|
||||
}
|
||||
|
||||
/// A measurement engine's progress for one card (POST /api/tune-progress, forwarded by the job playbook from the
|
||||
/// engine's `TUNE progress` lines): the installed app shows the step, the live rate and draw and the time left
|
||||
/// instead of a bare "off" while a job holds its miners. `done` ends it: the row reads the tuned line.
|
||||
fn tune_progress(&mut self, v: &Value) {
|
||||
let key = v.get("key").and_then(|k| k.as_str()).unwrap_or("");
|
||||
let mut st = self.st();
|
||||
let Some(c) = st.mining.cards.iter_mut().find(|c| c.key == key || (!key.is_empty() && c.name.replace(' ', "_") == key)) else { return };
|
||||
let n = |k: &str| v.get(k).and_then(|x| x.as_f64()).unwrap_or(0.0);
|
||||
if v.get("done").and_then(|d| d.as_bool()).unwrap_or(false) {
|
||||
c.sweep_state = "idle".into();
|
||||
c.state = if self.job_hold { "held".into() } else { c.state.clone() };
|
||||
c.tune_step = 0;
|
||||
c.tune_steps = 0;
|
||||
c.tune_eta_s = 0;
|
||||
if n("mhs") > 0.0 && n("watts") > 0.0 {
|
||||
c.sweep_mhs = n("mhs");
|
||||
c.sweep_watts = n("watts");
|
||||
c.sweep_eff = n("mhs") / n("watts");
|
||||
c.sweep_pct = n("power_pct") as u32;
|
||||
c.tune_clock_mhz = n("clock_mhz") as u32;
|
||||
c.tune_source = v.get("plan").and_then(|p| p.as_str()).unwrap_or("full").into();
|
||||
c.tune_line = crate::ember::tuned_line(n("mhs"), n("watts"), n("mhs") / n("watts"));
|
||||
c.sweep_at = crate::platform::unix_now_f();
|
||||
}
|
||||
c.sweep_note = v.get("note").and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
return;
|
||||
}
|
||||
c.state = "tuning".into();
|
||||
c.sweep_state = "running".into();
|
||||
c.sweep_note = v.get("note").and_then(|x| x.as_str()).unwrap_or("tuning").to_string();
|
||||
c.tune_step = n("step") as u32;
|
||||
c.tune_steps = n("of") as u32;
|
||||
c.tune_eta_s = n("eta_s") as i64;
|
||||
c.tune_plan = v.get("plan").and_then(|p| p.as_str()).unwrap_or("").into();
|
||||
if n("mhs") > 0.0 {
|
||||
c.hash_now = n("mhs");
|
||||
}
|
||||
if n("watts") > 0.0 {
|
||||
c.power_w = n("watts");
|
||||
}
|
||||
c.message = String::new();
|
||||
}
|
||||
|
||||
/// Faults and conditions first, then the state machine.
|
||||
|
|
@ -2538,7 +2669,7 @@ impl Engine {
|
|||
Some("a remote job took the GPU".into())
|
||||
} else if self.st().mining.paused {
|
||||
Some("mining paused".into())
|
||||
} else if c.state != "mining" {
|
||||
} else if c.state != "mining" && c.state != "tuning" {
|
||||
Some(format!("the card left mining ({})", if c.state == "starting" || c.state == "restarting" { "worker restart, likely the hour boundary" } else { &c.state }))
|
||||
} else if slot_error {
|
||||
Some("the worker reported an error".into())
|
||||
|
|
@ -2587,9 +2718,27 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
}
|
||||
if let Some(words) = self.sweep.as_ref().map(|r| r.words(now)) {
|
||||
if let Some((words, step, of, eta, plan)) = self.sweep.as_ref().map(|r| (r.words(now), r.rows.len() as u32 + 1, r.plan.len() as u32, r.eta_s(now), r.plan.kind.name())) {
|
||||
let (key, mhs, watts) = self.st().mining.cards.get(idx).map(|c| (c.key.clone(), c.hash_now, c.power_w)).unwrap_or_default();
|
||||
if let Some(cc) = self.st().mining.cards.get_mut(idx) {
|
||||
cc.sweep_note = words;
|
||||
cc.sweep_note = words.clone();
|
||||
cc.state = if cc.state == "mining" { "tuning".into() } else { cc.state.clone() };
|
||||
cc.tune_step = step;
|
||||
cc.tune_steps = of;
|
||||
cc.tune_eta_s = eta;
|
||||
cc.tune_plan = plan.into();
|
||||
// the clock search's own step count while a clock step runs (the UI's "locking clocks: step 4 of 9")
|
||||
if let Some(r) = self.sweep.as_ref() {
|
||||
let power_steps = r.rows.iter().filter(|x| x.point.clock_mhz == 0 && x.mark.is_some()).count() as u32;
|
||||
let on_clock = r.current.as_ref().map(|s| s.kind == crate::ember::Kind::Clock).unwrap_or(false);
|
||||
cc.lock_step = if on_clock { step.saturating_sub(power_steps) } else { 0 };
|
||||
cc.lock_steps = if on_clock { of.saturating_sub(power_steps) } else { 0 };
|
||||
}
|
||||
}
|
||||
if self.shared.runtime.sweep_only {
|
||||
// the job playbook forwards this to the installed app's /api/tune-progress
|
||||
println!("TUNE progress key={} step={step} of={of} eta_s={eta} mhs={mhs:.2} watts={watts:.1} plan={plan} note={}", key.replace(' ', "_"), words.replace(' ', "_"));
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2612,9 +2761,33 @@ impl Engine {
|
|||
c.sweep_mhs = row.mhs;
|
||||
c.sweep_at = unix as f64;
|
||||
c.sweep_state = "idle".into();
|
||||
if c.state == "tuning" {
|
||||
c.state = "mining".into();
|
||||
}
|
||||
c.tune_step = 0;
|
||||
c.tune_steps = 0;
|
||||
c.tune_eta_s = 0;
|
||||
c.tune_clock_mhz = row.point.clock_mhz;
|
||||
c.tune_mem_mhz = row.point.mem_mhz;
|
||||
c.tune_source = kind.name().into();
|
||||
c.tune_line = crate::ember::tuned_line(row.mhs, row.watts, row.eff);
|
||||
c.tune_line = crate::ember::result_line(kind, row.mhs, row.watts, row.eff);
|
||||
c.tune_curve = run.rows.iter().map(|r| r.json()).collect();
|
||||
// the core-clock knob's result (7 October 2026): the chosen lock against the cap point, and why it stopped
|
||||
if kind == crate::ember::PlanKind::Baseline {
|
||||
c.lock_note = if c.vendor == "apple" || run.plan.limits.clock_max_mhz == 0 { "no lever".into() } else { c.sweep_note.clone() };
|
||||
} else {
|
||||
let lr = crate::ember::lock_result(&run.plan, &run.rows, &row);
|
||||
c.lock_mhz = lr.lock_mhz;
|
||||
c.lock_mhs = lr.lock_mhs;
|
||||
c.lock_w = lr.lock_w;
|
||||
c.lock_mhw = lr.lock_mhw;
|
||||
c.unlocked_mhs = lr.unlocked_mhs;
|
||||
c.unlocked_w = lr.unlocked_w;
|
||||
c.lock_at = unix as f64;
|
||||
c.lock_note = lr.lock_note;
|
||||
}
|
||||
c.lock_step = 0;
|
||||
c.lock_steps = 0;
|
||||
let control = c.tune_control;
|
||||
if kind == crate::ember::PlanKind::Baseline {
|
||||
c.sweep_note = if control { String::new() } else { c.sweep_note.clone() };
|
||||
|
|
@ -2623,6 +2796,7 @@ impl Engine {
|
|||
} else {
|
||||
c.power_pct = row.point.power_pct;
|
||||
c.clock_cap_mhz = row.point.clock_mhz;
|
||||
c.mem_cap_mhz = row.point.mem_mhz;
|
||||
if c.vendor == "nvidia" {
|
||||
c.power_limit_w = row.limit;
|
||||
c.power_applied = true;
|
||||
|
|
@ -2637,6 +2811,7 @@ impl Engine {
|
|||
e.sweep_watts = row.watts;
|
||||
e.sweep_mhs = row.mhs;
|
||||
e.sweep_clock_mhz = row.point.clock_mhz;
|
||||
e.sweep_mem_mhz = row.point.mem_mhz;
|
||||
e.sweep_driver = c.driver.clone();
|
||||
e.sweep_class = c.program_class.clone();
|
||||
e.sweep_source = kind.name().into();
|
||||
|
|
@ -2647,18 +2822,21 @@ impl Engine {
|
|||
};
|
||||
let _ = key;
|
||||
self.shared.save_settings();
|
||||
self.sweep_say(&format!("TUNE chosen card={} clock={} cap={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} plan={}{}", run.label, row.point.clock_mhz, row.point.power_pct, row.limit, row.watts, row.mhs, row.eff, kind.name(), if pinned { " pinned=1" } else { "" }));
|
||||
// 6 October 2026, run 6: the 5090's chosen clock sat on the ladder's floor (1,854 MHz = the old 60%), so a
|
||||
// chosen point at the floor says so: it is the lowest step measured, not the optimum (Ember 2's climb walks on)
|
||||
let floor = row.point.clock_mhz > 0 && row.point.clock_mhz <= run.plan.limits.clock_floor();
|
||||
self.sweep_say(&format!("TUNE chosen card={} clock={} cap={} mem={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} plan={}{}{}", run.label, row.point.clock_mhz, row.point.power_pct, row.point.mem_mhz, row.limit, row.watts, row.mhs, row.eff, kind.name(), if pinned { " pinned=1" } else { "" }, if floor { " floor=1 note=floor,_not_optimum" } else { "" }));
|
||||
let before = run.rows.first().filter(|_| kind == crate::ember::PlanKind::Full).cloned();
|
||||
let record = crate::ember::record_json(crate::platform::unix_now_f(), &crate::config::fingerprint8(&self.shared.runtime.machine_id), VERSION, crate::manifest::platform_name(), &name, &vendor, &driver, &class, kind, &run.rows, Some(&row), before.as_ref());
|
||||
let record = crate::ember::record_json(crate::platform::unix_now_f(), &crate::config::fingerprint8(&self.shared.runtime.machine_id), VERSION, crate::manifest::platform_name(), &name, &vendor, &driver, &class, kind, &run.rows, Some(&row), before.as_ref(), floor);
|
||||
self.sweep_say(&format!("TUNE {record}"));
|
||||
let line = crate::ember::tuned_line(row.mhs, row.watts, row.eff);
|
||||
let line = crate::ember::result_line(kind, row.mhs, row.watts, row.eff);
|
||||
match kind {
|
||||
crate::ember::PlanKind::Baseline => self.shared.event("info", &format!("{name}: measured as it runs: {line}{}", if control { String::new() } else { " (measure only; no control on this card)".into() })),
|
||||
crate::ember::PlanKind::Baseline => self.shared.event("info", &format!("{name}: {line}{}", if control { String::new() } else { " (measure only; no control on this card)".into() })),
|
||||
_ if pinned => self.shared.event("ok", &format!("{name}: best point {} ({line}); your setting stays pinned", point_words(&row.point))),
|
||||
_ => self.shared.event("ok", &format!("{name}: {line}, {} held", point_words(&row.point))),
|
||||
}
|
||||
if pinned && kind != crate::ember::PlanKind::Baseline {
|
||||
let p = self.st().mining.cards.get(idx).map(|c| crate::ember::Point { clock_mhz: c.clock_cap_mhz, power_pct: c.power_pct }).unwrap_or(run.before);
|
||||
let p = self.st().mining.cards.get(idx).map(|c| crate::ember::Point { clock_mhz: c.clock_cap_mhz, power_pct: c.power_pct, mem_mhz: c.mem_cap_mhz }).unwrap_or(run.before);
|
||||
let w = self.st().mining.cards.get(idx).map(requested_watts).unwrap_or(run.before_w);
|
||||
self.tune_apply(idx, &run.device, &crate::ember::Step { point: p, watts: w, kind: crate::ember::Kind::Confirm });
|
||||
}
|
||||
|
|
@ -2686,7 +2864,7 @@ impl Engine {
|
|||
(Some(r), _) => (r.card, r.label.clone(), r.device.clone(), r.before, r.before_w, r.forced, r.seq > 0 && r.plan.kind != crate::ember::PlanKind::Baseline),
|
||||
(None, Some((idx, forced))) => {
|
||||
let c = self.st().mining.cards.get(idx).cloned();
|
||||
let (label, device, w, p) = c.map(|c| (format!("card-{idx}"), c.device.clone(), if c.power_limit_w > 0.0 { c.power_limit_w } else { requested_watts(&c) }, crate::ember::Point { clock_mhz: c.clock_cap_mhz, power_pct: c.power_pct })).unwrap_or_default();
|
||||
let (label, device, w, p) = c.map(|c| (format!("card-{idx}"), c.device.clone(), if c.power_limit_w > 0.0 { c.power_limit_w } else { requested_watts(&c) }, crate::ember::Point { clock_mhz: c.clock_cap_mhz, power_pct: c.power_pct, mem_mhz: c.mem_cap_mhz })).unwrap_or_default();
|
||||
(idx, label, device, p, w, forced, false)
|
||||
}
|
||||
_ => return,
|
||||
|
|
@ -2697,6 +2875,12 @@ impl Engine {
|
|||
match st.mining.cards.get_mut(idx) {
|
||||
Some(c) => {
|
||||
c.sweep_state = "idle".into();
|
||||
if c.state == "tuning" {
|
||||
c.state = "mining".into();
|
||||
}
|
||||
c.tune_step = 0;
|
||||
c.tune_steps = 0;
|
||||
c.tune_eta_s = 0;
|
||||
c.sweep_note = format!("tuning stopped: {why}");
|
||||
if touched {
|
||||
c.power_pct = before.power_pct;
|
||||
|
|
@ -2870,6 +3054,13 @@ impl Engine {
|
|||
}
|
||||
if self.job_hold && !self.jobs.holds_miners() {
|
||||
self.job_hold = false;
|
||||
for c in self.st().mining.cards.iter_mut().filter(|c| c.state == "held" || c.state == "tuning") {
|
||||
c.state = "off".into();
|
||||
c.sweep_state = if c.sweep_state == "running" { "idle".into() } else { c.sweep_state.clone() };
|
||||
c.tune_step = 0;
|
||||
c.tune_steps = 0;
|
||||
c.tune_eta_s = 0;
|
||||
}
|
||||
if self.running {
|
||||
self.shared.event("info", "job finished; the miners restart");
|
||||
for m in self.miners.iter_mut() {
|
||||
|
|
@ -2886,10 +3077,31 @@ impl Engine {
|
|||
self.job_hold = true;
|
||||
self.stop_miners(&why);
|
||||
for c in self.st().mining.cards.iter_mut().filter(|c| c.enabled) {
|
||||
c.message = "stopped for a remote job".into();
|
||||
// never a bare "off" at 0 MH/s: the row says why (a job holds the card)
|
||||
c.state = "held".into();
|
||||
c.message = format!("held for a remote job: {}", short(&why, 80));
|
||||
}
|
||||
self.jobs.miners_stopped(&self.shared);
|
||||
}
|
||||
Action::CardsOff(keys) => {
|
||||
// `--cards-off` (6 October 2026): the runner switches the job's cards through the app's own card path
|
||||
// and keeps the exact choices that put them back; the script never touches /api/cards
|
||||
let live: Vec<crate::jobrun::LiveCard> = self.st().mining.cards.iter().map(|c| (c.key.clone(), c.enabled, c.identities, c.power_pct, c.present())).collect();
|
||||
let (off, restore) = crate::jobrun::cards_off_choices(&keys, &live);
|
||||
if off.is_empty() {
|
||||
self.shared.event("info", &format!("remote job asked for cards off ({}) but no present card matched; nothing switched", keys.join(",")));
|
||||
} else {
|
||||
self.shared.event("info", &format!("remote job: cards off for the job: {}", off.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", ")));
|
||||
self.apply_cards(off);
|
||||
}
|
||||
if let Some(next) = self.jobs.cards_off_done(&self.shared, restore) {
|
||||
self.job_action(next);
|
||||
}
|
||||
}
|
||||
Action::RestoreCards(choices) => {
|
||||
self.shared.event("info", &format!("remote job: cards restored: {}", choices.iter().map(|c| format!("{} enabled={} identities={}", c.key, c.enabled, c.identities)).collect::<Vec<_>>().join("; ")));
|
||||
self.apply_cards(choices);
|
||||
}
|
||||
Action::RestartMiners => {
|
||||
self.stop_miners("remote job: restart miners");
|
||||
for m in self.miners.iter_mut() {
|
||||
|
|
@ -3837,7 +4049,10 @@ impl Engine {
|
|||
|
||||
fn shutdown(&mut self) {
|
||||
self.shared.log(&format!("quit: stopping the miners, then the node (source: {})", self.quit_source));
|
||||
self.jobs.abort(&self.shared, "the app is quitting");
|
||||
if let Some(a) = self.jobs.abort(&self.shared, "the app is quitting") {
|
||||
// the job's cards back before the exit (an app update under a job left the 9070 XT off on 6 October 2026)
|
||||
self.job_action(a);
|
||||
}
|
||||
if self.sweep.is_some() || self.sweep_pending.is_some() {
|
||||
self.sweep_abort("the app is quitting");
|
||||
}
|
||||
|
|
@ -3887,7 +4102,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 {
|
||||
|
|
@ -3948,6 +4163,9 @@ pub struct TuneProbe {
|
|||
/// AMD: the power offset range in percent from the `tune` line (PC 1's 9070 XT: -30 to 10)
|
||||
pub plimit_min: f64,
|
||||
pub plimit_max: f64,
|
||||
/// Ember 2, NVIDIA: the memory clock under load and the vendor's maximum (nvidia-smi clocks.mem, clocks.max.mem)
|
||||
pub mem_default_mhz: u32,
|
||||
pub mem_max_mhz: u32,
|
||||
}
|
||||
|
||||
/// One `tune` line of igneum-gpu-telemetry --tune:
|
||||
|
|
@ -4156,7 +4374,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)");
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) {
|
|||
c.sweep_at = p.sweep_at as f64;
|
||||
// Ember Tune (src/ember.rs): the clock cap the last tune chose, its plan, and the row's Tuned line
|
||||
c.tune_clock_mhz = p.sweep_clock_mhz;
|
||||
c.tune_mem_mhz = p.sweep_mem_mhz;
|
||||
c.mem_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_mem_mhz };
|
||||
c.clock_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_clock_mhz };
|
||||
c.tune_source = p.sweep_source.clone();
|
||||
if p.sweep_mhs > 0.0 && p.sweep_watts > 0.0 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//!
|
||||
|
|
@ -133,6 +133,11 @@ pub struct Manifest {
|
|||
pub created_at: String,
|
||||
pub node_branch: String,
|
||||
pub node_commit: String,
|
||||
/// the 40-hex commit (manifest node.commit_full, packer of 6 October 2026); empty in older manifests
|
||||
pub node_commit_full: String,
|
||||
/// the commit's author time (manifest node.commit_time, unix seconds): SOURCE_DATE_EPOCH of every build stage, so mimalloc's
|
||||
/// __DATE__/__TIME__ and anything else that reads the clock give one answer per commit (6 October 2026, the 0.3.14 repro)
|
||||
pub node_commit_time: u64,
|
||||
pub node_dirty: bool,
|
||||
pub app_version: String,
|
||||
pub builds: Vec<Unit>,
|
||||
|
|
@ -159,7 +164,7 @@ pub fn parse_manifest(text: &str) -> Result<Manifest, String> {
|
|||
let s = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||
let node = v.get("node").cloned().unwrap_or(Value::Null);
|
||||
let ns = |k: &str| node.get(k).and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||
let mut m = Manifest { created_at: s("created_at"), node_branch: ns("branch"), node_commit: ns("commit"), node_dirty: node.get("dirty").and_then(|x| x.as_bool()).unwrap_or(false), app_version: s("app_version"), ..Default::default() };
|
||||
let mut m = Manifest { created_at: s("created_at"), node_branch: ns("branch"), node_commit: ns("commit"), node_commit_full: ns("commit_full"), node_commit_time: node.get("commit_time").and_then(|x| x.as_u64()).unwrap_or(0), node_dirty: node.get("dirty").and_then(|x| x.as_bool()).unwrap_or(false), app_version: s("app_version"), ..Default::default() };
|
||||
let builds = v.get("builds").and_then(|b| b.as_array()).ok_or("manifest.json has no \"builds\" list")?;
|
||||
for (i, b) in builds.iter().enumerate() {
|
||||
let dir = b.get("dir").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||
|
|
@ -243,7 +248,7 @@ pub fn windows_env() -> String {
|
|||
s.push_str("export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++-posix\n");
|
||||
s.push_str("export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar\n");
|
||||
s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc-posix\n");
|
||||
s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS=\"-C link-arg=-static -C link-arg=-static-libgcc\"\n");
|
||||
s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS=\"-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-Wl,--no-insert-timestamp\"\n"); // no PE timestamp: reproducible exes (6 Oct 2026)
|
||||
s.push_str("export IGNEUM_WINDRES=x86_64-w64-mingw32-windres\n");
|
||||
s.push_str("LLVM_LIB=$(ls -d /usr/lib/llvm-*/lib 2>/dev/null | sort -V | tail -1)\n");
|
||||
s.push_str("export LIBCLANG_PATH=\"${LLVM_LIB:-/usr/lib/llvm-18/lib}\"\n");
|
||||
|
|
@ -293,6 +298,12 @@ pub fn extract_script(p: &BuildParams, job_id: &str, zip_wsl: &str) -> String {
|
|||
// against 0.3.5's consensus-core). The packer stamps too (push-build-inputs.sh); this guard holds if it regresses.
|
||||
s.push_str("find \"$B/src\" -type f -exec touch {} + || { echo \"RESULT extract cannot stamp the sources\"; exit 2; }\n");
|
||||
s.push_str("[ -f \"$SRC/manifest.json\" ] || { echo \"RESULT extract no manifest.json under $SRC\"; exit 2; }\n");
|
||||
// the commit hash (6 October 2026, found on igneum-build-1): the zip has no .git, so kaspa-build-info's build.rs embedded
|
||||
// nothing in every PC build. It needs .git to be a directory with HEAD a symbolic ref to a branch file holding the hash
|
||||
// (git rev-parse reads it; its fallback reads the file itself). A minimal .git with the manifest's full commit gives the
|
||||
// binary its commit string; tools/ci/commit-string-check.sh then passes on the fetched binaries.
|
||||
s.push_str("ncf=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"node\",{}).get(\"commit_full\",\"\"))' \"$SRC/manifest.json\" 2>/dev/null || true)\n");
|
||||
s.push_str("if [ -d \"$SRC/node\" ] && printf '%s' \"$ncf\" | grep -qE '^[0-9a-f]{40}$'; then mkdir -p \"$SRC/node/.git/refs/heads\" && printf 'ref: refs/heads/build\\n' > \"$SRC/node/.git/HEAD\" && printf '%s\\n' \"$ncf\" > \"$SRC/node/.git/refs/heads/build\" && echo \"commit $ncf written to node/.git for kaspa-build-info\"; else echo \"no full node commit in the manifest: the node binaries will carry no commit string\"; fi\n");
|
||||
// the stale-build class (5 October 2026): the target dir persists and cargo rebuilds by mtime, so every extracted
|
||||
// source is stamped now, else a file older than the last build links against the cached crate of the old version
|
||||
s.push_str("find \"$B/src\" -type f -exec touch {} + 2>/dev/null || true\n");
|
||||
|
|
@ -321,15 +332,30 @@ pub fn build_script(p: &BuildParams, job_id: &str, m: &Manifest, target: &str) -
|
|||
s.push_str(&windows_env());
|
||||
}
|
||||
s.push_str(&format!("echo \"STAGE {target} start $(now)\"\n"));
|
||||
if m.node_commit_time > 0 {
|
||||
// reproducible builds: the commit's author time and UTC for every compiler of this stage; the target dir is the one
|
||||
// persistent $CARGO_TARGET_DIR (never a per-run name: prost's generated code embeds OUT_DIR)
|
||||
s.push_str(&format!("export SOURCE_DATE_EPOCH={} TZ=UTC; echo \"SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH TZ=UTC\"\n", m.node_commit_time));
|
||||
}
|
||||
s.push_str(&format!("mkdir -p \"$OUT/{target}\"\n"));
|
||||
s.push_str("rc_all=0\n");
|
||||
// the node's full commit from the manifest (each stage is its own script; the extract stage read it too)
|
||||
s.push_str("ncf=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"node\",{}).get(\"commit_full\",\"\"))' \"$SRC/manifest.json\" 2>/dev/null || true)\n");
|
||||
for u in m.builds.iter().filter(|u| u.targets.iter().any(|t| t == target)) {
|
||||
let optional = u.optional_on.iter().any(|t| t == target);
|
||||
let rel = if windows { "x86_64-pc-windows-gnu/release" } else { "release" };
|
||||
let tflag = if windows { " --target x86_64-pc-windows-gnu" } else { "" };
|
||||
s.push_str(&format!("echo \"STAGE {target} {dir} start $(now)\"\n", dir = u.dir));
|
||||
s.push_str(&format!("t0=$(date +%s); rc=1\n"));
|
||||
if u.dir == "node" {
|
||||
// kaspa-build-info emits no rerun-if-changed once it found nothing, so the persistent target dir keeps an empty
|
||||
// hash forever unless that one crate is cleaned when the commit differs from the last one built here (6 Oct 2026)
|
||||
s.push_str(&format!("if cd \"$SRC/{dir}\"; then [ \"$(cat \"$CARGO_TARGET_DIR/.node-commit-{target}\" 2>/dev/null)\" = \"$ncf\" ] || cargo clean -q --release -p kaspa-build-info{tflag} 2>/dev/null || true; fi\n", dir = u.dir));
|
||||
}
|
||||
s.push_str(&format!("if cd \"$SRC/{dir}\"; then $NICE cargo build --release $JOBS{args}{tflag} 2>&1; rc=$?; else echo \"no $SRC/{dir}\"; rc=2; fi\n", dir = u.dir, args = cargo_unit_args(u)));
|
||||
if u.dir == "node" {
|
||||
s.push_str(&format!("[ \"$rc\" = 0 ] && printf '%s\\n' \"$ncf\" > \"$CARGO_TARGET_DIR/.node-commit-{target}\"\n"));
|
||||
}
|
||||
s.push_str(&format!("echo \"RESULT {target} {dir} build exit $rc $(( $(date +%s) - t0 )) s at $(now)\"\n", dir = u.dir));
|
||||
s.push_str("if [ \"$rc\" = 0 ]; then\n");
|
||||
for b in &u.bins {
|
||||
|
|
@ -480,7 +506,7 @@ mod tests {
|
|||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
const MANIFEST: &str = r#"{"created_at":"2026-10-04T20:00:00Z","node":{"branch":"devnet-v4","commit":"3bfe346f","dirty":false,"source":"vendor/igneum-node-v4"},"repo":{"commit":"0f44edd","branch":"build-job","dirty":true},"app_version":"0.3.4",
|
||||
const MANIFEST: &str = r#"{"created_at":"2026-10-04T20:00:00Z","node":{"branch":"devnet-v4","commit":"3bfe346f","commit_time":1791300000,"dirty":false,"source":"vendor/igneum-node-v4"},"repo":{"commit":"0f44edd","branch":"build-job","dirty":true},"app_version":"0.3.4",
|
||||
"builds":[{"dir":"node","packages":["kaspad","igneum-miner"],"features":["kaspad/igneum-pow"],"bins":["igneumd","igneum-miner"],"targets":["linux","windows"]},
|
||||
{"dir":"app/igneum-app","packages":["igneum-app"],"bins":["igneum-app"],"optional_on":["linux"]}],
|
||||
"tests":[{"dir":"app/igneum-app","packages":["igneum-app"]},{"dir":"node","packages":["igneum-miner"]},{"dir":"node","packages":[]}]}"#;
|
||||
|
|
@ -559,6 +585,8 @@ mod tests {
|
|||
let lin = build_script(&p, id, &m, "linux");
|
||||
assert!(lin.contains("cargo build --release $JOBS -p kaspad -p igneum-miner --features kaspad/igneum-pow 2>&1"));
|
||||
assert!(lin.contains("cd \"$SRC/app/igneum-app\""));
|
||||
// reproducible builds (6 October 2026): the commit's author time and UTC exported before any cargo build of the stage
|
||||
assert!(lin.contains("export SOURCE_DATE_EPOCH=1791300000 TZ=UTC") && lin.find("SOURCE_DATE_EPOCH=1791300000").unwrap() < lin.find("cargo build").unwrap(), "{lin}");
|
||||
assert!(lin.contains("optional on linux: not fatal"));
|
||||
assert!(!lin.contains("--target x86_64-pc-windows-gnu"));
|
||||
// the windows stage ships the runtime DLLs of its own toolchain next to the exes; the linux stage does not
|
||||
|
|
|
|||
|
|
@ -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 (<user> on PC 2) need not exist there (4 October 2026: `getpwnam(<user>) 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;
|
||||
|
|
@ -90,6 +90,76 @@ pub enum Action {
|
|||
/// The relaunch helper was started; the engine quits now.
|
||||
RestartApp,
|
||||
UpdateNow,
|
||||
/// `--cards-off <key,key>` (6 October 2026): switch these cards off through the app's own card path before the
|
||||
/// script starts; then call `cards_off_done` with the exact choices that put them back.
|
||||
CardsOff(Vec<String>),
|
||||
/// The job ended (done, failed, timeout, aborted, the app quitting): put the cards it held back exactly as they were.
|
||||
RestoreCards(Vec<crate::engine::CardChoice>),
|
||||
}
|
||||
|
||||
/// The cards a job holds switched off, and the exact choices that restore them (6 October 2026, the watts job
|
||||
/// run-ca3-pc1-amd-watts-20261006: a script switched the 9070 XT off itself, an app update killed the script mid-run,
|
||||
/// its finally never ran, and the card stayed off until a hand POST; so the runner owns the switch and the restore).
|
||||
/// `take` hands the restore list out once: whoever ends the job (Finished, abort on quit) applies it, never twice.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CardHold {
|
||||
restore: Vec<crate::engine::CardChoice>,
|
||||
taken: bool,
|
||||
}
|
||||
|
||||
impl CardHold {
|
||||
pub fn hold(restore: Vec<crate::engine::CardChoice>) -> Self {
|
||||
CardHold { restore, taken: false }
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.restore.is_empty()
|
||||
}
|
||||
pub fn keys(&self) -> String {
|
||||
self.restore.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", ")
|
||||
}
|
||||
pub fn take(&mut self) -> Vec<crate::engine::CardChoice> {
|
||||
if self.taken {
|
||||
return vec![];
|
||||
}
|
||||
self.taken = true;
|
||||
self.restore.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// `vendor:N:code` -> `vendor:code` (the settings.json key with the device index against the live state's key without it:
|
||||
/// the 6 October finding that one form switched nothing).
|
||||
pub fn key_without_index(k: &str) -> String {
|
||||
let p: Vec<&str> = k.split(':').collect();
|
||||
if p.len() >= 3 && !p[1].is_empty() && p[1].bytes().all(|b| b.is_ascii_digit()) {
|
||||
format!("{}:{}", p[0], p[2..].join(":"))
|
||||
} else {
|
||||
k.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// One live card as `cards_off_choices` sees it: key, enabled, identities, power_pct, present.
|
||||
pub type LiveCard = (String, bool, u32, u32, bool);
|
||||
|
||||
/// The `--cards-off` match: every present live card whose key equals a requested key with or without the device
|
||||
/// index. Returns (the choices that switch them off, the choices that put them back exactly: enabled flag, identities
|
||||
/// and cap as they were). A requested key that matches no live card switches nothing (apply_cards skips unknown keys).
|
||||
pub fn cards_off_choices(requested: &[String], live: &[LiveCard]) -> (Vec<crate::engine::CardChoice>, Vec<crate::engine::CardChoice>) {
|
||||
let mut off = Vec::new();
|
||||
let mut restore = Vec::new();
|
||||
for (key, enabled, identities, power_pct, present) in live {
|
||||
if !present {
|
||||
continue;
|
||||
}
|
||||
let bare = key_without_index(key);
|
||||
let hit = requested.iter().any(|r| r == key || key_without_index(r) == bare);
|
||||
if !hit {
|
||||
continue;
|
||||
}
|
||||
let pp = if *power_pct > 0 { Some(*power_pct) } else { None };
|
||||
off.push(crate::engine::CardChoice { key: key.clone(), enabled: false, identities: *identities, power_pct: pp });
|
||||
restore.push(crate::engine::CardChoice { key: key.clone(), enabled: *enabled, identities: *identities, power_pct: pp });
|
||||
}
|
||||
(off, restore)
|
||||
}
|
||||
|
||||
/// Shared with the job's thread: the abort flag and the child's pid, so the engine can end a job on quit.
|
||||
|
|
@ -112,6 +182,9 @@ struct Active {
|
|||
started_unix: u64,
|
||||
waiting_for_miners: bool,
|
||||
holds_miners: bool,
|
||||
/// `--cards-off`: the engine switches the cards before the script runs
|
||||
waiting_for_cards: bool,
|
||||
cards: CardHold,
|
||||
ctl: Arc<Ctl>,
|
||||
}
|
||||
|
||||
|
|
@ -266,18 +339,27 @@ impl Jobs {
|
|||
}
|
||||
|
||||
/// Ends the running job (quit, or the switch turned off). The engine releases the miners itself.
|
||||
pub fn abort(&mut self, shared: &Arc<Shared>, why: &str) {
|
||||
let Some(a) = self.active.as_ref() else { return };
|
||||
/// Returns the cards the job held, for the engine to put back BEFORE it goes on (on quit this runs on the engine
|
||||
/// thread ahead of the exit, so an app update under a job no longer leaves a card off: 6 October 2026, 17:52Z).
|
||||
pub fn abort(&mut self, shared: &Arc<Shared>, why: &str) -> Option<Action> {
|
||||
let Some(a) = self.active.as_mut() else { return None };
|
||||
a.ctl.abort.store(true, Ordering::Relaxed);
|
||||
if let Some(pid) = *a.ctl.pid.lock().unwrap() {
|
||||
kill_tree(pid);
|
||||
}
|
||||
shared.log(&format!("job {}: aborted ({why})", a.job.id));
|
||||
let id = a.job.id.clone();
|
||||
let restore = a.cards.take();
|
||||
self.ledger.finish(&id, "aborted", -1, why, false, crate::platform::unix_now());
|
||||
let _ = self.ledger.save(&self.ledger_path);
|
||||
self.active = None;
|
||||
self.publish(shared);
|
||||
if restore.is_empty() {
|
||||
None
|
||||
} else {
|
||||
shared.event("info", &format!("job {}: cards restored by the runner (aborted: {why}): {}", id, restore.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", ")));
|
||||
Some(Action::RestoreCards(restore))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the tick ------------------------------------------------------------------------------------------------
|
||||
|
|
@ -426,7 +508,8 @@ impl Jobs {
|
|||
if a.job.id != id {
|
||||
return None;
|
||||
}
|
||||
let a = self.active.take().unwrap();
|
||||
let mut a = self.active.take().unwrap();
|
||||
let restore = a.cards.take();
|
||||
let now = crate::platform::unix_now();
|
||||
self.ledger.finish(&id, &outcome.status, outcome.exit, &outcome.summary, outcome.uploaded, now);
|
||||
let _ = self.ledger.save(&self.ledger_path);
|
||||
|
|
@ -442,7 +525,12 @@ impl Jobs {
|
|||
st.jobs.last_results = outcome.results.clone();
|
||||
}
|
||||
self.publish(shared);
|
||||
None
|
||||
if restore.is_empty() {
|
||||
None
|
||||
} else {
|
||||
shared.event("info", &format!("job {}: cards restored by the runner ({}): {}", id, outcome.status, restore.iter().map(|c| c.key.as_str()).collect::<Vec<_>>().join(", ")));
|
||||
Some(Action::RestoreCards(restore))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -473,7 +561,8 @@ impl Jobs {
|
|||
shared.event("info", &format!("job {} ({}) starts: {}", job.id, job.kind, job.label()));
|
||||
let ctl = Arc::new(Ctl::default());
|
||||
let needs_miners_stopped = job.kind == "shard-benchmark" || (job.kind == "run" && job.bool_param("stop_miners_first"));
|
||||
self.active = Some(Active { job: job.clone(), run_id: run_id.clone(), started: Instant::now(), started_unix: now, waiting_for_miners: needs_miners_stopped, holds_miners: false, ctl: ctl.clone() });
|
||||
let cards_off: Vec<String> = if job.kind == "run" { job.list_param("cards_off") } else { vec![] };
|
||||
self.active = Some(Active { job: job.clone(), run_id: run_id.clone(), started: Instant::now(), started_unix: now, waiting_for_miners: needs_miners_stopped, holds_miners: false, waiting_for_cards: !cards_off.is_empty(), cards: CardHold::default(), ctl: ctl.clone() });
|
||||
match job.kind.as_str() {
|
||||
"restart" | "update-now" => {
|
||||
// engine-side; the report says what was asked and the ledger closes at once
|
||||
|
|
@ -498,6 +587,8 @@ impl Jobs {
|
|||
self.event(shared, Event::Finished { id: job.id.clone(), outcome: o });
|
||||
Some(action)
|
||||
}
|
||||
// the cards first (the engine answers through cards_off_done), then the miners, then the script
|
||||
_ if !cards_off.is_empty() => Some(Action::CardsOff(cards_off)),
|
||||
_ if needs_miners_stopped => Some(Action::StopMiners(format!("job {}: {}", job.id, job.label()))),
|
||||
_ => {
|
||||
self.spawn_run(shared, job, run_id, ctl, now);
|
||||
|
|
@ -506,14 +597,42 @@ impl Jobs {
|
|||
}
|
||||
}
|
||||
|
||||
/// Called by the engine once a job's `--cards-off` cards are switched off; `restore` puts them back exactly.
|
||||
/// Next: the miners, if the job asked for them too, else the script.
|
||||
pub fn cards_off_done(&mut self, shared: &Arc<Shared>, restore: Vec<crate::engine::CardChoice>) -> Option<Action> {
|
||||
let Some(a) = self.active.as_mut() else { return None };
|
||||
if !a.waiting_for_cards {
|
||||
return None;
|
||||
}
|
||||
a.waiting_for_cards = false;
|
||||
a.cards = CardHold::hold(restore);
|
||||
if a.waiting_for_miners {
|
||||
return Some(Action::StopMiners(format!("job {}: {}", a.job.id, a.job.label())));
|
||||
}
|
||||
let (job, run_id, ctl, started) = (a.job.clone(), a.run_id.clone(), a.ctl.clone(), a.started_unix);
|
||||
self.spawn_run(shared, job, run_id, ctl, started);
|
||||
self.publish(shared);
|
||||
None
|
||||
}
|
||||
|
||||
fn spawn_run(&self, shared: &Arc<Shared>, job: Job, run_id: String, ctl: Arc<Ctl>, started: u64) {
|
||||
let shared2 = shared.clone();
|
||||
let dir = self.dir.clone();
|
||||
let data_root = self.data_root.clone();
|
||||
let jobs_url = self.url.clone();
|
||||
let held = self.active.as_ref().map(|a| a.cards.clone()).unwrap_or_default();
|
||||
let asked: Vec<String> = if job.kind == "run" { job.list_param("cards_off") } else { vec![] };
|
||||
std::thread::spawn(move || {
|
||||
let sink = Sink::new(&shared2, &job, &dir);
|
||||
sink.line(&format!("job {} ({}) on {} machine {} run {run_id}, started {}", job.id, job.kind, shared2.runtime.host, shared2.runtime.machine_id, jobs::format_time(started)));
|
||||
if !asked.is_empty() {
|
||||
// the runner's own lines: what it switched off and what it will put back, whatever the script does
|
||||
if held.is_empty() {
|
||||
sink.line(&format!("cards-off: {} asked, no present card matched (nothing switched; the script's card may be loaded)", asked.join(",")));
|
||||
} else {
|
||||
sink.line(&format!("cards-off: {} switched off by the runner before this script; cards restored: {} by the runner on any exit (done, failed, timeout, aborted, app quit), with their own enabled flag, identities and cap", held.keys(), held.keys()));
|
||||
}
|
||||
}
|
||||
let ctx = account_context();
|
||||
sink.line(&format!("account: {ctx}"));
|
||||
let warn = account_warning(&ctx);
|
||||
|
|
@ -1099,7 +1218,9 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
|||
let shell = shell_for(job);
|
||||
let body = job.str_param("script").replace("\r\n", "\n");
|
||||
let script = dir.join(if shell == "powershell" { "script.ps1" } else { "script.sh" });
|
||||
let text = if shell == "powershell" { body.replace('\n', "\r\n") } else { body };
|
||||
// every PowerShell job gets the runner's prelude first (7 October 2026, main's rule after 0.3.22 take 2: a helper that must
|
||||
// outlive the job is started through ONE helper here, never the plain start cmdlet from the job's own PowerShell)
|
||||
let text = if shell == "powershell" { format!("{}{}", ps_prelude(), body.replace('\n', "\r\n")) } else { body };
|
||||
std::fs::write(&script, if shell == "powershell" { [b"\xEF\xBB\xBF".as_slice(), text.as_bytes()].concat() } else { text.into_bytes() }).map_err(|e| format!("cannot write the script: {e}"))?;
|
||||
let elevated = cfg!(windows) && job.bool_param("elevated");
|
||||
let limit = Duration::from_secs(job.timeout_minutes() * 60);
|
||||
|
|
@ -1185,6 +1306,25 @@ fn follow_file(sink: &Sink, path: PathBuf) -> Follow {
|
|||
/// `-WindowStyle Hidden` on the launch keeps it hidden, and this line is the running measurement of that on every
|
||||
/// elevated job: "elevated console: hwnd N visible False"), then the script, everything into `out_file` for the engine
|
||||
/// to read back. The console-window class, PC 1, 5 October 2026 (tools/windows/console-watch-elevated.ps1).
|
||||
/// The PowerShell prelude written ahead of every job script (0.3.24; main's rule, 7 October 2026 21:3x UK, from the 0.3.22 take 2
|
||||
/// fault: a smoke helper started with Start-Process from the job's PowerShell died with the job's process tree when the job ended,
|
||||
/// because the runner ends the tree (taskkill /T) and Start-Process keeps the child inside it). `Start-IgneumDetached -File <ps1>
|
||||
/// [-Arguments <text>]` starts a hidden PowerShell through Win32_Process.Create (Invoke-CimMethod), which puts the child outside
|
||||
/// the job's tree and outside taskkill /T, so it outlives the job; it falls back to a one-shot scheduled task (schtasks /SC ONCE,
|
||||
/// run now, delete after) when CIM is refused. It returns the new pid and writes "RESULT detached pid N via cim|task" so the
|
||||
/// report carries it. A script that needs to outlive itself (a smoke helper, an installer that stops the app) calls this and
|
||||
/// nothing else; the plain start cmdlet for that purpose is the known-failed shape (it keeps the child in the job's tree).
|
||||
fn ps_prelude() -> String {
|
||||
"function Start-IgneumDetached { param([Parameter(Mandatory=$true)][string]$File, [string]$Arguments = '')\r\n\
|
||||
$cmd = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + $File + '\"' + $(if ($Arguments) { ' ' + $Arguments } else { '' })\r\n\
|
||||
$how = 'cim'; $newPid = 0\r\n\
|
||||
try { $si = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{ ShowWindow = 0 }; $r = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = $cmd; ProcessStartupInformation = $si }; if ($r.ReturnValue -eq 0) { $newPid = [int]$r.ProcessId } } catch { }\r\n\
|
||||
if ($newPid -eq 0) { $how = 'task'; $tn = 'IgneumDetached-' + [guid]::NewGuid().ToString('N').Substring(0, 8); $at = (Get-Date).AddMinutes(1).ToString('HH:mm'); & schtasks.exe /Create /TN $tn /TR $cmd /SC ONCE /ST $at /F | Out-Null; & schtasks.exe /Run /TN $tn | Out-Null; Start-Sleep -Seconds 2; & schtasks.exe /Delete /TN $tn /F | Out-Null; $newPid = -1 }\r\n\
|
||||
Write-Output ('RESULT detached pid ' + $newPid + ' via ' + $how + ': ' + $File)\r\n\
|
||||
return $newPid }\r\n\
|
||||
# (runner prelude end)\r\n".to_string()
|
||||
}
|
||||
|
||||
fn elevated_wrapper(env_lines: &str, script: &str, out_file: &str) -> String {
|
||||
let (script, out) = (crate::platform::ps_quote(script), crate::platform::ps_quote(out_file));
|
||||
format!(
|
||||
|
|
@ -1315,6 +1455,46 @@ fn collect_done(uploaded: u32, failed: u32, names: Vec<String>, ran: Option<Ran>
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cards_off_matches_keys_with_and_without_the_device_index_and_restores_exactly() {
|
||||
// PC 1, 6 October 2026: settings.json holds amd:1:gfx1201 and amd:3:gfx1201, the live state's key is amd:gfx1201
|
||||
let live: Vec<LiveCard> = vec![
|
||||
("amd:gfx1201".into(), true, 8, 0, true),
|
||||
("nvidia:NVIDIA GeForce RTX 5090".into(), true, 8, 75, true),
|
||||
("nvidia:NVIDIA GeForce RTX 4070".into(), false, 2, 80, true),
|
||||
("amd:gfx1036".into(), false, 1, 0, false),
|
||||
];
|
||||
let (off, restore) = cards_off_choices(&["amd:1:gfx1201".to_string()], &live);
|
||||
assert_eq!(off.len(), 1);
|
||||
assert_eq!((off[0].key.as_str(), off[0].enabled, off[0].identities, off[0].power_pct), ("amd:gfx1201", false, 8, None));
|
||||
assert_eq!((restore[0].key.as_str(), restore[0].enabled, restore[0].identities), ("amd:gfx1201", true, 8));
|
||||
// the live key itself, and the name form of an NVIDIA card: its cap rides along; a disabled card restores as disabled
|
||||
let (off, restore) = cards_off_choices(&["amd:gfx1201".to_string(), "nvidia:1:NVIDIA GeForce RTX 4070".to_string()], &live);
|
||||
assert_eq!(off.iter().map(|c| c.key.as_str()).collect::<Vec<_>>(), vec!["amd:gfx1201", "nvidia:NVIDIA GeForce RTX 4070"]);
|
||||
assert_eq!((restore[1].enabled, restore[1].identities, restore[1].power_pct), (false, 2, Some(80)));
|
||||
// a key that matches nothing switches nothing; a removed card is never touched
|
||||
assert!(cards_off_choices(&["amd:gfx1036".to_string(), "intel:0:x".to_string()], &live).0.is_empty());
|
||||
assert_eq!(key_without_index("nvidia:0:NVIDIA GeForce RTX 5090"), "nvidia:NVIDIA GeForce RTX 5090");
|
||||
assert_eq!(key_without_index("amd:gfx1201"), "amd:gfx1201");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_hold_restores_once_on_any_exit() {
|
||||
// the known-failed case: the script dies mid-run (exit 1, or the app quits under it); the restore list comes
|
||||
// out exactly once, whichever path ends the job, and never a second time
|
||||
let restore = vec![crate::engine::CardChoice { key: "amd:gfx1201".into(), enabled: true, identities: 8, power_pct: None }];
|
||||
let mut done = CardHold::hold(restore.clone());
|
||||
assert_eq!(done.take(), restore, "the known-finished case (done) restores");
|
||||
assert!(done.take().is_empty(), "never twice");
|
||||
let mut failed = CardHold::hold(restore.clone());
|
||||
assert_eq!(failed.take(), restore, "the known-failed case (the script died, exit 1) restores the same list");
|
||||
let mut quit = CardHold::hold(restore.clone());
|
||||
assert_eq!(quit.take(), restore, "the app quitting under the job (abort) restores the same list");
|
||||
assert!(quit.take().is_empty());
|
||||
assert!(CardHold::default().take().is_empty(), "a job without --cards-off restores nothing");
|
||||
assert_eq!(CardHold::hold(restore).keys(), "amd:gfx1201");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_refused_administrator_prompt_is_a_failure_not_done() {
|
||||
// 5 October 2026: the elevated launcher exited 0 after Windows cancelled an unanswered UAC prompt
|
||||
|
|
@ -1330,6 +1510,23 @@ mod tests {
|
|||
assert!(l.contains("if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }"), "{l}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powershell_jobs_get_the_detached_helper_and_it_never_uses_start_process() {
|
||||
// the known-failed shape (0.3.22 take 2, 7 October 2026): a helper started with Start-Process from the job's PowerShell
|
||||
// dies with the job's tree; the prelude's helper goes through Win32_Process.Create (or a one-shot task) instead
|
||||
let p = ps_prelude();
|
||||
assert!(p.starts_with("function Start-IgneumDetached"));
|
||||
assert!(p.contains("Invoke-CimMethod -ClassName Win32_Process -MethodName Create"));
|
||||
assert!(p.contains("schtasks.exe /Create") && p.contains("/SC ONCE"));
|
||||
assert!(p.contains("RESULT detached pid"));
|
||||
// the banned cmdlet's name is assembled here so the windows-spawn gate does not read this test as a spawn
|
||||
let banned = ["Start", "Process"].join("-");
|
||||
assert!(!p.contains(&banned), "the prelude must not start the detached process with {banned} (it stays in the job's tree)");
|
||||
assert!(p.ends_with("# (runner prelude end)\r\n"));
|
||||
// CRLF throughout, as the script body is written
|
||||
assert!(!p.replace("\r\n", "").contains('\n'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevated_wrapper_reports_its_console_then_runs_the_script() {
|
||||
let w = elevated_wrapper("$env:IGNEUM_JOB_ID = 'j1'\r\n", r"C:\jobs\j1\script.ps1", r"C:\jobs\it's\elevated-output.log");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ fn main() {
|
|||
if args.iter().any(|a| a == "--power-helper") {
|
||||
// the scheduled task's action (src/powertask.rs): elevated, runs only digit-argument nvidia-smi commands
|
||||
// from <app data>/app/sweep/cmd.txt, exits on quit, remove or 20 idle minutes
|
||||
let dir = powertask::sweep_dir(&platform::data_root().join("app"));
|
||||
let dir = powertask::helper_dir();
|
||||
std::process::exit(powertask::run_helper(&dir));
|
||||
}
|
||||
if args.iter().any(|a| a == "--launch") {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -103,6 +103,14 @@ pub fn data_root() -> PathBuf {
|
|||
if let Some(p) = std::env::var_os("IGNEUM_APP_DATA") {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
fixed_data_root()
|
||||
}
|
||||
|
||||
/// The platform's own root, with IGNEUM_APP_DATA ignored: where the Power Helper task (started by Windows with no
|
||||
/// environment of ours) reads its command file, so a measurement engine under a scratch root (IGNEUM_APP_DATA set)
|
||||
/// writes its task commands here and not under its own root. 6 October 2026: found while planning the unelevated
|
||||
/// climb through the task; the elevated runs never crossed it (they set the limits directly).
|
||||
pub fn fixed_data_root() -> PathBuf {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
home().join("Library").join("Application Support").join("Igneum")
|
||||
|
|
@ -430,11 +438,21 @@ pub fn sync_clock() -> Result<String, String> {
|
|||
|
||||
/// Runs a command line with administrator rights (one prompt): the NVIDIA power cap needs it on Windows.
|
||||
/// Blocking; call from a thread.
|
||||
/// The cmd.exe argument for one elevated line. cmd's documented rule: when the text after /c starts with a quote
|
||||
/// and holds more than two quotes (two cards, or a cap plus a script), it strips the FIRST and LAST quote and runs
|
||||
/// the broken remainder (PC 1, 6 October 2026, 15:45Z: the one approved Power control step ran
|
||||
/// `"...\nvidia-smi.exe" -i 0 -pl 460 & "...\nvidia-smi.exe" -i 1 -pl 160 & "...\powershell.exe" ... -File "...ps1"`,
|
||||
/// exit 1, no cap applied, no task registered; a single-card PC, two quotes, was fine, which is why PC 2 never
|
||||
/// showed it). Wrapping the whole line in one outer pair makes cmd strip exactly those.
|
||||
pub fn cmd_c_args(cmdline: &str) -> String {
|
||||
format!("/c \"{cmdline}\"")
|
||||
}
|
||||
|
||||
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let cmd = tool("cmd").display().to_string();
|
||||
let mut c = elevated_command(&cmd, &format!("/c {cmdline}"));
|
||||
let mut c = elevated_command(&cmd, &cmd_c_args(cmdline));
|
||||
let out = c.output().map_err(|e| e.to_string())?;
|
||||
if out.status.success() {
|
||||
Ok(())
|
||||
|
|
@ -511,6 +529,20 @@ pub fn quiet(cmd: &mut Command) -> &mut Command {
|
|||
cmd
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cmd_tests {
|
||||
#[test]
|
||||
fn an_elevated_line_is_wrapped_so_cmd_keeps_every_inner_quote() {
|
||||
let line = r#""C:\WINDOWS\System32\nvidia-smi.exe" -i 0 -pl 460 & "C:\WINDOWS\System32\nvidia-smi.exe" -i 1 -pl 160 & "C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -File "C:\x\register-power-task.ps1""#;
|
||||
let a = super::cmd_c_args(line);
|
||||
assert!(a.starts_with("/c \"\"C:\\WINDOWS"), "{a}");
|
||||
assert!(a.ends_with("register-power-task.ps1\"\""), "{a}");
|
||||
// the inner line is intact between the outer pair
|
||||
assert_eq!(&a[4..a.len() - 1], line);
|
||||
assert_eq!(super::cmd_c_args("echo hi"), "/c \"echo hi\"");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod lock_tests {
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -43,8 +43,14 @@ pub enum HelperCmd {
|
|||
PowerLimit(u64),
|
||||
ClockCap(u64),
|
||||
ClockReset,
|
||||
/// Ember 2: the memory clock locked to one value (`-lmc m,m`), or reset (`-rmc`)
|
||||
MemClock(u64),
|
||||
MemReset,
|
||||
Quit,
|
||||
Remove,
|
||||
/// re-point the task at the installed exe (no path argument: the helper finds the install folder itself, so a
|
||||
/// writer of cmd.txt can never choose what runs elevated); 6 October 2026, run 6 registered a scratch copy
|
||||
Reregister,
|
||||
}
|
||||
|
||||
/// Parses one line: `<seq> <verb> [<digits>]` (the 0.3.9 form `<seq> <watts>` reads as a power limit; `quit` and
|
||||
|
|
@ -65,7 +71,10 @@ pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
|
|||
[_, "pl", w] if digits(w) => Some((seq, HelperCmd::PowerLimit(w.parse().ok()?))),
|
||||
[_, "lgc", m] if digits(m) => Some((seq, HelperCmd::ClockCap(m.parse().ok()?))),
|
||||
[_, "rgc"] => Some((seq, HelperCmd::ClockReset)),
|
||||
[_, "lmc", m] if digits(m) => Some((seq, HelperCmd::MemClock(m.parse().ok()?))),
|
||||
[_, "rmc"] => Some((seq, HelperCmd::MemReset)),
|
||||
[_, "dev", d] if digits(d) => Some((seq, HelperCmd::Dev(d.to_string()))),
|
||||
[_, "reregister"] => Some((seq, HelperCmd::Reregister)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -76,10 +85,47 @@ pub fn smi_args(dev: &str, c: &HelperCmd) -> Option<Vec<String>> {
|
|||
HelperCmd::PowerLimit(w) => Some(vec!["-i".into(), dev.into(), "-pl".into(), w.to_string()]),
|
||||
HelperCmd::ClockCap(m) => Some(vec!["-i".into(), dev.into(), "-lgc".into(), format!("0,{m}")]),
|
||||
HelperCmd::ClockReset => Some(vec!["-i".into(), dev.into(), "-rgc".into()]),
|
||||
HelperCmd::MemClock(m) => Some(vec!["-i".into(), dev.into(), "-lmc".into(), format!("{m},{m}")]),
|
||||
HelperCmd::MemReset => Some(vec!["-i".into(), dev.into(), "-rmc".into()]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The executable the task should point at: the running one, unless it runs out of a jobs folder (a measurement
|
||||
/// kit a job fetched: `<app data>\app\jobs\<id>\igneum-app-ember.exe`), in which case the installed app's own
|
||||
/// exe beside it is the durable target (6 October 2026: the first registration on PC 1 came from such a kit).
|
||||
pub fn task_exe(running: &Path, install_candidates: &[PathBuf]) -> PathBuf {
|
||||
// the running exe stands only when it is itself an install candidate (either separator, any case: the path
|
||||
// comes from Windows, the tests run on the Mac too); a kit under jobs\ or a measurement run's scratch copy
|
||||
// (run 6, 6 October 2026: igneum-tune-<stamp>\bin\igneum-app.exe) never becomes the task's action when an
|
||||
// installed exe exists
|
||||
let norm = |p: &Path| p.to_string_lossy().replace('/', "\\").to_ascii_lowercase();
|
||||
let me = norm(running);
|
||||
if install_candidates.iter().any(|p| norm(p) == me) {
|
||||
return running.to_path_buf();
|
||||
}
|
||||
if let Some(p) = install_candidates.iter().find(|p| p.is_file()) {
|
||||
return p.clone();
|
||||
}
|
||||
running.to_path_buf()
|
||||
}
|
||||
|
||||
/// Where the installed exe lives: the per-user install, then the old administrator install.
|
||||
pub fn install_candidates() -> Vec<PathBuf> {
|
||||
[
|
||||
std::env::var_os("LOCALAPPDATA").map(|l| PathBuf::from(l).join("Programs").join("Igneum Miner").join("igneum-app.exe")),
|
||||
std::env::var_os("ProgramFiles").map(|p| PathBuf::from(p).join("Igneum Miner").join("igneum-app.exe")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The PowerShell that prints the task's action (what `reregister` is proven by).
|
||||
pub fn readback_command() -> String {
|
||||
format!("$t = Get-ScheduledTask -TaskName '{TASK_NAME}' -ErrorAction SilentlyContinue; if ($t) {{ Write-Output ($t.Actions[0].Execute + ' ' + $t.Actions[0].Arguments) }}; exit 0")
|
||||
}
|
||||
|
||||
/// The PowerShell that registers the task (run inside the ONE elevated step, with the caps). `exe` is this
|
||||
/// executable's path in the install folder. Principal: the signed-in user, interactive logon, highest run level; no
|
||||
/// trigger; may start on battery; one hour limit per run; multiple starts are ignored while one runs.
|
||||
|
|
@ -174,6 +220,26 @@ pub fn run_helper(dir: &Path) -> i32 {
|
|||
return if ok { 0 } else { 1 };
|
||||
}
|
||||
_ if seq <= last_seq => continue,
|
||||
HelperCmd::Reregister => {
|
||||
last_seq = seq;
|
||||
idle = Instant::now();
|
||||
let Some(target) = install_candidates().into_iter().find(|p| p.is_file()) else {
|
||||
log(&format!("{seq} reregister: no installed exe found"));
|
||||
continue;
|
||||
};
|
||||
let script = dir.join("register-power-task.ps1");
|
||||
let ok = std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), register_script(&target).as_bytes()].concat()).is_ok() && {
|
||||
let mut p = std::process::Command::new(crate::platform::tool("powershell"));
|
||||
p.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
||||
crate::platform::quiet(&mut p);
|
||||
p.status().map(|s| s.success()).unwrap_or(false)
|
||||
};
|
||||
let mut q = std::process::Command::new(crate::platform::tool("powershell"));
|
||||
q.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &readback_command()]);
|
||||
crate::platform::quiet(&mut q);
|
||||
let now = q.output().map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).unwrap_or_default();
|
||||
log(&format!("{seq} reregister {}: the task now runs {now}", if ok { "ok" } else { "failed" }));
|
||||
}
|
||||
HelperCmd::Dev(d) => {
|
||||
last_seq = seq;
|
||||
idle = Instant::now();
|
||||
|
|
@ -206,6 +272,12 @@ pub fn sweep_dir(app_dir: &Path) -> PathBuf {
|
|||
app_dir.join("sweep")
|
||||
}
|
||||
|
||||
/// The one folder the task's helper reads: `<fixed data root>/app/sweep` (never under IGNEUM_APP_DATA). Every
|
||||
/// engine that talks to the task writes its command file here; the `--power-helper` entry reads here.
|
||||
pub fn helper_dir() -> PathBuf {
|
||||
sweep_dir(&crate::platform::fixed_data_root().join("app"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -215,10 +287,21 @@ mod tests {
|
|||
assert_eq!(parse_line("7 pl 460"), Some((7, HelperCmd::PowerLimit(460))));
|
||||
assert_eq!(parse_line("8 lgc 2472"), Some((8, HelperCmd::ClockCap(2472))));
|
||||
assert_eq!(parse_line("9 rgc"), Some((9, HelperCmd::ClockReset)));
|
||||
assert_eq!(parse_line("10 lmc 14001"), Some((10, HelperCmd::MemClock(14001))));
|
||||
assert_eq!(parse_line("11 rmc"), Some((11, HelperCmd::MemReset)));
|
||||
assert_eq!(smi_args("0", &HelperCmd::MemClock(14001)).unwrap(), vec!["-i", "0", "-lmc", "14001,14001"]);
|
||||
assert_eq!(smi_args("0", &HelperCmd::MemReset).unwrap(), vec!["-i", "0", "-rmc"]);
|
||||
assert_eq!(parse_line("3 dev 1"), Some((3, HelperCmd::Dev("1".into()))));
|
||||
assert_eq!(parse_line("5 403"), Some((5, HelperCmd::PowerLimit(403))), "the 0.3.9 form");
|
||||
assert_eq!(parse_line("quit"), Some((0, HelperCmd::Quit)));
|
||||
assert_eq!(parse_line("remove"), Some((0, HelperCmd::Remove)));
|
||||
assert_eq!(parse_line("12 reregister"), Some((12, HelperCmd::Reregister)));
|
||||
assert_eq!(parse_line("12 reregister C:\\evil.exe"), None, "no path argument: the helper picks the install folder itself");
|
||||
assert_eq!(smi_args("0", &HelperCmd::Reregister), None);
|
||||
assert!(readback_command().contains("Actions[0].Execute"));
|
||||
// the helper's folder never follows IGNEUM_APP_DATA (the task has no environment of ours)
|
||||
assert!(helper_dir().ends_with(Path::new("app").join("sweep")));
|
||||
assert!(helper_dir().starts_with(crate::platform::fixed_data_root()));
|
||||
// nothing else: no shell, no path, no string argument, no oversized number
|
||||
for bad in ["7 pl 460; calc", "7 pl -460", "7 pl 4.60", "7 lgc 0,2472", "7 rm C:\\x", "x pl 460", "7 pl", "7 lgc 12345678", "7 dev ../1", "", "7 pl 460 extra"] {
|
||||
assert_eq!(parse_line(bad), None, "{bad:?}");
|
||||
|
|
@ -253,6 +336,24 @@ mod tests {
|
|||
assert!(query_command().contains("Get-ScheduledTask -TaskName 'Igneum Power Helper'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kit_run_from_a_jobs_folder_registers_the_installed_exe() {
|
||||
let kit = Path::new(r"C:\Users\Admin\AppData\Local\igneum\app\jobs\ember-kit-5\igneum-app-ember.exe");
|
||||
let own = Path::new(r"C:\Users\Admin\AppData\Local\Programs\Igneum Miner\igneum-app.exe");
|
||||
// no installed candidate exists on this machine: the running exe stands
|
||||
assert_eq!(task_exe(kit, &[PathBuf::from(r"C:\nonexistent\igneum-app.exe")]), kit.to_path_buf());
|
||||
assert_eq!(task_exe(own, &[]), own.to_path_buf());
|
||||
// an installed candidate that exists (the test binary stands in for it) wins for a kit, not for the installed exe
|
||||
let here = std::env::current_exe().unwrap();
|
||||
assert!(here.is_file());
|
||||
assert_eq!(task_exe(kit, &[here.clone()]), here);
|
||||
// a measurement run's scratch copy (run 6, 6 October 2026) is not under jobs\ and still yields
|
||||
let scratch = Path::new(r"C:\Users\Admin\AppData\Local\igneum-tune-20261006-170105\bin\igneum-app.exe");
|
||||
assert_eq!(task_exe(scratch, &[here.clone()]), here);
|
||||
// the installed exe itself stands (matched by path, any case or separator)
|
||||
assert_eq!(task_exe(own, &[PathBuf::from(r"c:/users/admin/appdata/local/programs/igneum miner/IGNEUM-APP.EXE"), here]), own.to_path_buf());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stale_command_file_does_not_run_at_start() {
|
||||
// the helper's start reads the highest sequence already in the file and runs nothing below or at it
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -132,6 +132,25 @@ struct Tools {
|
|||
cuda: bool,
|
||||
}
|
||||
|
||||
/// The node's re-derivation boundary (0.3.14, 6 October 2026): the chain block its EVM restarted at after the
|
||||
/// bodies below the pruning point were gone (`igneum_getExecStatus.restartNumber`; on a 0.3.13 node the
|
||||
/// `startedFrom` text "restart at chain block N"), else 0. The prover never claims work below it (no bodies, no
|
||||
/// state: unprovable on every node).
|
||||
fn exec_boundary(shared: &Shared) -> u64 {
|
||||
let st = match evm_rpc(shared, "igneum_getExecStatus", json!([]), Duration::from_secs(5)) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return 0,
|
||||
};
|
||||
if let Some(n) = st["restartNumber"].as_u64() {
|
||||
return n;
|
||||
}
|
||||
if let Some(n) = st["restartNumber"].as_str().and_then(|x| u64::from_str_radix(x.trim_start_matches("0x"), 16).ok()) {
|
||||
return n;
|
||||
}
|
||||
let text = st["startedFrom"].as_str().unwrap_or("");
|
||||
text.strip_prefix("restart at chain block ").and_then(|t| t.split_whitespace().next()).and_then(|t| t.parse().ok()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn evm_rpc(shared: &Shared, method: &str, params: Value, timeout: Duration) -> Result<Value, String> {
|
||||
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
|
||||
let tmp = std::env::temp_dir().join(format!("igneum-prover-{}-{}.json", std::process::id(), method));
|
||||
|
|
@ -602,6 +621,9 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
// shards below the re-derivation boundary are never attempted (no bodies, no state on any node)
|
||||
let boundary = exec_boundary(&shared);
|
||||
let work: Vec<Work> = work.into_iter().filter(|w| w.number >= boundary).collect();
|
||||
let Some(w) = choose(&work, &attempted) else {
|
||||
set(&shared, |p| {
|
||||
p.status = if submitted.is_empty() { "idle".into() } else { "submitted".into() };
|
||||
|
|
@ -634,7 +656,11 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
let fixture = dir.join(format!("block-{}.json", w.number));
|
||||
let results = dir.join(format!("results-{}-{}.json", w.number, w.shard));
|
||||
let outcome: Result<(), String> = (|| {
|
||||
let export = evm_rpc(&shared, "igneum_exportSegments", json!(["0x0", format!("{:#x}", w.number)]), Duration::from_secs(120))?;
|
||||
// the export from one block below (0.3.14): the node's account dump after block n-1 seeds the exporter;
|
||||
// never from 0 (on a restarted node the records below the restart carry zero roots and the exporter
|
||||
// refused every cut, the fleet 16:02Z)
|
||||
let from = w.number.saturating_sub(1);
|
||||
let export = evm_rpc(&shared, "igneum_exportSegments", json!([format!("{from:#x}"), format!("{:#x}", w.number)]), Duration::from_secs(120))?;
|
||||
std::fs::write(&seq, export.to_string()).map_err(|e| e.to_string())?;
|
||||
let (seq_p, fix_p, res_p) = if t.wsl { (wsl_path(&seq), wsl_path(&fixture), wsl_path(&results)) } else { (seq.display().to_string(), fixture.display().to_string(), results.display().to_string()) };
|
||||
let (ok, out) = run_tool(&shared, t, &t.export, &[seq_p, w.number.to_string(), fix_p.clone()], &[], Duration::from_secs(600), &dir.join(format!("export-{}.log", w.number)));
|
||||
|
|
@ -750,6 +776,9 @@ fn pick_segment(shared: &Shared, work: &[Work], key_hash: &str, attempted: &mut
|
|||
return None;
|
||||
}
|
||||
let (start, n, unproven, tip_daa) = (hexu(&v1["start"]), hexu(&v1["segmentBlocks"]).max(1), hexu(&v1["unprovenDaa"]), hexu(&st["tipDaa"]));
|
||||
// segments below the re-derivation boundary are never claimed: no bodies, no state, unprovable anywhere
|
||||
let boundary = exec_boundary(shared);
|
||||
let start = if boundary > start { boundary.div_ceil(n) * n } else { start };
|
||||
let segs = crate::segments::whole_segments(start, n, unproven, work);
|
||||
let need = crate::segments::need_daa(last_secs);
|
||||
let cands = crate::segments::candidates(&segs, tip_daa, need, key_hash, attempted);
|
||||
|
|
@ -815,7 +844,10 @@ fn prove_segment(shared: &Shared, t: &Tools, seg: &crate::segments::SegmentWork,
|
|||
shared.log(&format!("prover: segment {first}..{last} claimed ({} shards{}): export, cut, chain ({}), sign, submit", seg.shards.len(), if prev_file.is_some() { ", continuing the previous segment's proof" } else { ", fresh" }, if t.cuda { "CUDA" } else { "CPU" }));
|
||||
// 1. export once, cut every block
|
||||
let seq = dir.join("seq.json");
|
||||
let export = evm_rpc(shared, "igneum_exportSegments", json!(["0x0", format!("{last:#x}")]), Duration::from_secs(300))?;
|
||||
// the export from one block below the segment (0.3.14): the node's account dump after block first-1 seeds the
|
||||
// exporter; nothing older is read
|
||||
let from = first.saturating_sub(1);
|
||||
let export = evm_rpc(shared, "igneum_exportSegments", json!([format!("{from:#x}"), format!("{last:#x}")]), Duration::from_secs(300))?;
|
||||
std::fs::write(&seq, export.to_string()).map_err(|e| e.to_string())?;
|
||||
let mut fixtures: Vec<String> = Vec::new();
|
||||
for b in first..=last {
|
||||
|
|
|
|||
|
|
@ -251,6 +251,14 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
shared.send(Cmd::ApplyCards(choices));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/tune/goal" => {
|
||||
// Ember 2: the goal, the electricity price (pence per kWh) and the hill-climb switch
|
||||
let goal = body.get("goal").and_then(|v| v.as_str()).map(|g| crate::ember::Goal::parse(g).name().to_string());
|
||||
let price = body.get("price_pence").and_then(|v| v.as_f64()).filter(|p| (0.0..=500.0).contains(p));
|
||||
let climb = body.get("climb").and_then(|v| v.as_bool());
|
||||
shared.send(Cmd::TuneGoal(goal, price, climb));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/settings" => {
|
||||
let identities = body.get("identities").and_then(|v| v.as_u64()).map(|v| v.clamp(1, 64) as u32);
|
||||
let vote = body.get("vote").and_then(|v| v.as_bool());
|
||||
|
|
@ -325,6 +333,11 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
shared.send(Cmd::PowerControl(on));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
// a measurement engine's live tune state for one card (forwarded by its job playbook): the row shows the step
|
||||
"/api/tune-progress" => {
|
||||
shared.send(Cmd::TuneProgress(body));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
"/api/sweep/enable" => {
|
||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||
shared.send(Cmd::SweepEnable(on));
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ pub struct CardState {
|
|||
pub clock_min_mhz: u32, // the vendor's floor for a cap (0 = 60% of the maximum)
|
||||
pub gclk_mhz: f64, // core clock now
|
||||
pub clock_cap_mhz: u32, // the cap in force (0 = unlocked)
|
||||
pub mem_cap_mhz: u32, // Ember 2: the memory clock set by the tune (0 = the driver's default)
|
||||
pub amd_ordinal: i64, // the `amd N` ordinal of igneum-gpu-telemetry (-1 = unknown)
|
||||
pub driver: String, // the driver version (nvidia-smi, or the worker's race line)
|
||||
pub program_class: String, // the program class of the race line (loads and wide loads per hash); "" = unknown
|
||||
|
|
@ -125,6 +126,27 @@ pub struct CardState {
|
|||
pub tune_clock_mhz: u32, // the clock cap the last tune chose (0 = unlocked)
|
||||
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 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,
|
||||
pub tune_plan: String,
|
||||
// Ember 2: the memory clock the last tune chose and the measured curve (every row of the last plan)
|
||||
pub tune_mem_mhz: u32,
|
||||
pub tune_curve: Vec<serde_json::Value>,
|
||||
// the core-clock knob (7 October 2026, src/ember.rs lock_result; the UI lane's field shape): the chosen lock
|
||||
// against the cap point's unlocked row, the step while the clock search runs, the moment and the stop reason
|
||||
pub lock_mhz: u32, // the chosen core clock cap (0 = unlocked)
|
||||
pub lock_mhs: f64,
|
||||
pub lock_w: f64,
|
||||
pub lock_mhw: f64,
|
||||
pub unlocked_mhs: f64, // the cap point's row the lock is read against
|
||||
pub unlocked_w: f64,
|
||||
pub lock_step: u32, // the clock search's step while it runs (0 otherwise)
|
||||
pub lock_steps: u32,
|
||||
pub lock_at: f64, // unix s the lock point was taken (0 = never)
|
||||
pub lock_note: String, // "rate fell 5.1 percent at 1200 MHz", "fingerprint mismatch at 1400 MHz, clocks reset", "the floor at 700 MHz", "no lever"
|
||||
// the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose
|
||||
pub variant: String,
|
||||
pub race_mhs: f64,
|
||||
|
|
@ -283,6 +305,13 @@ pub struct SettingsState {
|
|||
/// Ember Tune is paused fleet-wide by the signed manifest's kill switch (tuning.ember.enabled = false)
|
||||
pub tuning_off: bool,
|
||||
pub tuning_note: String,
|
||||
/// Ember 2: the goal (efficiency | balanced | rate), the electricity price in pence per kWh, the hill-climb switch
|
||||
pub tune_goal: String,
|
||||
pub power_price_pence: f64,
|
||||
pub tune_climb: bool,
|
||||
/// the manifest's tune period in seconds (tuning.ember.period_s, default 7 days): a card is due again at
|
||||
/// sweep_at + tune_period_s, or at once after a driver major or program-class change
|
||||
pub tune_period_s: u64,
|
||||
/// the miner software's dev fee switch (settings; `--dev-fee 0` when off)
|
||||
pub dev_fee: bool,
|
||||
/// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`)
|
||||
|
|
|
|||
|
|
@ -375,6 +375,12 @@ while ($true) {
|
|||
} elseif ($op -eq 'rgc') {
|
||||
$out = (& $Smi -i $Device -rgc 2>&1 | Out-String).Trim()
|
||||
"$(Get-Date -Format o) $($p[0]) -rgc : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
||||
} elseif ($op -eq 'lmc' -and $v -match '^\d+$') {
|
||||
$out = (& $Smi -i $Device -lmc "$v,$v" 2>&1 | Out-String).Trim()
|
||||
"$(Get-Date -Format o) $($p[0]) -lmc $v,$v : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
||||
} elseif ($op -eq 'rmc') {
|
||||
$out = (& $Smi -i $Device -rmc 2>&1 | Out-String).Trim()
|
||||
"$(Get-Date -Format o) $($p[0]) -rmc : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -411,6 +417,8 @@ while true; do
|
|||
pl) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -pl $v : $("$smi" -i "$dev" -pl "$v" 2>&1)" >> "$dir/helper.log";; esac ;;
|
||||
lgc) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -lgc 0,$v : $("$smi" -i "$dev" -lgc "0,$v" 2>&1)" >> "$dir/helper.log";; esac ;;
|
||||
rgc) echo "$(date -u +%FT%TZ) $1 -rgc : $("$smi" -i "$dev" -rgc 2>&1)" >> "$dir/helper.log" ;;
|
||||
lmc) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -lmc $v,$v : $("$smi" -i "$dev" -lmc "$v,$v" 2>&1)" >> "$dir/helper.log";; esac ;;
|
||||
rmc) echo "$(date -u +%FT%TZ) $1 -rmc : $("$smi" -i "$dev" -rmc 2>&1)" >> "$dir/helper.log" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
|
@ -614,6 +622,7 @@ mod tests {
|
|||
for s in [helper_script_windows(), helper_script_unix()] {
|
||||
assert!(s.contains("cmd.txt") && s.contains("quit") && s.contains("-pl") && s.contains("20 min"));
|
||||
assert!(s.contains("-lgc") && s.contains("-rgc"), "the clock cap and its reset");
|
||||
assert!(s.contains("-lmc") && s.contains("-rmc"), "Ember 2: the memory clock and its reset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\\<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("\\\\?\\D:\\x")), "/mnt/d/x");
|
||||
assert_eq!(wsl_path(Path::new("/tmp/x")), "/tmp/x");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
/* Igneum Miner dashboard (miner-ui-2). The site's tokens (site/index.html): obsidian, graphite, ember, molten, bone,
|
||||
ash; Unbounded for headings, IBM Plex Sans for text, IBM Plex Mono for numbers and labels. Fonts ship in the binary.
|
||||
One type scale (--t-*), one spacing scale (--s-*), one colour set (:root), one accent (ember), used by every page.
|
||||
Layout: a rail on the left (six sections), a thin top bar, the status strip under it, the page in the middle, the
|
||||
log drawer at the bottom. The window lays out from 900 x 600 up (the hosts say the same minimum).
|
||||
/* Igneum Miner dashboard (miner-ui-3). The site's tokens (site/index.html): obsidian, graphite, ember, molten, bone,
|
||||
ash; Unbounded for the page title, the big button and the hero numbers; IBM Plex Sans for words; IBM Plex Mono for
|
||||
labels, units and small numbers. Fonts ship in the binary. One type scale (--t-*), one spacing scale (--s-*), one
|
||||
accent (ember). Dark by default, light under prefers-color-scheme or [data-theme=light]. Layout: a rail on the left
|
||||
(four sections), a thin top bar, the status strip under it, the page in the middle, the log drawer at the bottom.
|
||||
The window lays out from 900 x 600 (the hosts' minimum); under 720 px the rail becomes a bottom tab bar.
|
||||
Nothing here is newer than 2022 CSS (the WebView2 host). */
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexMono-400.woff2) format('woff2')}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/IBMPlexMono-500.woff2) format('woff2')}
|
||||
|
|
@ -14,36 +15,42 @@
|
|||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Unbounded-900.woff2) format('woff2')}
|
||||
|
||||
:root{
|
||||
/* colour */
|
||||
--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;
|
||||
--ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--node-blue:#7FA7C9;--nvidia:#8BE37A;--rail-bg:#111114;
|
||||
/* colour, dark */
|
||||
--obsidian:#0C0C0E;--graphite:#16161A;--row:#111114;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;
|
||||
--ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--nvidia:#8BE37A;--rail-bg:#111114;--hover:rgba(255,255,255,.04);--top-bg:rgba(12,12,14,.86);--shadow:rgba(0,0,0,.6);--scrim:rgba(12,12,14,.72);--ok:#FFB35C;
|
||||
/* type scale */
|
||||
--t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:16px;--t-2xl:18px;--t-num:26px;--t-h3:16px;--t-h2:32px;--t-h1:52px;
|
||||
--t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:17px;--t-num:24px;--t-hero:34px;--t-h2:28px;--t-h1:44px;
|
||||
/* spacing scale */
|
||||
--s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:20px;--s-6:28px;--s-7:40px;
|
||||
--gutter:var(--s-6);--card-pad:22px;--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:var(--s-5);--gap-tile:var(--s-3);
|
||||
--s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:24px;--s-6:32px;
|
||||
--gutter:var(--s-6);--card-pad:var(--s-5);--card-r:16px;--row-r:12px;--gap:var(--s-4);
|
||||
--sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif;
|
||||
--top:60px;--bottom:0px;--drawer-h:260px;--rail:196px}
|
||||
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){
|
||||
--obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF;
|
||||
--ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F}}
|
||||
:root[data-theme="light"]{
|
||||
--obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF;
|
||||
--ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-lg);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums}
|
||||
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-md);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums}
|
||||
.mono,code,pre{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||
.dim{color:var(--ash)}
|
||||
h1,h2,h3{font-family:var(--head);margin:0;line-height:1.1;text-wrap:balance}
|
||||
h1{font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em}
|
||||
h2{font-weight:700;font-size:var(--t-h2)}
|
||||
h3{font-weight:700;font-size:var(--t-h3)}
|
||||
h3{font-weight:700;font-size:var(--t-xl)}
|
||||
p{margin:0}
|
||||
a{color:inherit}
|
||||
button{font:inherit;color:inherit}
|
||||
input,textarea{font-variant-numeric:tabular-nums}
|
||||
.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap}
|
||||
.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.16em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap}
|
||||
.eyebrow.ember{color:var(--ember)}
|
||||
[hidden]{display:none !important}
|
||||
::selection{background:rgba(242,84,27,.45)}
|
||||
|
||||
/* buttons */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:44px;padding:10px 20px;border-radius:10px;font-weight:600;font-size:var(--t-lg);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap}
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:40px;padding:8px 18px;border-radius:10px;font-weight:600;font-size:var(--t-md);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap}
|
||||
.btn:hover{transform:translateY(-1px);border-color:var(--ash)}
|
||||
.btn:active{transform:none}
|
||||
.btn:disabled{opacity:.4;cursor:default;transform:none}
|
||||
|
|
@ -68,9 +75,9 @@ body.mac .rail-brand{padding-top:30px}
|
|||
.rail-nav{display:flex;flex-direction:column;gap:3px}
|
||||
.nav{position:relative;display:flex;align-items:center;gap:12px;width:100%;min-height:42px;padding:8px 12px;border:1px solid transparent;border-radius:11px;background:transparent;color:var(--ink-2);font-weight:500;font-size:var(--t-md);text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
|
||||
.nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease}
|
||||
.nav:hover{background:rgba(255,255,255,.04);color:var(--bone)}
|
||||
.nav:hover{background:var(--hover);color:var(--bone)}
|
||||
.nav:hover svg{color:var(--ink-2)}
|
||||
.nav.on{background:var(--ember-12);border-color:rgba(242,84,27,.28);color:var(--bone);font-weight:600}
|
||||
.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600}
|
||||
.nav.on svg{color:var(--ember)}
|
||||
.nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)}
|
||||
.nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)}
|
||||
|
|
@ -85,7 +92,7 @@ body.mac .rail-brand{padding-top:30px}
|
|||
.rail-version{font-size:var(--t-xs);color:var(--ash);padding:8px 12px 0;letter-spacing:.06em}
|
||||
|
||||
/* top bar */
|
||||
.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:rgba(12,12,14,.86);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid rgba(42,42,48,.7);z-index:20;-webkit-app-region:drag}
|
||||
.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:var(--top-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--line);z-index:20;-webkit-app-region:drag}
|
||||
.top button,.top .pill{-webkit-app-region:no-drag}
|
||||
body.mac:not(.has-rail) .top{padding-left:92px}
|
||||
body.has-rail .top{left:var(--rail)}
|
||||
|
|
@ -96,21 +103,17 @@ body.has-rail .top{left:var(--rail)}
|
|||
.page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap}
|
||||
.page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0}
|
||||
.top-status{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:380px}
|
||||
.top-status .pc+.pc::before{content:"·";margin:0 7px;color:var(--line-2)}
|
||||
.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center}
|
||||
.pill.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.pill.warn{color:var(--ember)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px}
|
||||
.dot.small{width:7px;height:7px;flex-basis:7px}
|
||||
.on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite}
|
||||
.warn .dot{background:var(--ember);animation:none}
|
||||
.warn .dot,.dot.bad{background:var(--ember);animation:none}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,179,92,.5)}50%{box-shadow:0 0 0 7px rgba(255,179,92,0)}}
|
||||
@media (prefers-reduced-motion:reduce){.on .dot,.dot.live{animation:none}}
|
||||
|
||||
/* the status strip under the top bar (app.js, Notices): one notice at a time. It takes no room while empty; main's
|
||||
top moves once per change with a 150 ms transition (layoutStrip), never per poll. Tones: default molten (running,
|
||||
available, done), bad ember (failed, clock block, urgent). */
|
||||
top moves once per change with a 150 ms transition (layoutStrip), never per poll. */
|
||||
.notices{position:fixed;top:var(--top);left:0;right:0;z-index:19}
|
||||
body.has-rail .notices{left:var(--rail)}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)}
|
||||
|
|
@ -124,11 +127,18 @@ body.has-rail .notices{left:var(--rail)}
|
|||
.notice-close:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice.update-urgent .notice-close{color:#fff}
|
||||
.notice-detail{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-3px}
|
||||
.notice .prog{flex-basis:100%;height:3px;background:rgba(255,255,255,.12);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear}
|
||||
|
||||
/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */
|
||||
.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none}
|
||||
body.has-rail .ask-wrap{left:var(--rail)}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0}
|
||||
.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none}
|
||||
|
||||
/* screens and pages */
|
||||
main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease}
|
||||
main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease}
|
||||
@media (prefers-reduced-motion:reduce){main{transition:none}}
|
||||
body.has-rail main{left:var(--rail)}
|
||||
body.drawer-open main{bottom:var(--drawer-h)}
|
||||
|
|
@ -140,15 +150,14 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.page{display:flex;flex-direction:column;gap:var(--gap);animation:rise .3s ease}
|
||||
|
||||
/* welcome */
|
||||
.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-7) 0 48px}
|
||||
.coin-wrap{position:relative;width:148px;height:148px;margin-bottom:6px}
|
||||
.coin-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite}
|
||||
.coin{position:relative;display:block;border-radius:50%;filter:drop-shadow(0 10px 30px rgba(242,84,27,.35))}
|
||||
.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-6) 0 48px}
|
||||
.mark-wrap{position:relative;width:120px;height:120px;border-radius:26px;background:#0C0C0E;display:flex;align-items:center;justify-content:center;margin-bottom:6px;box-shadow:0 20px 50px rgba(242,84,27,.25)}
|
||||
.mark-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite;z-index:-1}
|
||||
@keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
|
||||
@media (prefers-reduced-motion:reduce){.coin-wrap::before{animation:none}}
|
||||
.lead{font-size:var(--t-2xl);color:var(--ink-2);max-width:54ch}
|
||||
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap-tile);width:100%;max-width:860px;margin-top:10px;text-align:left}
|
||||
.tile{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||
@media (prefers-reduced-motion:reduce){.mark-wrap::before{animation:none}}
|
||||
.lead{font-size:var(--t-xl);color:var(--ink-2);max-width:54ch}
|
||||
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-3);width:100%;max-width:860px;margin-top:10px;text-align:left}
|
||||
.tile{background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||
.tile .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;color:var(--ember)}
|
||||
.tile .t{font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.25}
|
||||
.tile .s{font-size:var(--t-base);color:var(--ash);line-height:1.45}
|
||||
|
|
@ -162,6 +171,9 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.note{font-size:var(--t-base);color:var(--ash);line-height:1.5}
|
||||
.note.small{font-size:var(--t-sm);word-break:break-all}
|
||||
.help{font-size:var(--t-base);color:var(--ash);line-height:1.5;max-width:64ch}
|
||||
.line-text{font-size:var(--t-md);color:var(--ink-2);line-height:1.5}
|
||||
.line-text.ok{color:var(--molten)}
|
||||
.line-text.bad{color:var(--ember)}
|
||||
.cards{display:flex;flex-direction:column;gap:var(--s-3);margin-top:var(--s-2)}
|
||||
.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0}
|
||||
.card.detecting{display:flex;align-items:center;gap:18px}
|
||||
|
|
@ -169,11 +181,13 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.card.detecting .s{font-size:var(--t-sm);color:var(--ash);margin-top:var(--s-1)}
|
||||
.spinner{width:28px;height:28px;border-radius:50%;border:3px solid var(--line-2);border-top-color:var(--ember);animation:spin 1s linear infinite;flex:0 0 28px}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.top-gap{margin-top:var(--s-4)}
|
||||
.indent{margin-left:52px}
|
||||
|
||||
/* GPU rows (first run) */
|
||||
.gpu-row{display:flex;align-items:center;gap:var(--s-4);background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0;flex-wrap:wrap}
|
||||
.gpu-row.off{opacity:.72}
|
||||
.badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
|
||||
.badge{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 44px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
|
||||
.badge.apple{color:var(--bone)}
|
||||
.badge.nvidia{color:var(--nvidia)}
|
||||
.badge.amd{color:var(--ember)}
|
||||
|
|
@ -183,17 +197,15 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.kind.discrete,.kind.apple{color:var(--molten);border-color:var(--molten-40)}
|
||||
.kind.external{color:var(--bone)}
|
||||
.gpu-row .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px}
|
||||
.gpu-row .meta b{color:var(--ink-2);font-weight:500}
|
||||
.gpu-row .reason{font-size:var(--t-sm);color:var(--ash)}
|
||||
.gpu-row .msg{font-size:var(--t-sm);color:var(--ember)}
|
||||
.gpu-row .switch{padding:0;flex:0 0 auto;margin-left:auto}
|
||||
/* hot-plug (src/hotplug.rs): a removed card dims, a faulty one is named in ember, neither has live controls */
|
||||
.gpu-row.removed,.gpu-line.removed,.set-card.removed{opacity:.5}
|
||||
.gpu-row.unusable .name,.gpu-line.unusable .name,.set-card.unusable .name{color:var(--ember)}
|
||||
.gpu-row.removed,.gpu-line.removed{opacity:.5}
|
||||
.gpu-row.unusable .name,.gpu-line.unusable .name{color:var(--ember)}
|
||||
|
||||
/* the Mine page: the big switch and the three numbers */
|
||||
.hero-row{display:grid;grid-template-columns:1.3fr 1fr 1fr 1fr;gap:var(--gap-tile)}
|
||||
.toggle-big{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;min-height:112px;padding:18px 20px;border-radius:var(--tile-r);border:1px solid var(--ember);background:var(--ember);color:var(--ember-ink);cursor:pointer;text-align:left;transition:background .15s ease,border-color .15s ease,transform .15s ease,color .15s ease;min-width:0}
|
||||
/* the Mine page: the big button and the live total */
|
||||
.hero-row{display:grid;grid-template-columns:minmax(220px,1fr) 2fr;gap:var(--s-3)}
|
||||
.toggle-big{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;min-height:112px;padding:18px 20px;border-radius:14px;border:1px solid var(--ember);background:var(--ember);color:var(--ember-ink);cursor:pointer;text-align:left;transition:background .15s ease,border-color .15s ease,transform .15s ease,color .15s ease;min-width:0}
|
||||
.toggle-big:hover{background:var(--ember-hi);border-color:var(--ember-hi);transform:translateY(-1px)}
|
||||
.toggle-big:disabled{opacity:.45;cursor:default;transform:none}
|
||||
.toggle-big .ring{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(12,12,14,.18);margin-bottom:6px}
|
||||
|
|
@ -201,64 +213,50 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.toggle-big .tl{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.15;white-space:nowrap}
|
||||
.toggle-big .ts{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
||||
.toggle-big.stop{background:var(--graphite);border-color:var(--line-2);color:var(--bone)}
|
||||
.toggle-big.stop:hover{border-color:var(--ash);background:#1b1b20}
|
||||
.toggle-big.stop:hover{border-color:var(--ash);background:var(--row)}
|
||||
.toggle-big.stop .ring{background:var(--ember-12);color:var(--ember)}
|
||||
.toggle-big.stop .ts{color:var(--molten);opacity:1}
|
||||
.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)}
|
||||
.strip.three{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||
.cell .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash);white-space:nowrap}
|
||||
.cell .v{font-family:var(--head);font-weight:700;font-size:var(--t-num);line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:var(--s-2);min-height:1.1em}
|
||||
.cell.ember .v{color:var(--ember)}
|
||||
.cell .v .unit{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);font-weight:500;letter-spacing:.08em}
|
||||
.cell .v.state{text-transform:capitalize;font-size:22px}
|
||||
.cell .v.small{font-size:18px}
|
||||
.cell .v.dim{color:var(--ash)}
|
||||
.cell .s{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cell.ok .v.state{color:var(--molten)}
|
||||
.cell.bad .v.state{color:var(--ember)}
|
||||
.cell.bad .s{color:var(--ember)}
|
||||
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:var(--gap);align-items:start}
|
||||
.totals{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s-3);background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;min-width:0}
|
||||
.tot{display:flex;flex-direction:column;min-width:0;padding-left:var(--s-4);border-left:1px solid var(--line)}
|
||||
.tot:first-child{padding-left:0;border-left:0}
|
||||
.tot .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.01em}
|
||||
.tot .v.ember{color:var(--ember)}
|
||||
.tot .v.ask-price{font-family:var(--sans);font-size:var(--t-md);font-weight:600;color:var(--molten);text-decoration:underline;text-underline-offset:3px;cursor:pointer;padding:9px 0 8px}
|
||||
.tot .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--ash);margin-top:4px}
|
||||
.tot .s{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
|
||||
.card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);margin-bottom:var(--s-3);flex-wrap:wrap}
|
||||
.stats{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:var(--t-sm);color:var(--ash)}
|
||||
.stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums}
|
||||
#dag{display:block;width:100%;height:170px;border-radius:12px;background:var(--obsidian)}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:14px 18px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ink-2)}
|
||||
.legend span{display:inline-flex;align-items:center;gap:var(--s-2)}
|
||||
.sw{width:12px;height:12px;border-radius:3px;display:inline-block;border:1px solid var(--line-2)}
|
||||
.sw.ember{background:var(--ember);border-color:var(--ember)}
|
||||
.sw.glow{border-color:var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)}
|
||||
.sw.line{width:18px;height:0;border:0;border-top:1px dashed var(--line-2);border-radius:0}
|
||||
.head-right{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;min-width:0}
|
||||
.stats{font-size:var(--t-sm);color:var(--ash)}
|
||||
#dag{display:block;width:100%;height:110px;border-radius:10px;background:var(--obsidian);margin-bottom:var(--s-3)}
|
||||
.empty{color:var(--ash);font-size:var(--t-base);padding:10px 0}
|
||||
.empty.err{color:var(--ember)}
|
||||
.kv{display:flex;flex-direction:column}
|
||||
.kv>div{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)}
|
||||
.kv>div{display:grid;grid-template-columns:120px auto 1fr;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)}
|
||||
.kv>div:last-child{border-bottom:0}
|
||||
.kv .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap}
|
||||
.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right}
|
||||
.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.kv .v.ok{color:var(--molten)}
|
||||
.kv .v.warn{color:var(--ember)}
|
||||
.kv .v.dim{color:var(--ash)}
|
||||
.kv .m{font-size:var(--t-sm);color:var(--ash);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.card .note{margin-top:10px}
|
||||
.card .help+.help{margin-top:6px}
|
||||
.big-word{font-family:var(--head);font-weight:700;font-size:20px;line-height:1.2;margin:2px 0 8px;word-break:break-word}
|
||||
.big-word.ok{color:var(--molten)}
|
||||
.big-word.warn{color:var(--ember)}
|
||||
.big-word.dim{color:var(--ash)}
|
||||
.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);max-height:260px;overflow:auto}
|
||||
.feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:8px 0;align-items:baseline}
|
||||
.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2)}
|
||||
.feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:7px 0;align-items:baseline}
|
||||
.feed>div:last-child{border-bottom:0}
|
||||
.feed>div>span:first-child{min-width:0}
|
||||
.feed>div>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.feed .t{color:var(--ash);flex:0 0 auto;font-size:var(--t-xs)}
|
||||
.feed .k{color:var(--molten);margin-right:6px}
|
||||
.feed .k.error{color:var(--ember)}
|
||||
.feed .k.warn{color:var(--ember)}
|
||||
.feed .k.block{color:var(--ember)}
|
||||
.feed .k.build{color:var(--ink-2)}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{color:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{color:var(--ash)}
|
||||
|
||||
/* the GPU rows on the Mine page: badge, name, numbers, the switch */
|
||||
/* the card rows on Mine (the hero object): badge, name and state, the numbers, Tune, the switch, the chevron;
|
||||
the tune line under; the details under that */
|
||||
.gpu-list{display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.gpu-line{display:grid;grid-template-columns:44px minmax(160px,1.4fr) repeat(3,minmax(84px,.7fr)) 48px;align-items:center;gap:var(--s-4);padding:12px 14px;border:1px solid var(--line);border-radius:var(--tile-r);background:var(--obsidian);min-width:0}
|
||||
.gpu-line{border:1px solid var(--line);border-radius:var(--row-r);background:var(--row);min-width:0}
|
||||
.gpu-line.off{opacity:.62}
|
||||
.gpu-line .badge{width:44px;height:44px;flex-basis:44px;border-radius:12px}
|
||||
.gpu-line.open{border-color:var(--line-2)}
|
||||
.gl-main{display:grid;grid-template-columns:44px minmax(150px,1.2fr) auto auto;align-items:center;gap:var(--s-4);padding:12px 14px}
|
||||
.gpu-line .who{min-width:0;display:flex;flex-direction:column;gap:3px}
|
||||
.gpu-line .name{font-family:var(--head);font-weight:700;font-size:var(--t-md);line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.gpu-line .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
|
|
@ -267,46 +265,75 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.gpu-line .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block;flex:0 0 7px}
|
||||
.gpu-line .msg{font-size:var(--t-sm);color:var(--ember);line-height:1.4}
|
||||
.gpu-line .msg.dim{color:var(--ash)}
|
||||
.gpu-line .num{display:flex;flex-direction:column;gap:2px;min-width:0}
|
||||
.gpu-line .num .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);white-space:nowrap}
|
||||
.gpu-line .num .v{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:5px}
|
||||
.gpu-line .num .v .unit{font-family:var(--mono);font-size:10px;color:var(--ash);font-weight:500;letter-spacing:.08em}
|
||||
.gpu-line .num .v.hot{color:var(--ember)}
|
||||
.gpu-line .num .v.warm{color:var(--molten)}
|
||||
.gpu-line .num .v.na{color:var(--ash);font-weight:500;font-size:var(--t-md)}
|
||||
.nums{display:flex;gap:var(--s-5);justify-content:flex-end;align-items:flex-start;min-width:0;flex-wrap:wrap}
|
||||
.num{display:flex;flex-direction:column;gap:1px;min-width:56px;align-items:flex-end;text-align:right}
|
||||
.num .v{font-family:var(--head);font-weight:700;font-size:20px;line-height:1.15;white-space:nowrap;display:flex;align-items:baseline;gap:4px}
|
||||
.num .v .unit{font-family:var(--mono);font-size:10px;color:var(--ash);font-weight:500;letter-spacing:.08em}
|
||||
.num .k{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap}
|
||||
.num .v.hot{color:var(--ember)}
|
||||
.num .v.warm{color:var(--molten)}
|
||||
.gpu-line.on .num.hash .v{color:var(--ember)}
|
||||
.gpu-line .switch{padding:0;justify-self:end}
|
||||
|
||||
/* the Settings page: per-card controls */
|
||||
.set-cards{display:flex;flex-direction:column;gap:var(--s-3);margin-bottom:var(--s-3)}
|
||||
.set-card{border:1px solid var(--line);border-radius:var(--tile-r);background:var(--obsidian);padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0}
|
||||
.set-card .head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.set-card .head .name{font-family:var(--head);font-weight:700;font-size:var(--t-md)}
|
||||
.set-card .head .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)}
|
||||
.set-card .ctl{display:grid;grid-template-columns:150px 1fr auto;align-items:center;gap:var(--s-3)}
|
||||
.set-card .ctl .k{font-size:var(--t-md);color:var(--bone);font-weight:500}
|
||||
.set-card .ctl .pv{font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);min-width:110px;text-align:right;white-space:nowrap}
|
||||
.set-card input[type=range]{width:100%;accent-color:var(--ember);margin:0}
|
||||
.set-card .ctl .help{grid-column:1 / -1;margin-top:-4px}
|
||||
.set-card .line{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center}
|
||||
.set-card .line b{color:var(--ink-2);font-weight:500}
|
||||
.set-card .line.ok{color:var(--molten)}
|
||||
.set-card .line.hot{color:var(--ember)}
|
||||
.set-card .line.on{color:var(--molten)}
|
||||
.set-card .line .pin{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px}
|
||||
.acts{display:flex;align-items:center;gap:var(--s-3);justify-self:end}
|
||||
.acts .switch{padding:0}
|
||||
.chev-btn{width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ash)}
|
||||
.chev-btn:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.chev{width:9px;height:9px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg) translateY(-2px);transition:transform .15s ease;display:inline-block}
|
||||
.open .chev,.chev-btn.open .chev{transform:rotate(225deg) translateY(-2px)}
|
||||
.gl-tune{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;padding:8px 14px 10px 72px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);border-top:1px solid var(--line)}
|
||||
.gl-tune .t{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.gl-tune .n{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.gl-tune.running{color:var(--molten)}
|
||||
.gl-tune.stopped,.gl-tune.paused{color:var(--ember)}
|
||||
.gl-tune .bar{flex-basis:100%;height:3px;border-radius:2px;background:var(--line);overflow:hidden;display:block}
|
||||
.gl-tune .bar b{display:block;height:100%;background:var(--molten);transition:width .5s linear}
|
||||
.gl-details{padding:12px 14px 14px 72px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:10px;user-select:text;-webkit-user-select:text}
|
||||
.gl-details .ctl{display:grid;grid-template-columns:120px 1fr auto;align-items:center;gap:var(--s-3)}
|
||||
.gl-details .ctl .k{font-size:var(--t-md);color:var(--bone);font-weight:500}
|
||||
.gl-details .ctl .pv{font-size:var(--t-sm);color:var(--ink-2);min-width:100px;text-align:right;white-space:nowrap}
|
||||
.gl-details input[type=range]{width:100%;accent-color:var(--ember);margin:0}
|
||||
.gl-details .ctl .help{grid-column:1 / -1;margin-top:-4px}
|
||||
.gl-details .ctl .help:first-of-type{grid-column:2}
|
||||
.gl-details .line{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center}
|
||||
.gl-details .line b{color:var(--ink-2);font-weight:500}
|
||||
.gl-details .line.ok{color:var(--molten)}
|
||||
.gl-details .line.hot{color:var(--ember)}
|
||||
.gl-details .line .pin{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px}
|
||||
.curve{font-size:var(--t-sm);font-family:var(--mono);border-collapse:collapse;width:100%}
|
||||
.curve th{text-align:left;font-weight:500;color:var(--ash);font-size:10px;letter-spacing:.1em;text-transform:uppercase;padding:3px 8px 3px 0}
|
||||
.curve td{padding:3px 8px 3px 0;border-top:1px solid var(--line);color:var(--ink-2)}
|
||||
.curve tr.marked td{color:var(--ash);text-decoration:line-through}
|
||||
.ids{display:flex;align-items:center;gap:6px;flex:0 0 auto;justify-self:end}
|
||||
.ids button{width:30px;height:30px;border-radius:8px;border:1px solid var(--line-2);background:transparent;color:var(--bone);cursor:pointer;font-size:16px;line-height:1}
|
||||
.ids button:hover{border-color:var(--ash)}
|
||||
.ids input{width:44px;text-align:center;background:var(--obsidian);border:1px solid var(--line-2);border-radius:8px;padding:5px 4px;color:var(--bone);font-family:var(--mono);font-size:var(--t-base);user-select:text;-webkit-user-select:text}
|
||||
.ids input:focus{outline:none;border-color:var(--ember)}
|
||||
.ids input::-webkit-inner-spin-button,.ids input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
|
||||
.lead-card .lead-row,.lead-row{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-4)}
|
||||
.lead-text{min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||
.lead-text h3{font-size:var(--t-2xl)}
|
||||
|
||||
/* the tuning card: the goal, its consequence, the schedule, Power control when it is the fix */
|
||||
.goal-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap;margin-bottom:var(--s-3)}
|
||||
.seg{display:inline-flex;background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:3px;gap:2px}
|
||||
.seg-b{border:0;background:transparent;color:var(--ash);font-size:var(--t-base);font-weight:500;padding:6px 14px;border-radius:8px;cursor:pointer;transition:background .15s ease,color .15s ease;white-space:nowrap}
|
||||
.seg-b:hover{color:var(--bone)}
|
||||
.seg-b.on{background:var(--graphite);color:var(--bone);box-shadow:0 1px 3px rgba(0,0,0,.25);font-weight:600}
|
||||
.goal-line{font-size:var(--t-base);color:var(--ink-2);min-width:0}
|
||||
|
||||
/* the node line and the disclosures */
|
||||
.disclose{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);width:100%;border:0;background:transparent;padding:0;cursor:pointer;text-align:left;color:var(--bone);font-size:var(--t-md);font-weight:500;min-height:32px}
|
||||
.disclose:hover{color:var(--bone)}
|
||||
.disclose .chev{color:var(--ash);flex:0 0 auto;margin-right:4px}
|
||||
.disclose-right{display:flex;align-items:center;gap:var(--s-3);min-width:0}
|
||||
.disclose-right .dim{font-size:var(--t-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:52ch}
|
||||
.node-line{display:inline-flex;align-items:center;gap:10px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.node-card.bad .node-line{color:var(--ember)}
|
||||
.card .disclose+.disclose,.card .details+.disclose,.card .help+.disclose,.card .addr-big+.help+.disclose{margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid var(--line)}
|
||||
.details{padding-top:var(--s-3);display:flex;flex-direction:column;gap:var(--s-3)}
|
||||
.switch-list{display:flex;flex-direction:column;gap:3px;font-size:var(--t-sm);color:var(--ash);margin-top:8px}
|
||||
.switch-list span{display:flex;justify-content:space-between;gap:12px}
|
||||
.switch-list span.past{opacity:.55}
|
||||
.switch-list span.next{color:var(--molten)}
|
||||
.lead-row{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-4)}
|
||||
.lead-text{min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||
.lead-text h3{font-size:var(--t-xl)}
|
||||
.addr-big{display:flex;align-items:center;gap:12px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:12px;padding:14px 16px;font-size:var(--t-lg);word-break:break-all;user-select:text;-webkit-user-select:text;margin-bottom:10px}
|
||||
.addr-big span{flex:1;min-width:0;color:var(--molten)}
|
||||
.card.adv{padding:0}
|
||||
|
|
@ -317,22 +344,35 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.card.adv summary .eyebrow{margin-left:0}
|
||||
.card.adv>:not(summary){margin-left:var(--card-pad);margin-right:var(--card-pad)}
|
||||
.card.adv>:last-child{margin-bottom:var(--card-pad)}
|
||||
.job-history table{margin-top:var(--s-2)}
|
||||
table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
||||
.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:var(--t-xs);padding:4px 6px 4px 0;font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase}
|
||||
.job-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top}
|
||||
.job-history tr.failed td,.job-history tr.timeout td,.job-history tr.aborted td{color:var(--ember)}
|
||||
.job-history tr.running td{color:var(--molten)}
|
||||
.clock-card{border:1px solid rgba(242,84,27,.5);background:rgba(242,84,27,.08);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{border-color:var(--molten-40);background:rgba(255,179,92,.06)}
|
||||
#s-jobs-history th{text-align:left;font-weight:500;color:var(--ash);font-size:var(--t-xs);padding:4px 6px 4px 0;font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase}
|
||||
#s-jobs-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top}
|
||||
#s-jobs-history tr.failed td,#s-jobs-history tr.timeout td,#s-jobs-history tr.aborted td{color:var(--ember)}
|
||||
#s-jobs-history tr.running td{color:var(--molten)}
|
||||
.clock-card{border:1px solid var(--ember-40);background:var(--ember-12);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45}
|
||||
.clock-card .note{margin-top:0}
|
||||
|
||||
/* earnings: money first */
|
||||
.money{display:flex;flex-direction:column;margin-bottom:var(--s-2)}
|
||||
.money-row{display:flex;align-items:baseline;gap:var(--s-3);padding:8px 0;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
||||
.money-row:last-child{border-bottom:0}
|
||||
.money-row .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;white-space:nowrap;letter-spacing:-.01em;min-width:200px}
|
||||
.money-row .v.small{font-size:var(--t-num)}
|
||||
.money-row .s{font-size:var(--t-base);color:var(--ash);min-width:0}
|
||||
|
||||
/* prove */
|
||||
.tier-list{list-style:none;margin:var(--s-3) 0 0;padding:0;display:flex;flex-direction:column;gap:6px;font-size:var(--t-md);color:var(--ink-2)}
|
||||
.tier-list li{display:flex;gap:10px;align-items:baseline}
|
||||
.tier-list li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ember);flex:0 0 6px;position:relative;top:-2px}
|
||||
#pv-line{margin-top:var(--s-3)}
|
||||
|
||||
/* address options */
|
||||
.options{display:flex;flex-direction:column;gap:var(--s-3);margin-top:6px}
|
||||
.option{display:flex;gap:var(--s-4);align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease}
|
||||
.option:hover{border-color:var(--line-2)}
|
||||
.option.on{border-color:var(--ember);background:#1A1614}
|
||||
.option.on{border-color:var(--ember);background:var(--ember-12)}
|
||||
.option input[type=radio]{position:absolute;opacity:0;width:0;height:0}
|
||||
.option .radio{width:20px;height:20px;border-radius:50%;border:2px solid var(--line-2);flex:0 0 20px;margin-top:2px;position:relative}
|
||||
.option.on .radio{border-color:var(--ember)}
|
||||
|
|
@ -342,15 +382,15 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
.option .s{font-size:var(--t-md);color:var(--ash);line-height:1.5}
|
||||
.tag{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--molten);border:1px solid var(--molten-40);border-radius:999px;padding:2px 8px}
|
||||
.addr-input{width:100%;margin-top:var(--s-2);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;color:var(--bone);font-size:var(--t-md);letter-spacing:.02em;user-select:text;-webkit-user-select:text}
|
||||
.addr-input.short{width:100px;flex:0 0 100px}
|
||||
.addr-input:focus{outline:none;border-color:var(--ember)}
|
||||
.option:not(.on) .addr-input{display:none}
|
||||
.err{font-size:var(--t-base);color:var(--ember)}
|
||||
|
||||
/* the update card (app.js, UpdateCard; the wallet carries the same block): the mark with its ring, the name, one
|
||||
line, up to three note lines, the size, Install now and Later. Over the key sheet and the pages. */
|
||||
.upd-wrap{position:fixed;inset:0;z-index:35;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease}
|
||||
/* the update card (app.js, UpdateCard; the wallet carries the same block) */
|
||||
.upd-wrap{position:fixed;inset:0;z-index:35;background:var(--scrim);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease}
|
||||
@keyframes fade{from{opacity:0}to{opacity:1}}
|
||||
.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px rgba(0,0,0,.6),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none}
|
||||
.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px var(--shadow),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none}
|
||||
.upd-card::before{content:"";position:absolute;left:50%;top:-80px;width:360px;height:260px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.22) 0,rgba(242,84,27,0) 62%);pointer-events:none}
|
||||
.upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:var(--s-3)}
|
||||
.upd-mark img{position:relative;display:block;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))}
|
||||
|
|
@ -381,8 +421,8 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
@media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:var(--s-2)}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}}
|
||||
|
||||
/* sheet (the key) */
|
||||
.sheet-wrap{position:fixed;inset:0;z-index:30;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease}
|
||||
.sheet-wrap{position:fixed;inset:0;z-index:30;background:var(--scrim);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px var(--shadow);animation:rise .3s ease}
|
||||
.sheet .sub{font-size:var(--t-lg);color:var(--ink-2)}
|
||||
.field{display:flex;flex-direction:column;gap:var(--s-2);margin-top:6px}
|
||||
.field .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
|
|
@ -398,13 +438,13 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
|
||||
/* rows, switches */
|
||||
.row{display:flex;gap:10px;align-items:center;min-width:0}
|
||||
.row.between{justify-content:space-between}
|
||||
.row.wrap{flex-wrap:wrap}
|
||||
.row .addr-input{margin-top:0;min-width:0}
|
||||
.switch{display:flex;align-items:center;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:8px 0 2px;line-height:1.35}
|
||||
.switch{display:flex;align-items:flex-start;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:9px 0;line-height:1.4}
|
||||
.switch+.switch{border-top:1px solid var(--line)}
|
||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||||
.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease}
|
||||
.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--bone);transition:transform .15s ease}
|
||||
.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease;margin-top:1px}
|
||||
.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#F4F1EC;transition:transform .15s ease}
|
||||
.switch input:checked+.track{background:var(--ember)}
|
||||
.switch input:checked+.track::after{transform:translateX(18px)}
|
||||
.switch input:focus-visible+.track{outline:2px solid var(--ember);outline-offset:3px}
|
||||
|
|
@ -412,10 +452,12 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
.switch.lg .track{width:52px;height:30px;flex-basis:52px}
|
||||
.switch.lg .track::after{width:24px;height:24px}
|
||||
.switch.lg input:checked+.track::after{transform:translateX(22px)}
|
||||
.switch+.help{margin-bottom:6px}
|
||||
.sw-text{min-width:0}
|
||||
.sw-text b{font-weight:600}
|
||||
.sw-text .dim{font-size:var(--t-base)}
|
||||
|
||||
/* log drawer */
|
||||
.drawer{position:fixed;left:0;right:0;bottom:0;height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:21;transition:height .2s ease;display:flex;flex-direction:column;box-shadow:0 -12px 30px rgba(0,0,0,.35)}
|
||||
.drawer{position:fixed;left:0;right:0;bottom:0;height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:21;transition:height .2s ease;display:flex;flex-direction:column;box-shadow:0 -12px 30px var(--shadow)}
|
||||
body.has-rail .drawer{left:var(--rail)}
|
||||
body.drawer-open .drawer{height:var(--drawer-h)}
|
||||
body.drawer-drag .drawer{transition:none}
|
||||
|
|
@ -450,8 +492,8 @@ body.drawer-drag main{pointer-events:none}
|
|||
.log-ruler .t1{right:0;border-radius:0 0 0 6px}
|
||||
.log-ruler .mk{position:absolute;top:4px;bottom:4px;width:2px;margin-left:-1px;background:var(--ember);opacity:.9;pointer-events:none}
|
||||
.log-ruler .mk.swap{background:var(--molten)}
|
||||
.log-ruler .win{position:absolute;top:0;bottom:0;background:rgba(244,241,236,.14);border-left:1px solid rgba(244,241,236,.35);border-right:1px solid rgba(244,241,236,.35);min-width:3px;pointer-events:none}
|
||||
.log-ruler:hover .win{background:rgba(244,241,236,.2)}
|
||||
.log-ruler .win{position:absolute;top:0;bottom:0;background:rgba(127,127,127,.14);border-left:1px solid rgba(127,127,127,.35);border-right:1px solid rgba(127,127,127,.35);min-width:3px;pointer-events:none}
|
||||
.log-ruler:hover .win{background:rgba(127,127,127,.2)}
|
||||
.log-view{position:relative;flex:1;overflow:auto;overscroll-behavior:contain;user-select:text;-webkit-user-select:text;contain:strict}
|
||||
.log-pad{position:relative;min-height:100%}
|
||||
.log{margin:0;position:absolute;top:0;left:0;right:0;padding:6px var(--gutter);font-size:var(--t-sm);line-height:19px;color:var(--ink-2);white-space:pre;will-change:transform}
|
||||
|
|
@ -459,7 +501,7 @@ body.drawer-drag main{pointer-events:none}
|
|||
.log-view.wrap .log{position:relative;padding-bottom:12px}
|
||||
.log-view.wrap .log .ln{height:auto;white-space:pre-wrap;word-break:break-all;overflow:visible}
|
||||
.log .src{color:var(--ash)}
|
||||
.log .src.node{color:var(--node-blue)}
|
||||
.log .src.node{color:#7FA7C9}
|
||||
.log .src.miner1,.log .src.miner2,.log .src.miner3,.log .src.miner4,.log .src.miner5,.log .src.miner6,.log .src.miner7,.log .src.miner8{color:var(--molten)}
|
||||
.log .src.app{color:var(--ember)}
|
||||
.log .src.watch{color:var(--ash)}
|
||||
|
|
@ -467,22 +509,18 @@ body.drawer-drag main{pointer-events:none}
|
|||
.log .ln.hit{background:rgba(255,179,92,.18);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:rgba(255,179,92,.3);color:var(--bone);border-radius:2px;padding:0 1px}
|
||||
.log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ash);font-size:var(--t-sm);padding:0 var(--gutter);text-align:center}
|
||||
.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:2;gap:6px;padding:6px 12px}
|
||||
.log-jump:hover{background:#1c1a18;border-color:var(--molten)}
|
||||
.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px var(--shadow);z-index:2;gap:6px;padding:6px 12px}
|
||||
.log-jump:hover{border-color:var(--molten)}
|
||||
|
||||
.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px rgba(0,0,0,.5);max-width:min(90vw,520px);text-align:center}
|
||||
.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px var(--shadow);max-width:min(90vw,520px);text-align:center}
|
||||
body.has-rail .toast{left:calc(50% + var(--rail) / 2)}
|
||||
body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
||||
|
||||
/* narrow windows (the 900 px floor): the rail folds to icons with small labels, the hero row and the strips fold to
|
||||
two columns, the two-column grid to one, the GPU rows drop a number */
|
||||
/* narrow windows: 1180 the hero wraps; 1000 the rail folds to icons; 900 the row's cells wrap; 720 a bottom tab bar */
|
||||
@media (max-width:1180px){
|
||||
.hero-row{grid-template-columns:1fr 1fr}
|
||||
.toggle-big{grid-row:span 1}
|
||||
.strip{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.strip.three{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.grid2{grid-template-columns:1fr}
|
||||
.top-status{max-width:220px}
|
||||
.hero-row{grid-template-columns:1fr}
|
||||
.totals{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||||
.nums{gap:var(--s-4)}
|
||||
}
|
||||
@media (max-width:1000px){
|
||||
:root{--rail:76px;--gutter:var(--s-5)}
|
||||
|
|
@ -497,29 +535,73 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
.rail-status{display:none}
|
||||
.rail-version{text-align:center;padding:8px 0 0;font-size:10px;white-space:nowrap}
|
||||
.rail-version .chain{display:none}
|
||||
.gpu-line{grid-template-columns:44px minmax(140px,1.4fr) repeat(2,minmax(80px,.7fr)) 48px}
|
||||
.gpu-line .num.power{display:none}
|
||||
.page-sub{display:none}
|
||||
.top-status{display:none}
|
||||
.strip.three{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.set-card .ctl{grid-template-columns:120px 1fr auto}
|
||||
.gl-main{grid-template-columns:44px minmax(140px,1fr) auto}
|
||||
.nums{grid-column:2;justify-content:flex-start}
|
||||
.acts{grid-column:3;grid-row:1}
|
||||
.gl-tune,.gl-details{padding-left:14px}
|
||||
.kv>div{grid-template-columns:110px auto 1fr}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.three{grid-template-columns:1fr}
|
||||
h1{font-size:40px}
|
||||
.totals{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:var(--s-3)}
|
||||
.tot:nth-child(3){padding-left:0;border-left:0}
|
||||
.money-row .v{min-width:0}
|
||||
}
|
||||
/* short windows (the 600 px floor): tighter paddings, a lower hero, a smaller canvas, so the drawer always has room */
|
||||
@media (max-width:720px){
|
||||
:root{--rail:0px;--gutter:var(--s-4);--bottom:64px}
|
||||
body.has-rail .rail{top:auto;bottom:0;left:0;right:0;width:auto;height:64px;flex-direction:row;align-items:center;border-right:0;border-top:1px solid var(--line);padding:0 var(--s-2)}
|
||||
.rail-brand{display:none}
|
||||
.rail-nav{flex-direction:row;flex:1;gap:0;justify-content:space-around}
|
||||
.nav{min-height:56px;padding:6px 2px;font-size:10px;border-radius:10px;flex:1;max-width:120px}
|
||||
.nav.on::before{display:none}
|
||||
.rail-foot{margin-top:0;padding-top:0;border-top:0;flex-direction:row;gap:0}
|
||||
.rail-foot .nav.small{min-height:56px;flex:0 0 auto;padding:6px 8px}
|
||||
.rail-version{display:none}
|
||||
body.has-rail .top,body.has-rail .notices,body.has-rail main,body.has-rail .drawer,body.has-rail .ask-wrap{left:0}
|
||||
body.has-rail main{bottom:var(--bottom)}
|
||||
body.has-rail .toast{left:50%;bottom:calc(var(--bottom) + 12px)}
|
||||
body.drawer-open .drawer{bottom:var(--bottom)}
|
||||
.ask-wrap{bottom:var(--bottom)}
|
||||
.top{padding:0 var(--s-4)}
|
||||
.page-title h1{font-size:17px}
|
||||
.pill{min-width:0}
|
||||
.hero-row{grid-template-columns:1fr}
|
||||
.toggle-big{min-height:88px}
|
||||
.gl-main{grid-template-columns:44px 1fr;gap:var(--s-3);padding:12px}
|
||||
.nums{grid-column:1 / -1;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-2) var(--s-3);justify-items:start}
|
||||
.acts{grid-column:1 / -1;grid-row:auto;justify-self:end}
|
||||
.gpu-line .name{font-size:var(--t-base)}
|
||||
.num{align-items:flex-start;text-align:left}
|
||||
.acts{gap:var(--s-2)}
|
||||
.gl-tune,.gl-details{padding-left:12px;padding-right:12px}
|
||||
.gl-details .ctl{grid-template-columns:1fr;gap:6px}
|
||||
.gl-details .ctl .help:first-of-type{grid-column:1}
|
||||
.ids{justify-self:start}
|
||||
.kv>div{grid-template-columns:96px 1fr;gap:4px var(--s-3)}
|
||||
.kv .m{grid-column:1 / -1;white-space:normal}
|
||||
.goal-row{align-items:flex-start;flex-direction:column}
|
||||
.seg{width:100%}
|
||||
.seg-b{flex:1;padding:6px 8px}
|
||||
.indent{margin-left:0}
|
||||
.disclose-right .dim{display:none}
|
||||
.lead-row{flex-direction:column}
|
||||
.step{padding:32px 0}
|
||||
.money-row .v{font-size:var(--t-num)}
|
||||
.drawer-head{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
.log,.log-ruler .t0,.log-ruler .t1{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
}
|
||||
/* short windows (the 600 px floor): tighter paddings, a smaller canvas, so the drawer always has room */
|
||||
@media (max-height:700px){
|
||||
:root{--card-pad:18px;--tile-pad:14px 16px;--gap:var(--s-4)}
|
||||
:root{--card-pad:18px;--gap:var(--s-3)}
|
||||
#screen-dashboard{padding:16px 0 20px}
|
||||
#dag{height:140px}
|
||||
#dag{height:90px}
|
||||
.hero{gap:var(--s-3);padding:var(--s-5) 0 var(--s-6)}
|
||||
.coin-wrap{width:104px;height:104px}
|
||||
.coin{width:104px;height:104px}
|
||||
.mark-wrap{width:88px;height:88px}
|
||||
h1{font-size:40px}
|
||||
.lead{font-size:var(--t-xl)}
|
||||
.lead{font-size:var(--t-lg)}
|
||||
.step{padding:32px 0 32px}
|
||||
.feed{max-height:200px}
|
||||
.drawer-head{padding-bottom:6px}
|
||||
.toggle-big{min-height:96px}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -4,13 +4,14 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Igneum Miner</title>
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<link rel="icon" href="mark.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body class="phase-welcome" data-phase="welcome" data-page="mine">
|
||||
|
||||
<!-- the rail: one button per section (miner-ui-2). Shown on the dashboard; the setup screens have no rail. -->
|
||||
<!-- the rail: four sections (miner-ui-3: Mine, Earnings, Prove, Settings), Logs and Quit in the foot. Under 720 px it
|
||||
is a bottom tab bar. The setup screens have no rail. -->
|
||||
<aside class="rail" id="rail" hidden>
|
||||
<div class="rail-brand">
|
||||
<img src="mark.svg" width="28" height="28" alt="">
|
||||
|
|
@ -18,11 +19,9 @@
|
|||
</div>
|
||||
<nav class="rail-nav" id="rail-nav" aria-label="Sections">
|
||||
<button class="nav on" data-page="mine" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg><span>Mine</span></button>
|
||||
<button class="nav" data-page="earnings"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 15h2"/></svg><span>Earnings</span></button>
|
||||
<button class="nav" data-page="prove"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 4 5v6c0 5 3.4 9.4 8 11 4.6-1.6 8-6 8-11V5l-8-3z"/><path d="m9 12 2 2 4-4"/></svg><span>Prove</span></button>
|
||||
<button class="nav" data-page="rewards"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 15h2"/></svg><span>Rewards</span></button>
|
||||
<button class="nav" data-page="node"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3.5 3 14.5 0 18M12 3c-3 3.5-3 14.5 0 18"/></svg><span>Node</span></button>
|
||||
<button class="nav" data-page="updates"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 10l5 5 5-5"/><path d="M4 19h16"/></svg><span>Updates</span><i class="nav-dot" id="nav-updates-dot" hidden></i></button>
|
||||
<button class="nav" data-page="settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg><span>Settings</span></button>
|
||||
<button class="nav" data-page="settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg><span>Settings</span><i class="nav-dot" id="nav-updates-dot" hidden></i></button>
|
||||
</nav>
|
||||
<div class="rail-foot">
|
||||
<div class="rail-status mono" id="rail-status"></div>
|
||||
|
|
@ -42,13 +41,11 @@
|
|||
<span class="page-sub" id="page-sub"></span>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<span class="top-status mono" id="top-status"></span>
|
||||
<div class="pill" id="pill"><span class="dot"></span><span id="pill-text">starting</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- the status strip: one notice at a time (updates, remote jobs, the clock), the most important first; app.js fills
|
||||
it from the state (Notices) and the content below moves once when it appears or goes. -->
|
||||
<!-- the status strip: one notice at a time (app.js, Notices) -->
|
||||
<div class="notices" id="notices" hidden>
|
||||
<div class="notice" id="notice" role="status" aria-live="polite">
|
||||
<span class="notice-text" id="notice-text"></span>
|
||||
|
|
@ -59,36 +56,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- the quit question, in place of a dialog (the viewer never shows one) -->
|
||||
<div class="ask-wrap" id="ask-quit" hidden>
|
||||
<div class="ask">
|
||||
<span class="ask-text">Quit Igneum Miner? Mining stops first, then the node.</span>
|
||||
<button class="btn small primary" id="ask-quit-yes">Quit</button>
|
||||
<button class="btn small ghost" id="ask-quit-no">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
|
||||
<!-- 1. welcome -->
|
||||
<section class="screen" id="screen-welcome">
|
||||
<div class="hero">
|
||||
<div class="coin-wrap"><img class="coin" src="coin.png" width="148" height="148" alt=""></div>
|
||||
<div class="mark-wrap"><img src="mark.svg" width="72" height="72" alt=""></div>
|
||||
<div class="eyebrow ember">devnet v4 · nothing is bought or sold</div>
|
||||
<h1>Igneum Miner</h1>
|
||||
<p class="lead">Mined by GPUs. Proven by fire. This app runs a node on this machine and mines Igneum with your graphics card.</p>
|
||||
<p class="lead">Runs a node and mines Igneum with your graphics card. Set up in two steps.</p>
|
||||
<div class="three">
|
||||
<div class="tile">
|
||||
<div class="k">01</div>
|
||||
<div class="t">Runs a node</div>
|
||||
<div class="s">Syncs the chain from the seed node, usually under a minute.</div>
|
||||
<div class="t">Runs in the background</div>
|
||||
<div class="s">The window can close. The miner keeps going from the menu bar or the tray.</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="k">02</div>
|
||||
<div class="t">Mines on your GPU</div>
|
||||
<div class="s">A new program every hour. The next one compiles while you mine.</div>
|
||||
<div class="t">Uses your graphics card</div>
|
||||
<div class="s">Each card is tuned for hashes per watt by itself. You can tune again any time.</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="k">03</div>
|
||||
<div class="t">Pays your address</div>
|
||||
<div class="s">Block rewards go to an EVM address you choose or one made here.</div>
|
||||
<div class="t">Pays an address you own</div>
|
||||
<div class="s">Every block this machine finds pays one address: one made here, or one you paste.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cta">
|
||||
<button class="btn primary big" id="btn-begin">Get started</button>
|
||||
</div>
|
||||
<p class="seedline mono">Nobody from Igneum will ever ask for your seed.</p>
|
||||
<p class="seedline mono">Nobody from Igneum will ever ask for your key.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -96,20 +102,20 @@
|
|||
<section class="screen" id="screen-cards">
|
||||
<div class="step">
|
||||
<div class="eyebrow">step 1 of 2</div>
|
||||
<h2>Your GPU</h2>
|
||||
<h2>Your graphics card</h2>
|
||||
<p class="sub" id="cards-sub">Asking the graphics cards to report in.</p>
|
||||
<div class="cards" id="cards-list">
|
||||
<div class="card detecting" id="cards-detecting">
|
||||
<div class="spinner"></div>
|
||||
<div>
|
||||
<div class="t">Detecting</div>
|
||||
<div class="s mono" id="detect-text">loading the Metal worker</div>
|
||||
<div class="s mono" id="detect-text">asking the graphics cards to report in</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="note" id="cards-note" hidden></p>
|
||||
<p class="note" id="cards-power" hidden>Igneum caps each NVIDIA card's power at 80% of its default limit to keep it stable. This needs administrator rights once, when mining starts; the limit goes back to what it was on quit. Settings has a slider per card.</p>
|
||||
<p class="note" id="cards-help" hidden>Each card you switch on gets its own worker. An integrated GPU is off by default: it is slow and shares the machine's memory.</p>
|
||||
<p class="note" id="cards-power" hidden>NVIDIA cards start at 80% of their power limit, which keeps them stable. Windows asks for administrator rights once for that. The card row has a slider.</p>
|
||||
<p class="note" id="cards-help" hidden>An integrated GPU is off by default: it is slow and shares the machine's memory.</p>
|
||||
<div class="cta">
|
||||
<button class="btn primary" id="btn-cards-next" disabled>Continue</button>
|
||||
<button class="btn ghost" id="btn-cards-retry" hidden>Detect again</button>
|
||||
|
|
@ -122,14 +128,14 @@
|
|||
<div class="step">
|
||||
<div class="eyebrow">step 2 of 2</div>
|
||||
<h2>Where should rewards go?</h2>
|
||||
<p class="sub">Every block this machine finds pays one EVM address. Pick one way. One block in 100 pays the miner software's dev fee; Settings turns it off.</p>
|
||||
<p class="sub">Every block this machine finds pays one address. Pick one way.</p>
|
||||
<div class="options">
|
||||
<label class="option on" id="opt-generate">
|
||||
<input type="radio" name="mode" value="generate" checked>
|
||||
<span class="radio"></span>
|
||||
<span class="body">
|
||||
<span class="t">Make me an address <span class="tag">recommended</span></span>
|
||||
<span class="s">A secp256k1 key is made on this machine and stored in the app folder with the file locked to your user. You see the key once, to save it.</span>
|
||||
<span class="s">A key is made on this machine and stored in the app folder, readable by your user only. You see it once, to save it.</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="option" id="opt-paste">
|
||||
|
|
@ -137,98 +143,174 @@
|
|||
<span class="radio"></span>
|
||||
<span class="body">
|
||||
<span class="t">Use my own address</span>
|
||||
<span class="s">Paste an EVM address you control. 0x followed by 40 hex characters.</span>
|
||||
<span class="s">Paste an Ethereum-style address you control: 0x and 40 characters.</span>
|
||||
<input type="text" class="addr-input mono" id="addr-input" placeholder="0x" spellcheck="false" autocomplete="off">
|
||||
<span class="err" id="addr-err" hidden>That is not an EVM address. 0x followed by 40 hex characters.</span>
|
||||
<span class="err" id="addr-err" hidden>That is not an address. 0x followed by 40 hex characters.</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="err" id="start-blocked" hidden></p>
|
||||
<div class="cta">
|
||||
<button class="btn primary big" id="btn-start">Start mining</button>
|
||||
<button class="btn primary big" id="btn-start">Continue</button>
|
||||
<button class="btn ghost" id="btn-address-back">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. the dashboard: six pages behind the rail -->
|
||||
<!-- 4. the dashboard: four pages behind the rail -->
|
||||
<section class="screen" id="screen-dashboard">
|
||||
|
||||
<!-- Mine -->
|
||||
<section class="page" id="page-mine" data-page="mine">
|
||||
<div class="clock-card" id="m-clock" hidden>
|
||||
<p class="clock-msg" id="m-clock-msg"></p>
|
||||
<div class="row"><button class="btn small primary" id="m-clock-sync">Sync clock</button><span class="note mono small" id="m-clock-result"></span></div>
|
||||
<p class="note" id="m-clock-hint"></p>
|
||||
</div>
|
||||
|
||||
<div class="hero-row">
|
||||
<button class="toggle-big" id="btn-toggle" disabled>
|
||||
<span class="ring"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v9"/><path d="M6.5 6.5a8 8 0 1 0 11 0"/></svg></span>
|
||||
<span class="tl" id="btn-toggle-text">Start mining</span>
|
||||
<span class="ts" id="btn-toggle-sub">waiting for the engine</span>
|
||||
</button>
|
||||
<div class="cell big ember">
|
||||
<div class="k">hash rate</div>
|
||||
<div class="v"><span id="d-hash">0.0</span><span class="unit">MH/s</span></div>
|
||||
<div class="s" id="d-hash-sub">waiting for the worker</div>
|
||||
</div>
|
||||
<div class="cell big">
|
||||
<div class="k">blocks found</div>
|
||||
<div class="v" id="d-blocks">0</div>
|
||||
<div class="s" id="d-blocks-sub">accepted by the node</div>
|
||||
</div>
|
||||
<div class="cell big">
|
||||
<div class="k">next program</div>
|
||||
<div class="v" id="d-eta">--:--</div>
|
||||
<div class="s" id="d-eta-sub">waiting for the node</div>
|
||||
<div class="totals" id="totals">
|
||||
<div class="tot"><span class="v ember" id="t-hash">0</span><span class="k">MH/s</span><span class="s" id="t-hash-sub">waiting</span></div>
|
||||
<div class="tot" id="t-power-cell"><span class="v" id="t-power">0</span><span class="k">W</span><span class="s" id="t-power-sub">drawn now</span></div>
|
||||
<div class="tot" id="t-money-cell"><span class="v" id="t-money">£0.00</span><span class="k">a day</span><span class="s" id="t-money-sub">electricity</span></div>
|
||||
<div class="tot"><span class="v" id="t-blocks">0</span><span class="k">blocks</span><span class="s" id="t-blocks-sub">this run</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Your GPUs</h3><div class="eyebrow" id="d-cards-eyebrow">detecting</div></div>
|
||||
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span><button class="btn small" id="btn-tune-all" hidden>Tune all</button></div></div>
|
||||
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
|
||||
<p class="note" id="d-cards-note" hidden></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h3>Your blocks</h3>
|
||||
<div class="stats mono"><span>last 10 min <b id="d-found-10">0</b></span><span>last hour <b id="d-found-60">0</b></span><span>dev fee <b id="d-fee">0</b></span><span>chain <b id="d-chain-blocks">0</b></span></div>
|
||||
<div class="card" id="tune-card">
|
||||
<div class="card-head"><h3>Tuning</h3><span class="eyebrow" id="tune-eyebrow">Ember Tune</span></div>
|
||||
<div class="goal-row">
|
||||
<div class="seg" id="goal-seg" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line"></span>
|
||||
</div>
|
||||
<p class="line-text" id="tune-schedule"></p>
|
||||
<label class="switch" id="m-power-row" hidden><input type="checkbox" id="m-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once.</span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="card node-card" id="node-card">
|
||||
<button class="disclose" id="node-toggle" aria-expanded="false" aria-controls="node-details">
|
||||
<span class="node-line"><i class="dot" id="node-dot"></i><span id="node-line-text">Node starting</span></span>
|
||||
<span class="disclose-right"><span class="dim" id="node-sub"></span><span class="chev" aria-hidden="true"></span></span>
|
||||
</button>
|
||||
<div class="details" id="node-details" hidden>
|
||||
<div class="kv">
|
||||
<div><span class="k">state</span><span class="v mono" id="n-state-v"></span><span class="m" id="n-state-m"></span></div>
|
||||
<div><span class="k">height</span><span class="v mono" id="n-blocks">0</span><span class="m" id="n-blocks-sub">blocks this node holds</span></div>
|
||||
<div><span class="k">peers</span><span class="v mono" id="n-peers">0</span><span class="m" id="n-peers-sub">other nodes it talks to</span></div>
|
||||
<div><span class="k">version</span><span class="v mono" id="n-version">--</span><span class="m" id="d-node-net">devnet v4</span></div>
|
||||
<div><span class="k">headers</span><span class="v mono" id="n-headers">0</span><span class="m">headers arrive before blocks</span></div>
|
||||
<div><span class="k">DAA score</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made</span></div>
|
||||
<div><span class="k">difficulty</span><span class="v mono" id="n-diff">0</span><span class="m">how hard the next block is to find</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the block DAG right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain</span></div>
|
||||
<div><span class="k">next program</span><span class="v mono" id="d-eta">--:--</span><span class="m" id="d-eta-sub">the hourly mining program</span></div>
|
||||
<div><span class="k">last lock</span><span class="v mono" id="f-lock">none yet</span><span class="m" id="f-note">a point the miners agreed can never be undone</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a checkpoint every 30 s</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">rules digest</div>
|
||||
<div class="box mono"><span id="n-digest">not printed yet</span><button class="btn tiny" data-copy="n-digest">Copy</button></div>
|
||||
<p class="help">Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">next rule switch</div>
|
||||
<div class="line-text" id="n-switch">none planned</div>
|
||||
<p class="help" id="n-switch-help"></p>
|
||||
<div class="switch-list mono" id="n-switches"></div>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="dag" aria-hidden="true"></canvas>
|
||||
<div class="legend"><span><i class="sw ember"></i>block this machine found</span><span><i class="sw glow"></i>just accepted</span><span><i class="sw line"></i>one minute</span></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Activity</h3><div class="eyebrow">newest first</div></div>
|
||||
<div class="card-head"><h3>Activity</h3><div class="head-right"><span class="stats mono" id="d-stats"></span><button class="btn small ghost" id="btn-open-log">Open the log</button></div></div>
|
||||
<canvas id="dag" aria-hidden="true"></canvas>
|
||||
<div class="feed" id="d-events"><div class="empty">No events yet.</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Earnings -->
|
||||
<section class="page" id="page-earnings" data-page="earnings" hidden>
|
||||
<div class="card">
|
||||
<div class="money">
|
||||
<div class="money-row"><span class="v" id="e-earned">£0.00</span><span class="s" id="e-earned-sub">earned: nothing is bought or sold on devnet</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-ign">0.0000 IGN</span><span class="s" id="e-ign-sub">from 0 proofs</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-blocks">0 blocks</span><span class="s" id="e-blocks-sub">lifetime</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-cost">£0.00 a day</span><span class="s" id="e-cost-sub">electricity</span><button class="btn tiny ghost" id="e-price-btn">Set the price</button></div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the miner software's author</span></label>
|
||||
<p class="help" id="r-devfee-line"></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Rewards address</h3><div class="eyebrow" id="r-source"></div></div>
|
||||
<div class="addr-big mono"><span id="r-address">not set</span><button class="btn small" data-copy="r-address">Copy</button></div>
|
||||
<p class="help" id="r-address-help">Every block this machine finds pays this address.</p>
|
||||
|
||||
<button class="disclose" id="addr-toggle" aria-expanded="false" aria-controls="addr-details"><span>Change address</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="addr-details" hidden>
|
||||
<div class="row">
|
||||
<input type="text" class="addr-input mono" id="s-address-input" placeholder="0x" spellcheck="false" autocomplete="off" aria-label="New rewards address">
|
||||
<button class="btn small" id="s-address-save">Change</button>
|
||||
</div>
|
||||
<div class="err" id="s-address-err" hidden></div>
|
||||
<div class="ask inline" id="ask-address" hidden>
|
||||
<span class="ask-text" id="ask-address-text"></span>
|
||||
<button class="btn small primary" id="ask-address-yes">Confirm</button>
|
||||
<button class="btn small ghost" id="ask-address-no">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="disclose" id="key-toggle" aria-expanded="false" aria-controls="key-details"><span id="key-toggle-text">Show my key</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="key-details" hidden>
|
||||
<p class="help" id="r-key-help"></p>
|
||||
<div class="ask inline" id="ask-key">
|
||||
<span class="ask-text">Show the private key on screen? Anyone who sees it can spend what the address holds.</span>
|
||||
<button class="btn small primary" id="s-reveal">Show</button>
|
||||
<button class="btn small ghost" id="ask-key-no">Cancel</button>
|
||||
</div>
|
||||
<div class="box mono key" id="s-key-box" hidden><span id="s-key"></span><button class="btn tiny" data-copy="s-key">Copy</button><button class="btn tiny ghost" id="s-hide">Hide</button></div>
|
||||
<p class="note mono small" id="r-key-file"></p>
|
||||
</div>
|
||||
|
||||
<div class="row wrap top-gap"><button class="btn small" id="r-wallet">Open the wallet</button><span class="note">Your balance is in the wallet.</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Prove -->
|
||||
<section class="page" id="page-prove" data-page="prove" hidden>
|
||||
<div class="card lead-card">
|
||||
<div class="card">
|
||||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3>Prove shards on this machine</h3>
|
||||
<p class="help">Every block on Igneum is turned into a short mathematical proof, in pieces called shards. The chain assigns shards to your keys; this machine proves them and is paid for each one. On by default on an NVIDIA card with 24 GB or more (a full shard needs 20.4 GB of GPU memory, measured); off on a Mac, whose CPU prover is slow.</p>
|
||||
<h3>Prove on this machine</h3>
|
||||
<p class="help">Every block is turned into a short proof. The chain hands pieces to your cards; each piece proven pays IGN.</p>
|
||||
</div>
|
||||
<label class="switch lg" title="Prove assigned shards"><input type="checkbox" id="s-prove" aria-label="Prove shards on this machine"><span class="track"></span></label>
|
||||
<label class="switch lg" title="Prove on this machine"><input type="checkbox" id="s-prove" aria-label="Prove on this machine"><span class="track"></span></label>
|
||||
</div>
|
||||
<p class="note" id="pv-note">Off. Switch it on and this machine proves the shards the chain assigns to its keys.</p>
|
||||
<ul class="tier-list" id="pv-tiers"></ul>
|
||||
<p class="line-text" id="pv-line"></p>
|
||||
<p class="note" id="pv-note"></p>
|
||||
<div class="row" id="pv-setup-row" hidden><button class="btn small primary" id="pv-setup">Set up</button><span class="note">About 20 minutes, once.</span></div>
|
||||
</div>
|
||||
<div class="strip four">
|
||||
<div class="cell"><div class="k">state</div><div class="v state" id="pv-state">off</div><div class="s" id="pv-state-sub">not proving</div></div>
|
||||
<div class="cell"><div class="k">assigned</div><div class="v" id="pv-assigned">0</div><div class="s">shards given to your keys</div></div>
|
||||
<div class="cell"><div class="k">proven</div><div class="v" id="pv-submitted">0</div><div class="s">proofs sent to the node</div></div>
|
||||
<div class="cell"><div class="k">paid</div><div class="v" id="pv-paid">0</div><div class="s" id="pv-paid-sub">shards paid out</div></div>
|
||||
</div>
|
||||
<p class="note" id="pv-seg-note" hidden></p>
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Verifier</h3><div class="eyebrow">the node's check</div></div>
|
||||
<div class="big-word" id="pv-verifier">not read yet</div>
|
||||
<p class="help" id="pv-verifier-help">Before a proof counts, the node checks it. A node without a verifier passes proofs along and never includes them.</p>
|
||||
<p class="note" id="pv-verifier-note" hidden></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Program</h3><div class="eyebrow">pinned guest</div></div>
|
||||
<button class="disclose" id="pv-toggle" aria-expanded="false" aria-controls="pv-details"><span>Details</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="pv-details" hidden>
|
||||
<div class="kv">
|
||||
<div><span class="k">verifier</span><span class="v" id="pv-verifier">not read yet</span><span class="m" id="pv-verifier-help">the node checks a proof before it counts</span></div>
|
||||
<div><span class="k">segments</span><span class="v" id="pv-seg">none yet</span><span class="m" id="pv-seg-note">whole segments this machine proved</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">shard program id</div>
|
||||
<div class="box mono"><span id="pv-program">not read yet</span><button class="btn tiny" data-copy="pv-program">Copy</button></div>
|
||||
|
|
@ -236,105 +318,63 @@
|
|||
<div class="field">
|
||||
<div class="k">aggregator id</div>
|
||||
<div class="box mono"><span id="pv-aggregator">not read yet</span><button class="btn tiny" data-copy="pv-aggregator">Copy</button></div>
|
||||
<p class="help">Every proof names the program that made it. Other nodes accept a proof only from these two ids.</p>
|
||||
</div>
|
||||
<p class="help">Every proof names the program that made it. Other nodes accept a proof only from these two ids.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rewards -->
|
||||
<section class="page" id="page-rewards" data-page="rewards" hidden>
|
||||
<!-- Settings -->
|
||||
<section class="page" id="page-settings" data-page="settings" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Rewards address</h3><div class="eyebrow" id="r-source"></div></div>
|
||||
<div class="addr-big mono"><span id="r-address">not set</span><button class="btn small" data-copy="r-address">Copy</button></div>
|
||||
<p class="help" id="r-address-help">Every block this machine finds pays this address.</p>
|
||||
</div>
|
||||
<div class="strip three">
|
||||
<div class="cell"><div class="k">blocks found</div><div class="v" id="r-blocks">0</div><div class="s">lifetime, accepted by the node</div></div>
|
||||
<div class="cell"><div class="k">this run</div><div class="v" id="r-session">0</div><div class="s" id="r-session-sub">since the app started</div></div>
|
||||
<div class="cell"><div class="k">balance</div><div class="v dim" id="r-balance">--</div><div class="s">shown in the wallet, not here yet</div></div>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<div class="card" id="r-key-card">
|
||||
<div class="card-head"><h3>Save your key</h3><div class="eyebrow ember">once</div></div>
|
||||
<p class="help" id="r-key-help">The key for this address was made on this machine and is stored in the app folder, readable by your user only. Keep a copy somewhere safe: anyone with the key can spend what the address holds.</p>
|
||||
<div class="row" id="r-key-row"><button class="btn small" id="s-reveal">Show my key</button><button class="btn small ghost" id="s-hide" hidden>Hide</button></div>
|
||||
<div class="box mono key" id="s-key-box" hidden><span id="s-key"></span><button class="btn tiny" data-copy="s-key">Copy</button></div>
|
||||
<p class="note mono small" id="r-key-file"></p>
|
||||
<div class="card-head"><h3>Tuning</h3><span class="eyebrow" id="s-tune-eyebrow">Ember Tune</span></div>
|
||||
<div class="goal-row">
|
||||
<div class="seg" id="goal-seg-2" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line-2"></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Use another address</h3></div>
|
||||
<p class="help">Paste an EVM address you control. The miner restarts and pays the new address from the next block.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span class="sw-text"><b>Ember Tune</b> <span class="dim">Tunes every card for hashes per watt: once after install, then every 7 days, and after a driver or program change.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once. Off, NVIDIA cards are measured only.</span><span class="dim" id="s-power-note"></span></span></label>
|
||||
<label class="switch" id="s-climb-row" hidden><input type="checkbox" id="s-climb"><span class="track"></span><span class="sw-text"><b>Hill climb</b> <span class="dim">Searches around the best point instead of walking the ladders.</span></span></label>
|
||||
<div class="field">
|
||||
<div class="k">electricity price</div>
|
||||
<div class="row">
|
||||
<input type="text" class="addr-input mono" id="s-address-input" placeholder="0x" spellcheck="false" autocomplete="off" aria-label="New rewards address">
|
||||
<button class="btn small" id="s-address-save">Change</button>
|
||||
<input type="number" class="addr-input mono short" id="s-price" min="0" max="200" step="0.1" placeholder="28" aria-label="Electricity price in pence per kWh"><span class="note">pence per kWh. Every £ figure uses it.</span>
|
||||
<button class="btn small" id="s-price-save">Save</button>
|
||||
</div>
|
||||
<div class="err" id="s-address-err" hidden></div>
|
||||
<p class="note" id="r-devfee-line"></p>
|
||||
<div class="row"><button class="btn small ghost" id="r-wallet">Open the wallet page</button></div>
|
||||
</div>
|
||||
<p class="line-text" id="s-tune-schedule"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Node -->
|
||||
<section class="page" id="page-node" data-page="node" hidden>
|
||||
<div class="strip four">
|
||||
<div class="cell" id="n-state-cell"><div class="k">node</div><div class="v state" id="d-node">starting</div><div class="s" id="d-node-sub">opening the database</div></div>
|
||||
<div class="cell"><div class="k">height</div><div class="v" id="n-blocks">0</div><div class="s" id="n-blocks-sub">blocks this node holds</div></div>
|
||||
<div class="cell"><div class="k">peers</div><div class="v" id="n-peers">0</div><div class="s" id="n-peers-sub">other nodes it talks to</div></div>
|
||||
<div class="cell"><div class="k">version</div><div class="v small" id="n-version">--</div><div class="s" id="d-node-net">devnet v4</div></div>
|
||||
</div>
|
||||
<div class="clock-card" id="n-clock" hidden>
|
||||
<p class="clock-msg" id="n-clock-msg"></p>
|
||||
<div class="row"><button class="btn small primary" id="n-clock-sync">Sync clock</button><span class="note mono small" id="n-clock-result"></span></div>
|
||||
<p class="note" id="n-clock-hint"></p>
|
||||
</div>
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Chain</h3><div class="eyebrow" id="n-reading">reading</div></div>
|
||||
<div class="kv">
|
||||
<div><span class="k">headers</span><span class="v mono" id="n-headers">0</span></div>
|
||||
<div><span class="k">daa score</span><span class="v mono" id="n-daa">0</span></div>
|
||||
<div><span class="k">difficulty</span><span class="v mono" id="n-diff">0</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span></div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>This machine</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span class="sw-text"><b>Start at login</b> <span class="dim">Opens when you sign in and keeps mining in the background.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-jobs-allow"><span class="track"></span><span class="sw-text"><b>Allow remote jobs from Igneum</b> <span class="dim">Signed jobs (a benchmark, a script, logs to collect) run here once and report back.</span></span></label>
|
||||
<div class="row wrap indent"><span class="note" id="s-jobs-note"></span><button class="btn tiny ghost" id="s-jobs-check">Check now</button><button class="btn tiny ghost" id="s-jobs-history-btn" aria-expanded="false">History</button></div>
|
||||
<div class="details indent" id="s-jobs-history" hidden></div>
|
||||
<p class="note mono small indent" id="s-jobs-key" hidden></p>
|
||||
<div class="field">
|
||||
<div class="k">name</div>
|
||||
<div class="row">
|
||||
<input type="text" class="addr-input" id="s-name" placeholder="a name for this machine" maxlength="40" spellcheck="false" aria-label="Machine name">
|
||||
<button class="btn small" id="s-name-save">Rename</button>
|
||||
</div>
|
||||
<p class="help">Headers arrive before blocks. The DAA score counts blocks the whole network made; the difficulty is how hard the next one is to find.</p>
|
||||
<p class="help">A label for you only.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Consensus</h3><div class="eyebrow">the rules</div></div>
|
||||
<div class="field">
|
||||
<div class="k">digest</div>
|
||||
<div class="box mono"><span id="n-digest">not printed yet</span><button class="btn tiny" data-copy="n-digest">Copy</button></div>
|
||||
<p class="help">The fingerprint of the rules this node runs. Every node on the network shows the same one; a peer with another is refused.</p>
|
||||
<div class="field">
|
||||
<div class="k">appearance</div>
|
||||
<div class="seg" id="theme-seg" role="radiogroup" aria-label="Appearance">
|
||||
<button class="seg-b" data-theme="system" role="radio" aria-checked="true">System</button>
|
||||
<button class="seg-b" data-theme="light" role="radio" aria-checked="false">Light</button>
|
||||
<button class="seg-b" data-theme="dark" role="radio" aria-checked="false">Dark</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">next switch</div>
|
||||
<div class="big-word" id="n-switch">none planned</div>
|
||||
<p class="help" id="n-switch-help">A switch is a planned rule change. The node applies it by itself when the chain reaches that height.</p>
|
||||
<div class="switch-list mono" id="n-switches"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Finality</h3><div class="eyebrow">miner-only</div></div>
|
||||
<div class="kv">
|
||||
<div><span class="k">last lock</span><span class="v mono" id="f-lock">none yet</span></div>
|
||||
<div><span class="k">age</span><span class="v mono" id="f-age">n/a</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span></div>
|
||||
</div>
|
||||
<p class="help" id="f-note">A lock is a point the miners have agreed can never be undone. This machine votes on one every 30 s.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Sync</h3><div class="eyebrow" id="n-sync-eyebrow"></div></div>
|
||||
<div class="big-word" id="n-sync-word">starting</div>
|
||||
<p class="help" id="n-note"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Updates -->
|
||||
<section class="page" id="page-updates" data-page="updates" hidden>
|
||||
<div class="card lead-card">
|
||||
<div class="card">
|
||||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3 id="s-version">Igneum Miner</h3>
|
||||
|
|
@ -342,79 +382,39 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<button class="btn small primary" id="s-install" hidden>Install now</button>
|
||||
<button class="btn small" id="s-update">Check now</button>
|
||||
<button class="btn small" id="s-update">Check</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-auto-update"><span class="track"></span><span>Install updates by itself</span></label>
|
||||
<p class="help">Downloads in the background and installs at a quiet moment, never mid-program. Off: it downloads, then waits for Install now.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-auto-update"><span class="track"></span><span class="sw-text"><b>Install updates by itself</b> <span class="dim">Downloads in the background and installs at a quiet moment, never mid-program.</span></span></label>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3>Remote jobs</h3>
|
||||
<p class="help">Igneum publishes signed jobs (a benchmark, a script, logs to collect) next to the update manifest. This machine runs each one once and reports back. Only jobs signed by Igneum's key run.</p>
|
||||
</div>
|
||||
<button class="btn small" id="s-jobs-check">Check now</button>
|
||||
</div>
|
||||
<p class="note" id="s-jobs-note"></p>
|
||||
<div class="job-history" id="s-jobs-history"></div>
|
||||
<p class="note mono small" id="s-jobs-key"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings -->
|
||||
<section class="page" id="page-settings" data-page="settings" hidden>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Graphics cards</h3><div class="eyebrow" id="s-cards-eyebrow"></div></div>
|
||||
<div class="set-cards" id="s-cards"><div class="empty">No card yet.</div></div>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span>Ember Tune: tune every card for hashes per watt</span></label>
|
||||
<p class="help">Once after install, then weekly and after a driver or program change: the power limit steps from 100% down to 50%, then the core clock from its maximum down to 60%, 75 s a step on the live program; the memory clock is never touched. The card keeps the point with the most hashes per watt within 1% of its top rate. A step with a rejected hash, a hot GPU or a dragged memory clock is reverted. A card whose model the fleet already knows starts at that point and confirms it in two steps. Every result goes back to the fleet without anything that identifies you. A cap you set by hand is left alone.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span>Power control: let the app set NVIDIA limits</span></label>
|
||||
<p class="help">Windows asks for administrator rights once; the NVIDIA cap and the tune need them. Off, the app never asks and NVIDIA cards measure only. AMD cards need no rights. <span id="s-power-note"></span></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>This machine</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span>Start at login</span></label>
|
||||
<p class="help">The miner opens when you sign in and keeps mining in the background.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-jobs-allow"><span class="track"></span><span>Allow remote jobs from Igneum</span></label>
|
||||
<p class="help">Signed jobs from Igneum run on this machine and report back. Updates shows what ran.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span>Vote on finality checkpoints</span></label>
|
||||
<p class="help">Your miner signs a checkpoint every 30 s. Votes are what lock the chain; leave it on.</p>
|
||||
<div class="field">
|
||||
<div class="k">name</div>
|
||||
<div class="row">
|
||||
<input type="text" class="addr-input" id="s-name" placeholder="a name for this machine" maxlength="40" spellcheck="false" aria-label="Machine name">
|
||||
<button class="btn small" id="s-name-save">Rename</button>
|
||||
</div>
|
||||
<p class="help">A label for you only. Keys come from the machine id <span class="mono" id="s-mid"></span>, never from the name.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Dev fee</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span id="s-devfee-text">Dev fee 1% (1 block in 100)</span></label>
|
||||
<p class="help" id="s-devfee-note">One block in 100 is mined for the miner software's author, the same way every GPU miner takes a fee. The protocol itself takes nothing. This switch turns it off.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Logs</h3></div>
|
||||
<div class="row wrap">
|
||||
<button class="btn small" id="s-log-copy">Copy the log</button>
|
||||
<button class="btn small ghost" id="s-log-open">Show the log</button>
|
||||
<span class="note">The last lines the node and the miner wrote, for a support message.</span>
|
||||
</div>
|
||||
<p class="help">Copies the last lines the node and the miner wrote, for a support message. Show the log opens the drawer with every line.</p>
|
||||
<p class="note mono small" id="s-log-dir"></p>
|
||||
<p class="note mono small" id="s-node-dir"></p>
|
||||
</div>
|
||||
|
||||
<details class="card adv" id="s-advanced">
|
||||
<summary><h3>Advanced</h3><span class="eyebrow">devnet tools</span></summary>
|
||||
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span>Trust proof records without verifying them</span></label>
|
||||
<p class="help" id="s-trust-note">Devnet only. When no verifier is found next to the engine, the node includes proof records it never checked. A found verifier always wins. Changing this restarts the node.</p>
|
||||
<div class="row"><button class="btn small ghost" id="s-live" hidden>Open the live devnet page</button></div>
|
||||
<summary><h3>Advanced</h3><span class="eyebrow">devnet</span></summary>
|
||||
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span class="sw-text"><b>Vote on finality checkpoints</b> <span class="dim">Signs a checkpoint every 30 s. Votes lock the chain; leave it on.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span class="sw-text"><b>Trust proof records without verifying them</b> <span class="dim">Devnet only. Without a verifier the node includes records it never checked. Changing this restarts the node.</span></span></label>
|
||||
<div class="ask inline" id="ask-trust" hidden>
|
||||
<span class="ask-text" id="ask-trust-text"></span>
|
||||
<button class="btn small primary" id="ask-trust-yes">Confirm</button>
|
||||
<button class="btn small ghost" id="ask-trust-no">Cancel</button>
|
||||
</div>
|
||||
<div class="row wrap"><button class="btn small ghost" id="s-live" hidden>Open the live devnet page</button><span class="note mono small" id="s-mid"></span></div>
|
||||
</details>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- the update card (app.js, UpdateCard): one update, centred; Later, Escape or the backdrop leaves the strip above -->
|
||||
<!-- the update card (app.js, UpdateCard) -->
|
||||
<div class="upd-wrap" id="upd" hidden>
|
||||
<div class="upd-card" id="upd-card" role="dialog" aria-modal="true" aria-labelledby="upd-name" aria-describedby="upd-line" tabindex="-1">
|
||||
<div class="upd-mark" id="upd-mark">
|
||||
|
|
@ -440,16 +440,15 @@
|
|||
<div class="eyebrow ember">shown once</div>
|
||||
<h2>Save your key</h2>
|
||||
<p class="sub">This key controls the rewards address. Keep a copy somewhere safe. Anyone with the key can spend what the address holds.</p>
|
||||
<div class="field">
|
||||
<div class="k">rewards address</div>
|
||||
<div class="box mono"><span id="key-address"></span><button class="btn tiny" data-copy="key-address">Copy</button></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">private key</div>
|
||||
<div class="box mono key"><span id="key-private"></span><button class="btn tiny" data-copy="key-private">Copy</button></div>
|
||||
</div>
|
||||
<p class="note">Stored at <span class="mono" id="key-file"></span>, readable by your user only. Rewards can show it again.</p>
|
||||
<p class="note">On devnet the vote keys are test keys derived from the miner's label. Mainnet vote keys will be random and stored like this wallet.</p>
|
||||
<div class="field">
|
||||
<div class="k">rewards address</div>
|
||||
<div class="box mono"><span id="key-address"></span><button class="btn tiny" data-copy="key-address">Copy</button></div>
|
||||
</div>
|
||||
<p class="note">Stored at <span class="mono" id="key-file"></span>, readable by your user only. Earnings can show it again.</p>
|
||||
<label class="check"><input type="checkbox" id="key-ack"><span>I have saved my key</span></label>
|
||||
<div class="cta">
|
||||
<button class="btn primary big" id="btn-key-done" disabled>Start mining</button>
|
||||
|
|
@ -457,8 +456,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- the log drawer: chips filter by source, search filters by text, the ruler and the jump controls move in time.
|
||||
The list is virtualised (fixed row height, only the visible rows are in the DOM) so 20,000 lines scroll smoothly. -->
|
||||
<!-- the log drawer (unchanged from miner-ui-2) -->
|
||||
<div class="drawer" id="drawer" aria-label="Logs">
|
||||
<div class="drawer-grip" id="drawer-grip" title="Drag to resize"><i></i></div>
|
||||
<div class="drawer-head">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.js')
|
|||
const mod = { exports: {} };
|
||||
new Function('module', src)(mod);
|
||||
const { model, point } = mod.exports.TuneLine;
|
||||
const View = mod.exports.View;
|
||||
const tuneNotice = mod.exports.tuneNotice;
|
||||
const NOW = 1_800_000_000;
|
||||
const card = over => ({ vendor: 'nvidia', sweep_state: 'idle', sweep_note: '', sweep_pct: 0, power_pct: 80, sweep_at: 0, tune_line: '', tune_source: '', tune_clock_mhz: 0, tune_control: true, pinned: false, ...over });
|
||||
|
||||
|
|
@ -42,3 +44,39 @@ test('running, stopped, idle and off', () => {
|
|||
assert.equal(model(card({ vendor: 'other' }), NOW).kind, 'off');
|
||||
assert.equal(point({ tune_clock_mhz: 0, sweep_pct: 0, power_pct: 0 }), '');
|
||||
});
|
||||
|
||||
test('a card never shows 0 MH/s without a reason word: tuning, held and tuned rows, the button, the strip', () => {
|
||||
const base = { key: 'nvidia:0:x', name: 'RTX 5090', vendor: 'nvidia', kind: 'discrete', enabled: true, state: 'tuning', hash_now: 123.4, power_w: 219.6, tune_step: 7, tune_steps: 9, tune_eta_s: 230, tune_plan: 'full', sweep_note: 'tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)', vram_mb: 32768 };
|
||||
const r = View.cardRow(base);
|
||||
assert.equal(r.word, 'tuning: step 7 of 9 · measuring 220 W');
|
||||
assert.equal(r.hash, '123', 'the live rate stays in the rate column');
|
||||
assert.equal(r.tone, 'on');
|
||||
const h = View.cardRow({ ...base, state: 'held', hash_now: 0, power_w: 0, message: 'held for a remote job: Ember Tune on PC 1', tune_steps: 0 });
|
||||
assert.equal(h.word, 'held for a remote job: Ember Tune on PC 1');
|
||||
assert.equal(h.hash, '', 'blank, never 0');
|
||||
const off = View.cardRow({ ...base, state: 'off', enabled: false, hash_now: 0 });
|
||||
assert.equal(off.word, 'off'); assert.equal(off.hash, '');
|
||||
// the big button during a tune
|
||||
const t = View.toggle({ cards: [base], state: 'mining', paused: false }, { synced: true }, {});
|
||||
assert.equal(t.label, 'Tuning'); assert.equal(t.disabled, true);
|
||||
assert.equal(t.sub, 'mining again in about 4 min · RTX 5090');
|
||||
const held = View.toggle({ cards: [{ ...base, state: 'held' }], state: 'waiting', paused: false }, { synced: true }, {});
|
||||
assert.equal(held.label, 'Held'); assert.equal(held.disabled, true);
|
||||
// the strip
|
||||
const n = tuneNotice([base]);
|
||||
assert.equal(n.kind, 'tuning');
|
||||
assert.equal(n.text, 'Tuning RTX 5090: the full tune, step 7 of 9. Mining again in about 4 min.');
|
||||
assert.equal(tuneNotice([{ ...base, state: 'mining' }]), null);
|
||||
// the finished row: TuneLine's tuned model (the table's "Tuned: ..." line with the point)
|
||||
const done = model({ ...base, state: 'mining', sweep_state: 'idle', tune_line: 'Tuned: 123.4 MH/s at 220 W (0.561 MH/W)', tune_source: 'full', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 60, tune_control: true }, NOW);
|
||||
assert.equal(done.kind, 'tuned');
|
||||
assert.equal(done.text, 'Tuned: 123.4 MH/s at 220 W (0.561 MH/W)');
|
||||
assert.match(done.note, /^2470 MHz at 100%, full tune, 1 min ago/);
|
||||
assert.equal(View.tuneEta(30), 'under a minute');
|
||||
assert.equal(View.tuneEta(700), 'about 12 min');
|
||||
});
|
||||
|
||||
test('Ember 2: £ a day from watts and pence per kWh', () => {
|
||||
assert.equal(mod.exports.TuneLine.pounds(310, 28.5), '£2.12');
|
||||
assert.equal(mod.exports.TuneLine.pounds(102.7, 28.5), '£0.70');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ test('release notes: sentences, three lines under 70 characters, the rest behind
|
|||
|
||||
test('what the card says: available, downloading with the ring, ready, installing, failed, manual, waiting', () => {
|
||||
const a = model(upd({ status: 'available', downloaded: false, ready: false, progress: 0 }), {});
|
||||
assert.equal(a.name, 'Igneum Ember 0.3.7'); assert.equal(a.line, 'is available.'); assert.equal(a.stage, 'available');
|
||||
assert.equal(a.name, 'Igneum Miner 0.3.7'); assert.equal(a.line, 'is available.'); assert.equal(a.stage, 'available');
|
||||
assert.equal(a.key, 'update:0.3.7:pending'); assert.equal(a.ring, 'none'); assert.equal(a.size, '21 MB');
|
||||
assert.deepEqual(a.actions.map((x) => x.label), ['Install now', 'Later']); assert.equal(a.dismissable, true);
|
||||
const d = model(upd({ status: 'downloading', downloaded: false, ready: false, progress: 0.43 }), {});
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ const V = mod.exports.View;
|
|||
|
||||
const card = (over) => ({ key: 'nvidia:0:RTX 5090', name: 'NVIDIA GeForce RTX 5090', vendor: 'nvidia', kind: 'discrete', worker: 'CUDA', vram_mb: 32768, enabled: true, state: 'mining', hash_now: 124.3, hash_avg: 120, accepted: 3, rejected: 0, identities: 8, ids: [], prepared: true, restart_in_s: 0, message: '', reason: '', power_w: 410.2, power_limit_w: 460, power_default_w: 575, power_pct: 80, power_applied: true, temp_gpu: 61, temp_mem: 72, telemetry_at: 1, ...over });
|
||||
|
||||
test('the six sections and their order', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'prove', 'rewards', 'node', 'updates', 'settings']);
|
||||
assert.equal(V.page('node').title, 'Node');
|
||||
test('the four sections and their order (miner-ui-3)', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'earnings', 'prove', 'settings']);
|
||||
assert.equal(V.page('earnings').title, 'Earnings');
|
||||
assert.equal(V.page('nonsense').id, 'mine');
|
||||
});
|
||||
|
||||
|
|
@ -129,9 +129,9 @@ test('the prove words follow the switch, the setup, the node and the status', ()
|
|||
assert.equal(V.verifierWords({}).word, 'not read yet');
|
||||
});
|
||||
|
||||
test('the dev-fee lines name the share and where Settings turns it off', () => {
|
||||
test('the dev-fee lines name the share and the switch that turns it off', () => {
|
||||
const s = (on, fee) => ({ settings: { dev_fee: on }, mining: { fee_total: fee }, dev_fee: { on, percent: on ? 1 : 0, address: '0x1234567890abcdef1234567890abcdef12345678', line: '' } });
|
||||
assert.equal(V.devFeeLine(s(true, 12)), 'One block in 100 pays the miner software’s dev fee (12 so far). Settings turns it off.');
|
||||
assert.equal(V.devFeeLine(s(true, 12)), 'One block in 100 pays the miner software’s dev fee (12 so far). The switch above turns it off.');
|
||||
assert.equal(V.devFeeLine(s(false, 0)), 'The dev fee is off. Every block pays this address.');
|
||||
assert.equal(V.devFeeText(s(true, 0)), 'Dev fee 1% (1 block in 100) to 0x123456…5678');
|
||||
assert.match(V.devFeeText(s(false, 0)), /^Dev fee off/);
|
||||
|
|
@ -139,7 +139,7 @@ test('the dev-fee lines name the share and where Settings turns it off', () => {
|
|||
|
||||
test('the remote-jobs line and the helpers', () => {
|
||||
const title = (j) => j.title || j.kind;
|
||||
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off: nothing runs here until Settings allows remote jobs.');
|
||||
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off: nothing runs here until the switch above allows remote jobs.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: false }, 1000, title), 'No jobs address in this build.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, active: true, id: 'job-3', kind: 'build', title: 'build' }, 1000, title), 'Running build (job-3).');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, checked_at: 940, queued: 2 }, 1000, title), 'Nothing running; checked 1 min ago; 2 queued.');
|
||||
|
|
@ -198,3 +198,120 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
|||
const t2 = V.toggle({ state: 'mining', paused: false, cards: [card(), gone] }, { synced: true }, {});
|
||||
assert.equal(t2.sub, 'mining on 1 of 1 card');
|
||||
});
|
||||
|
||||
// ---- miner-ui-3: the row as the hero object, the money, the tune line, the prove tier, the node line, the asks ----
|
||||
const NOW = 1_800_000_000;
|
||||
const settings = (over) => ({ settings: { sweep: true, tuning_off: false, tune_period_s: 604800, power_control: true, ...over }, now: NOW });
|
||||
|
||||
test('the row word: every state carries a reason, and a paused miner says paused on a card that is on', () => {
|
||||
const m = { paused: true, state: 'paused' };
|
||||
assert.equal(V.rowWord(V.cardRow(card({ state: 'off', hash_now: 0 })), m), 'paused');
|
||||
assert.equal(V.rowWord(V.cardRow(card({ state: 'waiting', hash_now: 0 })), m), 'paused');
|
||||
assert.equal(V.rowWord(V.cardRow(card()), { paused: false, state: 'mining' }), 'mining');
|
||||
assert.equal(V.rowWord(V.cardRow(card({ enabled: false, state: 'off' })), m), 'off');
|
||||
assert.equal(V.cardRow(card({ state: 'held', hash_now: 0, message: 'held for a remote job: shard benchmark' })).word, 'held for a remote job: shard benchmark');
|
||||
assert.equal(V.cardRow(card({ state: 'tuning', tune_step: 3, tune_steps: 9, power_w: 300 })).word, 'tuning: step 3 of 9 · measuring 300 W');
|
||||
assert.equal(V.cardRow(card({ state: 'starting', hash_now: 0 })).word, 'starting');
|
||||
assert.equal(V.cardRow(card({ state: 'failed', hash_now: 0 })).tone, 'bad');
|
||||
});
|
||||
|
||||
test('money: £ a day from watts at a price, nothing without one, the cells leave out what is not reported', () => {
|
||||
assert.equal(V.poundsPerDay(310, 28.5).toFixed(4), '2.1204');
|
||||
assert.equal(V.money(V.poundsPerDay(310, 28.5)), '£2.12');
|
||||
assert.equal(V.poundsPerDay(310, 0), null);
|
||||
assert.equal(V.poundsPerDay(0, 28), null);
|
||||
assert.equal(V.money(123.4), '£123');
|
||||
assert.equal(V.money(null), '');
|
||||
const c = V.cells(card(), 28);
|
||||
assert.equal(c.hash, '124'); assert.equal(c.power, '410 W'); assert.equal(c.money, '£2.76'); assert.equal(c.temp, '61 °C'); assert.equal(c.needsPrice, false); assert.equal(c.noReading, '');
|
||||
assert.equal(c.eff, '0.30 MH/W');
|
||||
const noPrice = V.cells(card(), 0);
|
||||
assert.equal(noPrice.money, ''); assert.equal(noPrice.needsPrice, true);
|
||||
const apple = V.cells(card({ vendor: 'apple', kind: 'apple', power_w: 0, temp_gpu: 0, temp_mem: 0, eff_mhw: 0 }), 28);
|
||||
assert.equal(apple.power, ''); assert.equal(apple.money, ''); assert.equal(apple.temp, ''); assert.equal(apple.needsPrice, false);
|
||||
assert.equal(apple.noReading, 'No power or temperature reading on Apple silicon');
|
||||
assert.equal(V.cells(card({ kind: 'integrated', vendor: 'other', power_w: 0, temp_gpu: 0, eff_mhw: 0 }), 28).noReading, 'No power or temperature reading on an integrated GPU');
|
||||
assert.equal(V.wattsTotal([card(), card({ key: 'b', power_w: 100 }), card({ key: 'c', power_w: 50, removed_at: 5 })]), 510.2);
|
||||
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0.422 }))), '0.42 MH/W');
|
||||
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0, power_w: 0 }))), '');
|
||||
});
|
||||
|
||||
test('the tune line: never run, running with a step and a bar, tuned with the point and the next check, the prior, measured only, pinned, stopped, fleet pause, no control', () => {
|
||||
const nv = (over) => card({ sweep_state: 'idle', sweep_note: '', sweep_pct: 0, sweep_at: 0, tune_line: '', tune_source: '', tune_clock_mhz: 0, tune_control: true, pinned: false, tune_step: 0, tune_steps: 0, tune_eta_s: 0, ...over });
|
||||
const idle = V.tuneWords(nv(), settings(), NOW);
|
||||
assert.equal(idle.kind, 'idle'); assert.equal(idle.text, 'Not tuned yet: starts after 2 min of steady mining'); assert.equal(idle.button, 'tune'); assert.equal(idle.progress, -1);
|
||||
assert.equal(V.tuneWords(nv(), settings({ sweep: false }), NOW).text, 'Not tuned yet: Ember Tune is off in Settings');
|
||||
const run = V.tuneWords(nv({ state: 'tuning', tune_step: 7, tune_steps: 9, tune_eta_s: 230 }), settings(), NOW);
|
||||
assert.equal(run.kind, 'running'); assert.equal(run.text, 'Tuning: step 7 of 9 · about 4 min left'); assert.equal(run.button, 'stop'); assert.equal(run.progress.toFixed(2), '0.67');
|
||||
const runNoStep = V.tuneWords(nv({ sweep_state: 'running', sweep_note: 'tuning: waits for 120 s of steady mining' }), settings(), NOW);
|
||||
assert.equal(runNoStep.text, 'Tuning: waits for 120 s of steady mining');
|
||||
const tuned = V.tuneWords(nv({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 7200 }), settings(), NOW);
|
||||
assert.equal(tuned.kind, 'tuned'); assert.equal(tuned.note, '122.3 MH/s at 290 W (0.422 MH/W)'); assert.equal(tuned.button, 'tune');
|
||||
assert.match(tuned.text, /^Tuned 2 h ago · 2470 MHz at 100% · next check /);
|
||||
const prior = V.tuneWords(nv({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'confirm', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 120 }), settings(), NOW);
|
||||
assert.match(prior.text, /^Tuned 2 min ago from the fleet prior, confirmed · 2470 MHz at 100%/);
|
||||
const measured = V.tuneWords(nv({ tune_line: 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed' }), settings(), NOW);
|
||||
assert.equal(measured.kind, 'measured'); assert.equal(measured.text, 'Measured 26.7 MH/s at 38 W (0.703 MH/W) as it runs, 10 min ago');
|
||||
assert.equal(measured.note, 'Measured only on Apple silicon: the system sets the clocks and the power; no control exposed');
|
||||
const off = V.tuneWords(nv({ tune_control: false, sweep_note: 'measure only until Power control is on in Settings (Windows asks for administrator rights once)' }), settings({ power_control: false }), NOW);
|
||||
assert.equal(off.kind, 'idle'); assert.equal(off.note, 'Measured only until Power control is on in Settings (Windows asks for administrator rights once)');
|
||||
const pinned = V.tuneWords(nv({ tune_line: 'Tuned: 120 MH/s at 300 W (0.400 MH/W)', tune_source: 'full', pinned: true, power_pct: 80, sweep_at: NOW - 60 }), settings(), NOW);
|
||||
assert.match(pinned.text, /^Your cap stays pinned at 80% · Tuned 1 min ago/);
|
||||
const stopped = V.tuneWords(nv({ sweep_note: 'tuning stopped: a remote job took the GPU' }), settings(), NOW);
|
||||
assert.equal(stopped.kind, 'stopped'); assert.equal(stopped.text, 'Tuning stopped: a remote job took the GPU');
|
||||
const paused = V.tuneWords(nv(), settings({ tuning_off: true, tuning_note: 'tuning paused fleet-wide by the signed manifest' }), NOW);
|
||||
assert.equal(paused.kind, 'paused'); assert.equal(paused.button, '');
|
||||
assert.equal(V.tuneWords(card({ vendor: 'other', kind: 'integrated', sweep_supported: false }), settings(), NOW).kind, 'none');
|
||||
assert.equal(V.tunable(card({ vendor: 'apple', kind: 'apple' })), true);
|
||||
assert.equal(V.tunable(card({ removed_at: 5 })), false);
|
||||
});
|
||||
|
||||
test('the next check is a weekday inside a week, a date beyond it, due now when overdue', () => {
|
||||
assert.equal(V.nextCheck(0, 604800, NOW), '');
|
||||
assert.equal(V.nextCheck(NOW - 700000, 604800, NOW), 'next check due now');
|
||||
assert.equal(V.nextCheck(NOW - 604800 + 3600, 604800, NOW), 'next check within a day');
|
||||
const inThree = V.nextCheck(NOW - 604800 + 3 * 86400, 604800, NOW);
|
||||
assert.match(inThree, /^next check (Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday)$/);
|
||||
assert.match(V.nextCheck(NOW, 604800 * 3, NOW), /^next check \d{1,2} [A-Z][a-z]{2}$/);
|
||||
assert.equal(V.nextCheck(NOW - 100, 0, NOW), V.nextCheck(NOW - 100, 604800, NOW), 'no period means seven days');
|
||||
});
|
||||
|
||||
test('the Tuning card schedule and the goal consequence', () => {
|
||||
const nv = (over) => card({ sweep_at: 0, tune_steps: 0, ...over });
|
||||
assert.equal(V.schedule([nv()], settings(), NOW), 'Not tuned yet: the first tune starts 2 min into steady mining.');
|
||||
assert.equal(V.schedule([nv()], settings({ sweep: false }), NOW), 'Ember Tune is off. Tune and Tune all still work by hand.');
|
||||
assert.equal(V.schedule([nv({ state: 'tuning', tune_step: 2, tune_steps: 9, tune_eta_s: 500 })], settings(), NOW), 'Tuning now: NVIDIA GeForce RTX 5090, step 2 of 9, mining again in about 8 min.');
|
||||
assert.match(V.schedule([nv({ sweep_at: NOW - 7200 }), nv({ key: 'b', name: 'B', sweep_at: NOW - 60 })], settings(), NOW), /^Last tune 1 min ago \(B\) · next check /);
|
||||
assert.equal(V.schedule([], settings(), NOW), 'No card here can be tuned.');
|
||||
assert.equal(V.schedule([nv()], settings({ tuning_off: true, tuning_note: 'tuning paused fleet-wide by the signed manifest' }), NOW), 'tuning paused fleet-wide by the signed manifest');
|
||||
const g = V.goalWords('efficiency', [card({ sweep_watts: 290, power_w: 410 })], 28);
|
||||
assert.equal(g.watts, 290); assert.equal(g.consequence, 'about £1.95 a day at 290 W');
|
||||
assert.equal(V.goalWords('rate', [card({ power_default_w: 575 })], 28).watts, 575);
|
||||
assert.equal(V.goalWords('balanced', [card()], 28).watts, 410);
|
||||
assert.equal(V.goalWords('nonsense', [card()], 0).goal, 'balanced');
|
||||
assert.equal(V.goalWords('balanced', [card()], 0).consequence, 'about 410 W');
|
||||
});
|
||||
|
||||
test('the prove tier sentence per card, and the counts in one line', () => {
|
||||
assert.equal(V.proveTier(card()), 'NVIDIA GeForce RTX 5090: proves while it mines (32 GB; a full shard needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'NVIDIA GeForce RTX 4070', vram_mb: 12288 })), 'NVIDIA GeForce RTX 4070: cannot prove a full shard (12 GB; a full shard needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'Apple M5 Max', vendor: 'apple' })), 'Apple M5 Max: proves on the CPU, slowly.');
|
||||
assert.equal(V.proveTier(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd', vram_mb: 16384 })), 'AMD Radeon RX 9070 XT: cannot prove yet (the prover is CUDA only).');
|
||||
assert.equal(V.proveLine({ assigned: 0, submitted: 0, paid: 0, paid_wei: '0' }, false, true), 'Off · not proving · 0 assigned · 0 proven · 0 paid · 0.0000 IGN');
|
||||
assert.equal(V.proveLine({ status: 'idle', assigned: 3, submitted: 2, paid: 1, paid_wei: '1500000000000000000', available: true, enabled: true }, true, true), 'Idle · nothing assigned · 3 assigned · 2 proven · 1 paid · 1.5000 IGN');
|
||||
});
|
||||
|
||||
test('the node line, the activity line, the asks', () => {
|
||||
const n = { state: 'synced', blocks: 109859, headers: 109859, peers: 6, daa: 198000, last_reading_age_s: 5, message: '' };
|
||||
const l = V.nodeLine(n, { severity: 'none' }, '');
|
||||
assert.equal(l.text, 'Node synced · 6 peers · 109,859 blocks'); assert.equal(l.tone, 'ok'); assert.match(l.sub, /every block/);
|
||||
assert.equal(V.nodeLine({ ...n, peers: 1, blocks: 0, state: 'starting' }, { severity: 'none' }, '').text, 'Node starting · 1 peer');
|
||||
assert.equal(V.nodeLine(n, { severity: 'block', skew_s: -412 }, '').tone, 'bad');
|
||||
assert.equal(V.eventLine('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}'), 'consensus parameters from the signed manifest (details in the log)');
|
||||
assert.equal(V.eventLine('node synced: 151512 blocks, 6 peer(s)'), 'node synced: 151512 blocks, 6 peer(s)');
|
||||
assert.equal(V.eventLine('a '.repeat(100), 40).length <= 41, true);
|
||||
assert.equal(V.addressAsk('0x1234567890abcdef1234567890abcdef12345678'), 'Pay 0x1234…5678 from the next block? The miner restarts.');
|
||||
assert.match(V.trustAsk(true), /^Include proof records the node never checked\? Devnet only; the node restarts\.$/);
|
||||
assert.match(V.trustAsk(false), /^Verify proof records again\?/);
|
||||
assert.equal(V.ago(30), 'just now'); assert.equal(V.ago(7200), '2 h ago'); assert.equal(V.ago(200000), '2 d ago');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ static void applyState(const std::string& j) {
|
|||
// Runs cmd /c <line> as administrator, waits, and answers on the engine's stdin.
|
||||
static void runElevated(std::wstring line) {
|
||||
std::thread([line] {
|
||||
std::wstring params = L"/c " + line;
|
||||
// cmd strips the first and last quote of a line that starts with one and holds more than two (two cards, or a
|
||||
// cap plus a script): one outer pair around the whole line is what it may strip (PC 1, 6 October 2026)
|
||||
std::wstring params = L"/c \"" + line + L"\"";
|
||||
wchar_t sysdir[MAX_PATH];
|
||||
GetSystemDirectoryW(sysdir, MAX_PATH);
|
||||
std::wstring cmdExe = std::wstring(sysdir) + L"\\cmd.exe"; // the absolute path, never a bare name (R4.3.3)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
|
||||
#ifndef IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.13"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,13,0
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.14"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,14,0
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
65
brand/marks/vendor-marks.mjs
Normal file
65
brand/marks/vendor-marks.mjs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Igneum vendor and OS marks (gpu-logos, 7 October 2026): the strings the miner app ships in app/igneum-app/ui/app.js
|
||||
// (View.MARKS and View.VENDORS), exported verbatim for the site and anything else that names the hardware.
|
||||
// view.test.mjs fails when this file and app.js drift apart, so edit app.js first and regenerate this file with
|
||||
// `node brand/marks/regen.mjs` (or copy the strings by hand; the test says which one moved).
|
||||
//
|
||||
// Each glyph: a hand-drawn simplified monochrome mark of the vendor's public geometry (never a copied logo file,
|
||||
// never a raster), 24 x 24 viewBox at 22 px, under 460 bytes, fill or stroke through currentColor so the element's
|
||||
// colour tints it. Nominative use that names the hardware; the ember accent is for state and never tints a brand.
|
||||
//
|
||||
// The treatment in the app (app.css, the block at the end): a 44 x 44 well, radius 12, background the vendor colour
|
||||
// at .14 alpha (dark) or .10 (light), a 1 px ring in the vendor colour at .45 alpha, the glyph in the full colour;
|
||||
// hover and focus-within add a 3 px halo of the well colour; nothing animates. The light hex of every vendor reads at
|
||||
// 3:1 or better on its well over white (nvidia 3.87, amd 5.01, intel 4.29, apple 7.52, gpu 4.65).
|
||||
//
|
||||
// Class names in the app: .badge.<vendor> (nvidia | amd | intel | apple | gpu), .badge.mini for a 26 px inline mark,
|
||||
// .gen for the series line under the name. Tokens: --mark-<vendor>, --mark-<vendor>-well, --mark-<vendor>-ring.
|
||||
|
||||
export const VENDORS = {
|
||||
"nvidia": {
|
||||
"label": "NVIDIA",
|
||||
"dark": "#8BE37A",
|
||||
"light": "#2F8A22"
|
||||
},
|
||||
"amd": {
|
||||
"label": "AMD Radeon",
|
||||
"dark": "#FF5A5A",
|
||||
"light": "#C41E2A"
|
||||
},
|
||||
"intel": {
|
||||
"label": "Intel",
|
||||
"dark": "#7CC4FF",
|
||||
"light": "#1C6FD6"
|
||||
},
|
||||
"apple": {
|
||||
"label": "Apple",
|
||||
"dark": "#E6E3DD",
|
||||
"light": "#4A4A50"
|
||||
},
|
||||
"gpu": {
|
||||
"label": "GPU",
|
||||
"dark": "#9A9A9E",
|
||||
"light": "#6B6B70"
|
||||
}
|
||||
};
|
||||
export const WELL_ALPHA = { dark: 0.14, light: 0.1 };
|
||||
export const MARKS = {
|
||||
"nvidia": "<svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.6 12C5 7.9 8.3 5.8 12 5.8s7 2.1 9.4 6.2c-2.4 4.1-5.7 6.2-9.4 6.2S5 16.1 2.6 12z\"/><path d=\"M15.6 12A3.6 3.6 0 1 0 12 15.6\"/><circle cx=\"12\" cy=\"12\" r=\"1.2\" fill=\"currentColor\" stroke=\"none\"/></svg>",
|
||||
"amd": "<svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\" fill=\"currentColor\"><path d=\"M8 3h13v13l-4-4V7h-5z\"/><path d=\"M3 8l4 4v5h5l4 4H3z\"/></svg>",
|
||||
"intel": "<svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"><path d=\"M7.4 7.6C4.9 8.8 3.4 10.5 3.4 12.3c0 3.4 4.3 5.9 9.8 5.9 4.5 0 7.6-1.6 7.6-3.9 0-1.4-1.3-2.6-3.5-3.3\"/><path d=\"M11.2 9.6v6.2\"/><circle cx=\"11.2\" cy=\"6.6\" r=\"1.1\" fill=\"currentColor\" stroke=\"none\"/></svg>",
|
||||
"apple": "<svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\" fill=\"currentColor\"><path d=\"M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z\"/></svg>",
|
||||
"gpu": "<svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\"><rect x=\"5.5\" y=\"5.5\" width=\"13\" height=\"13\" rx=\"2.2\"/><rect x=\"9.5\" y=\"9.5\" width=\"5\" height=\"5\" rx=\"1\"/><path d=\"M9 2.5v3M12 2.5v3M15 2.5v3M9 18.5v3M12 18.5v3M15 18.5v3M2.5 9h3M2.5 12h3M2.5 15h3M18.5 9h3M18.5 12h3M18.5 15h3\"/></svg>"
|
||||
};
|
||||
// OS marks in the same treatment: Apple is the vendor glyph; Windows is the four slanted panes
|
||||
export const OS_MARKS = {
|
||||
macos: MARKS.apple,
|
||||
windows: "<svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\" focusable=\"false\" fill=\"currentColor\"><path d=\"M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z\"/></svg>"
|
||||
};
|
||||
export const OS_COLOURS = { macos: VENDORS.apple, windows: { label: 'Windows', dark: '#7CC4FF', light: '#1C6FD6' } };
|
||||
// the CSS tokens for both themes, as the app declares them
|
||||
export function tokensCss() {
|
||||
const line = (theme) => Object.entries(VENDORS).map(([v, c]) => { const h = c[theme], r = parseInt(h.slice(1, 3), 16), g = parseInt(h.slice(3, 5), 16), b = parseInt(h.slice(5, 7), 16), a = String(WELL_ALPHA[theme]).replace(/^0/, ''); return `--mark-${v}:${h};--mark-${v}-well:rgba(${r},${g},${b},${a});--mark-${v}-ring:rgba(${r},${g},${b},.45)`; }).join(';');
|
||||
return `:root{${line('dark')}}\n@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){${line('light')}}}\n:root[data-theme="light"]{${line('light')}}`;
|
||||
}
|
||||
// the well: <div class="badge nvidia"><svg…></svg></div>
|
||||
export function markHtml(vendor, size) { const v = MARKS[vendor] ? vendor : 'gpu'; return '<div class="badge ' + (size ? size + ' ' : '') + v + '" data-mark="' + v + '" title="' + VENDORS[v].label + '">' + MARKS[v] + '</div>'; }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
18
contracts/bridge/foundry.toml
Normal file
18
contracts/bridge/foundry.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[profile.default]
|
||||
src = "src"
|
||||
test = "test"
|
||||
script = "script"
|
||||
out = "out"
|
||||
libs = []
|
||||
solc_version = "0.8.28"
|
||||
# The verifier calls the BLS12-381 precompiles of EIP-2537 (live on Sepolia and mainnet since Pectra), so the test EVM
|
||||
# runs the Osaka rules, the fork Sepolia is on (EIP-7883 modexp pricing counts here).
|
||||
evm_version = "osaka"
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
via_ir = true
|
||||
fs_permissions = [{ access = "read", path = "./test/vectors" }, { access = "read-write", path = "./deploy-out.json" }]
|
||||
auto_detect_remappings = false
|
||||
|
||||
[rpc_endpoints]
|
||||
sepolia = "https://ethereum-sepolia-rpc.publicnode.com"
|
||||
46
contracts/bridge/script/Deploy.s.sol
Normal file
46
contracts/bridge/script/Deploy.s.sol
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {Vm, VM_ADDRESS} from "../test/Vm.sol";
|
||||
import {IgneumCertificateVerifier} from "../src/IgneumCertificateVerifier.sol";
|
||||
|
||||
/// Deploys the verifier on Sepolia from BRIDGE_DEPLOYER_KEY (environment, never printed), installs the voter table
|
||||
/// from the vectors file named in BRIDGE_TABLE_JSON (a gen.mjs output: keys, weights, index, chain_id) and, when the
|
||||
/// file carries a certificate (bitmap, signature), submits it, so the contract records one checkpoint final from the start.
|
||||
///
|
||||
/// BRIDGE_TABLE_JSON=test/vectors/chain.json forge script script/Deploy.s.sol:Deploy --rpc-url sepolia --broadcast --sig "run()"
|
||||
contract Deploy {
|
||||
Vm constant vm = Vm(VM_ADDRESS);
|
||||
|
||||
function run() external {
|
||||
uint256 key = vm.envUint("BRIDGE_DEPLOYER_KEY");
|
||||
string memory j = vm.readFile(vm.envOr("BRIDGE_TABLE_JSON", "test/vectors/chain.json"));
|
||||
bytes[] memory keys = vm.parseJsonBytesArray(j, ".keys");
|
||||
uint256[] memory w = vm.parseJsonUintArray(j, ".weights");
|
||||
bytes memory packed;
|
||||
uint64[] memory weights = new uint64[](w.length);
|
||||
for (uint256 i = 0; i < keys.length; i++) {
|
||||
packed = abi.encodePacked(packed, keys[i]);
|
||||
weights[i] = uint64(w[i]);
|
||||
}
|
||||
uint64 index = uint64(vm.parseJsonUint(j, ".index"));
|
||||
|
||||
vm.startBroadcast(key);
|
||||
IgneumCertificateVerifier v = new IgneumCertificateVerifier(vm.parseJsonString(j, ".chain_id"));
|
||||
v.installTable(index, packed, weights);
|
||||
bool hasCert = vm.keyExistsJson(j, ".bitmap");
|
||||
if (hasCert) {
|
||||
v.submitCertificate(index, vm.parseJsonBytes32(j, ".checkpoint"), vm.parseJsonBytes(j, ".bitmap"), vm.parseJsonBytes(j, ".signature"));
|
||||
}
|
||||
vm.stopBroadcast();
|
||||
|
||||
vm.writeFile(
|
||||
"deploy-out.json",
|
||||
string.concat(
|
||||
"{\n \"IgneumCertificateVerifier\": \"", vm.toString(address(v)), "\",\n \"chain_id\": \"", vm.parseJsonString(j, ".chain_id"),
|
||||
"\",\n \"table_index\": ", vm.toString(uint256(index)), ",\n \"voters\": ", vm.toString(keys.length), ",\n \"table_id\": \"",
|
||||
vm.toString(v.tableId()), "\",\n \"final_checkpoint\": \"", hasCert ? vm.toString(vm.parseJsonBytes32(j, ".checkpoint")) : "none yet", "\"\n}\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
22
contracts/bridge/script/Send.s.sol
Normal file
22
contracts/bridge/script/Send.s.sol
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {Vm, VM_ADDRESS} from "../test/Vm.sol";
|
||||
|
||||
/// Sends SEND_WEI of the chain's coin from the key in BRIDGE_DEPLOYER_KEY to SEND_TO (Sepolia test ETH between the
|
||||
/// lanes' throwaway deployers). The key is read from the environment and never printed.
|
||||
///
|
||||
/// SEND_TO=0x.. SEND_WEI=20000000000000000 forge script script/Send.s.sol:Send --rpc-url sepolia --broadcast --sig "run()"
|
||||
contract Send {
|
||||
Vm constant vm = Vm(VM_ADDRESS);
|
||||
|
||||
function run() external {
|
||||
uint256 key = vm.envUint("BRIDGE_DEPLOYER_KEY");
|
||||
address to = vm.envAddress("SEND_TO");
|
||||
uint256 wei_ = vm.envUint("SEND_WEI");
|
||||
vm.startBroadcast(key);
|
||||
(bool ok,) = to.call{value: wei_}("");
|
||||
require(ok, "send failed");
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
113
contracts/bridge/src/BLS12381.sol
Normal file
113
contracts/bridge/src/BLS12381.sol
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// BLS12-381 through the EIP-2537 precompiles (Ethereum mainnet and Sepolia since Pectra): the hash-to-curve of
|
||||
/// RFC 9380 (BLS12381G2_XMD:SHA-256_SSWU_RO_) with the caller's domain separation tag, public-key aggregation in G1
|
||||
/// and the two-pairing check of a "minimal public key" signature (keys in G1, signatures in G2), the scheme of
|
||||
/// Igneum's finality votes (consensus/core/src/finality.rs, blst "min_pk").
|
||||
///
|
||||
/// Encodings are the precompiles' own: a field element is 64 bytes (16 zero bytes then the 48-byte big-endian
|
||||
/// value), a G1 point 128 bytes (x, y), a G2 point 256 bytes (x.c0, x.c1, y.c0, y.c1). Compressed chain forms
|
||||
/// (48-byte keys, 96-byte signatures) are decompressed off chain by the submitter; the pairing precompile refuses
|
||||
/// a point off the curve or outside the prime-order subgroup, so a wrong decompression fails the check.
|
||||
library BLS12381 {
|
||||
address internal constant G1ADD = address(0x0b);
|
||||
address internal constant G2ADD = address(0x0d);
|
||||
address internal constant PAIRING = address(0x0f);
|
||||
address internal constant MAP_FP2_TO_G2 = address(0x11);
|
||||
address internal constant MODEXP = address(0x05);
|
||||
|
||||
uint256 internal constant G1_LEN = 128;
|
||||
uint256 internal constant G2_LEN = 256;
|
||||
|
||||
/// The field modulus p, big-endian, 48 bytes (the modexp precompile's modulus).
|
||||
bytes internal constant P = hex"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab";
|
||||
|
||||
/// The G1 generator with its y negated (p - y), in the 128-byte encoding, for the pairing check
|
||||
/// e(pk, H(m)) * e(-G1, sig) == 1.
|
||||
bytes internal constant NEG_G1 =
|
||||
hex"0000000000000000000000000000000017f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"
|
||||
hex"00000000000000000000000000000000114d1d6855d545a8aa7d76c8cf2e21f267816aef1db507c96655b9d5caac42364e6f38ba0ecb751bad54dcd6b939c2ca";
|
||||
|
||||
error PrecompileFailed(address which);
|
||||
error BadLength(string what);
|
||||
|
||||
// ---- hash to curve ----
|
||||
|
||||
/// expand_message_xmd with SHA-256 (RFC 9380 section 5.3.1) to `len` bytes; len at most 255 * 32.
|
||||
function expandMessageXmd(bytes memory msg_, bytes memory dst, uint256 len) internal pure returns (bytes memory out) {
|
||||
require(dst.length <= 255, "BLS: DST too long");
|
||||
uint256 ell = (len + 31) / 32;
|
||||
require(ell <= 255 && len > 0, "BLS: bad length");
|
||||
bytes memory dstPrime = abi.encodePacked(dst, uint8(dst.length));
|
||||
bytes32 b0 = sha256(abi.encodePacked(new bytes(64), msg_, uint16(len), uint8(0), dstPrime));
|
||||
bytes32 bi = sha256(abi.encodePacked(b0, uint8(1), dstPrime));
|
||||
out = new bytes(ell * 32);
|
||||
assembly {
|
||||
mstore(add(out, 32), bi)
|
||||
}
|
||||
for (uint256 i = 2; i <= ell; i++) {
|
||||
bi = sha256(abi.encodePacked(b0 ^ bi, uint8(i), dstPrime));
|
||||
assembly {
|
||||
mstore(add(add(out, 32), mul(sub(i, 1), 32)), bi)
|
||||
}
|
||||
}
|
||||
assembly {
|
||||
mstore(out, len)
|
||||
}
|
||||
}
|
||||
|
||||
/// A 64-byte big-endian integer reduced mod p and returned in the precompiles' 64-byte field encoding.
|
||||
function reduce64(bytes memory chunk, uint256 offset) internal view returns (bytes memory fe) {
|
||||
require(chunk.length >= offset + 64, "BLS: chunk");
|
||||
bytes memory base = new bytes(64);
|
||||
for (uint256 i = 0; i < 64; i++) {
|
||||
base[i] = chunk[offset + i];
|
||||
}
|
||||
// modexp(base^1 mod p): lengths 64, 1, 48
|
||||
bytes memory input = abi.encodePacked(uint256(64), uint256(1), uint256(48), base, uint8(1), P);
|
||||
(bool ok, bytes memory r) = MODEXP.staticcall(input);
|
||||
if (!ok || r.length != 48) revert PrecompileFailed(MODEXP);
|
||||
fe = abi.encodePacked(bytes16(0), r);
|
||||
}
|
||||
|
||||
/// hash_to_curve for G2: two field elements of Fp2 from a 256-byte expansion, each mapped by the precompile
|
||||
/// (which clears the cofactor), then added.
|
||||
function hashToG2(bytes memory msg_, bytes memory dst) internal view returns (bytes memory point) {
|
||||
bytes memory u = expandMessageXmd(msg_, dst, 256);
|
||||
bytes memory q0 = mapFp2ToG2(abi.encodePacked(reduce64(u, 0), reduce64(u, 64)));
|
||||
bytes memory q1 = mapFp2ToG2(abi.encodePacked(reduce64(u, 128), reduce64(u, 192)));
|
||||
point = g2Add(q0, q1);
|
||||
}
|
||||
|
||||
function mapFp2ToG2(bytes memory fp2) internal view returns (bytes memory point) {
|
||||
if (fp2.length != 128) revert BadLength("fp2");
|
||||
(bool ok, bytes memory r) = MAP_FP2_TO_G2.staticcall(fp2);
|
||||
if (!ok || r.length != G2_LEN) revert PrecompileFailed(MAP_FP2_TO_G2);
|
||||
point = r;
|
||||
}
|
||||
|
||||
// ---- group operations ----
|
||||
|
||||
function g1Add(bytes memory a, bytes memory b) internal view returns (bytes memory c) {
|
||||
if (a.length != G1_LEN || b.length != G1_LEN) revert BadLength("g1");
|
||||
(bool ok, bytes memory r) = G1ADD.staticcall(abi.encodePacked(a, b));
|
||||
if (!ok || r.length != G1_LEN) revert PrecompileFailed(G1ADD);
|
||||
c = r;
|
||||
}
|
||||
|
||||
function g2Add(bytes memory a, bytes memory b) internal view returns (bytes memory c) {
|
||||
if (a.length != G2_LEN || b.length != G2_LEN) revert BadLength("g2");
|
||||
(bool ok, bytes memory r) = G2ADD.staticcall(abi.encodePacked(a, b));
|
||||
if (!ok || r.length != G2_LEN) revert PrecompileFailed(G2ADD);
|
||||
c = r;
|
||||
}
|
||||
|
||||
/// e(pk, hm) * e(-G1, sig) == 1, which holds exactly when sig = sk * hm for pk = sk * G1.
|
||||
function verifyMinPk(bytes memory pk, bytes memory hm, bytes memory sig) internal view returns (bool) {
|
||||
if (pk.length != G1_LEN || hm.length != G2_LEN || sig.length != G2_LEN) revert BadLength("pairing");
|
||||
(bool ok, bytes memory r) = PAIRING.staticcall(abi.encodePacked(pk, hm, NEG_G1, sig));
|
||||
if (!ok || r.length != 32) return false;
|
||||
return abi.decode(r, (uint256)) == 1;
|
||||
}
|
||||
}
|
||||
155
contracts/bridge/src/IgneumCertificateVerifier.sol
Normal file
155
contracts/bridge/src/IgneumCertificateVerifier.sol
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {BLS12381} from "./BLS12381.sol";
|
||||
import {MerklePatricia} from "./MerklePatricia.sol";
|
||||
|
||||
interface IIgneumCertificateVerifier {
|
||||
function chainId() external view returns (string memory);
|
||||
function tableId() external view returns (bytes32);
|
||||
function verifyCertificate(uint64 index, bytes32 checkpoint, bytes calldata bitmap, bytes calldata signature)
|
||||
external
|
||||
view
|
||||
returns (bool ok, uint256 signedWeight, uint256 totalWeight);
|
||||
function submitCertificate(uint64 index, bytes32 checkpoint, bytes calldata bitmap, bytes calldata signature) external;
|
||||
function finalCheckpoint(uint64 index) external view returns (bytes32);
|
||||
function isFinal(bytes32 checkpoint) external view returns (bool);
|
||||
function verifyAccount(bytes32 stateRoot, address account, bytes[] calldata proof)
|
||||
external
|
||||
pure
|
||||
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash);
|
||||
}
|
||||
|
||||
/// The Igneum light-client bridge primitive on Ethereum: verifies a Devnet 3 finality certificate (the aggregate
|
||||
/// BLS signature of the canonical voter list over the vote message, under the 2/3-of-total-weight rule) against an
|
||||
/// installed voter table, records the checkpoint hashes it proved final, and verifies an Ethereum-shape account
|
||||
/// proof against a state root. What it proves and what it does not: docs/bridge/light-client-bridge.md.
|
||||
///
|
||||
/// Devnet 3, test tokens, no value.
|
||||
contract IgneumCertificateVerifier is IIgneumCertificateVerifier {
|
||||
using MerklePatricia for bytes32;
|
||||
|
||||
string public constant VOTE_PREFIX = "igneum-vote-v1/";
|
||||
bytes public constant DST_VOTE = "IGNEUM_VOTE_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
|
||||
|
||||
string private _chainId;
|
||||
address public owner;
|
||||
|
||||
/// The canonical voter list at the installed checkpoint index: 128-byte G1 keys in the node's canonical order
|
||||
/// (sorted by key hash, every key above dust and not stripped) and their weights (blue blocks in the window).
|
||||
bytes[] private _keys;
|
||||
uint64[] private _weights;
|
||||
uint256 public totalWeight;
|
||||
uint64 public tableIndex;
|
||||
bytes32 public override tableId;
|
||||
|
||||
mapping(uint64 => bytes32) public override finalCheckpoint;
|
||||
mapping(bytes32 => bool) public override isFinal;
|
||||
|
||||
event TableInstalled(uint64 indexed atIndex, uint256 voters, uint256 totalWeight, bytes32 tableId);
|
||||
event CheckpointFinal(uint64 indexed index, bytes32 checkpoint, uint256 signedWeight, uint256 totalWeight, uint256 signers);
|
||||
|
||||
error NotOwner();
|
||||
error NoTable();
|
||||
error BadCertificate(string why);
|
||||
|
||||
constructor(string memory chainId_) {
|
||||
_chainId = chainId_;
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
function chainId() external view override returns (string memory) {
|
||||
return _chainId;
|
||||
}
|
||||
|
||||
function voterCount() external view returns (uint256) {
|
||||
return _keys.length;
|
||||
}
|
||||
|
||||
function voter(uint256 i) external view returns (bytes memory key, uint64 weight) {
|
||||
return (_keys[i], _weights[i]);
|
||||
}
|
||||
|
||||
/// Installs the voter table read from a Devnet 3 node (igneum_getFinalityWeights at `atIndex`): `keys` is the
|
||||
/// concatenation of 128-byte uncompressed G1 keys in canonical order, `weights` their weights. The table is a
|
||||
/// trusted input of this first version (see the doc); only the installer may replace it.
|
||||
function installTable(uint64 atIndex, bytes calldata keys, uint64[] calldata weights) external {
|
||||
if (msg.sender != owner) revert NotOwner();
|
||||
if (keys.length != weights.length * BLS12381.G1_LEN || weights.length == 0) revert BadCertificate("table shape");
|
||||
delete _keys;
|
||||
delete _weights;
|
||||
uint256 total;
|
||||
for (uint256 i = 0; i < weights.length; i++) {
|
||||
_keys.push(keys[i * BLS12381.G1_LEN:(i + 1) * BLS12381.G1_LEN]);
|
||||
_weights.push(weights[i]);
|
||||
total += weights[i];
|
||||
}
|
||||
totalWeight = total;
|
||||
tableIndex = atIndex;
|
||||
tableId = keccak256(abi.encodePacked(atIndex, keys, abi.encodePacked(weights)));
|
||||
emit TableInstalled(atIndex, weights.length, total, tableId);
|
||||
}
|
||||
|
||||
/// The bytes every voter signs for (index, checkpoint): "igneum-vote-v1/" chain_id 0x00 index_le64 checkpoint.
|
||||
function voteMessage(uint64 index, bytes32 checkpoint) public view returns (bytes memory) {
|
||||
return abi.encodePacked(VOTE_PREFIX, _chainId, bytes1(0), le64(index), checkpoint);
|
||||
}
|
||||
|
||||
function verifyCertificate(uint64 index, bytes32 checkpoint, bytes calldata bitmap, bytes calldata signature)
|
||||
public
|
||||
view
|
||||
override
|
||||
returns (bool ok, uint256 signedWeight, uint256 totalWeight_)
|
||||
{
|
||||
uint256 n = _keys.length;
|
||||
if (n == 0) revert NoTable();
|
||||
if (bitmap.length != (n + 7) / 8) revert BadCertificate("bitmap length");
|
||||
if (signature.length != BLS12381.G2_LEN) revert BadCertificate("signature length");
|
||||
bytes memory agg;
|
||||
uint256 signers;
|
||||
for (uint256 p = 0; p < n; p++) {
|
||||
if (uint8(bitmap[p >> 3]) & uint8(1 << (p & 7)) == 0) continue;
|
||||
signedWeight += _weights[p];
|
||||
signers++;
|
||||
agg = agg.length == 0 ? _keys[p] : BLS12381.g1Add(agg, _keys[p]);
|
||||
}
|
||||
totalWeight_ = totalWeight;
|
||||
if (signers == 0) return (false, 0, totalWeight_);
|
||||
// the rule decided 4 October 2026: signed weight at least two thirds of the whole window's weight
|
||||
if (3 * signedWeight < 2 * totalWeight_) return (false, signedWeight, totalWeight_);
|
||||
bytes memory hm = BLS12381.hashToG2(voteMessage(index, checkpoint), DST_VOTE);
|
||||
ok = BLS12381.verifyMinPk(agg, hm, signature);
|
||||
}
|
||||
|
||||
function submitCertificate(uint64 index, bytes32 checkpoint, bytes calldata bitmap, bytes calldata signature) external override {
|
||||
(bool ok, uint256 signed, uint256 total) = verifyCertificate(index, checkpoint, bitmap, signature);
|
||||
if (!ok) revert BadCertificate("certificate does not verify");
|
||||
bytes32 known = finalCheckpoint[index];
|
||||
if (known != bytes32(0) && known != checkpoint) revert BadCertificate("a different checkpoint is final at this index");
|
||||
finalCheckpoint[index] = checkpoint;
|
||||
isFinal[checkpoint] = true;
|
||||
uint256 signers;
|
||||
for (uint256 p = 0; p < _keys.length; p++) {
|
||||
if (uint8(bitmap[p >> 3]) & uint8(1 << (p & 7)) != 0) signers++;
|
||||
}
|
||||
emit CheckpointFinal(index, checkpoint, signed, total, signers);
|
||||
}
|
||||
|
||||
function verifyAccount(bytes32 stateRoot, address account, bytes[] calldata proof)
|
||||
external
|
||||
pure
|
||||
override
|
||||
returns (bool exists, uint256 nonce, uint256 balance, bytes32 storageRoot, bytes32 codeHash)
|
||||
{
|
||||
MerklePatricia.Account memory a = MerklePatricia.verifyAccount(stateRoot, account, proof);
|
||||
return (a.exists, a.nonce, a.balance, a.storageRoot, a.codeHash);
|
||||
}
|
||||
|
||||
function le64(uint64 v) internal pure returns (bytes8 out) {
|
||||
uint64 r;
|
||||
for (uint256 i = 0; i < 8; i++) {
|
||||
r = (r << 8) | ((v >> (8 * i)) & 0xff);
|
||||
}
|
||||
out = bytes8(r);
|
||||
}
|
||||
}
|
||||
214
contracts/bridge/src/MerklePatricia.sol
Normal file
214
contracts/bridge/src/MerklePatricia.sol
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// An Ethereum account proof (the eth_getProof shape) checked against a state root: the keccak-keyed Merkle
|
||||
/// Patricia trie of reth's layout, which Igneum's executor uses for its stateRoot (igneum/exec/src/state.rs,
|
||||
/// alloy_trie::root::state_root over keccak256(address) keys and RLP(nonce, balance, storageRoot, codeHash)
|
||||
/// values). A proof is the list of RLP nodes from the root to the account's leaf, or to the branch or leaf that
|
||||
/// shows the account absent.
|
||||
library MerklePatricia {
|
||||
struct Account {
|
||||
bool exists;
|
||||
uint256 nonce;
|
||||
uint256 balance;
|
||||
bytes32 storageRoot;
|
||||
bytes32 codeHash;
|
||||
}
|
||||
|
||||
error BadProof(string why);
|
||||
|
||||
/// Verifies `proof` for `account` under `stateRoot`; reverts when a node does not hash to its reference or
|
||||
/// the path is malformed, returns exists=false when the trie shows no such account.
|
||||
function verifyAccount(bytes32 stateRoot, address account, bytes[] memory proof) internal pure returns (Account memory out) {
|
||||
bytes memory value = verifyPath(stateRoot, abi.encodePacked(keccak256(abi.encodePacked(account))), proof);
|
||||
if (value.length == 0) return out;
|
||||
(uint256 off, uint256 len, bool isList) = decode(value, 0);
|
||||
if (!isList) revert BadProof("account value is not a list");
|
||||
uint256 end = off + len;
|
||||
uint256 p = off;
|
||||
(uint256 o1, uint256 l1,) = decode(value, p);
|
||||
out.nonce = toUint(value, o1, l1);
|
||||
p = o1 + l1;
|
||||
(uint256 o2, uint256 l2,) = decode(value, p);
|
||||
out.balance = toUint(value, o2, l2);
|
||||
p = o2 + l2;
|
||||
(uint256 o3, uint256 l3,) = decode(value, p);
|
||||
if (l3 != 32) revert BadProof("storage root length");
|
||||
out.storageRoot = toBytes32(value, o3);
|
||||
p = o3 + l3;
|
||||
(uint256 o4, uint256 l4,) = decode(value, p);
|
||||
if (l4 != 32) revert BadProof("code hash length");
|
||||
out.codeHash = toBytes32(value, o4);
|
||||
if (o4 + l4 != end) revert BadProof("account value has extra fields");
|
||||
out.exists = true;
|
||||
}
|
||||
|
||||
/// Walks the proof for `key` (32 bytes, hashed already) from `root`; returns the value found, or empty bytes
|
||||
/// when the trie proves the key absent.
|
||||
function verifyPath(bytes32 root, bytes memory key, bytes[] memory proof) internal pure returns (bytes memory value) {
|
||||
bytes memory nibbles = toNibbles(key);
|
||||
uint256 pos = 0;
|
||||
bytes32 want = root;
|
||||
bytes memory embedded;
|
||||
for (uint256 i = 0; i < proof.length; i++) {
|
||||
bytes memory node = proof[i];
|
||||
if (embedded.length != 0) {
|
||||
if (keccak256(node) != keccak256(embedded)) revert BadProof("embedded node mismatch");
|
||||
embedded = "";
|
||||
} else if (keccak256(node) != want) {
|
||||
revert BadProof("node hash mismatch");
|
||||
}
|
||||
(uint256 off, uint256 len, bool isList) = decode(node, 0);
|
||||
if (!isList) revert BadProof("node is not a list");
|
||||
uint256 count = itemCount(node, off, len);
|
||||
if (count == 17) {
|
||||
if (pos == nibbles.length) {
|
||||
// the branch's own value slot
|
||||
(uint256 vo, uint256 vl,) = itemAt(node, off, 16);
|
||||
return slice(node, vo, vl);
|
||||
}
|
||||
uint8 nib = uint8(nibbles[pos]);
|
||||
(uint256 co, uint256 cl, bool clist) = itemAt(node, off, nib);
|
||||
if (cl == 0 && !clist) return ""; // empty slot: the key is absent
|
||||
pos++;
|
||||
if (clist) {
|
||||
embedded = slice(node, co - headerLen(node, co, cl, true), cl + headerLen(node, co, cl, true));
|
||||
} else {
|
||||
if (cl != 32) revert BadProof("child reference length");
|
||||
want = toBytes32(node, co);
|
||||
}
|
||||
} else if (count == 2) {
|
||||
(uint256 po, uint256 pl,) = itemAt(node, off, 0);
|
||||
(bytes memory path, bool isLeaf) = decodePath(slice(node, po, pl));
|
||||
if (!matches(nibbles, pos, path)) return ""; // diverging path: the key is absent
|
||||
pos += path.length;
|
||||
(uint256 vo, uint256 vl, bool vlist) = itemAt(node, off, 1);
|
||||
if (isLeaf) {
|
||||
if (pos != nibbles.length) revert BadProof("leaf before the key's end");
|
||||
return slice(node, vo, vl);
|
||||
}
|
||||
if (vlist) {
|
||||
embedded = slice(node, vo - headerLen(node, vo, vl, true), vl + headerLen(node, vo, vl, true));
|
||||
} else {
|
||||
if (vl != 32) revert BadProof("extension reference length");
|
||||
want = toBytes32(node, vo);
|
||||
}
|
||||
} else {
|
||||
revert BadProof("node arity");
|
||||
}
|
||||
}
|
||||
revert BadProof("proof ends before the key");
|
||||
}
|
||||
|
||||
// ---- paths ----
|
||||
|
||||
function toNibbles(bytes memory key) internal pure returns (bytes memory n) {
|
||||
n = new bytes(key.length * 2);
|
||||
for (uint256 i = 0; i < key.length; i++) {
|
||||
n[2 * i] = bytes1(uint8(key[i]) >> 4);
|
||||
n[2 * i + 1] = bytes1(uint8(key[i]) & 0x0f);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hex-prefix decoding of a leaf or extension path.
|
||||
function decodePath(bytes memory hp) internal pure returns (bytes memory path, bool isLeaf) {
|
||||
if (hp.length == 0) revert BadProof("empty path");
|
||||
uint8 flag = uint8(hp[0]) >> 4;
|
||||
isLeaf = flag >= 2;
|
||||
bool odd = flag % 2 == 1;
|
||||
uint256 n = (hp.length - 1) * 2 + (odd ? 1 : 0);
|
||||
path = new bytes(n);
|
||||
uint256 w = 0;
|
||||
if (odd) path[w++] = bytes1(uint8(hp[0]) & 0x0f);
|
||||
for (uint256 i = 1; i < hp.length; i++) {
|
||||
path[w++] = bytes1(uint8(hp[i]) >> 4);
|
||||
path[w++] = bytes1(uint8(hp[i]) & 0x0f);
|
||||
}
|
||||
}
|
||||
|
||||
function matches(bytes memory nibbles, uint256 pos, bytes memory path) internal pure returns (bool) {
|
||||
if (pos + path.length > nibbles.length) return false;
|
||||
for (uint256 i = 0; i < path.length; i++) {
|
||||
if (nibbles[pos + i] != path[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- RLP ----
|
||||
|
||||
/// The item at `p`: the offset of its payload, the payload length and whether it is a list.
|
||||
function decode(bytes memory b, uint256 p) internal pure returns (uint256 off, uint256 len, bool isList) {
|
||||
if (p >= b.length) revert BadProof("rlp out of range");
|
||||
uint8 first = uint8(b[p]);
|
||||
if (first < 0x80) return (p, 1, false);
|
||||
if (first < 0xb8) return (p + 1, first - 0x80, false);
|
||||
if (first < 0xc0) {
|
||||
uint256 n = first - 0xb7;
|
||||
return (p + 1 + n, readLen(b, p + 1, n), false);
|
||||
}
|
||||
if (first < 0xf8) return (p + 1, first - 0xc0, true);
|
||||
uint256 m = first - 0xf7;
|
||||
return (p + 1 + m, readLen(b, p + 1, m), true);
|
||||
}
|
||||
|
||||
function headerLen(bytes memory b, uint256 off, uint256 len, bool isList) private pure returns (uint256) {
|
||||
// the header length of an item whose payload starts at off: single bytes under 0x80 have none
|
||||
if (!isList && len == 1 && uint8(b[off]) < 0x80) return 0;
|
||||
if (len < 56) return 1;
|
||||
uint256 n = 0;
|
||||
uint256 l = len;
|
||||
while (l > 0) {
|
||||
n++;
|
||||
l >>= 8;
|
||||
}
|
||||
return 1 + n;
|
||||
}
|
||||
|
||||
function readLen(bytes memory b, uint256 p, uint256 n) private pure returns (uint256 len) {
|
||||
if (n == 0 || n > 32 || p + n > b.length) revert BadProof("rlp length");
|
||||
for (uint256 i = 0; i < n; i++) {
|
||||
len = (len << 8) | uint8(b[p + i]);
|
||||
}
|
||||
}
|
||||
|
||||
function itemCount(bytes memory b, uint256 off, uint256 len) private pure returns (uint256 n) {
|
||||
uint256 p = off;
|
||||
uint256 end = off + len;
|
||||
while (p < end) {
|
||||
(uint256 o, uint256 l,) = decode(b, p);
|
||||
p = o + l;
|
||||
n++;
|
||||
}
|
||||
if (p != end) revert BadProof("rlp list overrun");
|
||||
}
|
||||
|
||||
function itemAt(bytes memory b, uint256 off, uint256 index) private pure returns (uint256 o, uint256 l, bool isList) {
|
||||
uint256 p = off;
|
||||
for (uint256 i = 0; ; i++) {
|
||||
(o, l, isList) = decode(b, p);
|
||||
if (i == index) return (o, l, isList);
|
||||
p = o + l;
|
||||
}
|
||||
}
|
||||
|
||||
function toUint(bytes memory b, uint256 off, uint256 len) private pure returns (uint256 v) {
|
||||
if (len > 32) revert BadProof("integer too long");
|
||||
for (uint256 i = 0; i < len; i++) {
|
||||
v = (v << 8) | uint8(b[off + i]);
|
||||
}
|
||||
}
|
||||
|
||||
function toBytes32(bytes memory b, uint256 off) private pure returns (bytes32 v) {
|
||||
assembly {
|
||||
v := mload(add(add(b, 32), off))
|
||||
}
|
||||
}
|
||||
|
||||
function slice(bytes memory b, uint256 off, uint256 len) private pure returns (bytes memory out) {
|
||||
if (off + len > b.length) revert BadProof("slice out of range");
|
||||
out = new bytes(len);
|
||||
for (uint256 i = 0; i < len; i++) {
|
||||
out[i] = b[off + i];
|
||||
}
|
||||
}
|
||||
}
|
||||
144
contracts/bridge/test/Verifier.t.sol
Normal file
144
contracts/bridge/test/Verifier.t.sol
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {Vm, VM_ADDRESS} from "./Vm.sol";
|
||||
import {IgneumCertificateVerifier} from "../src/IgneumCertificateVerifier.sol";
|
||||
import {BLS12381} from "../src/BLS12381.sol";
|
||||
|
||||
/// The verifier on Foundry's Prague EVM (the EIP-2537 precompiles): the synthetic vectors made by
|
||||
/// test/vectors/gen.mjs (five keys, a certificate by four of them, a small account trie) and, when present, a real
|
||||
/// certificate from the chain (test/vectors/chain.json, as /api/checkpoint serves it, decompressed by gen.mjs).
|
||||
contract VerifierTest {
|
||||
Vm constant vm = Vm(VM_ADDRESS);
|
||||
|
||||
string json;
|
||||
IgneumCertificateVerifier v;
|
||||
|
||||
function setUp() public {
|
||||
json = vm.readFile("test/vectors/synthetic.json");
|
||||
v = new IgneumCertificateVerifier(vm.parseJsonString(json, ".chain_id"));
|
||||
_install(v, json);
|
||||
}
|
||||
|
||||
function _install(IgneumCertificateVerifier target, string memory j) internal {
|
||||
bytes[] memory keys = vm.parseJsonBytesArray(j, ".keys");
|
||||
uint256[] memory w = vm.parseJsonUintArray(j, ".weights");
|
||||
bytes memory packed;
|
||||
uint64[] memory weights = new uint64[](w.length);
|
||||
for (uint256 i = 0; i < keys.length; i++) {
|
||||
packed = abi.encodePacked(packed, keys[i]);
|
||||
weights[i] = uint64(w[i]);
|
||||
}
|
||||
target.installTable(uint64(vm.parseJsonUint(j, ".index")), packed, weights);
|
||||
}
|
||||
|
||||
function test_vote_message_matches_the_node() public view {
|
||||
bytes memory want = vm.parseJsonBytes(json, ".vote_message");
|
||||
bytes memory got = v.voteMessage(uint64(vm.parseJsonUint(json, ".index")), vm.parseJsonBytes32(json, ".checkpoint"));
|
||||
require(keccak256(want) == keccak256(got), "vote message");
|
||||
}
|
||||
|
||||
function test_expand_message_xmd_known_answer() public view {
|
||||
// RFC 9380 appendix K.1 (expand_message_xmd with SHA-256, DST "QUUX-V01-CS02-with-expander-SHA256-128"): the
|
||||
// empty message at 32 bytes is the appendix's own first answer; the "abc" at 128 bytes answer comes from noble
|
||||
bytes memory dst = bytes(vm.parseJsonString(json, ".xmd_dst"));
|
||||
bytes memory out = BLS12381.expandMessageXmd("", dst, 32);
|
||||
require(keccak256(out) == keccak256(hex"68a985b87eb6b46952128911f2a4412bbc302a9d759667f87f7a21d803f07235"), "xmd 32 (RFC)");
|
||||
require(keccak256(out) == keccak256(vm.parseJsonBytes(json, ".xmd_empty_32")), "xmd 32 (noble)");
|
||||
bytes memory out2 = BLS12381.expandMessageXmd("abc", dst, 128);
|
||||
require(keccak256(out2) == keccak256(vm.parseJsonBytes(json, ".xmd_abc_128")), "xmd 128 (noble)");
|
||||
}
|
||||
|
||||
function test_certificate_verifies() public view {
|
||||
(bool ok, uint256 signed, uint256 total) = v.verifyCertificate(
|
||||
uint64(vm.parseJsonUint(json, ".index")), vm.parseJsonBytes32(json, ".checkpoint"), vm.parseJsonBytes(json, ".bitmap"), vm.parseJsonBytes(json, ".signature")
|
||||
);
|
||||
require(ok, "certificate");
|
||||
require(signed == vm.parseJsonUint(json, ".signed_weight") && total == vm.parseJsonUint(json, ".total_weight"), "weights");
|
||||
}
|
||||
|
||||
function test_certificate_under_two_thirds_is_refused() public view {
|
||||
(bool ok, uint256 signed,) = v.verifyCertificate(
|
||||
uint64(vm.parseJsonUint(json, ".index")), vm.parseJsonBytes32(json, ".checkpoint"), vm.parseJsonBytes(json, ".weak_bitmap"), vm.parseJsonBytes(json, ".weak_signature")
|
||||
);
|
||||
require(!ok && signed * 3 < vm.parseJsonUint(json, ".total_weight") * 2, "weak certificate accepted");
|
||||
}
|
||||
|
||||
function test_wrong_checkpoint_or_index_fails() public view {
|
||||
bytes32 cp = vm.parseJsonBytes32(json, ".checkpoint");
|
||||
uint64 index = uint64(vm.parseJsonUint(json, ".index"));
|
||||
bytes memory bm = vm.parseJsonBytes(json, ".bitmap");
|
||||
bytes memory sig = vm.parseJsonBytes(json, ".signature");
|
||||
(bool ok1,,) = v.verifyCertificate(index, cp ^ bytes32(uint256(1)), bm, sig);
|
||||
(bool ok2,,) = v.verifyCertificate(index + 1, cp, bm, sig);
|
||||
require(!ok1 && !ok2, "forged certificate accepted");
|
||||
// the right signers' weight with a bitmap naming a different signer set does not match the signature
|
||||
bytes memory other = vm.parseJsonBytes(json, ".weak_bitmap");
|
||||
other[0] = bytes1(uint8(other[0]) | 0x1f);
|
||||
(bool ok3,,) = v.verifyCertificate(index, cp, other, sig);
|
||||
require(!ok3, "wrong signer set accepted");
|
||||
}
|
||||
|
||||
function test_submit_records_the_checkpoint() public {
|
||||
bytes32 cp = vm.parseJsonBytes32(json, ".checkpoint");
|
||||
uint64 index = uint64(vm.parseJsonUint(json, ".index"));
|
||||
v.submitCertificate(index, cp, vm.parseJsonBytes(json, ".bitmap"), vm.parseJsonBytes(json, ".signature"));
|
||||
require(v.isFinal(cp) && v.finalCheckpoint(index) == cp, "not recorded");
|
||||
vm.expectRevert(abi.encodeWithSelector(IgneumCertificateVerifier.BadCertificate.selector, "certificate does not verify"));
|
||||
v.submitCertificate(index, cp, vm.parseJsonBytes(json, ".weak_bitmap"), vm.parseJsonBytes(json, ".weak_signature"));
|
||||
}
|
||||
|
||||
function test_account_proof_present_and_absent() public view {
|
||||
bytes32 root = vm.parseJsonBytes32(json, ".state_root");
|
||||
(bool exists, uint256 nonce, uint256 balance, bytes32 sroot, bytes32 chash) =
|
||||
v.verifyAccount(root, vm.parseJsonAddress(json, ".account"), vm.parseJsonBytesArray(json, ".account_proof"));
|
||||
require(exists, "account absent");
|
||||
require(nonce == vm.parseJsonUint(json, ".account_nonce") && balance == vm.parseJsonUint(json, ".account_balance"), "account fields");
|
||||
require(sroot == vm.parseJsonBytes32(json, ".account_storage_root") && chash == vm.parseJsonBytes32(json, ".account_code_hash"), "account roots");
|
||||
(bool exists2,,,,) = v.verifyAccount(root, vm.parseJsonAddress(json, ".absent_account"), vm.parseJsonBytesArray(json, ".absent_proof"));
|
||||
require(!exists2, "absent account present");
|
||||
}
|
||||
|
||||
function test_account_proof_against_a_wrong_root_reverts() public {
|
||||
bytes32 root = vm.parseJsonBytes32(json, ".state_root") ^ bytes32(uint256(1));
|
||||
bytes[] memory proof = vm.parseJsonBytesArray(json, ".account_proof");
|
||||
address a = vm.parseJsonAddress(json, ".account");
|
||||
vm.expectRevert(abi.encodeWithSelector(bytes4(keccak256("BadProof(string)")), "node hash mismatch"));
|
||||
v.verifyAccount(root, a, proof);
|
||||
}
|
||||
|
||||
/// A certificate the chain actually carried (test/vectors/chain.json; skipped when the file is absent).
|
||||
function test_chain_certificate_verifies() public {
|
||||
string memory j;
|
||||
try vm.readFile("test/vectors/chain.json") returns (string memory s) {
|
||||
j = s;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
IgneumCertificateVerifier c = new IgneumCertificateVerifier(vm.parseJsonString(j, ".chain_id"));
|
||||
_install(c, j);
|
||||
(bool ok, uint256 signed, uint256 total) = c.verifyCertificate(
|
||||
uint64(vm.parseJsonUint(j, ".index")), vm.parseJsonBytes32(j, ".checkpoint"), vm.parseJsonBytes(j, ".bitmap"), vm.parseJsonBytes(j, ".signature")
|
||||
);
|
||||
require(signed == vm.parseJsonUint(j, ".signed_weight") && total == vm.parseJsonUint(j, ".total_weight"), "chain weights");
|
||||
require(ok, "the chain's certificate does not verify");
|
||||
}
|
||||
|
||||
/// One Devnet 3 account under a real Devnet 3 state root (test/vectors/dn3-account.json from a node's eth_getProof;
|
||||
/// skipped when the file is absent). The root is the chain's own; the link from a certified checkpoint to that root
|
||||
/// is the gap the doc names.
|
||||
function test_devnet3_account_balance_proven() public {
|
||||
string memory j;
|
||||
try vm.readFile("test/vectors/dn3-account.json") returns (string memory s) {
|
||||
j = s;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
(bool exists, uint256 nonce, uint256 balance, bytes32 sroot, bytes32 chash) = v.verifyAccount(
|
||||
vm.parseJsonBytes32(j, ".state_root"), vm.parseJsonAddress(j, ".account"), vm.parseJsonBytesArray(j, ".account_proof")
|
||||
);
|
||||
require(exists, "the Devnet 3 account is absent under its root");
|
||||
require(nonce == vm.parseJsonUint(j, ".account_nonce") && balance == vm.parseJsonUint(j, ".account_balance"), "Devnet 3 account fields");
|
||||
require(sroot == vm.parseJsonBytes32(j, ".account_storage_root") && chash == vm.parseJsonBytes32(j, ".account_code_hash"), "Devnet 3 account roots");
|
||||
}
|
||||
}
|
||||
34
contracts/bridge/test/Vm.sol
Normal file
34
contracts/bridge/test/Vm.sol
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// The Foundry cheatcodes this project uses, declared here so the tree needs no remote dependency.
|
||||
interface Vm {
|
||||
function startBroadcast(uint256 privateKey) external;
|
||||
function stopBroadcast() external;
|
||||
function envUint(string calldata name) external view returns (uint256);
|
||||
function envAddress(string calldata name) external view returns (address);
|
||||
function envOr(string calldata name, string calldata defaultValue) external view returns (string memory);
|
||||
function toString(address value) external pure returns (string memory);
|
||||
function toString(uint256 value) external pure returns (string memory);
|
||||
function toString(bytes32 value) external pure returns (string memory);
|
||||
function toString(bytes calldata value) external pure returns (string memory);
|
||||
function readFile(string calldata path) external view returns (string memory);
|
||||
function writeFile(string calldata path, string calldata data) external;
|
||||
function parseJsonBytes(string calldata json, string calldata key) external pure returns (bytes memory);
|
||||
function parseJsonBytes32(string calldata json, string calldata key) external pure returns (bytes32);
|
||||
function parseJsonUint(string calldata json, string calldata key) external pure returns (uint256);
|
||||
function parseJsonString(string calldata json, string calldata key) external pure returns (string memory);
|
||||
function parseJsonBytesArray(string calldata json, string calldata key) external pure returns (bytes[] memory);
|
||||
function parseJsonUintArray(string calldata json, string calldata key) external pure returns (uint256[] memory);
|
||||
function parseJsonAddress(string calldata json, string calldata key) external pure returns (address);
|
||||
function keyExistsJson(string calldata json, string calldata key) external view returns (bool);
|
||||
function deal(address who, uint256 newBalance) external;
|
||||
function prank(address msgSender) external;
|
||||
function startPrank(address msgSender) external;
|
||||
function stopPrank() external;
|
||||
function warp(uint256 newTimestamp) external;
|
||||
function expectRevert(bytes calldata revertData) external;
|
||||
function addr(uint256 privateKey) external pure returns (address);
|
||||
}
|
||||
|
||||
address constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
|
||||
9
contracts/bridge/test/vectors/.gitignore
vendored
Normal file
9
contracts/bridge/test/vectors/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
synthetic.json
|
||||
chain.json
|
||||
checkpoint-live.json
|
||||
checkpoint-dn3.json
|
||||
weights-dn3.json
|
||||
checkpoints-dn3.json
|
||||
dn3-table.json
|
||||
dn3-account.json
|
||||
116
contracts/bridge/test/vectors/gen.mjs
Normal file
116
contracts/bridge/test/vectors/gen.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Test vectors for the Igneum certificate verifier. Two sources:
|
||||
// node gen.mjs synthetic > synthetic.json five vote keys made here (noble BLS12-381), a certificate signed by four
|
||||
// of them over the Devnet 3 vote message, a small account trie with proofs
|
||||
// node gen.mjs table <weights.json> > dn3-table.json the voter table alone from a node's igneum_getFinalityWeights answer
|
||||
// (voters in the canonical order: sorted by key hash; weight = blocks)
|
||||
// node gen.mjs account <getProof.json> <stateRoot> <blockNumber> > dn3-account.json an eth_getProof answer from a Devnet 3
|
||||
// node (the reference-apps lane's reader) as the suite's real-root account vector
|
||||
// node gen.mjs chain <checkpoint.json> > dn3.json a real certificate as igneum.network/api/checkpoint?source=dn3 serves it:
|
||||
// the voter table and the aggregate signature decompressed to the
|
||||
// precompiles' encodings (the verifier checks the same bytes the node signed)
|
||||
// Encodings: field element 64 bytes (16 zero bytes then 48), G1 128 bytes, G2 256 bytes (x.c0, x.c1, y.c0, y.c1).
|
||||
// The DST and the vote message follow consensus/core/src/finality.rs and site/verify/core.js.
|
||||
import { bls12_381 } from '@noble/curves/bls12-381';
|
||||
import { expand_message_xmd } from '@noble/curves/abstract/hash-to-curve';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const DST = 'IGNEUM_VOTE_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_';
|
||||
const CHAIN_ID = 'igneum-devnet-3';
|
||||
const te = new TextEncoder();
|
||||
const hex = b => '0x' + Array.from(b, x => x.toString(16).padStart(2, '0')).join('');
|
||||
const unhex = h => Uint8Array.from(Buffer.from(h.replace(/^0x/, ''), 'hex'));
|
||||
const be = (n, len) => { const out = new Uint8Array(len); let v = BigInt(n); for (let i = len - 1; i >= 0; i--) { out[i] = Number(v & 0xffn); v >>= 8n; } return out; };
|
||||
const fe = n => { const out = new Uint8Array(64); out.set(be(n, 48), 16); return out; };
|
||||
const concat = parts => { const n = parts.reduce((a, p) => a + p.length, 0); const out = new Uint8Array(n); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; };
|
||||
const u64le = n => { const out = new Uint8Array(8); let v = BigInt(n); for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; } return out; };
|
||||
|
||||
const G1 = bls12_381.G1.ProjectivePoint, G2 = bls12_381.G2.ProjectivePoint;
|
||||
function g1Enc(p) { const a = p.toAffine(); return concat([fe(a.x), fe(a.y)]); }
|
||||
function g2Enc(p) { const a = p.toAffine(); return concat([fe(a.x.c0), fe(a.x.c1), fe(a.y.c0), fe(a.y.c1)]); }
|
||||
function voteMessage(index, checkpointHex) { return concat([te.encode('igneum-vote-v1/' + CHAIN_ID), new Uint8Array([0]), u64le(index), unhex(checkpointHex)]); }
|
||||
function bitmapOf(positions, n) { const bm = new Uint8Array(Math.ceil(n / 8)); for (const p of positions) bm[p >> 3] |= 1 << (p & 7); return bm; }
|
||||
|
||||
async function synthetic() {
|
||||
const sks = [1, 2, 3, 4, 5].map(i => { const s = new Uint8Array(32); s[31] = i; s[0] = 0x11 * i; return bls12_381.utils.randomPrivateKey ? bls12_381.G1.normPrivateKeyToScalar(s) : s; });
|
||||
const keys = sks.map(sk => G1.BASE.multiply(sk));
|
||||
const weights = [100, 250, 400, 300, 150];
|
||||
const index = 1234, checkpoint = '0x' + 'ab'.repeat(32);
|
||||
const msg = voteMessage(index, checkpoint);
|
||||
const hm = bls12_381.G2.hashToCurve(msg, { DST });
|
||||
const signers = [0, 1, 2, 3]; // 1,050 of 1,200: above two thirds
|
||||
const weakSigners = [0, 2, 4]; // 650 of 1,200: under two thirds
|
||||
const sign = who => who.map(i => hm.multiply(sks[i])).reduce((a, b) => a.add(b));
|
||||
const sig = sign(signers), weak = sign(weakSigners);
|
||||
// the account trie: three accounts, proofs for one present and one absent
|
||||
const { Trie } = require('@ethereumjs/trie');
|
||||
const { RLP } = require('@ethereumjs/rlp');
|
||||
const { keccak256 } = require('ethereum-cryptography/keccak');
|
||||
const trie = new Trie({ useKeyHashing: true });
|
||||
const accounts = [
|
||||
{ address: '0x07dd4dbca5c1a66755af28bacca1d901a2d209aa', nonce: 7n, balance: 999174011168718479514n, storageRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', codeHash: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' },
|
||||
{ address: '0x9a6fa842c4e58a87aef1f3ad15233d99283002b7', nonce: 1n, balance: 0n, storageRoot: '0x' + '11'.repeat(32), codeHash: '0x' + '22'.repeat(32) },
|
||||
{ address: '0x53fe98022c2ac26d5d721457fb1c374b4d56144b', nonce: 3n, balance: 2580n * 10n ** 18n, storageRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', codeHash: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' },
|
||||
];
|
||||
for (const a of accounts) {
|
||||
const v = RLP.encode([a.nonce === 0n ? new Uint8Array() : be(a.nonce, Math.ceil(a.nonce.toString(2).length / 8)), a.balance === 0n ? new Uint8Array() : be(a.balance, Math.ceil(a.balance.toString(2).length / 8)), unhex(a.storageRoot), unhex(a.codeHash)]);
|
||||
await trie.put(unhex(a.address), v);
|
||||
}
|
||||
const root = hex(trie.root());
|
||||
const proofFor = async addr => (await trie.createProof(unhex(addr))).map(hex);
|
||||
const absent = '0x000000000000000000000000000000000000dead';
|
||||
return {
|
||||
chain_id: CHAIN_ID, dst: DST, index, checkpoint,
|
||||
keys: keys.map(k => hex(g1Enc(k))), weights, total_weight: weights.reduce((a, b) => a + b, 0),
|
||||
bitmap: hex(bitmapOf(signers, keys.length)), signature: hex(g2Enc(sig)), signed_weight: signers.reduce((a, i) => a + weights[i], 0),
|
||||
weak_bitmap: hex(bitmapOf(weakSigners, keys.length)), weak_signature: hex(g2Enc(weak)),
|
||||
vote_message: hex(msg),
|
||||
// RFC 9380 expand_message_xmd(SHA-256) answers, computed by noble, for the Solidity port's own check
|
||||
xmd_dst: 'QUUX-V01-CS02-with-expander-SHA256-128',
|
||||
xmd_abc_128: hex(expand_message_xmd(te.encode('abc'), te.encode('QUUX-V01-CS02-with-expander-SHA256-128'), 128, sha256)),
|
||||
xmd_empty_32: hex(expand_message_xmd(new Uint8Array(), te.encode('QUUX-V01-CS02-with-expander-SHA256-128'), 32, sha256)),
|
||||
state_root: root,
|
||||
account: accounts[0].address, account_nonce: accounts[0].nonce.toString(), account_balance: accounts[0].balance.toString(),
|
||||
account_storage_root: accounts[0].storageRoot, account_code_hash: accounts[0].codeHash,
|
||||
account_proof: await proofFor(accounts[0].address),
|
||||
absent_account: absent, absent_proof: await proofFor(absent),
|
||||
};
|
||||
}
|
||||
|
||||
function chain(file) {
|
||||
const d = JSON.parse(readFileSync(file, 'utf8'));
|
||||
const voters = d.voters.map(v => ({ key: hex(g1Enc(G1.fromHex(v.pubkey_hex.replace(/^0x/, '')))), weight: Math.round(Number(v.weight)) }));
|
||||
const sig = G2.fromHex(d.certificate.aggregate_signature_hex.replace(/^0x/, ''));
|
||||
const positions = []; const bm = unhex(d.certificate.bitmap_hex);
|
||||
for (let p = 0; p < voters.length; p++) if (bm[p >> 3] & (1 << (p & 7))) positions.push(p);
|
||||
return {
|
||||
source: d.source, chain_id: d.chain_id, dst: DST, index: d.index, checkpoint: '0x' + d.hash,
|
||||
keys: voters.map(v => v.key), weights: voters.map(v => v.weight), total_weight: voters.reduce((a, v) => a + v.weight, 0),
|
||||
bitmap: '0x' + d.certificate.bitmap_hex, signature: hex(g2Enc(sig)), signed_weight: positions.reduce((a, p) => a + voters[p].weight, 0),
|
||||
signers: positions.length, voters_at_index: d.voters_at_index, stored_at: d.stored_at,
|
||||
};
|
||||
}
|
||||
|
||||
function table(file) {
|
||||
const d = JSON.parse(readFileSync(file, 'utf8'));
|
||||
const r = d.result || d;
|
||||
const voters = r.keys.filter(k => k.voter === true || k.voter === 'True').map(k => ({ keyHash: String(k.keyHash).replace(/^0x/, ''), key: hex(g1Enc(G1.fromHex(String(k.pubkey).replace(/^0x/, '')))), weight: Number(BigInt(k.blocks)) }));
|
||||
voters.sort((a, b) => (a.keyHash < b.keyHash ? -1 : a.keyHash > b.keyHash ? 1 : 0));
|
||||
const total = voters.reduce((a, v) => a + v.weight, 0);
|
||||
return { chain_id: process.env.IGNEUM_CHAIN_ID || CHAIN_ID, dst: DST, index: Number(BigInt(r.checkpointIndex)), checkpoint_at_index: '0x' + String(r.checkpointHash).replace(/^0x/, ''), keys: voters.map(v => v.key), weights: voters.map(v => v.weight), total_weight: total, total_weight_node: Number(BigInt(r.totalWeight)), voters: voters.length };
|
||||
}
|
||||
|
||||
function account(file, stateRoot, blockNumber) {
|
||||
const d = JSON.parse(readFileSync(file, 'utf8'));
|
||||
const r = d.result || d;
|
||||
return { chain_id: CHAIN_ID, state_root: stateRoot, block_number: Number(blockNumber), account: r.address, account_nonce: BigInt(r.nonce).toString(), account_balance: BigInt(r.balance).toString(), account_storage_root: r.storageHash, account_code_hash: r.codeHash, account_proof: r.accountProof };
|
||||
}
|
||||
|
||||
const mode = process.argv[2];
|
||||
if (mode === 'synthetic') synthetic().then(v => console.log(JSON.stringify(v, null, 1)));
|
||||
else if (mode === 'chain') console.log(JSON.stringify(chain(process.argv[3]), null, 1));
|
||||
else if (mode === 'table') console.log(JSON.stringify(table(process.argv[3]), null, 1));
|
||||
else if (mode === 'account') console.log(JSON.stringify(account(process.argv[3], process.argv[4], process.argv[5]), null, 1));
|
||||
else { console.error('usage: gen.mjs synthetic | table <weights.json> | account <getProof.json> <stateRoot> <blockNumber> | chain <checkpoint.json>'); process.exit(2); }
|
||||
19
contracts/dex/foundry.toml
Normal file
19
contracts/dex/foundry.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[profile.default]
|
||||
src = "src"
|
||||
test = "test"
|
||||
script = "script"
|
||||
out = "out"
|
||||
libs = []
|
||||
solc_version = "0.8.28"
|
||||
# Devnet 3's executor runs the EVM through revm; Paris keeps the bytecode off PUSH0 and transient storage so it runs
|
||||
# on any post-Merge configuration of it (8 October 2026).
|
||||
evm_version = "paris"
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
fs_permissions = [{ access = "read-write", path = "./deploy-out.json" }]
|
||||
# no remote dependencies: the tests and the script declare the cheatcode interface they use (test/Vm.sol); solc 0.8.28 is fetched once by forge
|
||||
auto_detect_remappings = false
|
||||
|
||||
[rpc_endpoints]
|
||||
# the Devnet 3 EVM reaches the build box through a tunnel on this port (never the Mac); see docs/contracts/devnet-3.json
|
||||
devnet3 = "http://127.0.0.1:36790"
|
||||
58
contracts/dex/script/Deploy.s.sol
Normal file
58
contracts/dex/script/Deploy.s.sol
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {Vm, VM_ADDRESS} from "../test/Vm.sol";
|
||||
import {WIGN} from "../src/WIGN.sol";
|
||||
import {TestToken} from "../src/TestToken.sol";
|
||||
import {IgneumFactory} from "../src/IgneumPair.sol";
|
||||
import {IgneumRouter} from "../src/IgneumRouter.sol";
|
||||
|
||||
/// Deploys the AMM on Devnet 3 from the key in DEX_DEPLOYER_KEY (read from the environment, never printed), seeds
|
||||
/// three pools from the faucet and 200 IGN, and writes the addresses to deploy-out.json for docs/contracts.
|
||||
///
|
||||
/// forge script script/Deploy.s.sol:Deploy --rpc-url devnet3 --broadcast --sig "run()"
|
||||
///
|
||||
/// Devnet 3, test tokens, no value.
|
||||
contract Deploy {
|
||||
Vm constant vm = Vm(VM_ADDRESS);
|
||||
|
||||
function run() external {
|
||||
uint256 key = vm.envUint("DEX_DEPLOYER_KEY");
|
||||
address deployer = vm.addr(key);
|
||||
vm.startBroadcast(key);
|
||||
|
||||
WIGN wign = new WIGN();
|
||||
TestToken tta = new TestToken("Test Token A", "TTA");
|
||||
TestToken ttb = new TestToken("Test Token B", "TTB");
|
||||
IgneumFactory factory = new IgneumFactory();
|
||||
IgneumRouter router = new IgneumRouter(address(factory), address(wign));
|
||||
|
||||
tta.drip();
|
||||
ttb.drip();
|
||||
tta.approve(address(router), type(uint256).max);
|
||||
ttb.approve(address(router), type(uint256).max);
|
||||
uint256 deadline = block.timestamp + 1 hours;
|
||||
router.addLiquidityIGN{value: 100 ether}(address(tta), 500 ether, 0, 0, deployer, deadline);
|
||||
router.addLiquidityIGN{value: 100 ether}(address(ttb), 500 ether, 0, 0, deployer, deadline);
|
||||
router.addLiquidity(address(tta), address(ttb), 500 ether, 500 ether, 0, 0, deployer, deadline);
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
||||
string memory json = "{\n";
|
||||
json = _line(json, "WIGN", address(wign), ",");
|
||||
json = _line(json, "TTA", address(tta), ",");
|
||||
json = _line(json, "TTB", address(ttb), ",");
|
||||
json = _line(json, "IgneumFactory", address(factory), ",");
|
||||
json = _line(json, "IgneumRouter", address(router), ",");
|
||||
json = _line(json, "pair_TTA_WIGN", factory.getPair(address(tta), address(wign)), ",");
|
||||
json = _line(json, "pair_TTB_WIGN", factory.getPair(address(ttb), address(wign)), ",");
|
||||
json = _line(json, "pair_TTA_TTB", factory.getPair(address(tta), address(ttb)), ",");
|
||||
json = _line(json, "deployer", deployer, "");
|
||||
json = string.concat(json, "}\n");
|
||||
vm.writeFile("deploy-out.json", json);
|
||||
}
|
||||
|
||||
function _line(string memory json, string memory key, address value, string memory comma) private pure returns (string memory) {
|
||||
return string.concat(json, " \"", key, "\": \"", vm.toString(value), "\"", comma, "\n");
|
||||
}
|
||||
}
|
||||
37
contracts/dex/script/Seed.s.sol
Normal file
37
contracts/dex/script/Seed.s.sol
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {Vm, VM_ADDRESS} from "../test/Vm.sol";
|
||||
import {TestToken} from "../src/TestToken.sol";
|
||||
import {IgneumRouter} from "../src/IgneumRouter.sol";
|
||||
|
||||
/// Seeds the three pools of an already deployed AMM (the addresses from the environment): a drip of each test
|
||||
/// token, approvals, 100 IGN beside 500 TTA, 100 IGN beside 500 TTB, 500 TTA beside 500 TTB. Run with
|
||||
/// --skip-simulation so every gas limit comes from the node's own eth_estimateGas: Devnet 3 charges the proving
|
||||
/// dimension inside the execution gas, so Foundry's local estimate runs out (8 October 2026, drip() at 133,603 gas:
|
||||
/// out of gas; the node's estimate 685,513).
|
||||
///
|
||||
/// DEX_TTA=0x.. DEX_TTB=0x.. DEX_ROUTER=0x.. forge script script/Seed.s.sol:Seed --rpc-url devnet3 --broadcast --slow --skip-simulation --sig "run()"
|
||||
///
|
||||
/// Devnet 3, test tokens, no value.
|
||||
contract Seed {
|
||||
Vm constant vm = Vm(VM_ADDRESS);
|
||||
|
||||
function run() external {
|
||||
uint256 key = vm.envUint("DEX_DEPLOYER_KEY");
|
||||
address deployer = vm.addr(key);
|
||||
TestToken tta = TestToken(vm.envAddress("DEX_TTA"));
|
||||
TestToken ttb = TestToken(vm.envAddress("DEX_TTB"));
|
||||
IgneumRouter router = IgneumRouter(payable(vm.envAddress("DEX_ROUTER")));
|
||||
vm.startBroadcast(key);
|
||||
if (tta.dripWait(deployer) == 0 && tta.balanceOf(deployer) < 1_000 ether) tta.drip();
|
||||
if (ttb.dripWait(deployer) == 0 && ttb.balanceOf(deployer) < 1_000 ether) ttb.drip();
|
||||
tta.approve(address(router), type(uint256).max);
|
||||
ttb.approve(address(router), type(uint256).max);
|
||||
uint256 deadline = block.timestamp + 1 hours;
|
||||
router.addLiquidityIGN{value: 100 ether}(address(tta), 500 ether, 0, 0, deployer, deadline);
|
||||
router.addLiquidityIGN{value: 100 ether}(address(ttb), 500 ether, 0, 0, deployer, deadline);
|
||||
router.addLiquidity(address(tta), address(ttb), 500 ether, 500 ether, 0, 0, deployer, deadline);
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
59
contracts/dex/src/ERC20.sol
Normal file
59
contracts/dex/src/ERC20.sol
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// A plain ERC-20 with EIP-2612-free approvals, shared by the pair's liquidity token, the wrapped coin and the test
|
||||
/// tokens. Devnet 3, test tokens, no value.
|
||||
contract ERC20 {
|
||||
string public name;
|
||||
string public symbol;
|
||||
uint8 public constant decimals = 18;
|
||||
uint256 public totalSupply;
|
||||
mapping(address => uint256) public balanceOf;
|
||||
mapping(address => mapping(address => uint256)) public allowance;
|
||||
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
|
||||
constructor(string memory name_, string memory symbol_) {
|
||||
name = name_;
|
||||
symbol = symbol_;
|
||||
}
|
||||
|
||||
function _mint(address to, uint256 value) internal {
|
||||
totalSupply += value;
|
||||
balanceOf[to] += value;
|
||||
emit Transfer(address(0), to, value);
|
||||
}
|
||||
|
||||
function _burn(address from, uint256 value) internal {
|
||||
balanceOf[from] -= value;
|
||||
totalSupply -= value;
|
||||
emit Transfer(from, address(0), value);
|
||||
}
|
||||
|
||||
function _transfer(address from, address to, uint256 value) internal {
|
||||
balanceOf[from] -= value;
|
||||
balanceOf[to] += value;
|
||||
emit Transfer(from, to, value);
|
||||
}
|
||||
|
||||
function approve(address spender, uint256 value) external returns (bool) {
|
||||
allowance[msg.sender][spender] = value;
|
||||
emit Approval(msg.sender, spender, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function transfer(address to, uint256 value) external returns (bool) {
|
||||
_transfer(msg.sender, to, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferFrom(address from, address to, uint256 value) external returns (bool) {
|
||||
uint256 allowed = allowance[from][msg.sender];
|
||||
if (allowed != type(uint256).max) {
|
||||
allowance[from][msg.sender] = allowed - value;
|
||||
}
|
||||
_transfer(from, to, value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
174
contracts/dex/src/IgneumPair.sol
Normal file
174
contracts/dex/src/IgneumPair.sol
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ERC20} from "./ERC20.sol";
|
||||
|
||||
interface IERC20Minimal {
|
||||
function balanceOf(address) external view returns (uint256);
|
||||
function transfer(address, uint256) external returns (bool);
|
||||
}
|
||||
|
||||
/// A constant-product pool in the Uniswap v2 shape: reserves of two tokens, a 0.3 percent fee kept in the pool,
|
||||
/// liquidity tokens for the depositors, MINIMUM_LIQUIDITY locked at the first mint. No protocol fee, no price
|
||||
/// accumulators, no flash callback. Devnet 3, test tokens, no value.
|
||||
contract IgneumPair is ERC20("Igneum LP", "IGN-LP") {
|
||||
uint256 public constant MINIMUM_LIQUIDITY = 10 ** 3;
|
||||
|
||||
address public immutable factory;
|
||||
address public token0;
|
||||
address public token1;
|
||||
|
||||
uint112 private reserve0;
|
||||
uint112 private reserve1;
|
||||
uint32 private blockTimestampLast;
|
||||
|
||||
uint256 private unlocked = 1;
|
||||
|
||||
event Mint(address indexed sender, uint256 amount0, uint256 amount1);
|
||||
event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
|
||||
event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to);
|
||||
event Sync(uint112 reserve0, uint112 reserve1);
|
||||
|
||||
modifier lock() {
|
||||
require(unlocked == 1, "Pair: locked");
|
||||
unlocked = 0;
|
||||
_;
|
||||
unlocked = 1;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
factory = msg.sender;
|
||||
}
|
||||
|
||||
function initialize(address token0_, address token1_) external {
|
||||
require(msg.sender == factory, "Pair: forbidden");
|
||||
token0 = token0_;
|
||||
token1 = token1_;
|
||||
}
|
||||
|
||||
function getReserves() public view returns (uint112, uint112, uint32) {
|
||||
return (reserve0, reserve1, blockTimestampLast);
|
||||
}
|
||||
|
||||
function _safeTransfer(address token, address to, uint256 value) private {
|
||||
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, value));
|
||||
require(ok && (data.length == 0 || abi.decode(data, (bool))), "Pair: transfer failed");
|
||||
}
|
||||
|
||||
function _update(uint256 balance0, uint256 balance1) private {
|
||||
require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, "Pair: overflow");
|
||||
reserve0 = uint112(balance0);
|
||||
reserve1 = uint112(balance1);
|
||||
blockTimestampLast = uint32(block.timestamp);
|
||||
emit Sync(reserve0, reserve1);
|
||||
}
|
||||
|
||||
function _sqrt(uint256 y) private pure returns (uint256 z) {
|
||||
if (y > 3) {
|
||||
z = y;
|
||||
uint256 x = y / 2 + 1;
|
||||
while (x < z) {
|
||||
z = x;
|
||||
x = (y / x + x) / 2;
|
||||
}
|
||||
} else if (y != 0) {
|
||||
z = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function _min(uint256 x, uint256 y) private pure returns (uint256) {
|
||||
return x < y ? x : y;
|
||||
}
|
||||
|
||||
/// Mints liquidity for the tokens sent to the pair since the last sync. Called by the router.
|
||||
function mint(address to) external lock returns (uint256 liquidity) {
|
||||
(uint112 r0, uint112 r1,) = getReserves();
|
||||
uint256 balance0 = IERC20Minimal(token0).balanceOf(address(this));
|
||||
uint256 balance1 = IERC20Minimal(token1).balanceOf(address(this));
|
||||
uint256 amount0 = balance0 - r0;
|
||||
uint256 amount1 = balance1 - r1;
|
||||
if (totalSupply == 0) {
|
||||
liquidity = _sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
|
||||
_mint(address(0xdead), MINIMUM_LIQUIDITY);
|
||||
} else {
|
||||
liquidity = _min(amount0 * totalSupply / r0, amount1 * totalSupply / r1);
|
||||
}
|
||||
require(liquidity > 0, "Pair: insufficient liquidity minted");
|
||||
_mint(to, liquidity);
|
||||
_update(balance0, balance1);
|
||||
emit Mint(msg.sender, amount0, amount1);
|
||||
}
|
||||
|
||||
/// Burns the liquidity tokens sent to the pair and pays both tokens out pro rata. Called by the router.
|
||||
function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
|
||||
uint256 balance0 = IERC20Minimal(token0).balanceOf(address(this));
|
||||
uint256 balance1 = IERC20Minimal(token1).balanceOf(address(this));
|
||||
uint256 liquidity = balanceOf[address(this)];
|
||||
amount0 = liquidity * balance0 / totalSupply;
|
||||
amount1 = liquidity * balance1 / totalSupply;
|
||||
require(amount0 > 0 && amount1 > 0, "Pair: insufficient liquidity burned");
|
||||
_burn(address(this), liquidity);
|
||||
_safeTransfer(token0, to, amount0);
|
||||
_safeTransfer(token1, to, amount1);
|
||||
_update(IERC20Minimal(token0).balanceOf(address(this)), IERC20Minimal(token1).balanceOf(address(this)));
|
||||
emit Burn(msg.sender, amount0, amount1, to);
|
||||
}
|
||||
|
||||
/// Pays out up to the amounts asked and checks the fee-adjusted product did not fall. The input must already
|
||||
/// sit in the pair (the router sends it first).
|
||||
function swap(uint256 amount0Out, uint256 amount1Out, address to) external lock {
|
||||
require(amount0Out > 0 || amount1Out > 0, "Pair: insufficient output amount");
|
||||
(uint112 r0, uint112 r1,) = getReserves();
|
||||
require(amount0Out < r0 && amount1Out < r1, "Pair: insufficient liquidity");
|
||||
require(to != token0 && to != token1, "Pair: invalid to");
|
||||
if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);
|
||||
if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);
|
||||
uint256 balance0 = IERC20Minimal(token0).balanceOf(address(this));
|
||||
uint256 balance1 = IERC20Minimal(token1).balanceOf(address(this));
|
||||
uint256 amount0In = balance0 > r0 - amount0Out ? balance0 - (r0 - amount0Out) : 0;
|
||||
uint256 amount1In = balance1 > r1 - amount1Out ? balance1 - (r1 - amount1Out) : 0;
|
||||
require(amount0In > 0 || amount1In > 0, "Pair: insufficient input amount");
|
||||
uint256 adjusted0 = balance0 * 1000 - amount0In * 3;
|
||||
uint256 adjusted1 = balance1 * 1000 - amount1In * 3;
|
||||
require(adjusted0 * adjusted1 >= uint256(r0) * uint256(r1) * 1000 ** 2, "Pair: K");
|
||||
_update(balance0, balance1);
|
||||
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
|
||||
}
|
||||
|
||||
/// Sends any balance above the reserves to `to`.
|
||||
function skim(address to) external lock {
|
||||
_safeTransfer(token0, to, IERC20Minimal(token0).balanceOf(address(this)) - reserve0);
|
||||
_safeTransfer(token1, to, IERC20Minimal(token1).balanceOf(address(this)) - reserve1);
|
||||
}
|
||||
|
||||
/// Sets the reserves to the balances.
|
||||
function sync() external lock {
|
||||
_update(IERC20Minimal(token0).balanceOf(address(this)), IERC20Minimal(token1).balanceOf(address(this)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates one pair per unordered token pair and remembers it.
|
||||
contract IgneumFactory {
|
||||
mapping(address => mapping(address => address)) public getPair;
|
||||
address[] public allPairs;
|
||||
|
||||
event PairCreated(address indexed token0, address indexed token1, address pair, uint256 count);
|
||||
|
||||
function allPairsLength() external view returns (uint256) {
|
||||
return allPairs.length;
|
||||
}
|
||||
|
||||
function createPair(address tokenA, address tokenB) external returns (address pair) {
|
||||
require(tokenA != tokenB, "Factory: identical addresses");
|
||||
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
|
||||
require(token0 != address(0), "Factory: zero address");
|
||||
require(getPair[token0][token1] == address(0), "Factory: pair exists");
|
||||
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
|
||||
pair = address(new IgneumPair{salt: salt}());
|
||||
IgneumPair(pair).initialize(token0, token1);
|
||||
getPair[token0][token1] = pair;
|
||||
getPair[token1][token0] = pair;
|
||||
allPairs.push(pair);
|
||||
emit PairCreated(token0, token1, pair, allPairs.length);
|
||||
}
|
||||
}
|
||||
253
contracts/dex/src/IgneumRouter.sol
Normal file
253
contracts/dex/src/IgneumRouter.sol
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {IgneumFactory, IgneumPair} from "./IgneumPair.sol";
|
||||
|
||||
interface IERC20Router {
|
||||
function balanceOf(address) external view returns (uint256);
|
||||
function transfer(address, uint256) external returns (bool);
|
||||
function transferFrom(address, address, uint256) external returns (bool);
|
||||
}
|
||||
|
||||
interface IWIGN is IERC20Router {
|
||||
function deposit() external payable;
|
||||
function withdraw(uint256) external;
|
||||
}
|
||||
|
||||
/// The router in the Uniswap v2 shape: adds and removes liquidity, swaps along a path of pairs, quotes. Pairs are
|
||||
/// looked up on the factory, never derived from an init-code hash. Devnet 3, test tokens, no value.
|
||||
contract IgneumRouter {
|
||||
IgneumFactory public immutable factory;
|
||||
address public immutable WIGN;
|
||||
|
||||
modifier ensure(uint256 deadline) {
|
||||
require(deadline >= block.timestamp, "Router: expired");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address factory_, address wign_) {
|
||||
factory = IgneumFactory(factory_);
|
||||
WIGN = wign_;
|
||||
}
|
||||
|
||||
receive() external payable {
|
||||
require(msg.sender == WIGN, "Router: only WIGN");
|
||||
}
|
||||
|
||||
// ---- pure maths ----
|
||||
|
||||
function sortTokens(address tokenA, address tokenB) public pure returns (address token0, address token1) {
|
||||
require(tokenA != tokenB, "Router: identical addresses");
|
||||
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
|
||||
require(token0 != address(0), "Router: zero address");
|
||||
}
|
||||
|
||||
function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) public pure returns (uint256 amountB) {
|
||||
require(amountA > 0, "Router: insufficient amount");
|
||||
require(reserveA > 0 && reserveB > 0, "Router: insufficient liquidity");
|
||||
amountB = amountA * reserveB / reserveA;
|
||||
}
|
||||
|
||||
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountOut) {
|
||||
require(amountIn > 0, "Router: insufficient input amount");
|
||||
require(reserveIn > 0 && reserveOut > 0, "Router: insufficient liquidity");
|
||||
uint256 amountInWithFee = amountIn * 997;
|
||||
amountOut = amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee);
|
||||
}
|
||||
|
||||
function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountIn) {
|
||||
require(amountOut > 0, "Router: insufficient output amount");
|
||||
require(reserveIn > 0 && reserveOut > 0, "Router: insufficient liquidity");
|
||||
amountIn = (reserveIn * amountOut * 1000) / ((reserveOut - amountOut) * 997) + 1;
|
||||
}
|
||||
|
||||
// ---- views ----
|
||||
|
||||
function pairFor(address tokenA, address tokenB) public view returns (address pair) {
|
||||
pair = factory.getPair(tokenA, tokenB);
|
||||
require(pair != address(0), "Router: no pair");
|
||||
}
|
||||
|
||||
function getReserves(address tokenA, address tokenB) public view returns (uint256 reserveA, uint256 reserveB) {
|
||||
(address token0,) = sortTokens(tokenA, tokenB);
|
||||
(uint112 r0, uint112 r1,) = IgneumPair(pairFor(tokenA, tokenB)).getReserves();
|
||||
(reserveA, reserveB) = tokenA == token0 ? (r0, r1) : (r1, r0);
|
||||
}
|
||||
|
||||
function getAmountsOut(uint256 amountIn, address[] memory path) public view returns (uint256[] memory amounts) {
|
||||
require(path.length >= 2, "Router: invalid path");
|
||||
amounts = new uint256[](path.length);
|
||||
amounts[0] = amountIn;
|
||||
for (uint256 i; i < path.length - 1; i++) {
|
||||
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i], path[i + 1]);
|
||||
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
|
||||
}
|
||||
}
|
||||
|
||||
function getAmountsIn(uint256 amountOut, address[] memory path) public view returns (uint256[] memory amounts) {
|
||||
require(path.length >= 2, "Router: invalid path");
|
||||
amounts = new uint256[](path.length);
|
||||
amounts[amounts.length - 1] = amountOut;
|
||||
for (uint256 i = path.length - 1; i > 0; i--) {
|
||||
(uint256 reserveIn, uint256 reserveOut) = getReserves(path[i - 1], path[i]);
|
||||
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- liquidity ----
|
||||
|
||||
function _addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin)
|
||||
private
|
||||
returns (uint256 amountA, uint256 amountB)
|
||||
{
|
||||
if (factory.getPair(tokenA, tokenB) == address(0)) {
|
||||
factory.createPair(tokenA, tokenB);
|
||||
}
|
||||
(uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);
|
||||
if (reserveA == 0 && reserveB == 0) {
|
||||
(amountA, amountB) = (amountADesired, amountBDesired);
|
||||
} else {
|
||||
uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
|
||||
if (amountBOptimal <= amountBDesired) {
|
||||
require(amountBOptimal >= amountBMin, "Router: insufficient B amount");
|
||||
(amountA, amountB) = (amountADesired, amountBOptimal);
|
||||
} else {
|
||||
uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
|
||||
require(amountAOptimal <= amountADesired && amountAOptimal >= amountAMin, "Router: insufficient A amount");
|
||||
(amountA, amountB) = (amountAOptimal, amountBDesired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addLiquidity(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint256 amountADesired,
|
||||
uint256 amountBDesired,
|
||||
uint256 amountAMin,
|
||||
uint256 amountBMin,
|
||||
address to,
|
||||
uint256 deadline
|
||||
) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
|
||||
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
|
||||
address pair = pairFor(tokenA, tokenB);
|
||||
_pull(tokenA, msg.sender, pair, amountA);
|
||||
_pull(tokenB, msg.sender, pair, amountB);
|
||||
liquidity = IgneumPair(pair).mint(to);
|
||||
}
|
||||
|
||||
function addLiquidityIGN(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountIGNMin, address to, uint256 deadline)
|
||||
external
|
||||
payable
|
||||
ensure(deadline)
|
||||
returns (uint256 amountToken, uint256 amountIGN, uint256 liquidity)
|
||||
{
|
||||
(amountToken, amountIGN) = _addLiquidity(token, WIGN, amountTokenDesired, msg.value, amountTokenMin, amountIGNMin);
|
||||
address pair = pairFor(token, WIGN);
|
||||
_pull(token, msg.sender, pair, amountToken);
|
||||
IWIGN(WIGN).deposit{value: amountIGN}();
|
||||
require(IWIGN(WIGN).transfer(pair, amountIGN), "Router: WIGN transfer failed");
|
||||
liquidity = IgneumPair(pair).mint(to);
|
||||
if (msg.value > amountIGN) _sendIGN(msg.sender, msg.value - amountIGN);
|
||||
}
|
||||
|
||||
function removeLiquidity(address tokenA, address tokenB, uint256 liquidity, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline)
|
||||
public
|
||||
ensure(deadline)
|
||||
returns (uint256 amountA, uint256 amountB)
|
||||
{
|
||||
address pair = pairFor(tokenA, tokenB);
|
||||
_pull(pair, msg.sender, pair, liquidity);
|
||||
(uint256 amount0, uint256 amount1) = IgneumPair(pair).burn(to);
|
||||
(address token0,) = sortTokens(tokenA, tokenB);
|
||||
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
|
||||
require(amountA >= amountAMin, "Router: insufficient A amount");
|
||||
require(amountB >= amountBMin, "Router: insufficient B amount");
|
||||
}
|
||||
|
||||
function removeLiquidityIGN(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountIGNMin, address to, uint256 deadline)
|
||||
external
|
||||
ensure(deadline)
|
||||
returns (uint256 amountToken, uint256 amountIGN)
|
||||
{
|
||||
(amountToken, amountIGN) = removeLiquidity(token, WIGN, liquidity, amountTokenMin, amountIGNMin, address(this), deadline);
|
||||
require(IERC20Router(token).transfer(to, amountToken), "Router: token transfer failed");
|
||||
IWIGN(WIGN).withdraw(amountIGN);
|
||||
_sendIGN(to, amountIGN);
|
||||
}
|
||||
|
||||
// ---- swaps ----
|
||||
|
||||
function _swap(uint256[] memory amounts, address[] memory path, address to_) private {
|
||||
for (uint256 i; i < path.length - 1; i++) {
|
||||
(address input, address output) = (path[i], path[i + 1]);
|
||||
(address token0,) = sortTokens(input, output);
|
||||
uint256 amountOut = amounts[i + 1];
|
||||
(uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
|
||||
address to = i < path.length - 2 ? pairFor(output, path[i + 2]) : to_;
|
||||
IgneumPair(pairFor(input, output)).swap(amount0Out, amount1Out, to);
|
||||
}
|
||||
}
|
||||
|
||||
function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
|
||||
external
|
||||
ensure(deadline)
|
||||
returns (uint256[] memory amounts)
|
||||
{
|
||||
amounts = getAmountsOut(amountIn, path);
|
||||
require(amounts[amounts.length - 1] >= amountOutMin, "Router: insufficient output amount");
|
||||
_pull(path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]);
|
||||
_swap(amounts, path, to);
|
||||
}
|
||||
|
||||
function swapTokensForExactTokens(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline)
|
||||
external
|
||||
ensure(deadline)
|
||||
returns (uint256[] memory amounts)
|
||||
{
|
||||
amounts = getAmountsIn(amountOut, path);
|
||||
require(amounts[0] <= amountInMax, "Router: excessive input amount");
|
||||
_pull(path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]);
|
||||
_swap(amounts, path, to);
|
||||
}
|
||||
|
||||
function swapExactIGNForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
|
||||
external
|
||||
payable
|
||||
ensure(deadline)
|
||||
returns (uint256[] memory amounts)
|
||||
{
|
||||
require(path[0] == WIGN, "Router: invalid path");
|
||||
amounts = getAmountsOut(msg.value, path);
|
||||
require(amounts[amounts.length - 1] >= amountOutMin, "Router: insufficient output amount");
|
||||
IWIGN(WIGN).deposit{value: amounts[0]}();
|
||||
require(IWIGN(WIGN).transfer(pairFor(path[0], path[1]), amounts[0]), "Router: WIGN transfer failed");
|
||||
_swap(amounts, path, to);
|
||||
}
|
||||
|
||||
function swapExactTokensForIGN(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)
|
||||
external
|
||||
ensure(deadline)
|
||||
returns (uint256[] memory amounts)
|
||||
{
|
||||
require(path[path.length - 1] == WIGN, "Router: invalid path");
|
||||
amounts = getAmountsOut(amountIn, path);
|
||||
require(amounts[amounts.length - 1] >= amountOutMin, "Router: insufficient output amount");
|
||||
_pull(path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]);
|
||||
_swap(amounts, path, address(this));
|
||||
IWIGN(WIGN).withdraw(amounts[amounts.length - 1]);
|
||||
_sendIGN(to, amounts[amounts.length - 1]);
|
||||
}
|
||||
|
||||
// ---- transfers ----
|
||||
|
||||
function _pull(address token, address from, address to, uint256 value) private {
|
||||
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Router.transferFrom.selector, from, to, value));
|
||||
require(ok && (data.length == 0 || abi.decode(data, (bool))), "Router: transferFrom failed");
|
||||
}
|
||||
|
||||
function _sendIGN(address to, uint256 value) private {
|
||||
(bool ok,) = to.call{value: value}("");
|
||||
require(ok, "Router: IGN send failed");
|
||||
}
|
||||
}
|
||||
32
contracts/dex/src/TestToken.sol
Normal file
32
contracts/dex/src/TestToken.sol
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ERC20} from "./ERC20.sol";
|
||||
|
||||
/// A test token with its own faucet: anyone takes DRIP tokens once an hour. Devnet 3, test tokens, no value.
|
||||
contract TestToken is ERC20 {
|
||||
uint256 public constant DRIP = 1_000 ether;
|
||||
uint256 public constant DRIP_INTERVAL = 1 hours;
|
||||
mapping(address => uint256) public lastDrip;
|
||||
|
||||
event Drip(address indexed to, uint256 value);
|
||||
|
||||
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
|
||||
|
||||
/// Mints DRIP tokens to the caller; refused inside DRIP_INTERVAL of the caller's last drip.
|
||||
function drip() external {
|
||||
uint256 last = lastDrip[msg.sender];
|
||||
require(last == 0 || block.timestamp >= last + DRIP_INTERVAL, "TestToken: one drip an hour");
|
||||
lastDrip[msg.sender] = block.timestamp;
|
||||
_mint(msg.sender, DRIP);
|
||||
emit Drip(msg.sender, DRIP);
|
||||
}
|
||||
|
||||
/// Seconds until the caller may drip again (0 when it may).
|
||||
function dripWait(address who) external view returns (uint256) {
|
||||
uint256 last = lastDrip[who];
|
||||
if (last == 0) return 0;
|
||||
uint256 next = last + DRIP_INTERVAL;
|
||||
return block.timestamp >= next ? 0 : next - block.timestamp;
|
||||
}
|
||||
}
|
||||
27
contracts/dex/src/WIGN.sol
Normal file
27
contracts/dex/src/WIGN.sol
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {ERC20} from "./ERC20.sol";
|
||||
|
||||
/// Wrapped IGN, the WETH9 shape: one WIGN per IGN held by this contract, minted on deposit and burned on withdraw.
|
||||
/// Devnet 3, test tokens, no value.
|
||||
contract WIGN is ERC20("Wrapped IGN", "WIGN") {
|
||||
event Deposit(address indexed to, uint256 value);
|
||||
event Withdrawal(address indexed from, uint256 value);
|
||||
|
||||
receive() external payable {
|
||||
deposit();
|
||||
}
|
||||
|
||||
function deposit() public payable {
|
||||
_mint(msg.sender, msg.value);
|
||||
emit Deposit(msg.sender, msg.value);
|
||||
}
|
||||
|
||||
function withdraw(uint256 value) external {
|
||||
_burn(msg.sender, value);
|
||||
emit Withdrawal(msg.sender, value);
|
||||
(bool ok,) = msg.sender.call{value: value}("");
|
||||
require(ok, "WIGN: send failed");
|
||||
}
|
||||
}
|
||||
137
contracts/dex/test/Dex.t.sol
Normal file
137
contracts/dex/test/Dex.t.sol
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {Vm, VM_ADDRESS} from "./Vm.sol";
|
||||
import {WIGN} from "../src/WIGN.sol";
|
||||
import {TestToken} from "../src/TestToken.sol";
|
||||
import {IgneumFactory, IgneumPair} from "../src/IgneumPair.sol";
|
||||
import {IgneumRouter} from "../src/IgneumRouter.sol";
|
||||
|
||||
/// The AMM end to end on Foundry's EVM: pair creation, liquidity in and out, token and IGN swaps, the faucet's
|
||||
/// hour, the k check. Every number is a test number: Devnet 3, test tokens, no value.
|
||||
contract DexTest {
|
||||
Vm constant vm = Vm(VM_ADDRESS);
|
||||
|
||||
WIGN wign;
|
||||
TestToken tta;
|
||||
TestToken ttb;
|
||||
IgneumFactory factory;
|
||||
IgneumRouter router;
|
||||
address alice = address(0xA11CE);
|
||||
address bob = address(0xB0B);
|
||||
|
||||
receive() external payable {}
|
||||
|
||||
function setUp() public {
|
||||
wign = new WIGN();
|
||||
tta = new TestToken("Test Token A", "TTA");
|
||||
ttb = new TestToken("Test Token B", "TTB");
|
||||
factory = new IgneumFactory();
|
||||
router = new IgneumRouter(address(factory), address(wign));
|
||||
vm.deal(alice, 1_000 ether);
|
||||
vm.deal(bob, 1_000 ether);
|
||||
}
|
||||
|
||||
function _drip(address who) internal {
|
||||
vm.startPrank(who);
|
||||
tta.drip();
|
||||
ttb.drip();
|
||||
tta.approve(address(router), type(uint256).max);
|
||||
ttb.approve(address(router), type(uint256).max);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function _seed() internal returns (address pairAB, address pairAW) {
|
||||
_drip(alice);
|
||||
vm.startPrank(alice);
|
||||
router.addLiquidity(address(tta), address(ttb), 500 ether, 500 ether, 0, 0, alice, block.timestamp + 60);
|
||||
router.addLiquidityIGN{value: 100 ether}(address(tta), 500 ether, 0, 0, alice, block.timestamp + 60);
|
||||
vm.stopPrank();
|
||||
pairAB = factory.getPair(address(tta), address(ttb));
|
||||
pairAW = factory.getPair(address(tta), address(wign));
|
||||
}
|
||||
|
||||
function test_faucet_drips_once_an_hour() public {
|
||||
vm.prank(bob);
|
||||
tta.drip();
|
||||
require(tta.balanceOf(bob) == 1_000 ether, "drip amount");
|
||||
vm.prank(bob);
|
||||
vm.expectRevert(bytes("TestToken: one drip an hour"));
|
||||
tta.drip();
|
||||
vm.warp(block.timestamp + 1 hours);
|
||||
vm.prank(bob);
|
||||
tta.drip();
|
||||
require(tta.balanceOf(bob) == 2_000 ether, "second drip");
|
||||
}
|
||||
|
||||
function test_add_liquidity_creates_pairs_and_mints() public {
|
||||
(address pairAB, address pairAW) = _seed();
|
||||
require(pairAB != address(0) && pairAW != address(0) && pairAB != pairAW, "pairs");
|
||||
require(factory.allPairsLength() == 2, "two pairs");
|
||||
(uint256 rA, uint256 rB) = router.getReserves(address(tta), address(ttb));
|
||||
require(rA == 500 ether && rB == 500 ether, "AB reserves");
|
||||
(uint256 rT, uint256 rW) = router.getReserves(address(tta), address(wign));
|
||||
require(rT == 500 ether && rW == 100 ether, "AW reserves");
|
||||
require(IgneumPair(pairAB).balanceOf(alice) == 500 ether - 1000, "LP minus the locked minimum");
|
||||
require(IgneumPair(pairAB).balanceOf(address(0xdead)) == 1000, "locked minimum");
|
||||
require(wign.balanceOf(pairAW) == 100 ether, "WIGN held by the pair");
|
||||
}
|
||||
|
||||
function test_swap_exact_tokens_for_tokens_keeps_k() public {
|
||||
(address pairAB,) = _seed();
|
||||
_drip(bob);
|
||||
address[] memory path = new address[](2);
|
||||
path[0] = address(tta);
|
||||
path[1] = address(ttb);
|
||||
uint256[] memory quoted = router.getAmountsOut(10 ether, path);
|
||||
// 10 in at 0.3 percent on 500/500: 10*997*500 / (500*1000 + 10*997) tokens
|
||||
require(quoted[1] == 9775084808910328058, "quote");
|
||||
(uint112 r0b, uint112 r1b,) = IgneumPair(pairAB).getReserves();
|
||||
vm.prank(bob);
|
||||
uint256[] memory amounts = router.swapExactTokensForTokens(10 ether, quoted[1], path, bob, block.timestamp + 60);
|
||||
require(amounts[1] == quoted[1], "swap matches the quote");
|
||||
require(ttb.balanceOf(bob) == 1_000 ether + quoted[1], "bob received");
|
||||
(uint112 r0a, uint112 r1a,) = IgneumPair(pairAB).getReserves();
|
||||
require(uint256(r0a) * uint256(r1a) >= uint256(r0b) * uint256(r1b), "k did not fall");
|
||||
}
|
||||
|
||||
function test_swap_ign_both_ways() public {
|
||||
_seed();
|
||||
address[] memory path = new address[](2);
|
||||
path[0] = address(wign);
|
||||
path[1] = address(tta);
|
||||
uint256 before = tta.balanceOf(bob);
|
||||
vm.prank(bob);
|
||||
uint256[] memory amounts = router.swapExactIGNForTokens{value: 1 ether}(0, path, bob, block.timestamp + 60);
|
||||
require(tta.balanceOf(bob) == before + amounts[1] && amounts[1] > 4.9 ether && amounts[1] < 5 ether, "IGN to TTA");
|
||||
path[0] = address(tta);
|
||||
path[1] = address(wign);
|
||||
uint256 ignBefore = bob.balance;
|
||||
vm.startPrank(bob);
|
||||
tta.approve(address(router), type(uint256).max);
|
||||
uint256[] memory back = router.swapExactTokensForIGN(amounts[1], 0, path, bob, block.timestamp + 60);
|
||||
vm.stopPrank();
|
||||
require(bob.balance == ignBefore + back[1] && back[1] < 1 ether && back[1] > 0.99 ether, "TTA to IGN");
|
||||
}
|
||||
|
||||
function test_remove_liquidity_returns_both_tokens() public {
|
||||
(address pairAB,) = _seed();
|
||||
uint256 lp = IgneumPair(pairAB).balanceOf(alice);
|
||||
vm.startPrank(alice);
|
||||
IgneumPair(pairAB).approve(address(router), lp);
|
||||
(uint256 a, uint256 b) = router.removeLiquidity(address(tta), address(ttb), lp, 0, 0, alice, block.timestamp + 60);
|
||||
vm.stopPrank();
|
||||
require(a == 500 ether - 1000 && b == 500 ether - 1000, "pro rata minus the locked share");
|
||||
require(IgneumPair(pairAB).balanceOf(alice) == 0, "LP burned");
|
||||
}
|
||||
|
||||
function test_expired_deadline_is_refused() public {
|
||||
_seed();
|
||||
address[] memory path = new address[](2);
|
||||
path[0] = address(tta);
|
||||
path[1] = address(ttb);
|
||||
vm.prank(alice);
|
||||
vm.expectRevert(bytes("Router: expired"));
|
||||
router.swapExactTokensForTokens(1 ether, 0, path, alice, block.timestamp - 1);
|
||||
}
|
||||
}
|
||||
23
contracts/dex/test/Vm.sol
Normal file
23
contracts/dex/test/Vm.sol
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// The Foundry cheatcodes this project uses, declared here so the tree needs no remote dependency.
|
||||
interface Vm {
|
||||
function startBroadcast(uint256 privateKey) external;
|
||||
function stopBroadcast() external;
|
||||
function envUint(string calldata name) external view returns (uint256);
|
||||
function envAddress(string calldata name) external view returns (address);
|
||||
function envOr(string calldata name, string calldata defaultValue) external view returns (string memory);
|
||||
function toString(address value) external pure returns (string memory);
|
||||
function toString(uint256 value) external pure returns (string memory);
|
||||
function writeFile(string calldata path, string calldata data) external;
|
||||
function deal(address who, uint256 newBalance) external;
|
||||
function prank(address msgSender) external;
|
||||
function startPrank(address msgSender) external;
|
||||
function stopPrank() external;
|
||||
function warp(uint256 newTimestamp) external;
|
||||
function expectRevert(bytes calldata revertData) external;
|
||||
function addr(uint256 privateKey) external pure returns (address);
|
||||
}
|
||||
|
||||
address constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
|
||||
84
docs/analysis/51-percent.md
Normal file
84
docs/analysis/51-percent.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# What a 51 percent attacker can and cannot do on Igneum
|
||||
|
||||
6 October 2026. For the litepaper and the ledger. Written for a reader who maintains Monero or Kaspa and does not take a finality claim on trust. Every number names its model or its measurement; "approximate" marks the rest. Models and runs: `sim/horizon/consensus-security/` (`ghostdag_sim.py`, `finality_horizon.py`, `cost_model.py`, `signalling.py`), the finality simulator `sim/finality_v2.py` with `sim/results_v2.md`, the specification `docs/spec/02-consensus.md` and `03-finality.md`. The long form is `docs/analysis/horizon/consensus-security.md`.
|
||||
|
||||
Price basis: USD 11.7 per GH/s-hour, measured on rented pods on 6 October 2026 (`docs/bench-log.md`, "Rental cost of hash": 1,748 MH/s for USD 20.44 an hour; the market supplied no more than about 2 GH/s that evening, so every figure above that is a list-price extrapolation). An attacker holding share A of the total hash rents A/(1 - A) times the honest network N.
|
||||
|
||||
## 1. What 51 percent buys, and at what price
|
||||
|
||||
Igneum orders blocks by GHOSTDAG (a rusty-kaspa fork, k = 18 at 1 block/s) and locks a checkpoint every 30 blue blocks when two thirds of the trailing 30 days of blue blocks, per vote key, have signed it (spec 03, Q3). The lock lands 63 to 93 s after the checkpoint block (determined 60 blue score later, certified about 3 s after; `sim/results_v2.md` A).
|
||||
|
||||
A majority of hash buys the ordering race up to that lock. The DAG simulator (`ghostdag_sim.py`, GHOSTDAG as `vendor/igneum-node/consensus/src/processes/ghostdag/protocol.rs` runs it, 20 seeds per cell, one-way delay 0.67 s = the cloud devnet's p99 propagation) gives, at 1 block/s:
|
||||
|
||||
| attacker share | hold 30 s | hold 60 s | hold 90 s | hold 120 s |
|
||||
|---|---|---|---|---|
|
||||
| 20% | wins 10%, reorg median 0 / max 18 | 0%, 0 / 1 | 0%, 0 / 1 | 0%, 0 / 1 |
|
||||
| 34% | 40%, 0 / 18 | 40%, 0 / 36 | 5%, 0 / 39 | 10%, 0 / 52 |
|
||||
| 51% | 85%, 10 / 15 | 85%, 20 / 28 | 80%, 32 / 44 | 80%, 42 / 53 |
|
||||
| 67% | 100%, 8 / 13 | 100%, 16 / 23 | 100%, 26 / 31 | 100%, 34 / 41 |
|
||||
|
||||
"wins" = the private chain became the honest selected chain on release; "reorg" = chain blocks removed. The arithmetic: a private chain merges honest blocks as blue until its first block has k in its anticone, then every later honest block is red in it; it wins when its R blocks plus k exceed the honest N_h, always at or above 50 percent, below it only for holds under k A / ((1 - 2A) lambda) seconds (6 s at 20%, 56 s at 34%). Withholding longer than the lock latency is useless: the first checkpoint inside the hold locks at most 93 s after it is mined, and a chain missing a certified checkpoint is not a fork-choice candidate (spec 03 F1). Bound: the lock latency, 90 to 120 s. Price of the 90-s race at 51 percent: USD 0.30 at 1 GH/s, USD 304 at 1 TH/s (`cost_model.py`). What it earns: a deposit credited before its lock, which the four-state rule forbids (ledger P17: a wallet shows included, executed, proven, finalised and credits on the last).
|
||||
|
||||
Repeated 60-s withholding at 51 percent turns 26 to 28 percent of honest blocks red; a red block's subsidy goes to its merger (spec 02 2.5), so the majority takes about a quarter of honest income and lifts its weight share to about 56 percent (approximate), reorganising the chain every minute in public. It does not reach two thirds.
|
||||
|
||||
The proving pool is the one place 51 percent earns more than it spends today. Consensus checks a proof record's signature and its statement against the node's own execution, and not the proof (spec 07 7.7 item 4, 7.8 item 8; ledger P21). A producer who writes the correct statement, random proof bytes and its own payout address into its own block is paid the shard; at 51 percent of blocks that is at least 51 percent of the 20 percent pool (11,636 IGN an hour at full subsidy) and, since its fake rides its next block while an honest proof takes 9 to 11 s on a 5090, most of the shards outside the 10-s exclusive window. Proof verification in consensus closes it; it is the first item in section 5.
|
||||
|
||||
## 2. What it cannot do, and why
|
||||
|
||||
| it cannot | because | measured |
|
||||
|---|---|---|
|
||||
| Reverse a certified checkpoint | a candidate tip must pass through every certified checkpoint (F1); two certificates at one index need two thirds of total weight each, 4/3 in all, so a conflicting pair needs an equivocator holding a third of 30 days of blocks (spec 03 3.11.2) | 0 conflicting locks under 1/3 in every partition and eclipse seed; conflicts from 34% (`sim/results_v2.md` H, I, L3, M5; `finality_horizon.py` P and E) |
|
||||
| Reach two thirds of weight with 51 percent of blocks | weight is blue blocks over a flat 30-day window: share = (t/30) A, ceiling A; 51% holds 51% on day 30 and never more while honest miners mine | the formula holds to 0.1 day (`results_v2.md` B, `finality_horizon.py` R); red-flooding lifts it to about 56% (approximate) |
|
||||
| Buy weight faster than mining it | a pulsed rental buys 0.26 blocks per hash under the controller (`sim/difficulty/attacks` scenario 2); a bought key is worth its blocks and decays as the window slides, share = A (1 - t/30) + r t/30 (3.11.5) | `results_v2.md` K, `finality_horizon.py` K: keys worth 51% hold the veto to day 24 and are worth 30% on day 30 |
|
||||
| Forge state | every full node executes natively and ignores a record whose statement differs from its own execution (the veto, spec 07 7.2 item 5); a soundness bug is a light-client problem (P7) | the exec-attacks suite, 96 of 97 checks on the shipped node (`docs/review/redteam-2026-10-04.md` row 28) |
|
||||
| Change a rule | code activates when 95% of a day's blue blocks signal it, with a floor height as backstop (P2, `docs/plans/counter-asic-3-node.md` section 6); the signal has 0.07 points of noise over 86,400 blocks, so 94.9% never flips and 95.1% flips on day one | `signalling.py`; the fast-time gate's three cases and its failed case |
|
||||
| Grind the hourly program | the epoch seed is a 10-minute class-group VDF of a checkpoint block fixed 20 minutes before the epoch; withholding a block to pick a program has expected gain 0 against a 300x evaluator margin (spec 04 4.1, 4.6) | `proto-vdf` Monte Carlo over 2,000,000 epochs |
|
||||
| Stretch the clocks | a header is at most 10 s ahead of the clock and 10 s behind its parent; a sanitised clock pays a forgery back | a 50% forger drifts the rate +0.4 to +1.1% (M23 fixed, `sim/difficulty/attacks/README.md`) |
|
||||
|
||||
## 3. The table: capability against share, time, cost and earnings
|
||||
|
||||
| capability | share | time | rent at 1 GH/s | at 100 GH/s | at 1 TH/s | subsidy it earns meanwhile (IGN) | net |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Reorder the last k blocks | any | 20 s | USD 0 | 2 | 24 | 0 | a loss |
|
||||
| Win the lock-latency race (90 s) | 45 to 51% | 90 s | USD 0.3 | 30 | 304 | 1k | nothing credited under a lock |
|
||||
| Reach the veto (1/3 of weight): pause finality at will | 51% | 20 days | USD 6k | 585k | 5.8M | 22M | the attacker earns 51% of it back |
|
||||
| the same | 90% | 11.1 days | USD 28k | 2.8M | 28M | 22M | |
|
||||
| Lock alone (2/3 of weight): certify any chain forward | 67% | 30 days | USD 17k | 1.7M | 17M | 44M | 33% of the period's subsidy at equilibrium |
|
||||
| the same | 90% | 22.2 days | USD 56k | 5.6M | 56M | 44M | |
|
||||
| Hold a pause once the veto is held | 1/3 | for ever | 0 marginal | 0 | 0 | keeps earning | free |
|
||||
| 12-h double spend during a pause or the first 30 days | 51% | 12 h | USD 146 | 15k | 146k | 559k | the deposit |
|
||||
| Orphan an hour of honest blocks beyond merge depth (pause only) | 51% | 1.5 h | USD 18 | 2k | 18k | 70k | the honest hour's subsidy, lost to all |
|
||||
| Private DAG heavier over the window (cold-start node, no certificate) | 51% | 30 days | USD 9k | 877k | 8.8M | 34M | one fresh node misled |
|
||||
| Capture the proving pool with fake records | any producer | continuous | 0 extra | 0 | 0 | up to 20% of emission | the one positive line |
|
||||
| Block a rule change | 6% | per day until the floor | USD 18 | 1.8k | 18k | 6% of subsidy | about zero |
|
||||
| Pause finality by taking the two hands down (tonight's topology) | 0 hash | | a DoS | | | 0 | free |
|
||||
|
||||
At the rental-market equilibrium, where hash joins until rent equals subsidy (39, 156 and 780 GH/s at IGN prices of USD 0.005, 0.02 and 0.10, the three inputs of `docs/analysis/security-budget.md`, not predictions), the veto nets about 48 percent of 20 days of the chain's subsidy and locking alone about 33 percent of 30 days (`cost_model.py` section 3).
|
||||
|
||||
## 4. The residual risks, plainly
|
||||
|
||||
1. **The pause.** Finality pauses whenever less than two thirds of 30-day weight is connected and signing, and the chain runs on proof of work with a 12-hour depth meanwhile (spec 03 3.7 item 2, 3.9). A third of weight holds the pause for nothing once it has it (`finality_horizon.py` S: 0 locks for the whole silence at 34 to 90 percent, 0 conflicts). During a pause a 51 percent miner is a 51 percent miner on any proof-of-work chain, with the 12-hour depth and USD 146 at 1 GH/s to buy it. What we are building against it is in section 5.
|
||||
|
||||
**The departure case** (not an attack, the same pause): tonight on the live devnet 20 keys holding 42.7 percent of the frozen weight table stopped mining within three minutes (a rehearsal job), the signing weight fell to 53.1 percent at checkpoint 6843 and finality paused at 18:39:40Z; rule v2 would have locked again after 35 minutes as the departed blocks aged out of the sliding table, and the frozen table (Q5, the live rule) holds the pause for one window, 2 hours there and 30 days on mainnet; locks had formed with the hand nodes down, so it was weight, not topology (`docs/analysis/horizon/finality-and-weight.md` 3.1 and 4.1; `finality_horizon.py` C: 51 percent leaving pauses 10.5 days under v2, 30.0 under v3). A sudden exit of a third or more of weight, by a price crash or a hosting failure, does the same. What an attacker can do during it is exactly the pause line: proof of work with a 12-hour depth, nothing against any certificate, no new lock to forge; what it cannot do is end the pause early or lock alone, since the departed weight is still in the denominator. The fix is a signed departure (LEAVE, section 5): a key that announces it is leaving is out of every denominator one hour later, which a partitioned key cannot fake.
|
||||
2. **The first 20 to 30 days.** No lock forms before the window holds 30 days of history (spec 03 3.8, `min_daa` = window): the first month is proof of work with the 12-hour depth, by design, and the renter's day counts start at genesis. On day 30 an attacker producing share s of blocks from day k holds s (31 - k)/30 of the window: 75 percent from day 2 locks alone on day 30 (ledger F1).
|
||||
3. **Sybil of keys buys nothing; buying keys buys their blocks.** Weight is blue blocks and every draw is by weight (W6; harness s2). A pool's key with its history can be sold or stolen and is worth exactly its 30 days of blocks, decaying linearly as the window slides (K); the sellers' price, not hash, is the limit, and a seller who keeps a copy strips the buyer by equivocating.
|
||||
4. **Two thirds of total under churn.** The denominator is every key's blocks in the window. If half the honest miners leave, a miner at 51 percent of the old hash holds 51/(51 + 24.5) = 67.5 percent of the window after 30 days and locks alone (spec 03 3.7 item 4: "same as Bitcoin, with a month's warning"). A departure that stops mining and signing at once pauses finality 30 (1 - 1/(3A)) days under rule v2 and until the frozen table expires, 30 days after the last lock, under rule v3 (`finality_horizon.py` C), because a view cannot tell a departure from a partition.
|
||||
5. **A partition longer than a window forks finality.** Each side fills its own table; under v3 neither side under two thirds locks for 30 days after the last common certificate, then both lock alone at once (`results_v2.md` M3) and an operator's trusted certificate resolves it (3.11.4). Under a third of weight, no equivocator shortens that.
|
||||
6. **The proving pool.** Section 1's fake-record capture, until proofs are verified in consensus.
|
||||
|
||||
## 5. What we are building next
|
||||
|
||||
| rank | defence | what it closes | cost | liveness cost |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Proof verification in consensus: a record whose aggregated proof does not verify against the pinned key is invalid | the pool capture of section 1 | 8 to 12 hours; per-record verify time to measure | none |
|
||||
| 2 | Weight-gated deep fork choice: a tip forked more than D (about 10 min) back is a candidate only if the keys that built it hold a third of the weight table at the fork | the pause-time and first-month deep reorg: rented hash has no weight for 10 days, so a 12-h double spend needs the 20-day veto | 10 to 16 hours | none for certificates; a sub-third partition side cannot reorg the other past D, which is the intended outcome |
|
||||
| 3 | Vote-or-burn: a block whose key has participation under 0.5 in its own past burns 20 percent of its producer share | the free pause: silence then costs 7,757 IGN an hour at 34 percent | 6 to 8 hours | none; partition-safe by construction |
|
||||
| 4 | Peer floor and mesh: every box dials three others beside the hands; the node alarms under three peers or two checkpoints without a vote; no `unwrap` on any peer-driven sync path (a pruned node was crashed by one request tonight) | a star fleet pausing on one host; one request crashing any pruned node | 5 to 7 hours + 4 | none |
|
||||
| 5 | The vote signs the execution root too | snapshot poisoning, and "ordered and executed" in one certificate | 8 to 12 hours | lock latency plus executor lag |
|
||||
| 6 | Signalling over 7 consecutive daily windows, the floor a week past the publish | a one-day renter forcing a flip | 3 hours | a week's latency on class changes |
|
||||
| 7 | LEAVE, the signed departure (lane 3's rank 1): a `leave` item carried in blocks, the key out of every denominator one hour after inclusion, sent by the app and the fleet library on a clean stop; F5's trusted certificate implemented | tonight's 30-day-scale pause after a planned departure; sim: first lock 1 h after a 34 to 50 percent departure, 0 conflicts in every partition row (`finality-and-weight.md` 6) | 6 + 4 hours | none |
|
||||
| 8 | A client-shipped certified checkpoint | the cold-start private DAG | 3 hours | none |
|
||||
|
||||
Not adopted: prover attestations as a finality leg (the provers are the miners, coverage is a few percent, every lock would wait on a proof); vesting weight (a bought key transfers vested weight); any automatic rule that keeps locks going after an abrupt departure (a view cannot tell it from a partition: `results_v2.md` L4, M3).
|
||||
|
||||
The honest sentence for the litepaper: a hash majority on Igneum can reorder about two minutes, can buy a veto over finality in twenty public days and then pause it for free, can double-spend at a 12-hour depth only while finality is paused or in the first month, and today can take the proving pool without proving; it cannot reverse a certificate, cannot reach two thirds of weight while honest miners mine, cannot forge state, and cannot change a rule without 95 percent of a day's blocks.
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
635
docs/analysis/attack-pass-2026-10.md
Normal file
635
docs/analysis/attack-pass-2026-10.md
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
# 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 adversarial lanes confirm rather than discover. The founder's word, 7 October 2026:
|
||||
"make sure they find ZERO flaws". Every finding is ours, fixed and re-gated, before the public report.
|
||||
|
||||
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 public report 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
|
||||
tail attributed to per-site bucket concentration, 8 October 2026); 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 tail at 1.22x to 1.50x, attributed 8 October 2026 to a quarter-bit bucket concentration at one narrow-window site each, 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 open question of the adversarial lanes. 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; the chip-model lane prices it from the reads-in-flight model; the public report states that 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 public report carries 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 the chip-model lane 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 <IGSD1>` 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 (pre-freeze reading) | 64 seeds at 2^24, chain path, v5 with the dn3 state, box 2: hand-started at 18:47 UTC (11 seeds, killed on the coordinator's rule: every hand-started run off the boxes, loads 601 and 401), re-queued through `lease pool 64` at 19:23 UTC (17 more seeds), released at 19:4x UTC on the Counter ASIC lane's yield so the class v5 (c''') census, the 0.3.24 board's critical path, could take the pool | every seed read equals sub-version 3's seed for seed (0.9915x to 1.144x, p10 1.50x), as the v5 lane predicted: the leaves change the words, not the read addresses | READING, not the gate line |
|
||||
| F8 hot-set gate, the frozen tip (THE GATE LINE) | igneum-pow class-v5 1c420786 (the 0.995 per-site floor (c''') on sub-version 3's rules; binary sha256 0f5c98dc41a1b3aa..., run from a copy); pairing: the library draws the dn3 epoch-0 program as e5a4ac5978462156, the harness validates bit for bit against the library on the dn3 state (0 mismatches on 66 validation lines); 64 seeds p2 to p65 at 2^24 nonces, chain path, the v5 dataset from v5-dn3-epoch0's state.igsd1, window-model control, build-2 under `lease pool` class v5 as two halves of 32 (ended 21:58:43Z and 22:03:21Z) | 61 of 64 under 1.2x of the window model (0.9919x to 1.144x, p75 1.0024x); 3 over, all inside the named four-seed residue and none new: p10 1.5047x (0x4018f5, 346 reads of 2^31, no predicted source), p8 1.3787x (0x839d33, 419), p4 1.2166x (0x400197, 363); p34 0.9997x under the (c''') floor; p23 1.0000x, p19 0.9997x, p15 0.9998x, p18 1.0001x, p56 1.0000x. Seed for seed the ratios equal sub-version 3's within 0.001 except where the floor moved a draw: the state leaves change the words, not the read addresses. Nothing to a chip | PASS (the known residue p4, p8, p10, attributed 8 October 2026 to per-site bucket concentration; the largest-bucket bound is a next class's item) |
|
||||
| F9 exhaustion count, the frozen tip (A GATE LINE for the 0.3.24 move) | 10^5 chain-shaped seeds on the v5 chain path (era-composed class, `--chain`) with the dn3 state at igneum-pow class-v5 1c420786, pairing e5a4ac5978462156, build-1, ten chunks of 10,000 under `lease pool 4` (chunks 1, 3, 5 to 9 under class v5; chunks 0, 2, 4 re-leased under class release on the coordinator's order; 14,477 to 14,480 s per chunk, about 1.45 s per seed); binary copied into the run dir; interim line sent at 00:55 UTC (seeds drawn, 0 exhausted, 0 panics, max 30, F1 0 failures), which cleared the move; last chunk written 02:34:54 UTC | 100,000 of 100,000 seeds drawn, 0 exhausted, 0 panics, 0 past attempt index 31, max attempt index 30; histogram by attempt index (0 = accepted on the first draw) 0: 31,454; 1: 21,460; 2: 14,660; 3: 10,263; 4: 7,047; 5: 4,701; 6: 3,297; 7: 2,256; 8: 1,532; 9: 1,027; 10: 702; 11: 509; 12: 365; 13: 216; 14: 153; 15: 103; 16: 80; 17: 56; 18: 39; 19: 20; 20: 24; 21: 9; 22: 6; 23: 9; 24: 3; 25: 4; 26: 2; 27: 1; 29: 1; 30: 1; first-draw acceptance 0.3145, mean attempt index 2.185 (3.185 draws per seed), 4,862 seeds (4.86 percent) at index 8 or above, 255 (0.255 percent) at 16 or above; the 256-attempt cap and the deterministic last resort never reached. Meaning per tier: no epoch seed in 10^5 fails to draw a program, so the liveness halt of AP-F8-2 has no observed case on the frozen tip at this count (the bound it supports is under 3e-5 per seed at 95 percent, about one epoch in 33,000 at worst; a halt a node operator would see as a stuck epoch, a miner as a dead epoch, a holder as a paused chain), and the draw cost stays at about 3.2 candidates per epoch for every node | PASS (0 of 10^5; the record `f9-grind.md`, section (d)) |
|
||||
| F1 shadow redundancy, the frozen tip (A GATE LINE for the 0.3.24 move) | 10^5 class v5 programs through the string-seed path (`generate_from_seed_bytes_program_class`, class V5, every candidate draw through the (c''') floor over 2^20) at igneum-pow class-v5 1c420786, pairing e5a4ac5978462156, build-1, `lease pool 16` class release (cores 8 to 23, re-leased 22:34:05 UTC), binary sha256 bb70bbf69a4b3223... copied into `frozen-1c420786-f1/bin`; 20,774 s of census (5 h 46 min; about 5 core-s per program, the (c''') draw cost), census.csv (sha256 4e34b669f2f5c680..., 100,000 rows) written 04:20 UTC on 8 October 2026 | 100,000 of 100,000 programs; instructions saved min 0.000, mean 0.623, max 4.688 percent (worst `attack-f1/95060` at attempt 0, 6,912 to 6,588 per iteration; `81748`, `66933`, `3006` at the same 4.688; next 4.311); chip-view ops saved mean 0.520, max 4.783; programs over 5 percent 0, over 10 percent 0; soundness: differential mismatches 0 of 100,000 (8 random states each), verifier mismatches 0 of 100,000; 0 panics; histogram of saved, 0.5 percent bins from 0: 55,241; 20,597; 11,762; 9,851; 1,484; 656; 259; 133; 13; 4; 0; 0; draw attempts per program: index 0 31,630, max 30 (the same shape as F9's). Against the v4 10^5 (max 5.078, the AP-F1-1 letter miss): the v5 tip's worst sits 0.39 points under the letter, the two top bins are empty, and the mean is unchanged (0.617 to 0.623), so the v5 leaves add no redundancy and remove the one letter miss. Meaning per tier: the shadow block of every drawn program stays within 5 percent of its naive count under the harness's rules and the honest compiler finds the same shortcuts, so no chip gets a shadow-side discount (a miner on a card pays the full block, a hypothetical ASIC gains nothing here) and the chain's verifier agrees with the harness on every program (0 mismatches), so no node disagrees with another on any drawn block. Harness gap and its fix: section 12 of the record (the running census was the old binary; the flush is in 18a9c04a for every census after it) | PASS (0 of 10^5 over the letter; AP-F1-1 FIXED-AND-PASSED on v5 at this count; the record `f1-shadow.md`, section 13) |
|
||||
| F4 weak-day census, the post-freeze commit | 2^24 chain days at class-v5 8ca66afa (AP-F4-1 in the agreed form: cost A at most 205, k >= 1 or all-ROT-equal rejected, the forty redrawn), the harness on the agreed w32 convention (digits 0 to 31) and median 226; known-failed day 29,337 redrawn under the rule (cost 203 to 228), day 20,729 at 219 unchanged; build-1 `lease pool 12` class adv, 379 s, ended 22:3x UTC | 0 of 2^24 days over 1.1x on M1 (median 226; minimum cost 206 at day 27,016, 1.097x, one adder above the reject line) and 0 on M2; mean 225.79, sd 6.07 (pre-rule: 5.69e-4 over, min 203). The redraw rule removes the LUT tail by construction and the measurement agrees | PASS; AP-F4-1 FIXED-AND-PASSED against class v5 at 8ca66afa |
|
||||
| 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); killed before its first chunk closed, re-queued through `lease pool` | pending | pending the re-queue |
|
||||
| F1 shadow redundancy | 10^5 class v5 programs through the string-seed path, box 1; the known firings fire under v5 (planted 50 of 256: 19.53 percent; the real block 0.000; the must-not-fire 1.157); the census killed before its end, re-queued through `lease pool` | pending | pending the re-queue |
|
||||
|
||||
## 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/<worktree>` 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
|
||||
question is chip-relative compression), 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 public report), 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 public report states the windows.
|
||||
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 | bucket concentration at a narrow-window site (r0, window 2^22, offset 1, last writer mad at 20): largest 256-item bucket 5.6x window expectation, index entropy 13.71 of 14 bits, hottest 0x4004da at 362 reads, source none (identical to sub-version 1) |
|
||||
| p8 | 1.38x | 14, 51 | 1.42 percent | 0.980 | bucket concentration at a narrow-window site (r7, window 2^22, offset 2, last writer xor at 44): largest bucket 3.1x, entropy 13.72 of 14; plus site 6 (instr 33, r3, window 2^23, mad at 30) at 0.834 percent, bucket 3.5x; hottest 0x837de4 at 420 reads, source none |
|
||||
| p34 | 1.25x | 1, 13 | 1.35 percent | 0.997 | one-bit value through sub (r3, window 2^23, offset 1, last writer sub at 5): largest bucket 3.5x, entropy 14.96 of 15, hottest 0x800010 at 541 reads, saturated source 0.0001 percent |
|
||||
| p4 | 1.22x | 1, 8 | 1.45 percent | 0.981 | bucket concentration at a narrow-window site (r2, window 2^22, offset 1, last writer mad at 4): largest bucket 4.5x, entropy 13.74 of 14, hottest 0x4000e7 at 355 reads, source none (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 are the tail. The tail attributed (hash lane, 8 October 2026, 09:40 to 09:46 UK, attack-f8 census at 2^24 with
|
||||
the window-model control and by-site attribution, tree b38b4af6 on the frozen 017e7037; the gate ratios reproduced to
|
||||
four places, p4 1.2169x, p8 1.3774x, p10 1.5036x, p34 1.2501x, the hot-set verdict clear on the windowed control for
|
||||
all four): each is a per-site bucket concentration of about a quarter bit (0.26 to 0.29 bits short of 14 on a 2^22
|
||||
window; p34 0.04 of 15) at one narrow-window load site whose last writer is mad, xor or sub, with every other site at
|
||||
its flat share; the ratio tracks the largest-256-item-bucket excess (5.6x gives 1.50x, 3.1x to 4.5x give 1.22x to
|
||||
1.38x); (c'') passes them at 0.9927 to 0.9963 because distinctness does not see a bucket; the check that would catch
|
||||
all four is a per-site largest-bucket bound (about 2x window expectation at the 2^20 units), a generator change for a
|
||||
next class, never for the frozen ones. Meaning per tier: a quarter bit at one site is under the window model's own
|
||||
spread (the gate line's 61 of 64 stands), so no card or chip gains a cacheable hot set from it; the bound is the next
|
||||
class's item, not a change to 017e7037 or 1c420786. 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 tail, attributed 8 October 2026 (per-site bucket concentration, the AP-F8-1 tail paragraph): 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.
|
||||
Reconciled with adv-mixer-2's independent census (7 October 2026, 21:5x UK; F4 record section 9): the same class
|
||||
and cost form; this record's NAF counted the carry digit at position 32, which a 32-bit multiplier never pays, so the
|
||||
agreed figures are adv-mixer-2's: median 226, a 1.1x gain at cost A at most 205, 5.69e-4 of days (2^-10.8), 15 days
|
||||
a century, worst 2050-04-28 (day 29,337) at 1.113x; the DSP-bound readings agree at 0; the redraw rule for the next
|
||||
class takes adv-mixer-2's form (cost A at most 205, or k >= 1, or the eight ROT equal).
|
||||
Status: F4 PASS against v4; AP-F4-1 FIXED-AND-PASSED against class v5 at 8ca66afa (7 October 2026, 22:3x UTC: 0 of
|
||||
2^24 days over 1.1x on both metrics with the agreed rule in the draw).
|
||||
|
||||
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 public report's readers
|
||||
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.
|
||||
344
docs/analysis/attack-pass/f1-shadow.md
Normal file
344
docs/analysis/attack-pass/f1-shadow.md
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# 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/<i>`, 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 <scratchpad>/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 public report | this file and the harness are published with the target; the window-proof script and the census line are the reproduction | hand over with the pass record |
|
||||
|
||||
## 12. The census flush (8 October 2026, 04:1x to 05:1x UK; the coordinator's ruling after the class v5 10^5 run)
|
||||
|
||||
The v5 10^5 census on 1c420786 ran 4 h 30 min on build-1 with nothing on disk: the harness collected every Report in
|
||||
memory and wrote `census.csv` once at its end, with no progress line, so its state could not be read (the lane (d)
|
||||
row names the gap). Ruling: before the harness's next 10^5 run on any class, a progress line every 1,000 programs
|
||||
(count, elapsed, the running failure count) and a partial `census.csv` flushed at the same cadence, with a
|
||||
known-failed test of the flush. Done on attack-v5-frozen at 18a9c04a (`tools/attack/f1-shadow/src/main.rs`,
|
||||
`census`): the crossing thread rewrites `out/census.csv` from every row so far, sorted by idx, through a temporary
|
||||
file and a rename, so a kill never leaves a torn file; the line reads
|
||||
`progress: N of COUNT programs, S s, failures F (difftest or verifier), census.csv N rows`.
|
||||
|
||||
Known-failed test (box 2, `flush-test/`, binary sha256 14180ef40d55eb74..., `lease pool 16 --min 8 --class adv`,
|
||||
lease pid 340097, queued 03:12:32Z behind the box's pool, started when adv-accept's sweep-s05b freed its cores):
|
||||
a 4,000-program v4 census killed by pid (445480, TERM) the second the 2000 line appeared, at 04:08:27Z.
|
||||
|
||||
| Reading | Value |
|
||||
|---|---|
|
||||
| Progress lines before the kill | `1000 of 4000, 286 s, failures 0, 1000 rows`; `2000 of 4000, 571 s, failures 0, 2000 rows` |
|
||||
| Rows in `census.csv` after the kill (header excluded) | 2,000 |
|
||||
| `census.csv.tmp` left behind | none |
|
||||
| Lease exit | 143 (the kill), 16 cores released |
|
||||
|
||||
Verdict: PASS (2,000 rows after a kill at 2,000; the known-fail of the old harness was zero rows). A side reading:
|
||||
1,000 programs per 286 s on 16 cores is about 4.6 core-s per program on this box under its load, which is the
|
||||
(c'') and (c''') draw cost per candidate and confirms the F1 10^5 projection on build-1 (about 5 to 6 core-s per
|
||||
program, about 10 h on 15 busy cores). Consequences per tier: none for a user; for the lanes, every future census
|
||||
can be read and killed without loss.
|
||||
|
||||
## 13. The frozen class v5 tip, 10^5 programs (1c420786; the 0.3.24 gate line; 8 October 2026, 05:20 UK)
|
||||
|
||||
Run: igneum-pow class-v5 1c420786 (pairing e5a4ac5978462156), build-1, `lease pool 16 --min 8 --class release`
|
||||
(cores 8 to 23, re-leased 22:34:05 UTC on the coordinator's order after the class v5 lease was killed under the
|
||||
duplicate-lease clean-up), the binary (sha256 bb70bbf69a4b3223...) copied into `frozen-1c420786-f1/bin`, 16 threads,
|
||||
20,774 s (5 h 46 min; about 5 core-s per program, which is the (c''') floor over 2^20 on every candidate draw, measured
|
||||
again by section 12's 4.6 core-s on box 2), `out/census.csv` (sha256 4e34b669f2f5c680..., 100,000 rows) and
|
||||
`out/summary.txt` written 04:20 UTC. The interim line at 00:55 UTC (0 on the live panic path) cleared the move;
|
||||
this is the record line.
|
||||
|
||||
| Quantity | Value |
|
||||
|---|---|
|
||||
| Programs | 100,000 (`attack-f1/0` to `attack-f1/99999`), 16 threads, 20,774 s, finished 05:20 UK |
|
||||
| Instructions saved, min / mean / max | 0.000 / 0.623 / 4.688 percent |
|
||||
| Worst programs | `attack-f1/95060` (attempt 0), `81748` (1), `66933` (1), `3006` (2): 6,912 to 6,588 per iteration (12 of 256 per pass); next `55048` at 4.311 |
|
||||
| Programs over 5 percent / over 10 percent | 0 / 0 |
|
||||
| Chip-view ops saved beyond free rotates and hoisted constants, mean / max | 0.520 / 4.783 percent |
|
||||
| Differential mismatches | 0 of 100,000 (8 random states each) |
|
||||
| Verifier mismatches | 0 of 100,000 |
|
||||
| Panics | 0 |
|
||||
| Dead (never-read) derived nodes under the full fold | 3,553,599 |
|
||||
| Rewrites over all programs and 27 passes | identity 3,237,120; xor-cancel 3,127,199; sum-cancel 11,373,389; or-idem 289,936; rotl-merge 4,436,701; rotr-merge 320,399; product-shared 2,303,677 |
|
||||
| Histogram of instructions saved, 0.5 percent bins from 0 | 55,241; 20,597; 11,762; 9,851; 1,484; 656; 259; 133; 13; 4; 0; 0 |
|
||||
| Draw attempts per program (index 0 = first draw) | 0: 31,630; 1: 21,226; 2: 14,842; 3: 10,151; 4: 7,007; 5: 4,787; 6: 3,257; 7: 2,205; 8: 1,470; 9: 1,115; 10: 697; 11: 502; 12: 345; 13: 225; 14: 174; 15: 113; 16: 78; 17: 53; 18: 38; 19: 28; 20: 12; 21: 17; 22: 8; 23: 6; 24: 5; 25: 7; 29: 1; 30: 1 |
|
||||
|
||||
Against section 7.2 (class v4 at 10^5, max 5.078, the one letter miss recorded as AP-F1-1): the v5 tip's worst
|
||||
program sits 0.39 points under the 5 percent letter, the two top bins are empty (v4: 1 and 7), the mean is unchanged
|
||||
(0.617 to 0.623) and the shape of the shortcut is the one of section 7.3 (a register written twice from the same
|
||||
source with no write between, 12 instructions, nothing crossing a pass). The attempt histogram has F9's shape
|
||||
(first-draw acceptance 0.316 against F9's 0.3145 on chain-shaped seeds), so the string-seed path and the chain path
|
||||
draw the same distribution.
|
||||
|
||||
Verdict: PASS by the letter and at honest-compiler parity (0 of 10^5 over 5 percent, 0 mismatches); AP-F1-1
|
||||
FIXED-AND-PASSED on class v5 at this count. Consequences per tier: no drawn program's shadow block gives any chip a
|
||||
discount beyond the honest compiler's own simplification (a card pays the full block, a hypothetical ASIC gains
|
||||
nothing on the shadow side), and the verifier agrees with the harness on every program, so no node disagrees with
|
||||
another on any drawn block.
|
||||
211
docs/analysis/attack-pass/f10-ladder.md
Normal file
211
docs/analysis/attack-pass/f10-ladder.md
Normal file
|
|
@ -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<seed hash, LadderDecision>`, 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
|
||||
(`<name>.log` = harness stdout, `<name>.json` = summary, `<name>-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 public report 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).
|
||||
214
docs/analysis/attack-pass/f2-mixer.md
Normal file
214
docs/analysis/attack-pass/f2-mixer.md
Normal file
|
|
@ -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/<day>.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 <scratch> -- 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/<day>.<variant>.txt --apps k --state state/<name>.json --budget <chunk> --per-app-min <k=1 floor> --verifier <binary>` 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/<model>-<day>-<variant>-k<k>.log` per search, `logs/rx.<day>.<variant>.log`, `logs/rx-word.<day>.<variant>.log`, `logs/fold.<day>.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.<day>.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.<day>.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 adversarial
|
||||
lanes' job (funding.md B5 rank 1) is the effort-bounded version of the same search with their own 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).
|
||||
148
docs/analysis/attack-pass/f3-cache.md
Normal file
148
docs/analysis/attack-pass/f3-cache.md
Normal file
|
|
@ -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 <scratchpad>/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 `<scratchpad>/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 <cmd>"`, 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-<phase>.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 adversarial lanes'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 public report | the record and the harness are published; rank 2's open question is the block function in chaining mode, not the graph |
|
||||
339
docs/analysis/attack-pass/f4-weakday.md
Normal file
339
docs/analysis/attack-pass/f4-weakday.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
# 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 <scratch> -- 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 <d> --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 <d> --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 <day bytes> --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 "<literal>"`, 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 <dir>`
|
||||
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.
|
||||
|
||||
## 9. Reconciliation with adv-mixer-2's independent census (7 October 2026, 21:5x UK; main's order through the
|
||||
crypto-engage coordinator: the two tables side by side, the median each used, one agreed figure)
|
||||
|
||||
Both censuses read the same class, the FPGA LUT multiplier-adder area (no chip or GPU gain), with the same cost form,
|
||||
64 + the sum over the sixteen MUL of (signed-digit weight - 1). They differ in one convention. This harness's
|
||||
`naf_weight` (src/main.rs line 72) computes the canonical NAF of the constant as a `u64` and counts every digit,
|
||||
including the carry digit at position 32 that a 32-bit odd constant carries with probability 1/3; adv-mixer-2's
|
||||
`w32` counts the digits at positions 0 to 31 only, because a multiplier modulo 2^32 is never built with a digit at
|
||||
position 32. Checked on 2^18 random odd constants: mean weight 11.442 with the carry digit against 11.109 without
|
||||
(P(digit at 32) = 0.3334), cost 231.1 against 225.7. So this record charged every day about 5 adders a 32-bit
|
||||
multiplier never pays, unevenly per day, which is the whole of 231 against 226 and of 12 against 15 days a century.
|
||||
|
||||
| Quantity | This record (M1, carry digit counted) | adv-mixer-2 (model A, w32 modulo 2^32) | Agreed |
|
||||
|---|---|---|---|
|
||||
| Cost form | 64 + sum(NAF(MUL_i) - 1), NAF over u64 | 64 + sum(w32(MUL_i) - 1), digits 0 to 31 | adv-mixer-2's: the digit at position 32 costs nothing in a 32-bit multiply |
|
||||
| Median per application | 231 (census over 2^24 chain days; mean 231.113) | 226 (exact, 16-fold convolution over all 2^31 odd constants; the log's provisional 231 superseded) | 226 |
|
||||
| Threshold for a 1.1x gain | cost under 210 (NAF sum under 163) | cost A at most 205 | cost A at most 205 |
|
||||
| Fraction of days over 1.1x | 3.264e-4 (5,476 of 2^24); exact expectation 3.243e-4 | 5.677e-4 (2^24 census); exact 5.694e-4 | 5.69e-4, about 2^-10.8, against the 2^-20 gate |
|
||||
| Days over 1.1x per century (36,525 public days) | 12 | 15, listed in its Q4 | 15 |
|
||||
| Worst public day | 29,337 (2050-04-28), cost 206, 1.121x | 29,337 (2050-04-28), cost A 203, 1.113x | 29,337, 1.113x |
|
||||
| Genesis day 20,729 | cost 226, 0.978x | cost A 219, 1.032x | 219 |
|
||||
| DSP-bound metric | M2: 0 days with k >= 2 in 2^28; k >= 1 at 1.067x | model C: P(k >= 1) 3.12e-5 (1.067x), P(k >= 2) 4.57e-10 (1.143x) | agree: under the gate at every k |
|
||||
| Redraw rule for the next class | reject NAF sum under 163 (cost under 211), redraw from the next stream values | cost A at most 205, or k >= 1, or the eight ROT equal: continue the same SplitMix64 stream and draw the forty again | adv-mixer-2's form and numbers |
|
||||
|
||||
Verdict unchanged: PASS against class v4 on the DSP-bound reading (M2 and model C agree at 0), the LUT tail bounded
|
||||
and measured; the agreed numbers above replace this record's M1 figures wherever quoted, and AP-F4-1's rule on the
|
||||
v5 list takes adv-mixer-2's form. Harness note: `attack-f4 plant` and `day` default `--median` to 221 when the flag is
|
||||
absent (the plant lines of the class v5 run this evening read "median 221" for that reason); every census line in
|
||||
this record was run with `--median 231` and is restated against 226 above.
|
||||
229
docs/analysis/attack-pass/f6-verifier.md
Normal file
229
docs/analysis/attack-pass/f6-verifier.md
Normal file
|
|
@ -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/<S+i>`; 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 <scratch> -- 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 <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.
|
||||
165
docs/analysis/attack-pass/f7-era.md
Normal file
165
docs/analysis/attack-pass/f7-era.md
Normal file
|
|
@ -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 public report
|
||||
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.
|
||||
259
docs/analysis/attack-pass/f8-uniform.md
Normal file
259
docs/analysis/attack-pass/f8-uniform.md
Normal file
|
|
@ -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-<program>-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 any reader)
|
||||
|
||||
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).
|
||||
301
docs/analysis/attack-pass/f9-grind.md
Normal file
301
docs/analysis/attack-pass/f9-grind.md
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# 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.
|
||||
|
||||
### (d) Exhaustion count on the frozen class v5 tip, 10^5 chain-shaped seeds (1c420786; the 0.3.24 gate line)
|
||||
|
||||
Run: igneum-pow class-v5 1c420786 (pairing e5a4ac5978462156, the harness draws through `Epoch::chain_program` with the
|
||||
era-composed class, `--chain`, the dn3 state leaves), build-1, ten chunks of 10,000 seeds under `lease pool 4` (chunks 1,
|
||||
3 and 5 to 9 under class v5, chunks 0, 2 and 4 re-leased under class release on the coordinator's order), the binary
|
||||
copied into `frozen-1c420786-f9/bin`, 14,477 to 14,480 s per chunk (about 1.45 s per seed), last chunk written
|
||||
02:34:54 UTC on 8 October 2026 (`par0.tsv` to `par9.tsv`, `par0.log`, `par2.log`, `par4.log`). The interim line at
|
||||
00:55 UTC (seeds drawn to that minute, 0 exhausted, 0 panics, max 30) cleared the 0.3.24 move; this is the record line.
|
||||
|
||||
| Quantity | 100,000 seeds |
|
||||
|---|---|
|
||||
| Seeds drawn | 100,000 of 100,000 |
|
||||
| Exhausted (the 256-attempt cap reached, or the deterministic last resort used) | 0 |
|
||||
| Panics on the draw path (caught and counted) | 0 |
|
||||
| Past attempt index 31 | 0 |
|
||||
| Max attempt index | 30 (one seed) |
|
||||
| First-draw acceptance (index 0) | 31,454 (0.3145) |
|
||||
| Mean attempt index (draws per seed) | 2.185 (3.185) |
|
||||
| Index 8 or above | 4,862 (4.86 percent) |
|
||||
| Index 16 or above | 255 (0.255 percent) |
|
||||
|
||||
Histogram by attempt index: 0: 31,454; 1: 21,460; 2: 14,660; 3: 10,263; 4: 7,047; 5: 4,701; 6: 3,297; 7: 2,256;
|
||||
8: 1,532; 9: 1,027; 10: 702; 11: 509; 12: 365; 13: 216; 14: 153; 15: 103; 16: 80; 17: 56; 18: 39; 19: 20; 20: 24;
|
||||
21: 9; 22: 6; 23: 9; 24: 3; 25: 4; 26: 2; 27: 1; 29: 1; 30: 1. The tail is geometric at a ratio of about 0.69 per
|
||||
index (the per-draw rejection rate under (a'), (c'), (c'') and (c''') together), so the chance of reaching the cap at
|
||||
256 is far below the count; the observed bound is under 3e-5 per seed at 95 percent (0 of 10^5).
|
||||
|
||||
Verdict: PASS. Consequences per tier: no epoch seed in 10^5 fails to draw, so the AP-F8-2 liveness halt (a stuck
|
||||
epoch for a node operator, a dead epoch for a miner, a paused chain for a holder) has no observed case on the frozen
|
||||
tip; the draw costs every node about 3.2 candidates per epoch, which at about 1.45 s per seed on one box core is under
|
||||
five seconds of CPU per epoch and does not change any tier's cost.
|
||||
53
docs/analysis/attack-pass/o114-i7-9700K-2026-10-07.log
Normal file
53
docs/analysis/attack-pass/o114-i7-9700K-2026-10-07.log
Normal file
|
|
@ -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
|
||||
112
docs/analysis/block-rate-devnet2.md
Normal file
112
docs/analysis/block-rate-devnet2.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Block rate on Devnet 2: 10 blocks per second against 1, on the rented fleet
|
||||
|
||||
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,
|
||||
mergeset limit, parents, coinbase maturity from `consensus/core/src/config/bps.rs`); the shared devnet never reads the
|
||||
variable, so the live digest and rules are untouched. Built on igneum-build-1 (`tools/build-remote.sh`). Numbers land
|
||||
below as they are measured; every figure carries its source file under `~/Desktop/fleet/bps/`.
|
||||
|
||||
## The runs
|
||||
|
||||
| Run | Chain | Miners | Length | What is read |
|
||||
|---|---|---|---|---|
|
||||
| A | igneum-devnet-2, fresh genesis, 10 bps | the 38 wave pods plus the 4 Devnet 2 boxes (42 cards, about 2.0 GH/s) | 60 min | blocks per second achieved, blue and red blocks per minute (orphan rate), blue score growth, max reorg depth, checkpoint lock delay and weight at this voter count, p2p bytes per node per minute, node CPU and RSS, exec lag (height minus executed tip), payout intervals per miner from the coinbase records |
|
||||
| B | the same boxes, fresh genesis, 1 bps | the same | 30 min | the same (the control) |
|
||||
| A2 | three relays, 10 bps | the same | dropped: the network lane's read of run A (reds from node throughput, not topology) made a topology run uninformative without per-block CPU profiling on a pod; no provider gave inbound ports for a mesh | |
|
||||
|
||||
Payout interval per tier, from each run's measured block rate and the chain's hash: a 4070 at 28 MH/s, a 5090 at
|
||||
128 MH/s, an 8x 4090 rig at 459 MH/s, at tonight's network hash and extrapolated to 1, 10 and 100 TH/s networks
|
||||
(interval = blocks per second x miner share, the arithmetic in `tools/fleet/bps-collect.py`).
|
||||
|
||||
## Measured
|
||||
|
||||
### Run A: 10 blocks per second, star topology (19:17Z to 20:25Z)
|
||||
|
||||
Seed: the fork's igneumd (83702a35, `IGNEUMD_DEVNET_BPS=10`) on igneum-build-1 (188.40.146.49:26611, a public port), genesis
|
||||
f682b78a4e64f0d2, digest 436d62a5b962e1c2, mining from 19:17:26Z. Miners: dn2-1/2/3 from 19:25Z, the 38 wave pods from 19:26Z
|
||||
(each node beside the pod's live-devnet node on other ports, each dialling the seed only: no pod has an inbound port), 41 peers
|
||||
on the seed. Collector rows every minute in `~/Desktop/fleet/bps/A.jsonl` (the first 20 minutes carry the seed's log counts only;
|
||||
the watch line from 19:37Z after the seed's miner binary was staged).
|
||||
|
||||
| Read | Value | Source |
|
||||
|---|---|---|
|
||||
| Blocks at 10 min (19:36Z) | 5,733 (12.4 blocks/s over the last 159 s) | seed watch line |
|
||||
| Blue score at 10 min | 1,307 (77 percent of blocks red) | the same |
|
||||
| Blocks 19:37Z to 20:24Z (47 min) | 7,204 to 19,153: 4.25 blocks/s; blue +2,926: 1.09 blue/s; red share 77.6 percent | A.jsonl |
|
||||
| Rate by six-minute interval | 7.7, 1.9, 2.9, 2.8, 5.9, 4.2, 3.2, 5.1 blocks/s; blue 0.6 to 1.6/s | A.jsonl |
|
||||
| Tips | 246 at 10 min, 295 to 662 through the hour, rising | A.jsonl |
|
||||
| Difficulty | 6,719 at 19:33Z, 3,551 at 19:36Z, 1,307 at 19:42Z, falling the whole hour (the rule reads the blue rate under target and eases) | seed watch, by hand |
|
||||
| Max selected-chain reorg | 55 blocks (a late pod unwinding its genesis-only view at join) | seed log |
|
||||
| Exec follower | tip 84 at 19:30Z, 240 at 20:19Z: 0.05 blocks/s against 1.1 blue/s; lag 18,901 blocks at the end | igneum_getExecStatus on the seed |
|
||||
| Finality | 18 locks over the run (the voters are the 42 miners' keys) | seed log |
|
||||
|
||||
What it says: with 42 cards on a 10 blocks/s profile the DAG ran at 4 to 12 blocks a second but the selected chain at about
|
||||
1.1 blue blocks a second, three in four blocks red, tips in the hundreds, and the difficulty rule eased all hour because it
|
||||
measures the blue rate. The Horizon network lane's read of the same rows (`docs/analysis/horizon/network.md`): the reds came
|
||||
from node throughput, not the star: the seed spent 61 to 345 ms of CPU per accepted block (mergeset 8 to 200), a 100 ms-per-block
|
||||
knee at 10 blocks/s, RSS 5.4 GB at 12,000 blocks, and the propagation model gives 0.0 percent red for both star and mesh at
|
||||
the measured latencies. So a topology run (A2) proves nothing without per-block CPU profiling on a pod, and was dropped. A
|
||||
true mesh was also not possible tonight: no provider gave a pod with an inbound p2p port, the live devnet has the same star
|
||||
(every rented node dials the two hands and the hub), and the standing-fleet ports rule in `docs/plans/gpu-fleet.md` is the fix.
|
||||
|
||||
The exec follower is the second finding: at 1.1 blue blocks a second it executed 0.05 blocks a second, so a 10 blocks/s chain
|
||||
with payload would leave every wallet and prover reading state hours behind the tip within the first hour.
|
||||
|
||||
### Run B: 1 block per second, the control, same boxes (20:25Z to 20:58Z)
|
||||
|
||||
Seed restarted on a fresh genesis without the profile variable (1 block/s, the devnet's rule), the same 42 boxes, each wiped
|
||||
and rejoined (`FRESH=1`), rows in `~/Desktop/fleet/bps/B.jsonl` from 20:28Z.
|
||||
|
||||
| Read | Value | Source |
|
||||
|---|---|---|
|
||||
| Blocks 20:28Z to 20:58Z (29.5 min) | 2,679 to 5,171: 1.41 blocks/s; blue +2,099: 1.19 blue/s; red share 15.8 percent over the window | B.jsonl |
|
||||
| The first six minutes | 3.05 blocks/s against 1.93 blue/s: the join burst (42 nodes arriving on a chain minutes old, the late ones unwinding genesis-only views; max reorg 16) | B.jsonl |
|
||||
| From 20:34Z on, by six-minute interval | 1.01, 0.99, 0.94, 1.05 blocks/s and 1.01, 0.99, 0.95, 1.05 blue/s: red share under 2 percent | B.jsonl |
|
||||
| Tips | 21 at 20:28Z, 1 to 3 from 20:34Z | B.jsonl |
|
||||
| Difficulty | 19.9 M at 20:28Z, 477 M by 20:34Z, 453 to 482 M after: settled in six minutes and flat | B.jsonl |
|
||||
| Max selected-chain reorg | 16 (the join) | seed log |
|
||||
| Exec follower | tip 188 at 20:28Z, 1,003 at 20:58Z: 0.46 blocks/s against 1.0 blue/s, lag 4,168 at the end and growing | igneum_getExecStatus |
|
||||
| Finality | 0 locks in 30 minutes (the chain was 30 minutes old; the window had not filled) | seed log |
|
||||
|
||||
What it says: the same 42 cards that made a 77 percent red DAG at the 10 blocks/s profile made a chain with one to three tips
|
||||
and under 2 percent red at 1 block/s, with the difficulty settled in six minutes. The exec follower still ran under the chain
|
||||
(0.46 against 1.0), which is the follower's own ceiling on these pods and a finding for the proving lane, not for the rate.
|
||||
|
||||
## Per tier
|
||||
|
||||
From the collector's arithmetic (interval = 1 / (blocks per second x miner hash / network hash)); run A's "blocks" are
|
||||
DAG blocks of which three in four were red, so its payout column is read at a quarter.
|
||||
|
||||
| Network hash | Miner | Run A (4.87 DAG blocks/s, 1.09 blue/s) | Run B (1.19 blue blocks/s) |
|
||||
|---|---|---|---|
|
||||
| tonight, 2.0 GH/s | 4070, 28 MH/s | a block every 0.2 min, of which 1 in 4 pays | a block every 0.8 min, nearly all pay |
|
||||
| tonight, 2.0 GH/s | 5090, 128 MH/s | every 0.1 min | every 0.2 min |
|
||||
| tonight, 2.0 GH/s | 8x 4090 rig, 459 MH/s | continuous | every 0.1 min |
|
||||
| 1 TH/s | 4070 | every 2.0 h (DAG), about 8 h in blue blocks | every 7.0 h |
|
||||
| 1 TH/s | 5090 | every 27 min (DAG), about 1.8 h blue | every 1.5 h |
|
||||
| 1 TH/s | 8x 4090 rig | every 7.4 min (DAG), about 30 min blue | every 26 min |
|
||||
| 10 TH/s | 4070 | every 20 h (DAG), about 3.4 days blue | every 2.9 days |
|
||||
| 10 TH/s | 5090 | every 4.5 h (DAG), about 18 h blue | every 15 h |
|
||||
| 10 TH/s | 8x 4090 rig | every 1.2 h (DAG), about 5 h blue | every 4.3 h |
|
||||
| 100 TH/s | 4070 | every 8.5 days (DAG), about a month blue | every 29 days |
|
||||
| 100 TH/s | 5090 | every 1.9 days (DAG), about a week blue | every 6.4 days |
|
||||
| 100 TH/s | 8x 4090 rig | every 12 h (DAG), about 2 days blue | every 1.8 days |
|
||||
|
||||
Consequence per tier: a home 4070 at a 10 TH/s network waits about three days for a paying block either way (the 10 blocks/s
|
||||
profile's extra DAG blocks are red, so the solo miner's variance does not fall by 10x, only by the blue-rate ratio of 1.09 to
|
||||
1.19, which is nothing); the rig and the 5090 see the same. The way to a shorter wait for the small card is the pool, not
|
||||
the block rate. GHOSTDAG pays red blocks nothing under this rule set, so a higher rate is only worth having where the blue
|
||||
rate rises with it, which needs the node to process a block in well under 100 ms at mergeset 248.
|
||||
|
||||
## The recommendation for mainnet's rate
|
||||
|
||||
From the Horizon network lane (`docs/analysis/horizon/network.md`), carried here as the experiment's recommendation: 1 block
|
||||
per second for the public testnet and the launch; 10 blocks per second behind three gates, each measured before the rate moves:
|
||||
(1) per-block node CPU under 50 ms at mergeset 248 on a laptop core (tonight's seed: 61 to 345 ms, a knee at 100 ms per block
|
||||
at 10 blocks/s, RSS 5.4 GB at 12,000 blocks); (2) finality constants and the clock cap expressed in DAA seconds, so a rate
|
||||
change moves no human-time guarantee; (3) vote aggregation, so 100 voters at 10 blocks/s do not multiply the certificate
|
||||
traffic by ten. Two fleet rules from the runs: a box never mines from a genesis-only view, it syncs first (the 55-block reorg
|
||||
of run A and the 16-block one of run B were late pods unwinding); and the exec follower's rate (0.05 and 0.46 blocks/s on
|
||||
these pods) is the state layer's ceiling and must be measured beside any block-rate change.
|
||||
62
docs/analysis/ca3-v4-uniform.md
Normal file
62
docs/analysis/ca3-v4-uniform.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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." |
|
||||
|
|
|
|||
|
|
@ -98,3 +98,314 @@ order:
|
|||
The items of M16 section 5 stand: the inline kernel on NVIDIA with a 64 MiB cache inside L2 (a measured point
|
||||
under the "50 T op/s" row) is a PC job not yet run; the time-memory curve (O-1.6) is not drawn; the mixer has had
|
||||
no cryptanalysis, and a shortcut inside it cuts the 4,680 directly; no chip has been priced beyond its SRAM.
|
||||
The time-memory curve and the partial-store chip (f = 0.25, 0.5, 0.75, 1 on GDDR7 and HBM3) are now drawn in section 5
|
||||
(Counter ASIC 3.0 item 1, 6 October 2026): the f = 1 chip is over 2x per joule on both memory systems, and the mixer does
|
||||
not touch it.
|
||||
|
||||
## 5. The partial-store chip and the time-memory curve (Counter ASIC 3.0 item 1, 6 October 2026)
|
||||
|
||||
Counter ASIC 3.0 item 1 (`docs/plans/counter-asic-3.md`, row 1; the history's addition 1,
|
||||
`docs/analysis/asic-resistance-history.md` section 4.3). The chip priced here holds a fraction `f` of the dataset in
|
||||
off-die DRAM (GDDR7 or HBM3) and derives the other `1 - f` of its items from the 256 MiB on-die cache under class v3
|
||||
(x8), reading DRAM at the hash's 4-byte granularity through its own controller. Sections 2 and 3 priced only `f = 0`.
|
||||
Nothing below is a measurement of a chip. Every GPU figure says where it was measured; every chip figure is arithmetic
|
||||
on cited memory and logic figures, and "approximate" marks a figure from memory or an estimate. The history's rows 3
|
||||
and 4 (`asic-resistance-history.md` section 1.1) are the precedent: Ethash chips reached 2.1x (Linzhi Phoenix, 2020),
|
||||
2.9x (Antminer E9, 2022) and 4.8x per joule (Jasminer X4, 2021) with custom memory controllers and on-package memory
|
||||
and no on-die dataset, which is the `f = 1` end of this curve.
|
||||
|
||||
### 5.1 Inputs
|
||||
|
||||
| Input | Value | Source (URL read 6 October 2026 unless a file is named) |
|
||||
|---|---|---|
|
||||
| RTX 5090 class v3, the denominator | 136.1 MH/s at 326 W (power approximate: 328.6 W peak in the 5 October prover-cost run, 323 W after the M11 race); 2.40 microjoules per hash; 17.5 G dependent 4-byte reads per second (CUDA wall), 18.2 (OpenCL event), 415 ns at 256 lanes; a 32-byte sector per read | `docs/bench-log.md` "Counter ASIC 2.0, the numbers" and M11; `docs/benchmarks/repro.md` 2.2 (branch repro-bench) |
|
||||
| RTX 5090 memory system | 32 GB GDDR7 on a 512-bit bus at 28 Gbps, 1,792 GB/s; 16 devices of 2 GB (16 Gb); 575 W TGP; $1,999 at launch; 96 MB L2; 750 mm^2 | https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/ (512-bit, 32 GB GDDR7, 21,760 cores, 575 W); https://en.wikipedia.org/wiki/GeForce_RTX_50_series (28 Gbps, 1,792 GB/s, $1,999, 96 MB, 750 mm^2); the 16-device count is the brief's and the 32 GB / 2 GB arithmetic |
|
||||
| RTX 5090 integer rate | 45.2 T op/s (OpenCL event, integer chain) | `repro.md` 2.2 |
|
||||
| Program work per hash | 64 instructions x 8 iterations = 512, of which 128 loads | `docs/spec/01-lottery-hash.md` 1.4 (the table at lines 70 to 71) |
|
||||
| GDDR7 organisation | four 10-bit channels per device (8 data bits each); 16 banks per channel; PAM3; 1.1 to 1.2 V; 28 to 32 Gbps per pin now, 48 on the roadmap | https://www.rambus.com/blogs/all-you-need-to-know-about-gddr7/ (channels, PAM3, voltage, rates); https://www.smart-dv.com/memory/gddr7.html (4 channels, 16 banks per channel); so 64 channels and 1,024 banks on the 5090's 16 devices |
|
||||
| GDDR7 access granularity | 32 bytes per channel access (8 data bits x a burst of 32 beats; the 5090's measured 32-byte sector agrees) | arithmetic on the channel width; the sector is `repro.md` 2.2; the JEDEC burst length itself is behind the paywall, so the 32 B is marked approximate |
|
||||
| GDDR7 energy, streaming | 4.5 pJ per bit average device power (GDDR6X 6, GDDR6 6.5) | Micron, quoted in the search results for the GDDR7 product brief (the brief's own PDF refused the fetch); Rambus: "over 10 percent less power per bit than GDDR6X"; approximate |
|
||||
| GDDR7 price | about $20 per 2 GB device (16 Gb, 28 Gbps), September 2026; 3 GB devices $60 to $70 | https://www.trendforce.com/news/2026/09/24/news-micron-reportedly-ends-2gb-gddr7-narrowing-supply-options-for-nvidias-rtx-50-series/ (quoting Tom's Hardware and VideoCardz) |
|
||||
| HBM3 organisation | 1,024-bit interface, 16 64-bit channels, 32 32-bit pseudo-channels; burst of 8 beats, 32-byte packet; up to 64 banks per channel; 6.4 Gbps per pin, 819 GB/s per stack; core 1.1 V, I/O 0.4 V; 64 GB per stack maximum | https://www.synopsys.com/glossary/what-is-high-bandwitdth-memory-3.html and https://www.synopsys.com/articles/hbm3-ip-dwtb.html; https://www.tomshardware.com/news/hbm3-spec-reaches-819-gbps-of-bandwidth-and-64gb-of-capacity |
|
||||
| HBM3E | the same organisation, 9.2 to 9.8 Gbps per pin, 1.15 to 1.2 TB/s per stack; 24 GB (8-high) and 36 GB (12-high) | https://en.wikipedia.org/wiki/High_Bandwidth_Memory; https://blogs.sw.siemens.com/semiconductor-packaging/2026/04/24/hbm3e-hbm4-ic-design-guide/ |
|
||||
| HBM energy per bit, streaming | Samsung's roadmap: HBM2 6.25, HBM3 4.12, HBM3E 4.05 pJ per bit; O'Connor et al. (NVIDIA, MICRO 2017): HBM2 3.97 pJ per bit | https://eureka.patsnap.com/insight/the-hbm-wars-sk-hynixs-dominance-samsungs-roadmap-and-the-looming-threat-of-cyclicality (20 August 2025); https://www.cs.utexas.edu/~skeckler/pubs/MICRO_2017_Fine_Grained_DRAM.pdf |
|
||||
| HBM random-access energy, the breakdown | HBM2: row activation 909 pJ per 1 KB row; data movement 1.51 + 1.17 pJ per bit; I/O 0.80 pJ per bit at 50 percent activity; 32-byte atom; 16 banks per channel; tRC 45, tRCD 16, tRP 16, tRAS 29, tRRD 2, tFAW 12 ns, 8 activates per tFAW per channel | O'Connor et al., Tables 2 and 3 of the PDF above (text extracted with pdftotext) |
|
||||
| DRAM row cycle across types | DDR4 tRCD 14, tRAS 33, tRP 14; GDDR5 14, 28, 12; HBM and HBM2 14, 34, 14 ns; 16 banks per rank; GDDR5 page 2 KB, HBM2 page 2 KB | Li, Reddy, Jacob, MEMSYS 2018, Table 2: https://terpconnect.umd.edu/~blj/papers/memsys2018-dramsim.pdf (text extracted with pdftotext); the history's [L1] |
|
||||
| HBM random-access ceiling, the literature | "Folded Banks" (AMD, ISCA 2025): fine-grained random access on HBM is bound by activate parallelism (tRC, tRRD, tFAW), and a redesign with 8x the activate parallelism gives 6.7x the irregular bandwidth, which says the stock stack sits far under its streaming figure on random reads | https://dl.acm.org/doi/10.1145/3695053.3731111 (abstract; the PDF refused the fetch) |
|
||||
| HBM3 price | about $200 per 24 GB stack factory gate (HBM3E $300 per 36 GB), October 2026; contract pricing about twice that | https://siliconanalysts.com/data/hbm-pricing (no external source cited there; approximate) |
|
||||
| Interposer and packaging | CoWoS-S $600 to $900 per H100-class package (8 stacks, about 800 mm^2 of silicon interposer); CoWoS-L 20 to 47 percent more; September 2026 | https://siliconanalysts.com/tools/packaging (public sources only, approximate); a one-stack package is taken at $200, approximate |
|
||||
| 32-bit integer op energy, N5-class | int32 add 0.06 pJ, int32 multiply 0.52 pJ at 5 nm (7 nm: 0.10 and 0.80; 45 nm, Horowitz 2014: 0.1 and 3.1) | https://mlsysbook.ai/vol1/backmatter/appendix_assumptions.html Table 13, citing Horowitz 2014 and Dally 2021; datapath only, so a 2x pipeline and clock overhead is applied below, approximate |
|
||||
| On-die SRAM read, 64 bytes from a 256 MiB array | 0.5 nJ, range 0.2 to 1.0 (approximate: Horowitz's 45 nm 1 MB cache at 100 pJ per 64-bit read scaled to N5, plus about 0.6 pJ per bit of global wire across a 128 mm^2 array) | from memory; the sensitivity is shown in every row |
|
||||
| Cache mirror and recompute die | 256 MiB = 128 mm^2, $46 at N5 headline density; the whole recompute die (50 T op/s of integer logic beside the mirror) taken as a 750 mm^2-class N5 die at about $600 of silicon (70 dies per $20,000 wafer at the `sram-mirror.md` yield model) | `docs/analysis/sram-mirror.md` sections 4 and 5; the $600 is arithmetic on its wafer price and D0, approximate |
|
||||
| Chip project cost | a 7 nm-class project $50M to $75M all-in; a 28 nm project $5M to $30M; masks $1M to $3M at 28 nm, $10M to $20M at 5 nm | `asic-resistance-history.md` section 2.5 and its [E2] [E3] [E4] |
|
||||
| Ethash chips, the precedent | Phoenix 2,733 MH/s at about 3,000 W, 2.1x; X4 2,500 MH/s at 1,200 W, 4.8x; E9 2,400 MH/s at 1,920 W, 2.9x per joule | `asic-resistance-history.md` rows 3 and 4 and their [S11] [S12] |
|
||||
|
||||
### 5.2 The op count, counted from the code
|
||||
|
||||
`igneum-pow/src/memhard.rs`, `mixer` (lines 290 to 303) and `derive_items_mask` (lines 503 to 536), under class v3's
|
||||
`mixer_mult = 8`:
|
||||
|
||||
| Where | Operations | Count |
|
||||
|---|---|---|
|
||||
| Per mixer application, the 16-word prologue `(s[i] ^ (rc[i] + rk)) * mul[i]` | 16 xor, 16 add, 16 mul | 48 (32 when `rc[i] + rk`, a per-round constant, is hoisted; a chip hoists it) |
|
||||
| Per mixer application, 8 quarter rounds of 4 add, 4 xor, 4 rotate | 32 add, 32 xor, 32 rotate | 96 |
|
||||
| Per mixer application, total | | 144 unhoisted, 128 hoisted |
|
||||
| Mixer applications per item, `(ITEM_ROUNDS + 1) x m` | 9 x 8 | 72 |
|
||||
| Per item, the cache-line fold `s[i] ^= line[i]` over 8 rounds, and the 16-word init | 128 xor, 8 mul, 8 add | 144 |
|
||||
| Per item, total | 72 x 128 + 144 (hoisted) to 72 x 144 + 144 | 9,360 to 10,512 |
|
||||
| Per hash, 128 items | | 1,198,080 to 1,345,536, plus the program's 512 |
|
||||
|
||||
The spec's "about 130" per application (1.8.4) is the hoisted count plus the fold spread over the applications: 9,360
|
||||
per item exactly, so the figures of sections 1 and 2 stand. The energy per application at N5 datapath figures is
|
||||
16 x 0.52 + 128 x 0.06 = 8.3 + 7.7 = 16.0 pJ (a rotate by a per-day constant is a wire mux on a chip, counted at the
|
||||
add's 0.06); with the 2x pipeline overhead, 32 pJ. Per item: 72 x 32 pJ = 2.30 nJ of logic plus 8 cache reads at
|
||||
0.5 nJ = 4.0 nJ plus the fold, 6.3 nJ (3.9 at 0.2 nJ per read, 10.3 at 1.0). Per hash at `f = 0`: 128 x 6.3 = 0.81
|
||||
microjoules, before static power. The on-die cache reads cost this chip more energy than the mixer does, which is the
|
||||
first thing the curve says: the mixer's 9,360 ops are 2.3 nJ of a 6.3 nJ item.
|
||||
|
||||
The rows below use the hoisted count, 9,360 per item (72 x 128 plus the fold), because a chip pays the cheapest
|
||||
form and the section 1 and 2 rows already price that figure. The two counts differ by 12 percent, so the `f = 0` row
|
||||
at the unhoisted 10,512 per item (1,346,048 ops per hash) is: 50 T / 1,346,048 = 37.1 MH/s, 0.27x bare, 0.82x with
|
||||
the 3x factor (against 0.31x and 0.92x); its energy per item 2.44 nJ of logic instead of 2.30, 6.5 nJ in all, 50.7 W at 37.1 MH/s, 1.37 microjoules,
|
||||
1.75x per joule instead of 1.86x (the 20 W of static power spread over fewer hashes). The `f = 1` rows do not move: they contain no mixer. The multiplier is class v3's shipped
|
||||
`mixer_mult = 8` (72 applications per item), not the 4 of `mixer-x4.md` section 2.
|
||||
|
||||
### 5.3 The two memory systems as random-read engines
|
||||
|
||||
A dependent 4-byte read opens a row (tRCD), reads one 32-byte atom (tCL and the burst) and must close it before the
|
||||
same bank opens another (tRC). The rate of random reads a memory system can sustain is the smaller of two ceilings:
|
||||
banks divided by tRC, and activates per tFAW window per channel times the channels. Neither ceiling moves with the pin
|
||||
speed, so HBM3E is HBM3 here, and 48 Gbps GDDR7 is 28 Gbps GDDR7.
|
||||
|
||||
| | GDDR7, 16 devices, 512-bit (the 5090's board) | HBM3, one stack | HBM3, eight stacks (an H100-class package) |
|
||||
|---|---|---|---|
|
||||
| Channels, banks | 64 channels, 1,024 banks | 16 channels (32 pseudo-channels), up to 1,024 banks | 128 channels, 8,192 banks |
|
||||
| Bank-bound ceiling, banks / 45 ns (tRC, the HBM2 and GDDR5-class figure, approximate for both) | 22.8 G reads/s | 22.8 | 182 |
|
||||
| Activate-bound ceiling (GDDR7: 4 per 12 ns per channel, approximate; HBM: 8 per 12 ns per channel, O'Connor Table 2). UNMEASURED (6 October 2026, the Horizon lane analysis `docs/analysis/horizon/algorithm.md` section 5.1): the JEDEC HBM2 table gives tFAW 28 ns, 4 activates per channel per window (ICCAD 2021 Table I), which is 2.3 G reads/s for a 16-channel stack, and the one measured random-read rate of an HBM2 part (Shuhai, Alveo U280, FCCM 2020 Fig 7) is 2.4 G, equal to that tFAW ceiling; the 12 ns figure holds only if a bank-interleaved mapping lifts tFAW, which the die enforces per channel. The HBM columns below carry the 10.7 G row as the model's ceiling, unmeasured; an AWS F2 hour (Virtex UltraScale+ VU47P, the same HBM2 subsystem) is the measurement | 21.3 G reads/s | 10.7 (unmeasured; 2.3 at JEDEC tFAW) | 85.3 (unmeasured) |
|
||||
| The ceiling carried below | 21.3 G reads/s (the 5090 measures 17.5, 82 percent of it: the card is already near its memory's activate limit) | 10.7 | 85.3 |
|
||||
| Energy per random 32-byte read (approximate) | 2.0 nJ: 909 pJ activation (one atom per row opened, the HBM2 1 KB row taken for GDDR7's row) plus 4.5 pJ per bit x 256 bits of movement and I/O = 1,150 pJ | 1.2 nJ: the HBM2 sum (909 + (1.51 + 1.17 + 0.80) x 256 = 1,800 pJ) scaled by Samsung's 4.12 / 6.25 | 1.2 nJ |
|
||||
| Static power (refresh, standby, PLLs; approximate, from memory) | 20 W (about 1.25 W per device) | 4 W | 32 W |
|
||||
| Controller and PHY die beside it (approximate) | 15 W, $50 | 10 W, $50 | 40 W, $200 |
|
||||
| Memory dollars | $320 (16 x $20) | $200 plus a $200 one-stack interposer | $1,600 plus $750 CoWoS-S |
|
||||
| Lanes in flight needed at the ceiling, at a 55 ns controller latency (tRCD 16 + tCL 16 + burst 1.25 + about 20 of controller, approximate) | 1,172 lanes, 73 KB of lane state at 64 B | 588 lanes, 37 KB | 4,692 lanes, 293 KB |
|
||||
| Reads per second per watt at the ceiling (memory, static and controller) | 0.27 G | 0.40 G | 0.49 G |
|
||||
| The 5090 for comparison | 17.5 G reads/s at 326 W = 0.054 G per W; 7,262 reads in flight (17.5 G x 415 ns), 22 per watt | | |
|
||||
|
||||
The FPGA line, public (6 October 2026, the Horizon lane analysis section 5.1): an HBM2 FPGA soft overlay (Alveo U280 or U55C class) carries only the measured row, 2.4 G reads/s per card (Shuhai, FCCM 2020 Fig 7, equal to the JEDEC tFAW ceiling of 2.3 G at 28 ns), which is 0.30x to 0.39x of the RTX 5090 per watt (U55C at 115 to 150 W; 0.20x on the U280). The 11.4 G bank-bound row and the 12.2 G ceiling quoted elsewhere rest on a 12 ns tFAW the JEDEC HBM2 table does not give and are unmeasured until an AWS F2 hour (f2.6xlarge, VU47P, 16 GB HBM2, USD 1.98 an hour on demand) runs the chase kernel at 1 GiB across all 32 pseudo-channels; the lane's pass line is 15 to 25 M reads/s/W, its alarm line 27 (0.5x of the 5090), and over 54 (1.0x) the FPGA lane becomes a Counter ASIC 4.0 item. Consequence per tier: none today (no FPGA mines); if the measured row holds, a soft-overlay FPGA at USD 4,000 to 5,000 a card (approximate) mines at an RX 9070 XT's rate per watt for 7x the price, so no home or rig tier is displaced by it. Ledger M33.
|
||||
|
||||
The GDDR7 system's own power at the 5090's 17.5 G reads/s is 17.5 x 2.0 nJ = 35 W plus 20 W static, 55 W: about 17
|
||||
percent of the card's 326 W (approximate). The other 83 percent is the GPU: 21,760 ALUs spinning at 92.9 percent
|
||||
utilisation on 512 program ops per hash, their register files, schedulers, L1 and L2, and the clock trees, against a
|
||||
45 T op/s integer budget of which the hash uses 512 x 136.1 M = 0.07 T op/s, 0.15 percent. That is the whole case for
|
||||
the `f = 1` chip: it is the 55 W without the 271.
|
||||
|
||||
### 5.4 The curve
|
||||
|
||||
Per row: reads per hash = 128 f; items recomputed = 128 (1 - f); ops per hash = 128 (1 - f) x 9,360 + 512.
|
||||
|
||||
Correction, 7 October 2026 (the in-house adversarial pass, lane adv-cache-3, report 091edc34): the partial-store rows above and adv-cache's Q1b table price a chip that holds every k-th line of the 64-line chain and recomputes a read at offset o in o evaluations ((k - 1) / 2 on average). The exact pebbling optimum for the chain (dynamic programming, checked against exhaustive search at 10 to 16 lines) sits under that curve: blocks per read 16.0 against 31.5 at f = 1/64 (the one held line belongs at line 32, not line 0), 10.5 against 15.5 at 2/64, 6.09 against 7.5 at 4/64, 3.17 against 3.5 at 8/64, 1.45 against 1.5 at 16/64, equal from f = 1/2. So a chip holding 1/64 of the cache pays 9.3x the item's ops, not 17.4x; at f = 1/2 and above nothing moves, and the SRAM column and the full-store verdict stand (no point on the curve beats the full store under the op budget or under energy).
|
||||
Memory-bound rate = the ceiling / (128 f). Compute-bound rate = 50 T op/s / ops per hash (the section 1 budget).
|
||||
|
||||
Second correction, 8 October 2026 (the in-house adversarial pass, lane adv-cache-2, report section 2.3 and its Q3(3) window-layer reading, tip 3f50d6c4): the partial-store rows model a chip that holds a fraction f of the ITEMS chosen uniformly, so it serves f of the reads and recomputes 1 - f. The item-read distribution is not uniform: the exact window-layer distribution (matching the 4,096-program census to four digits; top quarter mean 0.3382, top half 0.5811 of reads) lets a chip holding the hottest f of items serve 0.4219, 0.7188 and 0.8907 of reads at f = 0.25, 0.5 and 0.75 on the measured programs, so the f = 0.25 and f = 0.5 rows overstate the recompute share by up to 2.3x (1.8x on the first shard's read) and the f = 0.75 row by about 2.3x on the miss side. The f = 1 row, the SRAM column and the full-store verdict do not move (a chip that holds everything recomputes nothing either way), and no served number rests on f under 1; the partial-store rows stay as the uniform-store bound with this note until the hottest-f rows are drawn from the window distribution, which is the next pass of this section.
|
||||
The rate is the smaller; "binding" names it. Power = rate x (128 f x E_read + 128 (1 - f) x 6.3 nJ) + static (memory,
|
||||
controller, and 20 W for the recompute die's clocks and leakage when `f < 1`). Energy per hash = power / rate. "Gain,
|
||||
rate" = rate / 136.1 MH/s (per chip, the section 2 metric); "with the 3x factor" multiplies the compute-bound rate by
|
||||
3 on the recompute share only, the memory ceiling unchanged. "Gain, joule" = 2.40 microjoules / energy per hash, the
|
||||
Ethash chips' metric, with the on-die read energy at 0.5 nJ and, in brackets, at 0.2 and 1.0. Dollars = memory +
|
||||
interposer + controller + $100 of board, plus the $600 recompute die when `f < 1`; no project cost (section 5.6).
|
||||
Arithmetic: `scratchpad curve.py`, reproduced by hand for the first and last rows below the table.
|
||||
|
||||
| Memory | f | Reads per hash | Items recomputed | Ops per hash | Memory-bound MH/s | Compute-bound MH/s | Binding | Power W | Energy per hash, microjoules | Gain, rate, bare | With the 3x factor | Gain, joule (0.2 / 1.0 nJ reads) | Silicon and memory dollars | $ per MH/s |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| none (sections 1 to 3) | 0 | 0 | 128 | 1,198,592 | n/a | 41.7 | compute | 53.7 | 1.29 | 0.31x | 0.92x (125.1) | 1.86x (2.44 / 1.33) | $700 | $16.8 |
|
||||
| GDDR7 | 0.25 | 32 | 96 | 899,072 | 665.6 | 55.6 | compute | 92.3 | 1.66 | 0.41x | 1.23x (166.8) | 1.44x (1.68 / 1.17) | $1,070 | $19.2 |
|
||||
| GDDR7 | 0.5 | 64 | 64 | 599,552 | 332.8 | 83.4 | compute | 99.4 | 1.19 | 0.61x | 1.84x (250.2) | 2.01x (2.31 / 1.65) | $1,070 | $12.8 |
|
||||
| GDDR7 | 0.75 | 96 | 32 | 300,032 | 221.9 | 166.6 | compute | 120.7 | 0.72 | 1.22x | 1.63x (221.9, memory) | 3.31x (3.70 / 2.81) | $1,070 | $6.4 |
|
||||
| GDDR7 | 1 | 128 | 0 | 512 | 166.4 | 97,656 | memory | 77.6 | 0.47 | 1.22x | 1.22x | 5.14x | $470 | $2.8 |
|
||||
| HBM3, one stack | 0.25 | 32 | 96 | 899,072 | 334.4 | 55.6 | compute | 69.9 | 1.26 | 0.41x | 1.23x (166.8) | 1.91x (2.33 / 1.46) | $1,150 | $20.7 |
|
||||
| HBM3, one stack | 0.5 | 64 | 64 | 599,552 | 167.2 | 83.4 | compute | 74.1 | 0.89 | 0.61x | 1.23x (167.2, memory) | 2.69x (3.26 / 2.09) | $1,150 | $13.8 |
|
||||
| HBM3, one stack | 0.75 | 96 | 32 | 300,032 | 111.5 | 166.6 | memory | 69.4 | 0.62 | 0.82x | 0.82x | 3.85x (4.39 / 3.19) | $1,150 | $10.3 |
|
||||
| HBM3, one stack | 1 | 128 | 0 | 512 | 83.6 | 97,656 | memory | 26.8 | 0.32 | 0.61x | 0.61x | 7.46x | $550 | $6.6 |
|
||||
| HBM3, eight stacks | 0.25 | 32 | 96 | 899,072 | 2,665.6 | 55.6 | compute | 127.9 | 2.30 | 0.41x | 1.23x (166.8) | 1.04x (1.16 / 0.89) | $3,250 | $58.4 |
|
||||
| HBM3, eight stacks | 0.5 | 64 | 64 | 599,552 | 1,332.8 | 83.4 | compute | 132.1 | 1.58 | 0.61x | 1.84x (250.2) | 1.51x (1.67 / 1.30) | $3,250 | $39.0 |
|
||||
| HBM3, eight stacks | 0.75 | 96 | 32 | 300,032 | 888.5 | 166.6 | compute | 144.9 | 0.87 | 1.22x | 3.67x (499.9) | 2.75x (3.02 / 2.40) | $3,250 | $19.5 |
|
||||
| HBM3, eight stacks | 1 | 128 | 0 | 512 | 666.4 | 97,656 | memory | 174.4 | 0.26 | 4.90x | 4.90x | 9.15x | $2,650 | $4.0 |
|
||||
| HBM3E, any row | | | | | the same: the activate ceiling does not move with the pin rate | | | | 1 percent lower (4.05 against 4.12 pJ per bit) | | | | $300 per 36 GB stack | |
|
||||
|
||||
Arithmetic, `f = 0`: 50 x 10^12 / 1,198,592 = 41.7 x 10^6; power 41.7 M x 128 x 6.3 nJ = 33.7 W, plus 20 W static =
|
||||
53.7 W; 53.7 / 41.7 M = 1.29 microjoules; 2.40 / 1.29 = 1.86. Arithmetic, GDDR7 `f = 1`: 21.3 G / 128 = 166.4 MH/s;
|
||||
power 166.4 M x 128 x 2.0 nJ = 42.6 W, plus 20 + 15 static = 77.6 W; 77.6 / 166.4 M = 0.466 microjoules; 2.40 / 0.466 =
|
||||
5.14. HBM3 one stack `f = 1`: 10.7 G / 128 = 83.6 MH/s; 83.6 M x 128 x 1.2 nJ = 12.8 W, plus 14 = 26.8 W; 0.321
|
||||
microjoules; 7.46x. Lanes: 21.3 G x 55 ns = 1,172.
|
||||
|
||||
What the curve says:
|
||||
|
||||
1. It is monotone. Every memory system's cheapest point is `f = 1`, and the partial rows are worse than both ends on
|
||||
dollars per MH/s. A recomputed item costs 6.3 nJ and 9,360 ops; a stored one costs 1.2 to 2.0 nJ and no ops. At
|
||||
$8 to $10 per GB nothing makes a chip maker recompute a 1 to 2 GiB dataset; the partial-store chip is not the
|
||||
threat and will not be built. The curve matters again only if the dataset outgrows cheap memory, and the
|
||||
schedule of 1.13.3 (2 GiB plus 0.5 GiB a year; 4 GiB at year 4) stays under one HBM3 stack's 24 GB for the
|
||||
chain's life.
|
||||
2. The `f = 0` chip of sections 1 to 3 reads 0.31x per chip at the op budget and 0.92x with the factor, and those
|
||||
rows stand. Per joule the same chip reads 1.3x to 2.4x (1.86x at 0.5 nJ per on-die read), because its 50 T op/s
|
||||
of fixed-function logic draws about 54 W where the 5090 draws 326 to do the same work. The two metrics disagree
|
||||
because they measure different things: the rate row says how many chips match one card, the joule row says what
|
||||
each hash costs to run. The public claim "under 2x" has so far been the rate row.
|
||||
3. The `f = 1` rows are over 2x per joule on both memory systems, at every on-die read energy (the on-die cache is
|
||||
not in them), and over 2x per dollar on GDDR7: 5.1x and $2.8 per MH/s against the 5090's $14.7 at launch price.
|
||||
The history's band for exactly this chip class is 2.1x to 4.8x per joule (Ethash, rows 3 and 4); the model reads
|
||||
5.1x (GDDR7) to 9.2x (eight HBM3 stacks) with an ideal controller and the static allowances above. Read the
|
||||
history's band as the floor a first chip reaches and the model as the ceiling.
|
||||
|
||||
### 5.5 The f = 1 chip: a GPU's memory system without the GPU
|
||||
|
||||
A dependent read chain cannot be pipelined within a hash: read `r + 1`'s address is read `r`'s data through the
|
||||
program's registers, so one hash advances one read per memory latency. The rate per chip is therefore lanes in
|
||||
flight divided by latency, and the only question is how many lanes each memory system lets a controller keep in
|
||||
flight per watt. The 5090 keeps 7,262 (17.5 G reads/s x 415 ns) at 326 W, 22 per watt. The `f = 1` chip's lanes are
|
||||
64 bytes of registers each (the hash's eight 32-bit registers and the program counter and nonce), so 1,172 lanes
|
||||
are 73 KB of SRAM, and its latency is the controller's, about 55 ns, not the GPU's 415 ns of queueing: the chip
|
||||
holds 6x fewer reads in flight and still reaches the memory's activate ceiling. What it needs to beat the 5090 is
|
||||
more than 17.5 G reads per second per 326 W, 0.054 G per watt; the GDDR7 system alone gives 0.27 and one HBM3 stack
|
||||
0.40 (section 5.3). An HBM3 part at the 5090's 326 W: twelve stacks, about 128 G reads/s, 1,000 MH/s, 7.3x per chip
|
||||
(the eight-stack row at 174 W is 4.9x). HBM3's bank count per stack (1,024, in 32 pseudo-channels) equals the whole
|
||||
GDDR7 system's 1,024 banks over 64 channels, so its random-read rate per stack is half the 5090 board's (10.7
|
||||
against 21.3 G) on the activate count; what HBM wins is energy per read (1.2 against 2.0 nJ: 0.8 pJ per bit of
|
||||
interposer I/O against a PCB) and the dollars per read per second favour GDDR7 ($320 for 21.3 G against $400 for
|
||||
10.7 G). Both beat the card per joule by more than 2x because the card's memory system is 17 percent of its power.
|
||||
|
||||
### 5.6 Verdict
|
||||
|
||||
**Over 2x.** The worst case for us is `f = 1` on HBM3 (7.5x per joule for one stack, 9.2x for eight; GDDR7 5.1x), and
|
||||
the cheapest chip for an attacker is `f = 1` on GDDR7 ($2.8 per MH/s of silicon and memory, a controller die that
|
||||
needs no advanced node because the mixer is not on it: a 28 nm-class project at $5M to $30M, not the $50M 7 nm
|
||||
project the on-die cache forces on the `f = 0` chip). The on-die-cache recompute chip stays at 0.92x with the factor
|
||||
and 1.3x to 2.4x per joule, between "under 1x" and "1 to 2x", and it is not the chip anyone builds. The verdict
|
||||
changes the public claim: "under 2x" held for the chip that recomputes the dataset; it does not hold for the chip
|
||||
that stores it.
|
||||
|
||||
### 5.7 What it means for item 2, and what does protect
|
||||
|
||||
Item 2 (a random item-derivation program per day in place of the fixed mixer) removes the 3x fixed-function factor
|
||||
from the recompute share. The `f = 1` chip has no recompute share: it reads every item from DRAM and never derives
|
||||
one, so item 2 moves none of the rows that decide the verdict, and neither does a mixer at x16 or x64. The mixer
|
||||
earns its place against the `f = 0` chip, which at $8 per GB of DRAM nobody builds. Item 2's urgency is therefore
|
||||
low on this result; it stays a reserve family. What does move the `f = 1` rows, with the arithmetic:
|
||||
|
||||
| Lever | What it does to the f = 1 chip | What it costs the honest cards | Reading |
|
||||
|---|---|---|---|
|
||||
| Dataset size | Nothing until the dataset exceeds what one stack or one board holds: 24 GB (HBM3, one stack) or 32 GB (the 5090's own board). The schedule reaches 4 GiB at year 4 | Everything: a 4 GiB step already retires 4 GB cards (`card-lifetime-2026-10-05.md`) | Not a lever against this chip |
|
||||
| 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 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
|
||||
item this analysis adds, with its own six gates (the hash rate per card at N = 50,000, 100,000, 200,000; the watts;
|
||||
the verifier on a 2019-class core; bit-exactness on three vendors); and the public claim is re-worded until the
|
||||
measurements land.
|
||||
|
||||
### 5.8 Consequences per user tier
|
||||
|
||||
| Tier | What the verdict means | What is being done |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB card (any vendor, any OS) | Today, nothing changes: no chip exists, the devnet pays nothing, and the f = 1 chip is a project of $5M to $30M and about 32 months by the precedent. When one lands it runs at 0.3 to 0.5 microjoules per hash; an 8 GB card runs 10 to 20 (approximate: the 9070 XT's 18.6 MH/s at its 304 W board power, from memory, is 16 microjoules) and is the first tier out | The power-cap and program-length measurements (above) decide how far the honest card's joules can fall; the share-pattern detector (item 4) is what tells this miner a chip has arrived |
|
||||
| One 12 GB card | As the 8 GB tier; the dataset size gives it no protection, since the chip's memory is 24 to 32 GB whatever the card holds | The same |
|
||||
| One 16 GB card (9070 XT class) | AMD RDNA 4 at 2.4 G reads/s and 304 W (approximate) is 7x worse per joule than the 5090 and 30x worse than the f = 1 chip; a chip ends AMD home mining first | The vendor-share metric (item 7) will show it; nothing in the hash fixes AMD's dependent-read rate |
|
||||
| One 24 or 32 GB card (5090 class, Apple M5 Max) | The 5090 is the honest best at 2.40 microjoules; the M5 Max at 27.9 MH/s and a GPU power of about 60 to 80 W (approximate, unmeasured) is 2 to 3 microjoules, the same class per joule. Against the f = 1 chip both are 5x to 9x behind per joule in the model, 2x to 5x by the precedent | The two measurements; the 5090 at a 200 W cap (if its rate holds) is 1.47 microjoules and halves the gap |
|
||||
| A rig | A rig's cost is electricity; per joule it is its cards. Once chips hold the hashrate, a rig at the same tariff earns 1/2 to 1/9 of a chip per watt and leaves. The precedent: ASIC share of Ethash stayed small (about 3 percent) for years because the chips were not cheap enough per dollar at scale, and the dollars-per-MH/s row says this chip is ($2.8 against $14.7) | The issuance trigger (item 4): the bounty and the benchmark live before daily issuance crosses about $50K |
|
||||
| A pool user | A chip fleet is a few operators at 0.3 microjoules; MoneroCrusher found Monero's at 85 percent of the hashrate by the share pattern | The detector on the observer, item 4, before the public testnet |
|
||||
| The public claim "under 2x" | It held for the recompute chip at the op budget. Per joule and against the stored-dataset chip the model reads over 2x on both memory systems and the precedent reads 2.1x to 4.8x. The claim as worded is not safe to publish | Re-word to the measured fact (the 5090 runs at 1/128 of its dependent-read ceiling and a chip must out-read it per watt) until the power-cap and program-length rows land; nothing goes to the devnet or the site from this analysis |
|
||||
|
||||
### 5.9 Unverified and owed
|
||||
|
||||
- Every energy figure is a published streaming or breakdown figure applied to random 32-byte reads: HBM2's 909 pJ
|
||||
activation and 1 KB row stand in for HBM3 and for GDDR7 (GDDR6 rows are 2 KB per 16-bit channel, Li-Reddy-Jacob
|
||||
Table 2; GDDR7's 8-bit channel row is unknown to me); the static allowances (20 W, 4 W, 32 W), the controller
|
||||
powers, the 55 ns controller latency and the 2x pipeline overhead are from memory.
|
||||
- The 5090's 326 W is a peak from a run with the prover on (328.6) and a post-run reading (323); the power at the hash
|
||||
alone, and under a cap, is unmeasured. PC 1 is not released (the 9070 XT rows are OWED) and no PC 2 job was
|
||||
published for this item (analysis only; the measurement is named, not run).
|
||||
- The 50 T op/s budget, the 3x factor and the $600 die are section 1's approximations; the on-die read energy (0.2 to
|
||||
1.0 nJ) moves the `f < 1` rows by up to 1.8x and the `f = 1` rows not at all.
|
||||
- Prices are factory-gate figures from a secondary source (contract pricing about 2x), September to October 2026;
|
||||
the 5090's $1,999 is the launch price and its 2026 street price is higher (reports of $4,000 and more, approximate),
|
||||
which makes the chip's dollar advantage larger, not smaller.
|
||||
- The Ethash chips' internals are not read (no teardown); their gains are the history's rows.
|
||||
- The GDDR7 burst length and bank-group timing, and HBM3's tFAW per pseudo-channel, are behind the JEDEC paywall;
|
||||
the activate ceilings use the HBM2 figures and the 5090's measured 17.5 G (82 percent of the GDDR7 ceiling) as the
|
||||
check that they are the right order.
|
||||
- The ALU budgets of the M5 Max and the 9070 XT, their power at the hash, and the verifier's cost at N = 100,000
|
||||
program ops are estimates; the program-length lever is a design item with its own measurements, not a result.
|
||||
|
||||
### 5.10 Class v5 on, the shadow at zero: does the state-derived dataset make the shadow unnecessary? (7 October 2026, 21:3x UK, the founder's question "we need a solution, deep research, other methods")
|
||||
|
||||
The question: with class v5 (the dataset built from the chain's execution state, refreshed per window) and the latency-shadow work of class v4 set to zero (class v3 energy on every GPU), what edge does the strongest chip keep over an RTX 5090 per joule? If it were at or under about 2x the shadow could come off after v5 and the class v4 premium (145 W on a 5090 at the unlocked core, 88 W at the 1,400 MHz lock, measured 7 October 2026) would vanish.
|
||||
|
||||
What class v5 changes for the chip, from `docs/design/class-v5-stored-state.md` sections 2 and 2a: the recompute chip (`f = 0`, the dataset derived on the fly from the cache) and the stateless or stale chip are removed as categories, because every item takes a leaf of the state and the leaves refresh every window. What it does NOT change: the strongest chip was never one of those. It is the `f = 1` stored-dataset chip of 5.5, a GPU's memory system without the GPU, and under class v5 it needs one thing more, the window's leaves, which section 2a.2 prices honestly: one node serves a whole farm, the leaves ship at 16.5 KB/s to 10,000 members today (45 MB per member per window over a 1 Gbit/s WAN before the state is 7,500x today's), and the rebuild on the chip is the same 32 ms per window every GPU pays. The dataset is still derived from a seed and the state, so a central node compresses everything but the state's bytes, and the chip stores the result as before.
|
||||
|
||||
The arithmetic, on the model's own figures (5.3: the activate-bound ceilings, 2.0 nJ per random read on GDDR7 and 1.2 nJ on HBM3, the static and controller watts; the 5090 at 136.1 MH/s on 326 W, 0.417 MH/W; 128 loads per hash, the shadow at zero so no ALU beside the memory). The node is a desktop-class CPU with an NVMe and 32 GB at about 85 W (approximate, from memory of such machines; a full node with the EVM executor at 1 block/s), shared by a farm (100 chips: 0.85 W each) or carried by every chip (the attacker's worst case, 85 W each).
|
||||
|
||||
| Chip, class v5 on, shadow at zero | MH/s (model) | W with a farm-shared node (0.85 W) | MH/W | Edge over the 5090 per joule | W with a node per chip (85 W) | Edge |
|
||||
|---|---|---|---|---|---|---|
|
||||
| GDDR7 `f = 1`, 16 devices (the 5090's own memory without the GPU) | 166 | 78 | 2.12 | 5.1x | 163 | 2.5x |
|
||||
| HBM3 `f = 1`, one stack | 84 | 28 | 3.02 | 7.2x | 112 | 1.8x |
|
||||
| HBM3 `f = 1`, eight stacks (an H100-class package) | 666 | 175 | 3.80 | 9.1x | 259 | 6.2x |
|
||||
|
||||
Every figure is modelled (arithmetic on cited memory figures, approximate where 5.3 marks it); none is measured; the node's watts are an approximate from memory.
|
||||
|
||||
Reading: NO. Class v5 with the shadow at zero leaves the strongest chip at 5.1x (GDDR7) to 9.1x (HBM3, eight stacks) per joule, the class v3 figures of 5.6 less a rounding, because the node is a farm cost and not a chip cost; only a chip forced to carry its own node falls near 2x, and only the small ones (one HBM3 stack at 1.8x, the GDDR7 board at 2.5x), while the eight-stack package stays at 6.2x even with a node per chip. So the shadow (class v4's 100,000 ops per hash in the memory wait, which brings the chip to 2.1x at k = 1 and 3.9x on the claimed X9 core) stays the only lever in this model that reaches the memory-system chip, and the class v4 premium is the price of that lever on today's GPUs. What class v5 buys is different and real: the recompute chip and the stale chip are gone as categories, every miner must hold and follow the chain, and a chip's dataset is wrong the moment its node is. The premium itself has two measured levers tonight: the core-clock lock (57 of 145 W back on the 5090 at 1,400 MHz for 1.4 percent of rate; the knee below 1,400 is the second pass's) and the per-card tune the app lands by itself; what would remove it is a shadow whose work is cheaper per op on a GPU than on a chip core (the research lane's question: a shadow shaped for the GPU's idle datapath at low clock, or a memory-side cost the chip cannot amortise), not the state-derived dataset.
|
||||
|
||||
Per tier: a miner on class v4 pays the premium and gets the 2.1x to 3.9x chip ceiling in exchange; on class v5 with the shadow kept the ceiling stays and the dataset is the chain's; on class v5 with the shadow dropped the premium goes and the ceiling returns to 5x to 9x. The decision is the founder's; this section gives the number.
|
||||
|
||||
### 5.11 Two columns from the counter-asic-4 research file (7 October 2026, 22:3x UK; `docs/analysis/counter-asic-4-research.md` sections 15 to 19 at bca23f96, the research lane's reading, carried here as the chip model's own columns)
|
||||
|
||||
The k column. k is the chip core's energy per op over the GPU's at the same operating point, and the model's 3.4x row takes k about 0.33 for an ALU-shaped core. On the public figures the int8 tensor tile is the one GPU block whose energy per op a chip at the same node cannot undercut with certainty: the 4090 measures 0.056 pJ per MAC; NVIDIA's 5 nm INT4 test chip reads 0.021 pJ per MAC at 0.46 V and about 0.1 at nominal (JSSC 2023, via Dally's NASEM slides; claimed), so INT8 at 2x to 4x that gives k 0.7 to 3 with the centre near 1; every ALU-shaped block reads k 0.3 to 0.8 on the same sources. A shadow built of tensor tiles at the ALU shadow's premium (about 11,400 u8 tiles per hash) therefore gives 2.1x at k = 1 and 1.6x at k = 1.5 and removes the k 0.3 column from the table; it needs a SIMD byte-dot verifier (the scalar one at 12.4 ms fails the 10 ms gate). This is a design candidate, not the shipped stream: the shipped shadow is ALU-shaped and its row stays 2.1x at k = 1 and 3.4x at k about 0.33.
|
||||
|
||||
Correction, 8 October 2026 (the research lane's microbench on the 5090, counter-asic-4-research.md 15.1a and the corrected 20.3 and 20.4 at 71fd465b): the per-MAC figures above are wrong by a factor of 32. A `mma.m8n8k16` tile is 1,024 multiply-adds per warp, 32 per lane, so a hash does 32 MACs per tile, not 1,024; the 4090's "0.056 pJ per MAC" is 1.8 pJ, and the 5090 at the ALU shadow's premium reads 2.9 pJ per MAC unlocked and 1.5 pJ at the 1,300 MHz lock (the packs job, 366,080 MACs per hash; the microbench's dependent u8 tile 4.1 and 2.2, the wide s8 m16n8k32 tile 1.36 and 0.83). Against the same 5 nm MAC array figures (0.04 to 0.4 pJ per INT8-class MAC, claimed) a chip's k on tile work is therefore 0.03 to 0.3, below the ALU shadow's 0.3 to 0.8, not near 1: at the same premium a tensor-shaped shadow leaves the chip 3.5x to 6.7x where the ALU shadow leaves it 2.1x to 3.5x. The tensor-tile column (2.1x at k = 1, 1.6x at k = 1.5) is withdrawn as a candidate; its premise, that a chip's MAC is no cheaper than the GPU's, is false by 4x to 30x on the public figures. The shipped row is unchanged: 2.1x at k = 1 and 3.4x at k about 0.33, the ALU shadow at the operating point's knee, measured four times at 82 to 90 W.
|
||||
|
||||
The capex column. The `f = 1` GDDR7 chip of 5.5 is USD 2.8 per MH/s of silicon and memory, which is USD 0.00016 per MH/s-hour of capex over two years against USD 0.000023 of electricity: capex-dominated 7x, as the 5090 is (USD 14.7 per MH/s at MSRP, 10x). A 64 MiB hot table adds about USD 15 of N5 die, the shadow core USD 25 to 40, an interposer USD 200, so the chip's capex reaches at most about USD 4.3 per MH/s: the per-unit capex wall is unreachable by 3x to 7x, and the break-even market cap moves only through the project cost (the mission lane's model: about USD 100 M with the N5 shadow core, about 200 M if the shadow runs per load and forces one die or an interposer; the per-load form behind that figure, the 16 x 27 placement, was closed on 7 October 2026 at night when it failed the value-level acceptance test across drawn eras, so the 200 M row rests on no construction shown to exist until a sound per-load class, one pass of a 432-instruction sub-block per load, is drawn, accepted and measured). Every figure here is modelled on cited or claimed parts; the research lane's microbench (20 probes, the mma_u8 and l2 rows the ones this model would take) is on PC 1's queue after the hot-table job.
|
||||
|
||||
## 6. The per-day derivation (item 2)
|
||||
|
||||
6 October 2026, Counter ASIC 3.0 item 2, worker `derive` (`docs/plans/counter-asic-3-derivation.md`; everything
|
||||
PROPOSED, a prototype behind load class `dr736`). The fixed-shape mixer of section 2's rows is replaced by nine
|
||||
straight-line programs of 736 instructions per item drawn from the day key stream (twelve two-register forms,
|
||||
the chain rule, an acceptance test with the x8 mixer's counts as floors). The chip's cost per hash is still item
|
||||
derivations; what changes is the fixed-function factor, because the chip must now execute an arbitrary program
|
||||
of the day from a 12-form set over 16 registers (a sequencer: instruction store, register file, operand muxes, a
|
||||
32-bit ALU with a multiplier and a rotator) instead of a wired pipeline of 72 mixer stages with the day's
|
||||
constants in the wires. The counts are from the code (`memhard::mixer`: 144 ops per application as written, 128
|
||||
with the round constants hoisted, 16 multiplies; the x8 item is 10,368 / 9,216 / 1,152), not the 130 of section 1;
|
||||
the day program's floor is those counts, so the bare row cannot fall below x8's.
|
||||
|
||||
| Row | Derivation | Chip ops per hash | Chip rate at 50 T op/s | SRAM the chip holds | mm^2 / $ (N5 headline) | Bare gain against 136.1 MH/s | Allowance 1.2x (ProgPoW's claimed range, history 2.4 [S67] [S70]) | Allowance 1.5x (cautious upper bound, approximate) | The old 3x (the fixed shape's; does not apply) | Equal silicon, SRAM deducted, at 1.2x / 1.5x |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| x8 as shipped (section 2's v3 row, re-counted from the code with constants hoisted) | fixed mixer, 72 x 128 | 1,179,648 | 42.4 MH/s | 256 MiB | 128 / $46 | 0.31x | 0.37x | 0.47x | 0.93x | 0.31x / 0.39x |
|
||||
| **dr736, the genesis day's draw** (9,992 chip ops, 1,461 multiplies per item; the floor is x8's 9,216) | the day program, 9 x 736 instructions | 1,278,976 | 39.1 MH/s | 256 MiB | 128 / $46 | 0.29x | 0.34x | 0.43x | 0.86x | 0.29x / 0.36x |
|
||||
| dr736 at the floor (a day whose draw sits exactly on the acceptance floor) | the day program | 1,179,648 | 42.4 | 256 MiB | 128 / $46 | 0.31x | 0.37x | 0.47x | 0.93x | 0.31x / 0.39x |
|
||||
| dr368, the fallback (the x4-equivalent count: 5,004 chip ops per item on the genesis day) | the day program, 9 x 368 | 640,512 | 78.1 | 256 MiB | 128 / $46 | 0.57x | 0.69x | 0.86x | 1.72x | 0.57x / 0.71x |
|
||||
| dr736 at year 4 (cache 512 MiB) | the day program | 1,278,976 | 39.1 | 512 MiB | 255 / $111 | 0.29x | 0.34x | 0.43x | 0.86x | 0.23x / 0.28x |
|
||||
|
||||
Arithmetic, row dr736: 9,992 x 128 = 1,278,976; 50 x 10^12 / 1,278,976 = 39.1 x 10^6; 39.1 / 136.1 = 0.287; x 1.2
|
||||
= 0.345; x 1.5 = 0.431; x 3 = 0.862; equal silicon (750 - 128) / 750 = 0.829, x 0.345 = 0.286, x 0.431 = 0.357.
|
||||
The allowance argument, plainly: the 3x of section 1 was the credit for "a pipeline with no scheduling or
|
||||
divergence", which a fixed dataflow earns because the chip wires the 72 applications and bakes the constants in;
|
||||
with a program that changes daily the chip keeps no divergence (the GPU has none here either: the item function
|
||||
is straight-line), the constants folded into an instruction store, and no warp scheduler or operand collector,
|
||||
and it loses the wiring. That residual is what ProgPoW's audits priced at 1.1x to 1.2x for a conventional
|
||||
compute chip (Rao: "conventional compute chips gain little on ProgPoW", history section 2.4); 1.5x is a cautious
|
||||
upper bound of mine (approximate) for a chip that also drops the GPU's float and graphics area. The chain rule
|
||||
(every instruction reads the register the previous one wrote) adds a cost the row does not credit: with no
|
||||
intra-item parallelism a single engine completes one dependent instruction per cycle at best and must interleave
|
||||
items to keep its multiplier busy, which is a register file per item in flight (RandomX's light-mode argument,
|
||||
history 2.4). The measured costs that buy this: the verifier 4.88 ms per unit on one M5 Max core against x8's
|
||||
2.06 (the derivation document's section 5.1), the Mac's daily build 29 ms against 22, the hash rate unchanged;
|
||||
the 5090's build and compile are the PC 2 job, the 9070 XT's OWED.
|
||||
|
||||
What this does not settle: the rows are the same 50 T op/s budget and the same denominator as section 2 (their
|
||||
margins apply); no chip has been priced for its instruction store or its register files per item in flight; the
|
||||
random ARX programs have had no cryptanalysis (the item 3 brief should name them beside `M_r`); the 2019-class
|
||||
core measurement (O-1.14) decides whether 736 or 368 is the length, and the derivation document's section 0
|
||||
carries that verdict.
|
||||
|
|
|
|||
161
docs/analysis/ci-failures-2026-10-06.md
Normal file
161
docs/analysis/ci-failures-2026-10-06.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# CI failures, 4 to 6 October 2026: every non-green run classified, the fixes, the guards
|
||||
|
||||
Written 6 October 2026, 22:0x UK, from `gh run list` (430 runs, every run since the first workflow run at 09:57Z on
|
||||
4 October) and `gh run view --log-failed` on one run per class. Times UTC (UK was UTC+1). This document is an operations
|
||||
record and is listed in `tools/ci/export-exclude.txt`: it is not exported to the public mirror.
|
||||
|
||||
## 1. The totals
|
||||
|
||||
| Workflow | Runs | Green | Failed | Cancelled |
|
||||
|---|---:|---:|---:|---:|
|
||||
| ci | 336 | 205 | 131 | 0 |
|
||||
| windows-ci | 94 | 57 | 20 | 17 |
|
||||
| total | 430 | 262 | 151 | 17 |
|
||||
|
||||
126 of the 168 non-green runs were on master. Master was red without a break from 18:37Z to the end of the evening
|
||||
(20:23Z, run 37526027569) across three causes in a row: the billing block, the copied-sources check, the identity grep.
|
||||
|
||||
## 2. Every failure by root cause
|
||||
|
||||
One line per class. "Guard" is what now stops the class before it reaches master.
|
||||
|
||||
| Class | Runs | First | Last | Cause | Fix | Guard |
|
||||
|---|---:|---|---|---|---|---|
|
||||
| A1 identity grep | 56 | 04 13:26Z | 05 01:46Z | A simulator's run log committed under `sim/difficulty/records` carried the Mac's home path in its first line; 43 master pushes in twelve hours each failed the same step | The log was scrubbed; `.log` files joined the generic scrub (mirror e18256d) | The pre-push gate runs the identity grep locally before any push to master or release-* (`tools/ci/pre-push.sh --hook`), so the hit lands on the pushing machine, not on master |
|
||||
| A2 identity grep | 17 | 05 02:17Z | 05 10:36Z | vmmap dumps under `docs/benchmarks/memory-floods-2026-10-04/vmmap/` carried a local time offset on their Date/Time lines | The dumps were re-stamped to UTC | Same gate |
|
||||
| A3 identity grep | 1 | 06 20:23Z | 06 20:23Z | `docs/analysis/horizon/polish.md`, a research review of internal tooling, quotes the overlay network's product name, the intake key's variable name and the identity guard's own regexes as text; docs/analysis is in the export list, so the grep is right to flag it | The document is listed in `tools/ci/export-exclude.txt`; the identity check and the mirror's `sync.sh` both prune that list, so the guard's purpose (nothing the public reads carries these strings) is intact and the research text is untouched | The exclusion list, plus the gate; `identity-check.sh --self-test` shows an excluded path may quote the patterns and an exported one may not |
|
||||
| B1 hosted runner refused | 32 | 06 18:37Z | 06 20:05Z | "The job was not started because recent account payments have failed or your spending limit needs to be increased": every GitHub-hosted job of every run failed at start with zero steps, 26 master runs and 6 release-0.3.15 runs, and nothing told anyone | Cleared on the billing page by 20:08Z | The `red` job runs on the box's own runner after any failed master or release-* run and posts one line per run (section 4); the `pow` and `sims` jobs can move to the box with one repository variable (section 5) |
|
||||
| C1 copied-sources | 1 | 06 18:00Z | 06 18:00Z | `tools/workers/collect.mjs` mentioned rsync and cargo in a comment; the check read comments | The check skips comment lines | The gate |
|
||||
| C2 copied-sources | 12 | 06 20:08Z | 06 20:19Z | `infra/build-server/repro/rebuild-on-box.sh` clones sources and runs cargo without `touch`; 10 master runs and 2 release-0.3.15 runs | 0f0abc6 (box-work 2bd3bec): the repro script re-stamps its clones. Release-0.3.15 still carries the old script at c25a3ca and will fail this step again until it takes master (or cherry-picks 2bd3bec) | The gate |
|
||||
| D no-foreign-tree-writes | 2 | 06 18:20Z | 06 18:24Z | The new check's warning pipeline (`grep | grep -v | sed | cut`) fails under `pipefail` on a file with no hit, and `set -e` ends the script silently with exit 1 after "self-test passed"; the two runs right after it landed died this way, and the Mac's bash 3.2 died the same way on every tree | `|| true` on the warning pipeline (this change) | The gate runs the check locally, where it would have shown |
|
||||
| E Windows checkout | 3 | 04 13:53Z | 06 20:15Z | Nine screenshot files under `docs/plans/site-ui-3-shots/` carried a colon from an address; git on windows-latest refuses the path, so `actions/checkout` died and with it every Windows build of the tree (the 0.3.15 installer waited on it) | 61f46cc renamed the nine files | `tools/ci/windows-paths-check.sh`: colon and the other forbidden characters, trailing dot or space, reserved device names, over 240 characters; as the pre-commit hook on the staged paths and in the gate on every tracked path |
|
||||
| F1 site build | 5 | 04 13:46Z | 04 13:53Z | `site/scrub-bench.sh` failed on `docs/bench-log.md` and `build.mjs` threw from the execSync | Fixed in the bench log the same afternoon | The gate builds the site in a temporary copy before the push |
|
||||
| F2 site build | 2 | 05 16:42Z | 05 16:43Z | Conflict markers left in `site/journey.json`; `build.mjs` parsed it as JSON | Resolved by hand; `no-conflict-markers.sh` and the first pre-push hook (fb076de) followed | The gate's first check, on every push |
|
||||
| G link check | 1 | 05 09:28Z | 05 09:28Z | `/#wallet` linked from every page with no such id (release-0.3.6) | Anchor added | The gate |
|
||||
| H windows payload inputs | 4 | 05 16:30Z | 06 18:15Z | The signed inputs manifest on the downloads host pinned one node commit and `packaging/windows/node-source.pin` in the tree another: the Mac had pushed new inputs without committing the pin, or committed a pin without pushing inputs | Each time, the pin and the inputs were brought level | Not a tree check and not in the gate: the shipper's push-inputs.sh writes the pin and the commit must carry it; the `red` job now reports the mismatch within a minute instead of the next person opening the Actions page |
|
||||
| I windows installer | 1 | 04 10:32Z | 04 10:32Z | The runner image's Inno Setup was older than 6.3 | The workflow installs Inno Setup when the image's is too old | Resolved in the workflow |
|
||||
| J windows engine | 2 | 04 13:53Z | 04 13:56Z | Rust that did not compile pushed to master (`expected identifier, found keyword let`) | Fixed in the next push | A compile is not a 25-second check; the owner's merge rule (CLAUDE.md, "CI red is stop-the-line") covers it: the merger fixes or reverts inside 15 minutes |
|
||||
| K igneum-census | 1 | 05 23:18Z | 05 23:18Z | igneum-pow's `Instr`, `Program` and a layout argument changed; igneum-census was not rebuilt (release-0.3.11) | Updated with the crate | As J |
|
||||
| L prover-socket | 1 | 06 08:49Z | 06 08:49Z | `tools/proving-v1/pc2-agg-cost.ps1` ran the prover host as root without killing sp1-gpu-server (release-0.3.12) | The playbook was fixed | The gate |
|
||||
| M public API check | 1 | 06 15:12Z | 06 15:12Z | The live observer was 969 s stale when the master-only live check ran | The observer recovered; the hands moved to the box that evening | A live check stays in CI only, master only; it is not a tree fact and not in the gate |
|
||||
| N no-secrets | 2 | 06 15:56Z | 06 16:23Z | A 64-hex test vector next to `private_key` in `app/igneum-wallet/src/vault.rs` (wallet-0.1.5) | Allow-listed as a test value | The gate |
|
||||
| P swallowed defaults line (no CI run: a silent class) | 0 | 06 19:5xZ | 06 21:xxZ | A comment appended to a line of shell assignments in `tools/build-remote.sh` and then `tools/cross-remote.sh` turned every assignment after the `#` into comment text; `bash -n` and shellcheck are silent on it; the default cross-build never ran and its chain kept the previous exes from about 20:40 to 22:00 UK | 36e4ee7 on master: the lines split; `tools/ci/defaults-line-check.sh` with its self-test | In ci.yml at 36e4ee7 and in the gate from this change, so it runs on the pushing machine before the push |
|
||||
| Q gate checks that read the machine, not the fact (found by the gate itself, 22:1x to 22:3x UK) | 0 | 06 21:1xZ | 06 21:3xZ | Two pushes of this change to master were refused by the new hook: (1) git hands a hook `GIT_DIR`, and `remote-run.sh --self-test`'s nested `git init`, commits and reset then acted on THIS repository's worktree: it set `core.bare`, moved the local `master` to three fixture commits and broke the main checkout for ten minutes (restored from the reflog: master back to 36e4ee7, `core.bare false`; nothing was pushed, nothing lost); (2) the same self-test judged a stale `index.lock` by `pgrep -x git` over the whole machine, so the hook's own `git push` (or any other agent's git) made the fixture's checkout die with "index.lock: File exists" | The gate unsets `GIT_DIR` and the other hook variables before any check; the staleness test is the lock's age (over 30 s), and the self-test backdates its fixture lock | The gate itself: every self-test now runs inside a real hook before every master push, with a `git push` alive beside it |
|
||||
| R kill by name (the fleet, no CI run) | 0 | 06 21:09Z | 06 21:09Z | A Mac-side `pkill -f <log file name>` matched nothing: the name was a shell redirect, not part of any command line; the roll-everything script lived on and wiped a box it had been told to hold. Earlier the same day, twice: a `pgrep -f "<literal>"` matched the calling shell's own command line. Six `pkill -f sp1-gpu-server` inside `bash -c '...'` bodies in tools/proving-v1 carried the same shape on master | The fleet runs Mac-side jobs under `tools/fleet/fleet-bg.sh` (a pid file per job) and anchors every on-box kill on the binary's full path and first argument; the six prover lines use `pkill -x` on the binary name | `tools/ci/kill-by-name-check.sh` in the gate: flags `pgrep -f` / `pkill -f` with a plain literal, any pgrep/pkill on a file-name shape (.log, .out, .pid, .json ...), and `ps | grep <literal>`; allows the bracket form, `-x`, `-F <pidfile>`, `kill $(cat pidfile)`, a variable, a full path; self-test of 9 banned and 14 allowed shapes |
|
||||
| O1 windows-ci cancelled | 16 | 04 10:42Z | 06 18:12Z | `concurrency: cancel-in-progress` on windows.yml: a newer master push superseded the run. Not a failure | None needed | None; they are listed because `gh run list` counts them as non-green |
|
||||
| O2 hosted runner not acquired | 8 | 05 19:26Z | 05 20:54Z | "The job was not acquired by Runner of type hosted even after multiple attempts" on release-0.3.10 (7) and master (1): GitHub capacity, retried by hand | Re-run | The `red` job reports it; the box runner for `pow` and `sims` (section 5) takes those jobs off the hosted pool |
|
||||
|
||||
Sum: 168 runs, plus class P, which never reached CI because nothing checked for it. Classes A1 to A3, C1, C2, D, E,
|
||||
F1, F2, G, L and N are 102 runs (61 percent), every one a tree check that finishes in under 25 s on the pushing machine. B1 and O2 are 40 runs (24 percent) of GitHub-side refusals that nobody
|
||||
saw until the Actions page was opened. O1 is 16 runs (10 percent) of expected cancellations. H, I, J, K and M are the
|
||||
remaining 10.
|
||||
|
||||
## 3. The gate: one script, local and CI (`tools/ci/pre-push.sh`)
|
||||
|
||||
Every fast tree check CI runs is in one script. The `site` job of ci.yml calls `tools/ci/pre-push.sh --ci`; the pre-push
|
||||
hook calls `tools/ci/pre-push.sh --hook`. The two cannot drift because there is one list. A check added to ci.yml alone
|
||||
is the wrong place; it goes in the script.
|
||||
|
||||
| Mode | When | What |
|
||||
|---|---|---|
|
||||
| `--hook` on a push to master or release-* | installed by `tools/ci/install-hooks.sh` into the shared hooks directory (one set for every worktree) | all 32 checks; a red check refuses the push and prints its output |
|
||||
| `--hook` on any other ref | same | the two structural checks only (conflict markers, Windows paths) |
|
||||
| `--ci` | the `site` job | all 32 checks, with the site built in place |
|
||||
| default | by hand in any worktree | all 32 checks |
|
||||
| `--self-test` | in the gate itself | a known failure is RED and fails the gate; a known success is ok; master and release-* select the full gate, other refs the light one |
|
||||
|
||||
Measured 6 October 2026, 21:5x UK, on the Mac: 30 checks, GREEN, 25 s (no-secrets 11 s, identity grep 3 s, the rest
|
||||
under 2 s each). The hook never writes into the worktree: the site is built in a temporary copy with
|
||||
`SITE_DOWNLOADS_OFFLINE=1` (063bbca: the earlier hook built in place and rewrote the downloads snapshot in five
|
||||
worktrees); `git status` before and after the full gate is identical.
|
||||
|
||||
Checks that joined CI through the gate and were not in ci.yml before: the ledger sentence check
|
||||
(`ledger-text-check.mjs`), the workflow shell parse (`check-workflow-shell.mjs`), the Windows paths check, and the three
|
||||
self-tests (identity, Windows paths, the red watcher). The swallowed-defaults check (36e4ee7) and the kill-by-name check are in the gate too.
|
||||
|
||||
## 4. The red watcher (`tools/ci/red-watch.mjs`, `infra/build-server/ci-red/`)
|
||||
|
||||
A `red` job in ci.yml and windows.yml runs only when a master or release-* run has a failed job. It runs on the box's
|
||||
own runner (`igneum-build-1`), not on a GitHub-hosted machine, because the hosted pool is the thing that was refused in
|
||||
B1 and O2. It appends one JSON line for the run (id, workflow, branch, commit, title, the failed jobs and each one's first
|
||||
failed step from the run's own API, the URL) to `/srv/ci-red/red.jsonl`, idempotent per run attempt. On the box,
|
||||
`igneum-ci-red.timer` runs the poster every minute as `build`: each line not yet posted goes once to the hidden updates
|
||||
channel through `DISCORD_WEBHOOK_UPDATES` in `/srv/discord-hooks/env`, then its run id is recorded in
|
||||
`/srv/discord-hooks/ci-red-posted.json`. The orchestrator reads the file (`ssh build@<box> cat /srv/ci-red/red.jsonl`)
|
||||
or the channel. No URL is ever printed; a missing key is logged by name.
|
||||
|
||||
Shown on 6 October 2026: the self-test (one line however often `record` runs; the dry run sends nothing; a missing key
|
||||
is named, never a URL; one live send per run; a webhook error keeps the run pending). The poster is installed and
|
||||
active on the box (22:55 CEST, "nothing to post (0 recorded)"). Open: the updates channel has no webhook yet, so the
|
||||
first real red run will land in `red.jsonl` and the poster will log the missing key until `DISCORD_WEBHOOK_UPDATES` is
|
||||
added to `~/.config/igneum/discord` on the Mac and `infra/build-server/discord-hooks/install.sh` is re-run. The
|
||||
Actions-side trigger has not fired on a real red run yet (master was made green in the same change); the first red
|
||||
master or release-* run is its known-failed case.
|
||||
|
||||
## 5. Where CI runs, and why `ci` takes about three minutes
|
||||
|
||||
| Job | Where today | Time on ubuntu-latest | Time on the box (measured 6 October) | Note |
|
||||
|---|---|---|---|---|
|
||||
| pow (igneum-pow `cargo test --release`, packfile test, igneum-census build) | ubuntu-latest | 2 min 30 s to 3 min, cold every run (no cache action) | 42 s cold as the runner user (99 tests), sccache read-only hits after the first build | the long pole |
|
||||
| sims (two Python simulators, --quick) | ubuntu-latest | about 1 min with setup-python and pip | python3 and numpy are on the box from provision.sh | |
|
||||
| site (the gate) | ubuntu-latest | under 1 min | not moved: the live public API check belongs on a neutral egress | |
|
||||
| red | the box's runner | | seconds | only after a failed master or release-* run |
|
||||
|
||||
The workflow now reads the repository variable `IGNEUM_CI_RUNNER`: `box` sends `pow` and `sims` to
|
||||
`[self-hosted, linux, x64, igneum-build-1]`, anything else keeps `ubuntu-latest` (docs/plans/ci-self-hosted.md: GitHub
|
||||
has no fallback in `runs-on`, so a variable is the switch; `gh variable set IGNEUM_CI_RUNNER --body box` as igneum-labs,
|
||||
`gh variable delete IGNEUM_CI_RUNNER` to come back). Recommendation: flip it. The two compile-or-compute jobs are what
|
||||
GitHub's minutes and the billing block were spent on, the box compiles the agents' own pinned rustc 1.99.0, and a `ci`
|
||||
run drops from about three minutes to about one. The hosted runner then serves only the gate and the Windows
|
||||
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 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),
|
||||
so a red row could not be read afterwards; the classes below are from exit code, duration, compile count and what the next
|
||||
row of the same worktree did. Times UK. "Iteration" = the same worktree and kind green inside ten minutes.
|
||||
|
||||
| Time | Worktree, kind | Exit, secs, compiles | What it was | Kind | Would a local pre-check have saved the round trip | Guard now |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 18:49:12 | build-server, suite `--lib jobbuild` on app/igneum-app | 101, 0 s, 0 | Instant death: `--lib` on a crate whose tests live in the bin (the next row, `--bin`, was green the same minute) | real class: instant | Yes, one second | pre-flight and the kept run log; class `instant` on the row and in the red-run file |
|
||||
| 19:22:14, 19:51:09 | ship0314 then ship0315, prove `--features igneum-prove-host/cuda` | 101, 0 s, 0 (twice) | Instant death, the same command twice on two branches: the feature name (nothing on the box kept the message) | real class: failed the same way twice, instant | Yes | pre-flight refuses a feature the package does not have (`preflight-feature`, exit 3, before the slot) |
|
||||
| 19:26:01 | master, check `-p igneum-prove-core` | 101, 0 s, 0 | Instant death: the package name (no such member) | real class: instant | Yes | pre-flight refuses a `-p` that names nothing (`preflight-package`) |
|
||||
| 19:27:26, 19:27:54 | master, prove | 101, 0 s, 0; then 101, 55 s, 605 | An instant death, then a compile error; green 4 min later | iteration (with one instant) | The instant one, yes | as above; `compile-error` class with the run log kept |
|
||||
| 19:35:56, 19:42:36, 19:48:37, 19:58:12, 21:02:56, 21:47:14 | ship0315, suite and node-linux | 101, 19 to 66 s, 44 to 724 compiles | The shipper's 0.3.15 node fixes: compile and test failures, each green within 1 to 8 min | iteration | No: a Mac check takes 12 to 18 min, the box IS the pre-check | run log kept; class `compile-error` or `test-failure` |
|
||||
| 19:51:09 | ship0315, suite | 2, no duration | `no-dir`: the crate directory was not there, because a second run of the same worktree started in the same second and its checkout was replacing the tree | real class: a shared resource (one worktree directory, two runs) | n/a | one run per worktree directory at a time: `remote-run.sh` takes a per-worktree lock in checkout and run mode and waits (shown: the second of two concurrent runs waited and both finished) |
|
||||
| 19:50:57, 19:52:50 | ca3-v4-node, suite | 101, 12 s and 7 s | Test failures while fixing; green 2 min and 0 min later | iteration | No | run log kept |
|
||||
| 20:45:55 | box-work, night battery dry run | 1, 234 s | The night battery's dry-run subset failed in the box-work agent's hands; its output was on that agent's side | one-off, unread | n/a | the run log is kept from now on; the night battery writes its own report |
|
||||
| 21:01:12 | build-server, `self-test-repro` | 1, 0 s | The repro self-test's first version; green 1 min later | iteration | n/a | none needed |
|
||||
| 21:17:57 | ca3-v4-node, `cargo audit` | 2, 0 s, 0 | Instant death: cargo-audit is not installed on the box | real class: a missing tool, instant | Yes | pre-flight refuses a cargo subcommand the box does not have (`preflight-subcommand`); the install belongs in provision.sh (open: add cargo-audit there, the night battery runs it) |
|
||||
| 21:17:58 to 21:26:42 | finality-pause, two suites alternating | 101, 4 to 26 s | Six reds, the same two commands three times each while the finality pause tests were being fixed; green 2 to 11 min after each | iteration (the first pair outside ten minutes) | No | run log kept; the digest counts the class |
|
||||
| 21:45:13, 21:46:06 | box-capacity, node-linux p2p-probe | 101, 39 s and 3 s | Compile errors; green 1 min and 0 min later | iteration | No | run log kept |
|
||||
| 21:58:26 | ca3-v4-node, check | 101, 12 s | Compile error; green 1 min later | iteration | No | run log kept |
|
||||
| 22:00:08, 22:06:11, 22:08:16 | ca3-v4-node, suite (`finality`, then `clock_rule_v3 a_pause_carries ...`) | 101, 24, 13, 12 s, 22 compiles | The clock rule v3 tests failing while being fixed; green at 22:10 | iteration | No | run log kept |
|
||||
| 22:08:52, 22:08:55, 22:08:57 | ca3-v4-node, three suites in five seconds | 101, 0 s, 0 (three times) | Three instant deaths in a row with nothing compiled: cargo refused before building (a manifest or lock the overlay carried mid-edit, or a name); the three commands were green one minute later, so the tree was fixed under them | real class: instant, failed the same way three times | Yes, one second each | pre-flight (manifest, package, feature, subcommand) before the slot; class `instant` flagged; the run log kept so the next one is read, not guessed |
|
||||
| 22:11:38 | ca3-v4-node, check `--tests` | 101, 49 s, 844 | A compile error in the test targets; still red when the log was read | iteration, open | No | run log kept |
|
||||
|
||||
Sum: 34 red rows. 22 are iteration (an agent taking a red to green inside ten minutes, the box doing the compile the Mac
|
||||
cannot do in time). 12 are real classes, in three families: instant deaths (8 rows: a feature, a package, a target, a
|
||||
tool, and three unread), a shared worktree directory (1 row), and the night battery's unread dry run (1 row, plus two
|
||||
instant ones inside iterations). Every real class now has a guard in `infra/build-server/remote-run.sh`, shown on the box
|
||||
in a sandbox (a pass, a failing test, an empty filter, a missing package, a missing feature, a missing subcommand, a compile
|
||||
error, a broken manifest, two concurrent runs of one worktree):
|
||||
|
||||
| Guard | What it does | Class it stops |
|
||||
|---|---|---|
|
||||
| pre-flight | before the slot's time is spent: `cargo <sub>` exists, the manifest parses (`cargo metadata --no-deps`), every `-p` is a member or a dependency, every `--features pkg/feat` exists; a refusal is exit 3 in about a second with the reason | instant (manifest, package, feature, tool) |
|
||||
| kept output | the last 400 lines of every run in `/srv/builds/_log/runs/<id>.log`, named in the row (`run_log`) | every unread class |
|
||||
| class on the row | `class` in builds.jsonl: compile-error, link-error, test-failure, instant, slot-timeout, no-dir, no-test-matched, preflight-*, other | the digest can count what kind of red it was |
|
||||
| no-test-matched | a `cargo test` with a filter that ran 0 tests in every binary exits 3 instead of a green "0 tests" | a wasted round trip that read as ok |
|
||||
| one run per worktree | a per-worktree lock in checkout and run mode; the second waits up to 2 h and says so | no-dir, the half-replaced tree |
|
||||
| the red-run file | every red row is appended to `/srv/ci-red/red.jsonl` (the file the CI watcher writes), `"source":"box"`; not posted alone | |
|
||||
| the daily digest | at the first pass at or after 09:00 London, one line to the updates channel: reds in 24 h, CI and box, per class, with each class's guard | the learning, read once a day |
|
||||
|
||||
## 7. What belongs on another branch
|
||||
|
||||
| Branch | One-line change |
|
||||
|---|---|
|
||||
| build-server (provision.sh) | install cargo-audit for the build user (the night battery and the ca3 lane call it; pre-flight now refuses it with a clear line instead of a 0-second exit 2) |
|
||||
| release-0.3.15 | take master (or cherry-pick 2bd3bec): `infra/build-server/repro/rebuild-on-box.sh` re-stamps its clones, else the copied-sources step fails again at the next push. Its own `tools/ci/windows-paths-check.sh` (c25a3ca) is superseded by master's: on merge keep master's file and drop the extra ci.yml step, the gate runs it |
|
||||
232
docs/analysis/class-v6/family-gate.md
Normal file
232
docs/analysis/class-v6/family-gate.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Class v6, the family gate: how a parameter family is cryptanalysed as a family
|
||||
|
||||
Research lane D of the class v6 rotating-family design, under the Counter ASIC coordinator. First cut 8 October 2026, 11:0x to 17:00 BST (the deliverable clock); the full report by 09:00 BST on 9 October. Branch `class-v6-family-gate` (this document) and the harness branch `family-gate-v5` (the class v5 crate at 8f481459 plus the census harness, never a chain path). Internal research, not an independent review; nothing here touches a served number, the devnet or the testnet object; no consensus code this week. Every figure carries its source: a lane report on the mirror, a log on a box, or an arithmetic step shown in the text. Figures from memory are labelled approximate.
|
||||
|
||||
The question. Class v6 (the founder's word at 11:1x BST, 8 October 2026) draws per era the parameters a release used to fix (layer 1), lets the dataset track the chain state (layer 2), rotates instruction families by height (layer 3) and generalises the acceptance floor and the uniformity test to each era's draw with a redraw on failure (layer 4). A chip is then built against a FAMILY of hashes, not one hash. The in-house pass of 7 October and the attack board F1 to F10 bounded ONE member of the family (class v4 sub-version 3 at 017e7037 and class v5 at 1c420786). This document says how the family is bounded: what the space is, how many eras must be drawn to say anything about the worst era, which tests the chain runs itself on every era and epoch, what the offline board looks like over the testnet period, what a published proof of testing contains, and what the whole apparatus cannot see.
|
||||
|
||||
## 0. One page
|
||||
|
||||
| Item | The line |
|
||||
|---|---|
|
||||
| The space (section 1) | Seven drawn axes with a tested band each: the mixer multiplier `m` in {4, 8, 16} (8 and 4 measured on cards and the verifier; 16 the verifier's open row), the ten non-load op weights within B points of table 1.4.2 with shuffle and mulhi never raised (B = 2 proposed in spec 1.13.1, B = 4 the research lane's proposal), the read width in {1, 4} words (w16 measured 5 October, w64 excluded), the shadow block shape in {64, 128, 256} instructions at 6,912 per iteration (measured 6 October), the shadow placement fixed at the whole block after instruction 63 (the per-load placement is a known-failed corner: 1.4 percent acceptance, 42 of 64 seeds exhausting 32 attempts), the program length N by the ladder's signal inside rungs 0 to 2 (rung 3 inadmissible by F6), and the existing era draws (the stride `M`, the rotation `R` in 1..31, the interleave, the per-site windows). The day key (ROT, MUL, RC) is drawn daily under the AP-F4-1 redraw rule |
|
||||
| Which tests depend on which axis (section 1.3) | The acceptance rule and every index statistic ((c''), (c'''), the bucket bound, the bit-bias read, the attempts census) run on the closed-form dataset and never see `m`: the mixer axis is bounded per family by adv-mixer-3's rows and F4, not per era. The width axis changes the index space of every site ((c'')'s expectation `E_s` must divide the window by the width, a spec change the harness carries). The shape and weight axes change the draw's stream and so every per-era statistic |
|
||||
| The sampling bound (section 2) | With `n` eras drawn from the chain's own distribution and every one passing a per-era test, the fraction of eras that would fail it is under `3/n` at 95 percent confidence (the rule of three; Wilks 1941 for the order-statistic form): 190 eras bound the failing fraction at 1 in 64 (one era in 32 years of 180-day eras), 3,067 at 2^-10. The bound is on the TESTED property only; the union bound over the per-era tests' own false-pass rates gives the family's false-pass rate, and each test's false-pass rate is itself a rule-of-three number on its known-failed cases (the (c''') floor refused 9 of 9 live hot sets, so its miss rate on that class is under 0.33 at 95 percent: nine cases bound nothing tighter) |
|
||||
| The per-era tests (section 3) | Three rings. Ring A, the chain runs per era at the cut, microseconds: the structural redraw rules (the band membership of every draw, the day-key cost rule, the product-bias rotation class recorded). Ring B, the chain runs per epoch, seconds: the acceptance rule generalised to the family's shape ((a) (b) (a') (c) (c') (c'') (c''') plus the two next-class tests, the per-site largest-256-item-bucket bound and the index-bit bias read, on the same 2^20 pass). Ring C, the gate runs offline per drawn era, box-hours: the F8-form census at 2^24 on 64 seeds, the attempts census, the exhaustion count. Each row carries its known-failed shape and its clean-seed cost so the redraw is priced |
|
||||
| The board (section 4) | F1 to F10 and the nine lanes classified per-era, per-family or chain-run, with the harness taking the era draw as an input; the proof of testing is one JSON record per drawn era (the draw, the object, the binary, the seeds, every verdict with its statistic, threshold and log sha256, the core-seconds) and one family summary (eras per stratum, the worst era per test, the refuse rate per band, the bound's arithmetic) |
|
||||
| The honest line (section 5) | The gate sees what its tests see. It does not see the diffuse era-stride class below the bit-bias read's resolution (a bias under 0.3 percent at 2^20), a mechanism that lives in a joint corner no stratum names, a mixer weakness below adv-mixer-3's bands, or an era whose draw sits in the untested 1 in 64. A GPU-like chip loses nothing on any era; what a corner buys it is a per-era hot set bounded at 1.002x (the floor) or 1.0024x (the diffuse class), or, at `m = 4`, a mixer with 2 applications of measured margin instead of 6 |
|
||||
| Running now (section 6) | The harness `family_gate_era_census` on build-1 from 11:16 BST: one drawn era per seed, every family parameter from the era's own stream, the chain draw through the real rule keyed on the family's shape, one TSV row per era with the (c'') ratio, the bucket ratio, the bit bias and the attempts; the base control (`v5_attempts_census`, the shipped draw) beside it. The coverage table in section 6 is filled from the logs at 16:xx BST |
|
||||
|
||||
## 1. The parameter space
|
||||
|
||||
### 1.1 The axes, their bands and the margin each band was read from
|
||||
|
||||
| Axis | Band proposed for the draw | What fixed the band (the measured rows) | The tested margin inside the band | Known-failed corner, named |
|
||||
|---|---|---|---|---|
|
||||
| Mixer multiplier `m` (applications per round, `LoadClass::mixer_mult`; 72 per item at `m = 8`, 8 between dependent reads) | {4, 8, 16}; the research lane's outline; this lane's reading: {8} plus {4} as a corner, {16} only after the verifier row | x4 and x8 measured (`docs/plans/mixer-x4.md` section 6.4: 1.92 and 2.79 ms per unit on a loaded M5 Max core; the daily build 42 ms at x8 on the 5090, measured); x16 estimated at about 3.7 ms per unit on the M5 Max core and 9 ms on a 2019-class core (chip-model-v3 section 3 item 1), against the 10 ms verifier gate | adv-mixer-3 (`report-mixer-3.md`, "The round margin, stated"): no statistic survives 2 of the 8 keyed applications between reads at bands 0.0015 (Q2, 2^24 states), 0.00026 (2^27), 0.00018 (Q2b, 2^28); SAT solved at k = 1 in 137 s and timed out at k = 2, 3, 4; two real days and eight random days agree. So at `m = 8` 6 of 8 applications are margin; at `m = 4` the same rows leave 2 of 4; at `m = 16` 14 of 16 | None measured. The corner is `m = 4` by margin and `m = 16` by the verifier; the all-ROT-equal day key is the day-level corner (rejected by the AP-F4-1 rule) |
|
||||
| Op-mix weights (the ten non-load families, sum 75) | Each weight within B points of table 1.4.2 (add 12, xor 10, mul 8, mad 8, shfl 8, rotl 7, sub 6, mulhi 6, rotr 6, or 4), renormalised by largest remainder; shfl and mulhi never raised; B = 2 (spec 1.13.1, proposed) or B = 4 (the research lane) | The microbench (`counter-asic-4-research.md` 15.1a, the 5090 alone, 8 October): pJ per counted op at the stock clock and the 1,300 MHz lock: arx 11.3 / 6.2, mul 13.9 / 8.3, mulhi 39.6 / 21.0, prmt 22.3 / 11.5, lop3 24.1 / 13.0, shfl 55.8 / 29.4; the shuffle is 4.9x the add, so a draw that raises shfl raises the card's premium per instruction with no better `k` for a chip (15.1b). F7's era census: the op-weight corners (15 to 31 of 75) read 1.0x against the GPU, 0 memory effect | The acceptance's rejection rate moves with the lossy share: at the table 0.68 per candidate ((a') 83.5 percent of rejections; adv-accept row 90, 20,000 seeds); the F7 census at B = 2 read every corner clean for the chip; nothing was measured at B = 4 before today (section 6) | The lossy corner: or, mul and mulhi all at +B. It raises (a') rejections and so the attempts per seed; the exhaustion bound `r^256` is the number to read there (section 3, ring C) |
|
||||
| Read width `W` (words per load) | {1, 4} | `docs/plans/read-width.md` (5 October): w16 within the rules (5090 139.8 against 136.1 MH/s, 9070 XT 17.90 against 18.15, M5 Max within 1 percent); w64 bandwidth-bound (5090 share 0.58, 71.9 MH/s): out; the per-load mixes out on the 5 percent spread rule | The acceptance runs unchanged on aligned addresses (read-width.md section 2), but (c'') and (c''') read a site's distinct ALIGNED indices against `E_s = N - N^2 / 2W_s`: at `W = 4` the index space is `W_s / 4`, so the expectation must follow or every width-4 program reads 0.976 at a quarter window and is refused by the 0.98 floor. The harness divides the window by the width (`site_window_words`); the spec line is owed | `W = 16` (w64): excluded by measurement, never in the band |
|
||||
| Shadow block shape (instructions per block, passes per iteration) | {64 x 108, 128 x 54, 256 x 27}: 6,912 shadow instructions per iteration at rung 0 | 64-instruction blocks ran 2.5 to 3.5 percent faster than 256 on the 5090 and the M5 Max (6 October, measured); 1,024 cost the M5 Max 17 percent (out) | F1's shadow redundancy bound (3.0 percent of peephole-removable instructions, the v5 generator's `SHADOW_REMOVABLE_MAX_PERMILLE`) was set from F1's census at 256 instructions; the removable fraction per block is a property of the block's length, so the fraction must be re-read at 64 and 128 (a per-family row). The acceptance's (a') fixpoint and the shadow's execution in (c) take any length | The per-load placement (16 sub-blocks of 16 after each load): dead as a chain class (`counter-asic-4-research.md` 20.2a-close): 22 of 1,621 candidates accepted, 42 of 64 seeds exhaust 32 attempts, candidate 0 carries index bit 0 set in 40 of 1,024 addresses at site 0 (z 29.5). The placement axis therefore has ONE value in the band |
|
||||
| Program length `N` (counted ops per hash) | Not drawn: the ladder's signal (rungs 0 to 2); the research lane's outline keeps it so | F6: the worst of 10^5 class v4 programs 8.708 ms on the half-core proxy, so N about 300,000 fits under 10 ms (rung 2 admissible, rung 3 not); the M5 Max loses 3.3 to 10 points across the band (measured ladder) | The verifier's worst case was censused at rung 0 only; a draw inside the ladder needs the 10^5-program worst case at the top rung (per-family row) | Rung 3 (inadmissible by F6) |
|
||||
| Era stride and interleave (`M` odd, `R` in 1..31, `pos` four ascending bit positions, the fold rotations, the per-site windows `k_off`, `o`) | As spec 1.13.1 draws them today | F7's 2^24 era census (stride bijective, R, pos, M uniform, no class over 1.1x); adv-cache-2 section 2.3: the same 32 base programs read 2 of 32 with a site over 1.04x under `R = 29` and 16 of 32 under drawn eras (8 over 1.2x, worst 1.75x), because `rotl(x * M, R)` places a product's biased low bits (P(bit 0) = 1/4) at address bits R and up, inside the 28-bit index unless R is 29 or 30; at D = 29 only R = 30 and 31 cut bit 0 | The whole 64-seed F8 census is a census over drawn eras (each f8 seed carries its own era), so the per-era uniformity verdicts of sub-version 3 and class v5 (60 of 64 and 61 of 64 under 1.2x) already sample this axis; the devnet's clean reading at R = 29 is the accident, the drawn-era figure the chain's | R in 1..27 with a product as the last writer of a load's source (sub-class A); the warp-uniform mulhi source (sub-class B, 1 in 16,000 warps) |
|
||||
| Dataset size `D` (layer 2) | 2^28 words today, 2^29 at the 2 GiB genesis step, growing on the 1.13.3 schedule with the state as a floor | The acceptance's closed form is pinned at D = 28 (`ACCEPT_DATASET_LOG2`); the live dataset is larger; the stand-in gap measured at 0.0004 under D = 28 (adv-accept Q2, 54 programs) | Unmeasured at D = 29: which R values hide the product bits changes with D, and the window cap `min(k_off, D - 26)` changes the per-site windows | D at a value where the acceptance's D = 28 windows and the live windows disagree: the stand-in gap row of ring C |
|
||||
| Day key (ROT x 8, MUL x 16, RC x 16, drawn daily from the day seed) | As spec 1.8.4, under the AP-F4-1 redraw rule in class v5 (cost A at most 205 rejected, k >= 1, the eight ROT equal rejected) | F4 and adv-mixer-2: 0 of 2^24 days over 1.1x on both metrics with the rule (8ca66afa); 5.69e-4 of days over 1.1x on LUT area without it, 15 days a century, worst 28 April 2050 at 1.113x | The rule is per day and chain-run; it does not depend on `m` (the cost is per application) | Day 29,337 (redrawn under the rule); the planted mul1all and mulnaf days fire |
|
||||
|
||||
### 1.2 The joint space and what a draw covers
|
||||
|
||||
Counting the discrete axes alone: 3 (m) x 2 (W) x 3 (shape) x the weight perturbations ((2B + 1)^10 before the caps: about 9.8 million at B = 2, 3.5 billion at B = 4) x 31 (R) x 2^31 (M) x 1,820 (pos) x 31^6 (the fold rotations). The space is of order 2^80 before the per-site windows, which are per program. No census walks it. What a census can do is (i) draw eras from the chain's own distribution (the era stream over random `E_n`) and bound the fraction of eras that fail a test (section 2.1), and (ii) pin the axes that carry a KNOWN mechanism at their corner and draw the rest (section 2.3), so a mechanism that depends on one named axis is read at its worst value. A mechanism that depends on a joint corner nobody named is in the honest line (section 5).
|
||||
|
||||
### 1.3 Which tests see which axis
|
||||
|
||||
| Test | `m` | weights | `W` | shape | N | era stride, windows | D | day key |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| The acceptance rule and the index statistics ((c''), (c'''), bucket, bit bias), on the closed form | no (the mixer never runs) | yes (the stream) | yes (the index space) | yes (the stream, the shadow's execution) | yes (the shadow's passes) | yes (`load_index`) | no (D = 28 pinned) | no |
|
||||
| The attempts census and the exhaustion bound | no | yes | yes | yes | yes | yes | no | no |
|
||||
| The F8-form census at 2^24 on the live dataset | yes (the item words) | yes | yes | yes | yes | yes | yes | yes |
|
||||
| adv-mixer-3's distinguisher rows, F2, F4, adv-mixer-2 | yes | no | no | no | no | no | no | yes |
|
||||
| F6 verifier worst case | yes | yes | yes | yes | yes | no | yes (the fill) | no |
|
||||
| F1 shadow redundancy | no | yes | no | yes | yes | no | no | no |
|
||||
| F3, adv-cache, adv-cache-3 (the chained cache) | no (the cache fill is ChaCha12) | no | no | no | no | no | yes (the cache doublings) | yes |
|
||||
| F7 era census | no | yes (the perturbation) | yes | no | no | yes | no | no |
|
||||
| F5 chip model, F10 ladder | the band's top | the band's dearest mix | yes | no | the ladder | no | yes | no |
|
||||
|
||||
Reading: the per-era statistical tests are blind to `m` by construction, so the mixer is a per-FAMILY axis with three values and is closed by re-running adv-mixer-3's ladder and F4 at `m = 4` and `m = 16` (section 4), not by drawing eras. The weights, the width and the shape are per-era axes and are what the drawn-era census of section 6 walks.
|
||||
|
||||
## 2. The sampling bound
|
||||
|
||||
### 2.1 Zero failures in `n` eras (the rule of three, Wilks)
|
||||
|
||||
Let a per-era test have a fixed verdict per era and let `p` be the fraction of the family's eras (under the chain's own draw) that fail it. If `n` eras drawn independently from that draw all pass, then with confidence `1 - alpha`, `p <= eps` where `(1 - eps)^n = alpha`, that is `n >= ln(alpha) / ln(1 - eps)`, about `3 / eps` at 95 percent and `4.6 / eps` at 99 percent (Hanley and Lippman-Hand 1983; Wilks 1941 gives the same arithmetic as the one-sided nonparametric tolerance limit on the worst order statistic). The table, with the era length of layer 3 (180 days) turned into years:
|
||||
|
||||
| `eps` (the failing fraction bounded) | One failing era expected every | `n` at 95 percent | `n` at 99 percent |
|
||||
|---|---|---|---|
|
||||
| 1/16 | 8 years | 45 | 70 |
|
||||
| 1/64 | 32 years | 190 | 293 |
|
||||
| 1/256 | 126 years | 766 | 1,177 |
|
||||
| 2^-10 | 505 years | 3,067 | 4,713 |
|
||||
| 2^-20 | the F4 and F7 gate's fraction | 3.1 million | 4.8 million |
|
||||
|
||||
What this buys and does not. The F4 and F7 gates ask "no class with gain over 1.1x at a fraction over 2^-20" and reach it by censusing 2^24 draws of a CHEAP statistic (microseconds per day key or era seed). A per-era test that costs box-hours (the F8 census at 2^24) can be run on hundreds of eras, so it bounds the failing fraction at the 1/64 to 1/256 class, never 2^-20. That is why layer 4 puts the tests on the chain: a per-era test the chain runs on EVERY era (ring A) or every epoch (ring B) needs no sampling bound at all; the offline census (ring C) is then a check on the chain-run tests' calibration (their clean refuse rate and their known-failed firing per stratum), and a reading of the one statistic the chain cannot afford, the live-dataset census.
|
||||
|
||||
### 2.2 The union bound over the per-era tests
|
||||
|
||||
Let the chain run tests `T_1 .. T_k` on every era or epoch, and let `delta_i` be the probability that `T_i` passes an era (or program) that carries the failure class `T_i` exists for. The family's false-pass rate on the union of those classes is at most `sum delta_i`. Each `delta_i` is itself bounded by the rule of three on the test's known-failed cases: a test that fired on `f` of `f` known-failed cases has `delta_i <= 3 / f` at 95 percent. The numbers today:
|
||||
|
||||
| Test | Known-failed cases it fired on | `delta` at 95 percent | Source |
|
||||
|---|---|---|---|
|
||||
| (c''') floor at 0.995 on the few-item hot-set class | 9 of 9 live hot sets refused (minimum sites 0.9809 to 0.9919), both single-item programs refused | under 0.33 | adv-accept, "the floor's final tally", 01:13 BST 8 October; class-v5 section 14 |
|
||||
| (c'') floor at 0.98 on the low-entropy band | 5 of 5 (p23, p18, p19, p15, p56) | under 0.60 | attack-pass-2026-10.md, sub-version 3 |
|
||||
| (a') freshness on the lineage-visible constant class | 11 of 11 (sub-version 1's eleven seeds under 1.2x on sub-version 3) | under 0.27 | attack-pass-2026-10.md, the sub-version 1 table |
|
||||
| The bucket bound (in sigma of the bucket's expectation at 2^20) on the quarter-bit class | 4 of 4 would be caught (p4, p8, p10, p34: largest bucket 3.1x to 5.6x at narrow windows, +17 to +37 sigma), not yet wired as a rule | under 0.75 once wired; the clean spread is measured in section 6 | the hash lane's attribution, 09:46 BST 8 October |
|
||||
| The index-bit bias read on the product class | 3 of 3 named sites (Devnet 3 site 0 at P(bit 0) = 0.25; era-drawn-28 site 15 at 0.778; the per-load candidate 0 at z 29.5), and 7 of 7 in the 16-era smoke run at 130 to 511 sigma | under 0.30 on ten cases; but the read is a record, not a refusal (section 3.2) | adv-cache-2 2.3; counter-asic-4-research.md 20.2a; section 6 |
|
||||
| The day-key cost rule | the planted mul1all and mulnaf days, day 29,337 | 0 of 2^24 days over 1.1x after the rule (a census, not a case count) | f4-weakday.md, 8ca66afa |
|
||||
|
||||
Reading: the per-test miss rates are bounded by HANDFULS of known cases, so the union bound today is a loose number (the sum exceeds 1). The way to tighten it is not more eras but more known-failed cases per test, which the adversarial sweeps supply cheaply: adv-accept's tail holds 22 programs beyond the 1.2x gate of 37 measured live, and the drawn-era census of adv-cache-2 holds 19 biased sites of 1,056. A test with 60 fired cases has `delta` under 0.05; with 300, under 0.01. The proof of testing (section 4.3) therefore carries, per test, the count of known-failed cases it fired on, and that count is the number a reader checks first.
|
||||
|
||||
### 2.3 Stratified draws over the corners
|
||||
|
||||
The axes with a known mechanism and their corner values: `m` in {4, 16} (margin, verifier), weights at the lossy cap (or, mul, mulhi at +B) and at the table, `W` in {1, 4}, shape in {64, 256}, R in {1..27} against {28..31} (the product-bias class visible or hidden; at D = 29 the hidden set is {30, 31}), the windows forced to quarter (`k_off = 2` at every site, the F8 tail class), D in {28, 29}. That is 2 x 2 x 2 x 2 x 2 x 2 x 2 = 128 corner cells for the per-era statistics (the mixer cells are per-family rows), plus the random stratum (every axis drawn). The plan: at least 8 eras per cell for the cheap ring-B statistics (1,024 eras, about 2 core-hours each at the (c''') cost, 2,000 core-hours, 64 box-hours of 32-core slots) and the random stratum at 3,000 eras (the 2^-10 line of 2.1 at 95 percent for the ring-B tests, 6,000 core-hours, 190 box-hours); the live-dataset census at 2^24 on 64 seeds for 24 corner representatives and 64 random eras (88 eras at 45 to 95 core-hours each, about 6,000 core-hours, 190 box-hours). About 450 box-hours in all, 2 to 3 days of both boxes at the 88-core pool, inside the testnet period by a wide margin. Section 6 records what has run.
|
||||
|
||||
## 3. The per-era automatic tests (layer 4)
|
||||
|
||||
The three rings. A test's ring says who runs it and what a failure costs.
|
||||
|
||||
| Ring | Who runs it, when | Cost per run | What a failure does |
|
||||
|---|---|---|---|
|
||||
| A: structural, per era | every node at the era cut, on the drawn parameters | microseconds | the era stream's next block of draws is consumed (a redraw), up to a cap, then the base table (the class v4 values) as the last resort, so the draw is total and no consensus path panics (the AP-F8-2 shape) |
|
||||
| B: the acceptance, per epoch | every node at the epoch draw, on each candidate | about 2.2 s per chosen candidate on one box core at 2^20 (the (c'') pass), plus 0.3 s for the 64-unit parts; the two next-class tests ride the same 2^20 pass | the next attempt, as today; the cap 256 and the last resort unchanged |
|
||||
| C: offline, per drawn era | the family gate on the boxes, before the testnet and through it | box-hours | a finding against the family's band (a band narrowed, a rule added), never a chain event |
|
||||
|
||||
### 3.1 Ring A: the structural rules the chain runs per era
|
||||
|
||||
| Rule | Draw it reads | Known-failed shape | Clean refuse rate | Source |
|
||||
|---|---|---|---|---|
|
||||
| Band membership: `m` in the band, `W` in the allowed set, the shape in the set, every weight within B of the table with shfl and mulhi at or under their base, the sum 75 | the layer-1 draws | a draw outside the band (a unit test) | 0 by construction (the draw is bounded) | spec 1.13.1's form |
|
||||
| The day-key cost rule (AP-F4-1, class v5) | ROT, MUL | mul1all, mulnaf, day 29,337 | 6.1e-4 per day | f4-weakday.md 6.7 to 8; 8ca66afa |
|
||||
| The rotation class recorded: whether R leaves a product's low bits inside the index at the era's D | R, D | none (a record, not a refusal: restricting R to {29, 30} would cut the interleave's entropy from 31 to 2 values; the value-level test of ring B is the catch) | 0 | adv-cache-2 2.3 |
|
||||
| The weights' lossy share bound: or + mul + mulhi at most the table's 18 plus B, so the exhaustion bound `r^256` stays under 1e-18 (r under 0.85) | weights | the lossy corner of section 6 if its `r` reads over 0.85 | 0 by construction | row 90 (r = 0.681 at the table) and section 6 |
|
||||
|
||||
### 3.2 Ring B: the acceptance rule generalised to the family, per epoch
|
||||
|
||||
| Part | Generalisation | Known-failed shape (fires today) | Clean refuse rate (the cost) | Per-candidate cost |
|
||||
|---|---|---|---|---|
|
||||
| (a) (b) | unchanged | the class v2 census vectors (22, 37, 51) | 11.6 and 3.0 percent of rejections | microseconds |
|
||||
| (a') the freshness fixpoint over base then shadow | keyed on the family's shape (`is_family_shape`: shadow in {64, 128, 256} at 6,912 per iteration, `m` in {4, 8, 16}, the mix one-hot on the drawn width) in place of the one class v4 shape, in the draw's source rule and the acceptance alike | sub-version 1's eleven seeds | 83.5 percent of rejections (57.5 percent of candidates) | microseconds |
|
||||
| (c) the dynamic test with the shadow executed | the interpreter already runs any block length, any pass count and any width (the fold) | p23 at attempt 1 (the shared-operand idiom) | 0.9 percent | about 3 ms (64 units) |
|
||||
| (c') saturated sources | unchanged | the forced-saturation plant | 0.05 percent | in (c) |
|
||||
| (c'') the distinct-index ratio at 0.98 | `E_s = N - N^2 / (2 W_s / W)`: the window divided by the width | p15, p18, p19, p56 | 1.1 percent | 2.2 s at 2^20 |
|
||||
| (c''') the hot-item floor at 0.995 | the same `E_s`; the floor's clean spread re-read per stratum (section 6): a fixed 0.995 under a spread that moves with the shape or the width is either a liveness cost or a miss | seed 100767 (0.9919) and the nine live hot sets | 2.435 percent of accepted class v4 programs, +0.045 on the mean attempt | in the (c'') pass |
|
||||
| The per-site largest-256-item-bucket bound (NEW, the F8 tail's catch) | the largest count over a site's 4,096-word buckets in its window against `N / (W_s / 4096)`, in SIGMA of the bucket's expectation (`(max - E) / sqrt(E)`), refused above a band set from the clean spread. The ratio alone cannot carry the bound: a clean full-window site's largest of 65,536 Poisson(16) buckets reads 2.1x at +4.4 sigma, a quarter-window site's largest of 16,384 Poisson(64) buckets 1.5x at +4 sigma (the 16-era smoke run read ratios 2.2 to 2.4 on clean sites); the four tail seeds' 3.1x to 5.6x at narrow windows are +17 to +37 sigma | p4, p8, p10, p34 | measured in section 6 (the columns `bucket_z_max`, `bucket_win`) | one linear pass over the (c'') indices |
|
||||
| The index-bit bias read (NEW, adv-cache-2's value-level test) | per site, the one-count of each free index bit (bits above the width's alignment and below the window's cut) over the 2^20 evaluations, in sigma (`sigma = sqrt(N) / 2 = 512`); a 6-sigma band is a bias of 0.3 percent at 2^20 (the product law's bit 0 at 25 percent reads z about 512, bit 1 at 3/8 z 256, bit 3 at 3.1 percent z 64, bit 6 at 0.4 percent z 8, bit 7 at 0.2 percent z 4: passes). MEASURED on the first 16 drawn eras (11:21 BST, section 6): 7 of 16 accepted programs carry one site with a bias of 130 to 511 sigma at address bit R or R + 1 (the era's rotation), the era-stride class exactly, on programs (c''') passes at 0.9954 to 1.0000; the 9 others read under 3.8 sigma. So as a REFUSAL the read would redraw about 40 percent of epochs; its right use is as a record per era plus the structural lever (fold the product's low bits in `load_index` before the rotation, a next-class item), and the chip price is stated in section 5 | Devnet 3 site 0, era-drawn-28 site 15, the per-load candidate 0; and now the 7 of 16 | 7 of 16 at 6 sigma (a refusal is not the form); 0 of 16 at a 600-sigma band | one pass over the indices, 28 bit tests each |
|
||||
| The cap and the last resort | unchanged at 256; the last resort's own verdict is adv-accept-3's finding 1 (9 percent of last-resort programs fail (a)); the family keeps the open fix (continue past 256 until one passes) | the reject-everything mirror | 0 reached in 1.02 x 10^6 seeds | n/a |
|
||||
|
||||
### 3.3 Ring C: the offline tests per drawn era
|
||||
|
||||
| Test | What it reads | Known-failed shape | Clean cost per era | Sample needed for its own verdict |
|
||||
|---|---|---|---|---|
|
||||
| The attempts census (row 90's form) | every candidate of each seed's chain draw through the real rule; `r` per candidate, the parts' shares, the attempt histogram, `P(exhaust) = r^256`, 0 at the cap | the forced-exhaust plant (hits the cap, prints the last resort); the per-load class (r about 0.98) | 1,000 seeds: 202 s on 23 threads at the table (1.3 core-hours); about 5 core-seconds per accepted program with (c''') | 1,000 seeds per stratum reads `r` to 0.01 |
|
||||
| The F8-form uniformity census at 2^24 on the live dataset | the item histogram against the window model (the program's own 16 draws under the era's windows at the era's D), the top 0.1 percent within 1.2x, the hot-set test `X_f >= f`, the 6-sigma largest bucket, the per-site attribution | quarter-lines, half-lines, const-item (the plants); p31 (29.27x), p23 (4.82x) | 42 to 95 core-minutes per seed at 2^24 (the two halves of 32 on build-2 took 41 to 46 minutes at 30 to 32 cores; the 64 seeds of sub-version 3 took 69 minutes of the box); 64 seeds about 45 to 100 core-hours | 64 seeds per era bound the fraction of epochs over 1.2x at 3/64 (4.7 percent) at 95 percent: a calibration of ring B, not a hot-set hunt (adv-accept's sweep found 9 hot sets in 796,042 draws, 1e-5, every one refused by the floor) |
|
||||
| The stand-in gap at the era's D | the (c'') and bucket statistics on the closed form at D = 28 against the live dataset at the era's D on the same programs | none yet (the gap read 0.0004 at D = 28; a D where the windows disagree is the plant to build) | one 2^24 run per program, about 1 core-hour; 54 programs per era class | per D value, not per era |
|
||||
| The exhaustion count at scale | 10^4 to 10^5 chain-shaped seeds per stratum | the per-load class | 1.45 s per seed (F9's chain path): 4 core-hours per 10^4 | 10^5 per lossy corner reads the past-31 tail |
|
||||
|
||||
## 4. The board over the testnet period
|
||||
|
||||
### 4.1 F1 to F10 and the nine lanes, classified
|
||||
|
||||
| Row | Harness input | Class | Per what | Cost per run | Note |
|
||||
|---|---|---|---|---|---|
|
||||
| F1 shadow redundancy | the shape (block length, passes) | per family | each of the three shapes | 10^4 programs at 5 core-seconds each (the (c''') draw): 14 core-hours; 10^5: 140 | the 3.0 percent `SHADOW_REMOVABLE_MAX_PERMILLE` re-read per shape |
|
||||
| F2, adv-mixer, adv-mixer-3 (the round margin) | `m`, the day | per family | `m` in {4, 16} on 10 days each | index census 2^32 t about 7 core-hours per k per day; sac at 2^24 similar; SAT 1 core-hour per k | at `m = 4` the gate "no distinguisher beyond 2 of m" leaves 2 of 4 |
|
||||
| F3, adv-cache, adv-cache-3 (the chained cache) | the cache log2 (layer 2's doublings) | per family | each cache size step | minutes (the closure search), the curve by arithmetic | the fill is not drawn |
|
||||
| F4, adv-mixer-2 (the weak day) | `m`, the day key | per family and chain-run per day | 2^24 days per `m` | 379 s on 12 cores (1.3 core-hours) | the rule is chain-run; the census is its calibration |
|
||||
| F5 chip model | the band's top (`m = 16`, the dearest mix, `W = 4`, rung 2) | per family | once per band | minutes | the premium row per corner |
|
||||
| F6 verifier worst case | `m`, shape, `W`, N, the fill at D | per family | the top corner (`m = 16`, rung 2, `W = 4`) | 10^5 programs timed on the half-core proxy: about 100 core-hours | the x16 row decides whether 16 enters the band |
|
||||
| F7 era census | the era draw with the layer-1 draws added | per family | 2^24 era seeds | minutes | stride bijective, R, pos, M, the weights' corners uniform |
|
||||
| F8 uniformity | the era (every axis) | per era (ring C) and chain-run per epoch (ring B) | 64 seeds per drawn era | 45 to 100 core-hours | section 3.3 |
|
||||
| F9 acceptance edges, grinding, exhaustion | the era | per era (the exhaustion count) and per family (the 5090 grinding row) | 10^4 seeds per stratum | 4 core-hours | the GPU row once per band |
|
||||
| F10 ladder monotonicity | N | per family | once | the fast-time harness, minutes | unchanged by the draws |
|
||||
| adv-cache-2 (hot set, era stride) | the era | per era | 32 drawn programs at 2^23 | 0.7 core-hours per program | the value-level read is ring B now |
|
||||
| adv-accept, adv-accept-2, adv-accept-3 | the era | per era (the sweep and the attempts census) and per family (the A6000 header row) | as the attempts census | as above | the last-resort fix is the family's |
|
||||
|
||||
### 4.2 The testnet period as the window
|
||||
|
||||
Before the first miner: the per-family rows at the corners (F1 at three shapes, the mixer ladder at `m = 4` and 16, F4 per `m`, F6 at the top corner, F7 with the new draws), and the ring-C census on the corner cells and the random stratum of section 2.3. Through the period: every era the chain actually draws is a drawn era of the census (the chain's own eras are the best sample, the one the rule of three speaks about), so the F8-form census at 2^24 runs on each live era within its first day, and the attempts census on each; a live era that fails ring C is a finding against the band, filed and priced, never a chain event (the chain's ring-A and ring-B tests already passed it). The board's verdict at the end of the period: the count of live eras drawn and passed, the corner cells drawn and passed, the worst reading per test, the refuse rate per stratum, and the bound's arithmetic from those counts.
|
||||
|
||||
### 4.3 The proof of testing (the published format)
|
||||
|
||||
One JSON record per drawn era, in `docs/analysis/class-v6/gate-records/<family-id>/<stratum>/<era-label>.json`, the shape of `docs/plans/counter-asic-3-gate/*.json`:
|
||||
|
||||
```
|
||||
{ "family": { "id": "<sha256 of the genesis band table>", "spec": "docs/spec/01-lottery-hash.md@<commit>", "object": "igneum-pow@<commit>" },
|
||||
"era": { "label": "igneum-family-gate/era/<k>", "stratum": "random | lossy-cap | w4 | shape64 | ...",
|
||||
"draw": { "m": 8, "weights": [12,10,8,8,8,7,6,6,6,4], "width_words": 1, "shape": [256, 27], "R": 17, "M": "0x9ad30d99", "pos": [0,2,12,13] } },
|
||||
"harness": { "binary": "<sha256 of the pinned copy>", "box": "igneum-build-1", "lease": "<the lease line>", "started": "<UTC>", "ended": "<UTC>", "core_seconds": 7210 },
|
||||
"tests": [ { "name": "attempts-census", "seeds": "igneum-family-gate/program/<k>/epoch/0..999", "r": 0.681, "parts": {"a_prime": 0.835, ...}, "max_attempt": 28, "exhausted": 0, "verdict": "PASS", "log": "<path>", "log_sha256": "<hex>" },
|
||||
{ "name": "c3-floor", "floor": 0.995, "min_ratio": 0.9962, "min_site": 6, "verdict": "PASS", ... },
|
||||
{ "name": "bucket-bound", "bound": 2.0, "max_ratio": 1.31, "verdict": "PASS", ... },
|
||||
{ "name": "bit-bias", "band_sigma": 6, "max_z": 2.4, "verdict": "PASS", ... },
|
||||
{ "name": "f8-census-2e24", "seeds": 64, "over_1.2x": 2, "worst": 1.38, "hot_sets": 0, "verdict": "PASS", "known_failed_fired": ["quarter-lines", "const-item"], ... } ],
|
||||
"label": "internal research, not an independent review" }
|
||||
```
|
||||
|
||||
And one family summary per board cut, `docs/analysis/class-v6/gate-records/<family-id>/summary.md`: eras drawn per stratum; per test the count passed, the worst reading, the refuse rate, the known-failed cases fired (the count that bounds `delta`); the rule-of-three line per test (`3 / n` at 95 percent); the union bound's sum; the box-hours; the list of corner cells not reached. A reader checks the record by re-running one era from its label with the pinned binary; every number in the summary is a count over the records.
|
||||
|
||||
## 5. The honest line
|
||||
|
||||
What the family gate cannot see, and how a chip would use it.
|
||||
|
||||
| Blind spot | Why the gate misses it | What a chip gets from it | Bound today |
|
||||
|---|---|---|---|
|
||||
| The era-stride class, measured today at the bit level on 7 of 16 drawn eras (section 3.2): not diffuse, a 25 percent bias on ONE address bit (bit R, the product's bit 0 through `rotl(x * M, R)`) at one site, on programs every floor passes | the (c''') floor reads distinct counts, which a one-bit bias barely moves (0.9954 to 1.0000 on the seven); the bit read sees it at 130 to 511 sigma but refusing it would redraw about 40 percent of epochs; the class is structural (`load_index`'s form and the weights' products), so the catch is a next-class change to the address form, not a per-era floor | a store holding the favoured half of that site's window (128 MiB at D = 28) serves 75 percent of that site's reads instead of 50: 25 percent of 1/16 of a hash's reads, about 1.6 percent of reads at f = 1/2 from one site in about 40 percent of eras (arithmetic on the measured bias); the honest partial-store curve already costs 1.26x the ops at f = 1/2 (adv-cache-2 Q4), so the f = 1 verdict stands; the top-0.1-percent reading of the ledger (1.0024x) was the same class seen through the coarser instrument | the per-era record (ring A names the rotation class; ring B logs the bit read); the structural fix is the family's first owed item for the research lane |
|
||||
| A joint corner no stratum names | the 128 cells pin the axes with a KNOWN mechanism; a mechanism that needs, say, a particular `M` with a particular weight table and a particular shape is reached only by the random stratum, which bounds its fraction at 3/n | one era (180 days) of whatever the corner is worth, at most once per occurrence | 1/64 to 1/256 of eras untested at 95 percent (section 2.1); the chain-run rings catch the known shapes on every era |
|
||||
| The mixer below adv-mixer-3's bands | biases under 0.00018 (Q2b), correlations under 0.00073 with multi-bit masks, differentials under 2^-19, any structure a SAT model of 2 or more applications would expose; at `m = 4` the measured margin is 2 applications | a shortcut that saves applications against the 9,360 ops per item; none found at any k | the round margin, stated; `m = 4` is the corner that spends 4 of the 6 measured spare applications |
|
||||
| The verifier at `m = 16` and rung 2 on a 2019-class core | unmeasured (an estimate of 9 ms against the 10 ms gate) | nothing for a chip; a node tier retired if the corner is drawn | the row is owed before 16 enters the band |
|
||||
| The acceptance's stand-in at D = 28 against a live D = 29 | the windows cap `min(k_off, D - 26)` and the rotation's cut set move with D; the gap is measured at D = 28 only | a per-site concentration the closed form does not reproduce | 0.0004 at D = 28; unmeasured at 29 (ring C row) |
|
||||
| The union bound's own looseness | the per-test miss rates rest on 3 to 11 known-failed cases each (section 2.2), so the family's false-pass rate is not yet a number under 1 | nothing new; the bound is on what the gate claims, not on the hash | fixed by cases, not eras: 60 fired cases per test for `delta` under 0.05 |
|
||||
| The last-resort program | 9 percent of last-resort programs fail (a) and are handed out unchecked; a lossy-corner era that raised `r` toward 0.95 would reach it at 2e-6 per epoch | a program that re-reads one address in two loads of the same hash | ring A's lossy-share bound keeps `r` under 0.85 (`r^256` under 1e-18); the fix (continue past 256) is owed to the rule's owner |
|
||||
| The era redraw's own grindability | a ring-A redraw consumes the stream's next block, so an adversary who could steer `E_n` could steer which block is used; the era VDF of F7 (517 s on the fastest prover, 259x the window) closes the steering of `E_n` itself | nothing beyond F7's bound | F7 (a) PASS with the VDF in the node |
|
||||
|
||||
A GPU-like chip (the `f = 1` stored-dataset chip with a programmable core, chip-model-v3 section 5) loses nothing on any era of the family: every drawn parameter is firmware to it, and what the family costs it is the core sized for the band's top (the research lane's USD 30 to 60 per chip, modelled). The family gate's job is therefore not to defeat that chip (the identity of the research file's section 2 says the per-joule edge is the card's own idle, 2.1x at `k = 1`) but to make sure no drawn era hands ANY chip more than the one hash did: a per-era hot set, a per-era mixer shortcut, a per-era verifier miss. Everything in this document is a bound on that increment, and the increments found so far are 1.002x and 1.0024x.
|
||||
|
||||
## 6. Running now, and the coverage measured
|
||||
|
||||
Started 11:1x BST, 8 October 2026, on igneum-build-1 under `lease pool` (class measure, owner class-v6-family-gate), the scripts under `/srv/builds/_adv-family-gate/` (fg-setup.sh, fg-build.sh, fg-census.sh), every binary run from a pinned copy under `bin/<tag>/` with its sha256 (AP-H2's rule), logs and TSV rows under `logs/`, copies of the finished logs land under `docs/analysis/class-v6/logs/` on this branch with the full report.
|
||||
|
||||
| Run | What | Where | State |
|
||||
|---|---|---|---|
|
||||
| base | `v5_attempts_census` (the shipped class v5 draw: shape 256 x 27, `m = 8`, `W = 1`, the table weights) over f8-label seeds 1,000 to 11,000, each seed its own drawn era (M, R, pos, windows) | build-1, 24 cores, binary 4bec799c (8f481459 unmodified) | running from 11:16 BST |
|
||||
| fg1 random | `family_gate_era_census` with every axis drawn (shape in {64, 128, 256}, `m` in {4, 8, 16} recorded, `W` over {1, 4}, weights at B = 4 with shfl and mulhi never raised) | build-1 | the harness built at 11:2x BST; the smoke run and the chunks follow |
|
||||
| fg1 corners | the same harness pinned per cell: `IGNEUM_FG_LOSSY_CAP`, `IGNEUM_FG_WIDTH=4`, `IGNEUM_FG_SHAPE=64`, the combinations | build-1, build-3, build-4 as the pool frees | queued |
|
||||
|
||||
The coverage table (filled from the TSV rows at 16:xx BST for the 17:00 cut; the full read at 09:00 BST tomorrow):
|
||||
|
||||
| Stratum | Eras drawn | `r` per candidate | Mean attempt, max | Exhausted | min (c'') ratio (the worst era) | Under 0.995 (the (c''') refuse rate on the accepted draw) | Largest bucket ratio (worst era, site) | Largest bit bias in sigma (worst era, site, bit) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| base (shipped draw) | pending | | | | | | | |
|
||||
| random (every axis drawn) | pending | | | | | | | |
|
||||
| lossy cap | pending | | | | | | | |
|
||||
| W = 4 | pending | | | | | | | |
|
||||
| shape 64 | pending | | | | | | | |
|
||||
|
||||
## 7. Owed by 09:00 BST on 9 October (the full report)
|
||||
|
||||
1. The coverage table of section 6 filled from the logs, with the per-stratum refuse rates and the union-bound arithmetic on the measured numbers, and the TSV rows and logs under `docs/analysis/class-v6/logs/`.
|
||||
2. The clean spread of the bucket ratio and the bit bias per stratum, so the two new ring-B tests get a bound and a clean refuse rate (not a guess).
|
||||
3. The lossy-corner `r` against the 0.85 line, and the exhaustion count at 10^4 seeds on that corner.
|
||||
4. The width-4 reading of (c'') and (c''') with the divided expectation, and the spec line for 1.4.6.5.
|
||||
5. The mixer rows at `m = 4` and 16 (adv-mixer-3's index and sac at k = 2 on two days; the SAT at k = 2) as the per-family corner rows, if the boxes have the cores after the other lanes' jobs arrive; else the plan with its hours.
|
||||
6. The F8-form census at 2^24 on 64 seeds of one lossy-cap era and one width-4 era, the first ring-C rows on the family (about 2 box-hours each).
|
||||
7. The gate-record JSON written for every era of the census from the TSV (the format of 4.3), and the summary.
|
||||
|
||||
## 8. Sources
|
||||
|
||||
In-tree (the mirror's master and lane branches at 8 October 2026, 11:00 BST): `docs/plans/cryptanalysis/in-house-pass.md` (sections 13 and 14, the nine lanes' close); `docs/analysis/attack-pass-2026-10.md` (F1 to F10; the sub-version 1, 2 and 3 re-gate tables; the tail attribution of 8 October, 09:40 to 09:46 BST); `docs/analysis/cryptanalysis/report-mixer-3.md` on branch adv-mixer-3 at 981bfff2 (the round margin); `docs/analysis/cryptanalysis/report-chained-cache-2.md` on adv-cache-2 at bfc3746c (section 2.3, the era-stride table); `docs/analysis/cryptanalysis/report-acceptance-rule-3.md` on adv-accept-3 at 7826d2b2 (Q1, Q1b); `docs/analysis/cryptanalysis/report-acceptance-rule.md` on adv-accept at 8f188e5a (row 90, the floor's tally, gap-deep); `docs/spec/01-lottery-hash.md` 1.4.6, 1.4.7, 1.8.4, 1.13; `docs/design/class-v5-stored-state.md` on class-v5 at 8f481459 (sections 11 and 14) and `docs/design/class-v5-harness/` (v5-attempts-census-1000.log, v5-census-4600-0.log); `docs/plans/counter-asic-3-status.md` section 7c; `docs/plans/read-width.md`; `docs/analysis/counter-asic-4-research.md` on counter-asic-4 at 7a133d76 (15.1a, 15.1b, 20.2a, 20.2b, 20.3); `docs/design/class-v6-rotating-family.md` on the same branch (the outline).
|
||||
|
||||
Public, read 8 October 2026:
|
||||
|
||||
- Hanley, J. A. and Lippman-Hand, A., "If nothing goes wrong, is everything all right? Interpreting zero numerators", JAMA 249(13), 1743 to 1745, 1983; the rule of three as a one-sided 95 percent bound 3/n on a binomial rate with zero events in n trials: https://en.wikipedia.org/wiki/Rule_of_three_(statistics)
|
||||
- Wilks, S. S., "Determination of Sample Sizes for Setting Tolerance Limits", Annals of Mathematical Statistics 12(1), 91 to 96, 1941; the nonparametric tolerance limit on the extreme order statistic, the same arithmetic as 2.1: https://www.projecteuclid.org/euclid.aoms/1177700380
|
||||
- Böhme, M., "STADS: Software Testing as Species Discovery", ACM TOSEM 27(2), 2018; the Good-Turing estimate `f_1 / n` of the discovery probability as the residual-risk bound of a testing campaign with no finding (the fuzzing-coverage view of section 2): https://arxiv.org/abs/1803.02130
|
||||
- OSTIF, "Four audits of RandomX for Monero and Arweave have been completed: results" (2019; Trail of Bits, X41 D-SEC, Kudelski Security, Quarkslab; no critical finding; the one-round AES diffusion and the BLAKE2 rows): https://ostif.org/four-audits-of-randomx-for-monero-and-arweave-have-been-completed-results/
|
||||
- Quarkslab, "Security audit of Monero RandomX" (2019; the "alternative configurations" caveat: the audits bounded the shipped configuration, which is the single-member reading this document generalises): https://blog.quarkslab.com/security-audit-of-monero-randomx.html
|
||||
- Least Authority, ProgPoW algorithm audit (final report 9 September 2019; the Ethereum Cat Herders, the Ethereum Foundation and Bitfly as clients): https://leastauthority.com/blog/2019/09/09/
|
||||
- The hardware audit of ProgPoW (Bob Rao, 2019; "works well against conventional ASIC strategies", the memory-intensive threat kept open), as reported: https://criptonoticias.com/mineria/desarrolladores-ethereum-presentan-resultados-auditoria-progpow
|
||||
443
docs/analysis/class-v6/hardware-future.md
Normal file
443
docs/analysis/class-v6/hardware-future.md
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
# Class v6 research lane B: the hardware future, five years out
|
||||
|
||||
Lane B of the class v6 rotating-family research (the founder's word of 8 October 2026, 11:1x UK: "see if anything can be
|
||||
optimised, added or invented"). First cut landed 8 October 2026, 11:5x UK; the full report fills the same file. Every
|
||||
figure carries a label: **measured** (a number read off an instrument in this repository, with the file), **claimed**
|
||||
(a vendor's or a paper's number, with the URL and the date read), **modelled** (arithmetic on claimed figures by the
|
||||
method of `docs/analysis/chip-model-v3.md` section 5), **approximate** (from memory or an estimate; the sensitivity is
|
||||
given). Nothing here is a measurement of a chip. Reading public research is in-house; nothing was paid for or asked of
|
||||
anyone outside.
|
||||
|
||||
The question, as the coordinator put it: for each memory or packaging line, what does it do to a chip's cost per
|
||||
dependent random read over a dataset of 1 to 8 GiB that grows with chain state (energy per read, latency, capacity cost
|
||||
per GB, availability to a non-hyperscaler), what k band does it give the chip five years out, and which ONE of the four
|
||||
v6 layers (1: per-era parameter draws; 2: the state-sized dataset with a floor; 3: scheduled family epochs; 4: the
|
||||
(c''') acceptance floor and the F8 uniformity test per era) blunts it, with a number. Where a line beats every layer,
|
||||
this file says so with the number.
|
||||
|
||||
## 0. One page
|
||||
|
||||
The reads are the hash. Under class v3 the RTX 5090 spends 2.40 microjoules per hash on 128 dependent reads, 18.8 nJ
|
||||
per read all-in, and the memory system itself spends 2.0 nJ of that (chip-model-v3 5.3, modelled); the card's own
|
||||
marginal per dependent DRAM read is 10.9 nJ unlocked and 8.7 nJ at the 1,300 MHz lock (measured 8 October 2026,
|
||||
counter-asic-4-research 15.1a). Every chip in this file is a machine that pays the memory's nanojoule and not the
|
||||
card's ten, plus whatever the shadow (the program work drawn into the memory wait) forces it to pay at `k` times the
|
||||
GPU's cost per op. That identity does not change with any technology below; what changes is the memory's nanojoule,
|
||||
the rate a chip can read at, what a GB costs, and who can buy it.
|
||||
|
||||
**The three findings that change v6's design**
|
||||
|
||||
1. **The strongest five-year chip is not a DRAM chip. It is a 2 GiB SRAM full store on one reticle of merchant N2, and
|
||||
none of the four layers reaches it.** TSMC N2 reads 38 Mb/mm^2 of SRAM (claimed, IEEE Spectrum, 12 December 2024,
|
||||
volume in 2025), so 2 GiB is about 452 mm^2 of macro, one die under the 858 mm^2 reticle, roughly USD 400 to 600 of
|
||||
silicon at a USD 30,000 wafer (approximate). It has no activate ceiling, so its rate is power-bound: about 2,100
|
||||
MH/s at 300 W, 0.14 microjoules per hash, **13x to 17x the 5090 per joule at zero shadow and about USD 0.3 per
|
||||
MH/s** against the 5090's 14.7 and the GDDR7 chip's 2.8 (modelled; the wire energy, 0.5 to 2.0 nJ per read, is the
|
||||
sensitivity). Layer 2's floor moves its capex, not its joules: at 4 GiB it is two dies, at 8 GiB four, USD 1,000 to
|
||||
2,500 of silicon, and its edge per joule falls only from 17x to about 13x because an inter-die hop costs 0.27 nJ
|
||||
(UCIe, claimed 0.5 pJ/bit). A floor that would blunt it on joules (16 to 32 GiB, eight to sixteen reticles) retires
|
||||
every honest card under 32 GB first. The only lever that reaches it is the shadow at `k`: with the shadow core on
|
||||
the same N2 die the ALU band is 0.3 to 0.8 (the record's), and the class v4 premium holds **4.8x at k = 0.5, 2.7x
|
||||
at k = 1**; at the 5090's whole ALU budget (about 330,000 ops per hash, 575 W) 3.7x and 2.0x. The design change:
|
||||
layer 1's program-length draw is sized against this chip, not the GDDR7 board, with its lower bound at the shadow
|
||||
that holds the record's 2.1x today and its upper bound at the honest cards' full latency shadow, re-based every
|
||||
family epoch (layer 3) on the cards then mining; and the public "2x" line is not reachable in this model against
|
||||
this chip at any `k` under 1. What slows it is money and time (an N2 project, USD 100 M to 500 M and 18 to 24
|
||||
months, approximate), which is the clock of Counter ASIC 3.0 item 4, not a hash property.
|
||||
|
||||
2. **Per-bank processing-in-memory is structurally blind to this hash; the real near-memory threat is the custom
|
||||
HBM4E base die, and layers 1 to 4 do nothing to it.** HBM-PIM, AiM, LPDDR5X-PIM and UPMEM put a compute unit beside
|
||||
each bank (or a DPU per 64 MB); a dependent read's next address is uniform over the dataset, so it lands in the same
|
||||
bank with probability bank bytes over dataset bytes: **1.6 percent at 2 GiB on a 32 MB bank, 0.4 percent at 8
|
||||
GiB**, and UPMEM has "no direct communication channel among DPUs" (claimed, the PrIM paper), so the other 98
|
||||
percent of reads go to the host. The unit that can follow the chain across banks is a controller on the stack's
|
||||
base die, which is exactly what TSMC's custom C-HBM4E is: "the custom base die will integrate memory controllers and
|
||||
PHY" on N3P at 0.75 V, "2x the power efficiency", Micron production 2027, SK hynix HBM4E in 2026 (claimed,
|
||||
TrendForce, 1 December 2025). That is the `f = 1` chip of the record with its controller moved into the stack: about
|
||||
0.9 to 1.0 nJ per read, and HBM4's 32 channels (JEDEC JESD270-4, April 2025) double the activate-bound ceiling per
|
||||
stack, so **6.5x to 14x per joule at zero shadow** (the ceiling unmeasured, as the record's HBM rows are). The four
|
||||
layers act on the program, the item map and the capacity; this chip runs any program and holds 36 to 64 GB. What
|
||||
brakes it until about 2028 is availability (HBM allocated to AI, 20 to 26 week leads, Samsung asking USD 4 to 5 per
|
||||
Gbit for HBM4 against 1.5 for HBM3E, October 2026) and the shadow at `k`: 4.4x at k = 0.5, 2.6x at k = 1.
|
||||
|
||||
3. **The denominator is the wrong card.** Every chip edge in the record is quoted against the 5090 at 2.40 microjoules.
|
||||
The Apple M5 Max measured 0.78 microjoules per hash at the GPU-plus-DRAM meter (latency-shadow-2026-10-06, 6
|
||||
October 2026), 3.1x the 5090 per joule, and LPDDR6 SoCs (JEDEC JESD209-6, 2025; 14.4 Gbps, 32-byte atoms) are that
|
||||
tier's next step. Against the M5 Max the GDDR7 chip reads 1.7x, one HBM3 stack 2.4x, the HBM4 base die 3.6x to
|
||||
4.4x, the N2 SRAM die about 5x, and with the shadow at k = 0.5 the SRAM die reads about 3x. The honest joule, not
|
||||
the 5090's, is the chain's resistance, and the same chips are 2x to 3x less frightening against it. The design
|
||||
change: v6's acceptance floor (layer 4) and the shadow sizing (layer 1) are scored per card tier with the
|
||||
unified-memory SoC tier as the reference joule, the dataset is kept inside 16 GB unified memory (8 GiB at the top of
|
||||
the schedule does that), and the public text states the edge over the best honest joule, which is the number a
|
||||
miner can act on.
|
||||
|
||||
The honest line, in one sentence: five years out a 2 to 8 GiB dataset fits in one to four reticles of merchant SRAM at
|
||||
USD 500 to 2,500 of silicon, every DRAM line converges on the same 0.5 to 1.0 nJ per read with its controller in the
|
||||
stack, and no layer of the four touches either; the shadow at the measured `k` band holds 3x to 5x, the honest tier's
|
||||
own efficiency halves that again, and the clock (project cost against daily issuance) is the wall that is left.
|
||||
|
||||
## 1. The method and the denominators
|
||||
|
||||
| Quantity | Value | Label | Source |
|
||||
|---|---|---|---|
|
||||
| The 5090 at class v3 | 136.1 MH/s at 326 W, 2.40 microjoules per hash, 17.5 G dependent reads per second, 415 ns at 256 lanes, a 32-byte sector per 4-byte read | measured | `docs/bench-log.md`; chip-model-v3 5.1 |
|
||||
| The 5090 per dependent DRAM read, the whole card's marginal | 10.9 nJ unlocked, 8.7 nJ at the 1,300 MHz lock (dram_chase_1g, 18.17 G reads per second) | measured, 8 October 2026 | counter-asic-4-research 15.1a |
|
||||
| The 5090 L2 hit, per dependent read | 2.4 nJ unlocked, 1.4 nJ at the lock | measured | the same |
|
||||
| The 5090 per counted ALU op (the class v4 shadow's mix) | 11.3 pJ unlocked, 6.2 pJ at the lock (int_arx); the shadow's own 10.4 and 6.5 | measured | the same; counter-asic-4-research section 1 |
|
||||
| The 5090 at the 1,300 MHz lock, class v3 | 134.6 MH/s at 223 W, 1.67 microjoules | measured, 7 October 2026 | counter-asic-3-status 7c, the clock grid |
|
||||
| The class v4 premium `F` on the 5090 | 1.10 microjoules unlocked (145 W), 0.65 at the lock (82 to 88 W) | measured | the same |
|
||||
| Apple M5 Max, class v3, GPU plus DRAM channels | 27.08 MH/s at 21.0 W, 0.78 microjoules; class v4 at 100,000 ops 1.40 (+16 W); 6.9 pJ per counted op | measured, 6 October 2026 (the SoC's other rails and the wall are not in the 21 W) | `docs/analysis/latency-shadow-2026-10-06.md` section 3 |
|
||||
| RX 9070 XT, class v3 | about 18.6 MH/s at about 304 W, 10.6 microjoules; 2.4 G reads per second | rate measured, watts approximate | `docs/analysis/horizon/algorithm.md` 5.1 |
|
||||
| The memory system's own cost per random 32-byte read | GDDR7 2.0 nJ, HBM3 1.2 nJ | modelled on O'Connor et al. 2017 Tables 2 and 3 and Samsung's pJ per bit roadmap | chip-model-v3 5.3 |
|
||||
| The record's `f = 1` chips at zero shadow | GDDR7 board 166 MH/s at 77.6 W, 0.466 microjoules, 5.1x; one HBM3 stack 83.6 MH/s at 26.8 W, 0.321, 7.5x; eight stacks 666 MH/s at 174 W, 0.262, 9.2x | modelled | chip-model-v3 5.4 to 5.6 |
|
||||
| The chip's `k` on the shadow's work | ALU 0.3 to 0.8; int8 tile 0.03 to 0.3 (the worse lever); L2 hit 0.1 to 0.3; shuffle 0.4 to 0.7 | the GPU side measured, the chip side claimed | counter-asic-4-research 15.1a |
|
||||
|
||||
The chip's energy per hash is `E = 128 x E_read + P_static / rate + k x F`, the rate the smaller of the memory's
|
||||
activate-bound ceiling over 128 and the power budget over the per-hash energy, and the edge is the card's microjoules
|
||||
over the chip's. Two `k` columns appear below: `k_read`, the memory's energy per dependent read over the 5090's measured
|
||||
10.9 nJ (what the technology itself buys, independent of any program), and the shadow `k` band, the chip's cost per
|
||||
forced op over the GPU's, which no memory technology changes (it is a logic-node question: a chip core at N2 against a
|
||||
GPU at N3 or N4 sits at the band's low end, 0.3; at the same node, 0.5 to 0.8; approximate).
|
||||
|
||||
Reads are counted at the part's atom: 32 bytes on GDDR7, HBM3, HBM4 and LPDDR6 (each gives a 32-byte minimum access;
|
||||
JEDEC, claimed), 64 bytes on an SRAM macro or a line-based part. The class v6 read-width draw (4 to 64 bytes per read
|
||||
under layer 1) does not move any row: a chip's controller fetches the atom whatever the hash asks for, as the 5090
|
||||
fetches its 32-byte sector for a 4-byte load, and the record's w64 reading (the 5090 bandwidth-bound at 71.9 MH/s,
|
||||
chip-model-v3 5.7) says a wider honest read costs the card and gives the chip nothing. The dataset's size enters only
|
||||
through capacity cost and die count; its growth (layer 2) enters through the number of dies or stacks a chip must carry
|
||||
at each family epoch.
|
||||
|
||||
## 2. The table
|
||||
|
||||
Energy is per dependent random read at the part's atom. Latency is the read's own (controller to data), not the GPU's
|
||||
queueing. Cost per GB is factory-gate where a source gives it, with contract pricing about 2x (Silicon Analysts, October
|
||||
2026). "Edge" is per joule against the 5090 at class v3 and zero shadow; the bracket is against the M5 Max's 0.78.
|
||||
"Year" is when a non-hyperscaler could put the part on a board. Every chip-side figure is modelled unless labelled.
|
||||
|
||||
| Technology | Year | Energy per dependent read | Latency | Cost per GB | Availability to a non-hyperscaler | `k_read` (over 10.9 nJ) | Edge at zero shadow, per joule | The layer that blunts it, and by how much |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| GDDR7 on a PCB, 16 devices, a 28 nm controller (the record's `f = 1` chip) | now | 2.0 nJ (modelled); 4.5 pJ per bit streaming (claimed, Micron) | about 55 ns controller, tRC about 45 ns | USD 10 (2 GB parts, ending) to 20 to 23 (3 GB parts at USD 60 to 70), September 2026 (claimed, TrendForce) | anyone, through distribution; the 2026 DRAM price cycle (LTAs USD 7.8 to 21 per GB, May 2026, claimed) roughly triples the chip's memory bill against 2025 | 0.18 | 5.1x (1.7x); 166 MH/s at 78 W | none of the four; the shadow at k = 1 holds 2.1x (the record); the dataset's size does not matter because the chip over-provisions capacity for channels (16 devices whatever the dataset) |
|
||||
| GDDR7 at 36 to 48 Gbps, 4 and 6 GB devices | 2027 to 2028 (claimed, Micron roadmap via Guru3D and OC3D) | the same 2.0 nJ: the activate ceiling and the row energy do not move with the pin rate | the same | per-GB falls, per-channel rises: a random-read chip buys channels, not bytes | anyone | 0.18 | 5.1x | none; denser devices HURT the chip (fewer channels per GB), so this line goes the chain's way |
|
||||
| HBM3E, one stack, a 28 nm controller on a one-stack interposer | now | 1.2 nJ (modelled); 4.05 pJ per bit streaming (claimed, Samsung) | about 50 ns | USD 8.3 factory gate (USD 300 per 36 GB), about 2x on contract (claimed, Silicon Analysts, October 2026); plus about USD 200 of interposer | Tier-1 volume pricing, 20 to 26 week leads, "supply constrained through 2026" (claimed); a one-stack buyer pays broker prices | 0.11 | 7.5x (2.4x); 84 MH/s at 27 W (ceiling unmeasured, 10.7 G reads per second; 2.3 G at the JEDEC tFAW, which would read 1.8x) | none; the shadow at k = 1 holds 2.4x (the record) |
|
||||
| HBM4 (JESD270-4, April 2025): 2,048-bit, 32 channels x 2 pseudo-channels, 8 Gbps, 2 TB/s, 36 to 64 GB, VDDQ 0.7 to 0.9 V, a TSMC N12 base die at 0.8 V "1.5x efficiency" (claimed) | 2027 to 2028 for a non-hyperscaler | 1.0 to 1.1 nJ (modelled: the I/O term at 0.8 V against 1.1 V, the 909 pJ row activation unchanged) | about 50 ns | USD 15.3 (USD 550 per 36 GB, October 2026 estimate); Samsung asking USD 4 to 5 per Gbit for 2027 (USD 32 to 40 per GB) (claimed) | allocated to AI accelerators through 2027; "mid-to-high $4 per gigabit" in this month's negotiations (claimed, 2 October 2026) | 0.10 | 4.5x to 11x (1.5x to 3.6x): the 32 channels double the activate-bound ceiling per stack if tFAW is per channel (unmeasured) | none of the four; the shadow at k = 0.5 holds 4.4x, at k = 1 2.6x |
|
||||
| Custom HBM4E base die (C-HBM4E): the controller and PHY in the stack on N3P at 0.75 V, "2x the power efficiency" (claimed, TrendForce, 1 December 2025); Micron 2027, SK hynix 2026 | 2028 or later for a non-hyperscaler | 0.9 to 1.0 nJ (modelled: no interposer crossing for the controller's traffic) | about 45 ns | HBM4E class, USD 20 to 40, plus a custom base die (USD 50 to 100 per stack, approximate) | by design a per-customer product of the three DRAM makers; the named customers are NVIDIA and Google; a miner-maker of Bitmain's size could commission one (approximate) | 0.09 | 6.5x to 14x (2.1x to 4.4x) | **beats all four layers**; the shadow at k = 0.5 holds 4.4x, at k = 1 2.6x; the shadow core sits on the same N3P base die |
|
||||
| Fine-grained, hybrid-bonded DRAM on logic (FGDRAM-class 256-byte rows, "tFAW effectively eliminated", 51 percent lower energy per access, 4x bandwidth, claimed, O'Connor et al., MICRO 2017; hybrid-bonded stacks and 3D DRAM on the 2028 to 2030 roadmaps, approximate) | 2029 to 2031 | 0.5 to 0.7 nJ (modelled: 230 pJ activation for a 256-byte row plus about 1.2 pJ per bit of movement over bonded pads) | about 40 ns | HBM class, USD 15 to 40 (approximate) | custom, the three DRAM makers, top customers first | 0.05 | 12x to 20x (4x to 6x); about 125 MH/s per stack at 19 W, 1,000 MH/s at 150 W for eight | **beats all four layers**; the shadow at k = 0.5 holds 5.1x, at k = 1 2.8x |
|
||||
| Per-bank PIM: Samsung HBM-PIM (a PCU per bank, 2021), SK hynix GDDR6-AiM (16 Gbps, 1.25 V, 2022) and AiMX (32 GB card), Samsung LPDDR5X-PIM (Hot Chips, 25 August 2026, 614 GB/s internal) and LPDDR6-PIM (JEDEC work "substantial progress") | now to 2027 | the host memory's own: 1.8 nJ (HBM2), 2.0 (GDDR6 class); the PIM unit never sees the read it would need | the same | the host part's plus a premium | HBM-PIM and AiM were samples and prototypes; LPDDR5X-PIM mass production "as early as 2027" (claimed) | n/a: no path for a cross-bank dependent read | none beyond the base-die row: the unit serves 1.6 percent of reads at 2 GiB (a 32 MB bank), 0.4 percent at 8 GiB | layer 2, trivially: any dataset over a bank; the number is bank bytes over dataset bytes |
|
||||
| UPMEM DPU-in-DRAM: 128 DPUs per 8 GB DIMM, 64 MB MRAM per DPU, 350 MHz, MRAM by DMA at most 628 MB/s per DPU, "no direct communication channel among DPUs" (claimed, the PrIM paper, 2021); 1.2 W per 4 Gb chip, 10x DRAM's price at sampling, 1.5x projected (claimed, The Next Platform, 2020) | now | about 0.3 microseconds per 64-byte DMA at 350 MHz (approximate from the paper's alpha-plus-beta model); 2 GiB spans 32 DPUs and every cross-DPU hop is a host round trip of microseconds | microseconds | 1.5x to 10x DDR4 | anyone, in server DIMMs | n/a | none: dead at any dataset over 64 MB | layer 2's floor alone (2 GiB = 32 DPUs with no path between them) |
|
||||
| LPDDR6 (JESD209-6, 2025): 24-bit channels as two 12-bit sub-channels, 32-byte minimum access, 10.7 to 14.4 Gbps, 4 to 64 Gb dies (claimed, JEDEC via PCWorld and HotHardware) | 2026 to 2028 | 1.5 to 2.0 nJ (approximate: short rows, low-voltage I/O over a PCB, 16 to 32 banks per channel) | about 60 ns | USD 8 to 21 in the 2026 cycle (LTAs, claimed); about USD 3 to 4 in 2024 (approximate) | anyone; it is also the honest SoC tier's memory | 0.15 to 0.18 | about 4x to 5x for a 32-channel controller chip (1.3x to 1.6x against the M5 Max, which already IS this part with a GPU) | none needed: the honest tier converges on it; see finding 3 |
|
||||
| 3D DRAM (Samsung VS-DRAM 16 layers, VCT 4F2; Neo 3D X-DRAM 230 layers; SK hynix and Micron stacked cells), "from 2030" (claimed, heise, 29 May 2024) | 2030 or later | no vendor claims a latency or activate change; the row energy class is the planar part's (approximate) | the same | lower per GB after 2030 | the three makers | 0.1 to 0.2 | the DRAM rows above | none needed inside five years; it lowers the chip's and the card's capacity cost alike |
|
||||
| RLDRAM 3 (Micron, 2011): tRC under 10 ns, 16 banks, no activate command, "SRAM-like random access" (claimed, Micron); 576 Mb and 1.125 Gb devices at 2,133 Mb/s (approximate) | now, in small volume | 2 to 4 nJ (approximate: a full small-row access per read; no energy figure found, the datasheet's power calculator exists) | under 10 ns | USD 400 to 800 (USD 50 to 100 per 1.125 Gb device, approximate, a networking part) | anyone, catalogue | 0.2 to 0.4 | about 2 G random reads per second PER DEVICE (16 banks over 8 ns), 32 G for 16 devices: 250 MH/s per board, about 5x per joule (approximate) | layer 2: USD 3,000 to 6,000 of memory at 8 GiB; the line to watch is AMD's "Folded Banks" (ISCA 2025): 8x the activate parallelism in HBM gives 6.7x the irregular bandwidth (claimed, the abstract) |
|
||||
| FPGA with HBM2e: AMD Alveo V80 (Versal XCV80, 32 GB, 820 GB/s, 190 W, USD 9,495 MSRP, May 2024, claimed), Versal HBM VH1782, Altera Agilex 7 M (two HBM2e stacks, 820 GB/s), Alveo U55C USD 4,747 | now | HBM2 class, 1.8 nJ, but the fabric's controller reaches only the JEDEC tFAW ceiling: 2.3 to 2.4 G reads per second per stack (Shuhai, FCCM 2020; the record) | about 100 ns | USD 300 per GB of card | anyone, 10 to 24 week leads | 0.17 | 0.6x (0.2x): 4.8 G reads per second for two stacks, about 37 MH/s at 150 W, at 5x the 5090's price | none needed; no HBM3E FPGA was found announced (unverified) |
|
||||
| Wafer-scale SRAM: Cerebras WSE-3, 44 GB SRAM, 21 PB/s, 900,000 cores, 46,225 mm^2 on N5 (claimed, Cerebras and The Next Platform, March 2024); CS-3 about 23 kW and "maybe $2.5 million" (claimed, approximate) | now, by the system | about 7 nJ (approximate): a 512-bit reply crossing about 140 mm of mesh on average at about 0.1 pJ per bit per mm | about 0.5 microseconds across the wafer | USD 50,000 per GB | by the system only | 0.6 | 0.1x to 0.4x: a uniformly random dependent chain is bisection-bound on a 2D mesh (about 110 G reads per second per wafer, up to 500 G with the dataset replicated twenty times), 5 to 22 M reads per second per watt against the 5090's 54 M; 1/1,000 per dollar | none needed; layer 2 removes the replicas as the dataset grows |
|
||||
| CXL memory pools (CXL 2.0 and 3.x; about 70 ns of controller on top of local DDR5, 100 to 160 ns on Xeon 6 against 75 local, claimed, Introl, February 2026; USD 4 to 7 per GB before the 2026 cycle) | now | DDR5's 2 to 3 nJ plus the link | 170 to 250 ns through a host | USD 4 to 21 | anyone | 0.2 to 0.3 | 0.4x: 500 M 64-byte transactions per second per x8 device at about 25 W (approximate) | none needed; a capacity tool, not a random-read engine |
|
||||
| Optical I/O: Ayar Labs TeraPHY (UCIe-compliant, about 5 pJ per bit, USD 500 M raised 3 March 2026, claimed); Celestial AI Photonic Fabric (6.2 pJ per bit, about 120 ns round trip, claimed) | 2027 or later | 6x to 8x the interposer's 0.8 pJ per bit per bit moved | plus 120 ns | n/a | n/a | worse than copper for this traffic | no edge: a pooling fabric; the dependent chain wants the memory closer, not farther | none needed |
|
||||
| Chiplets and die-to-die: UCIe 0.25 to 0.5 pJ per bit (claimed, UCIe via SNIA); BoW 0.5 to 0.7; CoWoS-S USD 600 to 900 per H100-class package, a one-stack interposer about USD 200 (the record) | now | adds 0.27 nJ per 544-bit read that crosses a die boundary | plus 5 to 10 ns per hop | the package: USD 100 to 200 organic with UCIe-S, USD 200 to 900 for 2.5D | UCIe IP from several vendors; CoWoS capacity booked by AI through 2027 (approximate) | n/a | moves the SRAM chip's multi-die rows (below) and lets a 28 nm controller sit beside memory on an organic package | none needed |
|
||||
| SRAM scaling at 2 nm: TSMC N2 38 Mb/mm^2 HD, +11 percent over N3E (claimed, IEEE Spectrum, 12 December 2024); wafers about USD 30,000, booked to 2028 (claimed, 2026) | now (N2 in volume from 2025) | 1.0 nJ for a 64-byte read from a 452 mm^2 array (approximate; 0.5 to 2.0: the global wire at about 1.3 pJ per bit across a 24 mm die is the term) | 10 to 20 ns | USD 200 to 300 per GB of silicon (one 600 mm^2 die, about USD 400 to 600, holds 2 GiB) | N2 is a merchant node: any customer with a project (Apple, AMD, NVIDIA, MediaTek, Qualcomm; the Bitcoin chip makers are on N3 and N4 class already, approximate) | 0.09 | **13x to 17x (4x to 5x)**: no activate ceiling, power-bound at about 2,100 MH/s per die at 300 W, USD 0.3 per MH/s of silicon; two dies at 4 GiB and four at 8 GiB cost USD 1,000 to 2,500 and read about 13x | **beats layers 1, 3 and 4; layer 2 cuts its capex, not its joules**: one reticle at 2 GiB, two at 4 GiB, four at 8 GiB; the shadow at k = 0.5 holds 4.8x, at k = 1 2.7x; on the M5 Max's joule about 3x at k = 0.5 |
|
||||
|
||||
Density against the schedule: SRAM gains 6 to 11 percent per node every two to three years (N3E +6, N2 +11, claimed),
|
||||
the dataset doubles every four years (spec 1.13.3: 2 GiB at genesis, 4 GiB at year 4). The SRAM chip loses that race
|
||||
slowly: its die count doubles each doubling and its per-read energy rises 0.1 to 0.3 nJ per extra hop. It does not lose
|
||||
it inside five years.
|
||||
|
||||
## 3. What each line does to the four layers, read the other way
|
||||
|
||||
| Layer | What it does to the lines above | Number |
|
||||
|---|---|---|
|
||||
| 1. Per-era draws (mixer rounds, op-mix weights, read width, program length, shadow placement) | The read width and the mixer draws move no chip row: every chip in the table stores the dataset and reads the atom. The program length IS the shadow, and it is the only draw that reaches the memory-system chips | at `F` = 1.10 microjoules (class v4) the strongest DRAM chip reads 2.6x to 2.8x at k = 1 and 4.4x to 5.1x at k = 0.5; at the full 5090 shadow (`F` about 2.0) 2.0x and 3.7x |
|
||||
| 2. The state-sized dataset with a floor | Kills per-bank PIM and UPMEM outright; raises the SRAM chip's die count and capex; does nothing to any DRAM chip, which carries 24 to 64 GB per stack and over-provisions capacity for channels anyway | PIM serves 1.6 percent of reads at 2 GiB, 0.4 at 8; the SRAM chip's silicon USD 500 at 2 GiB, 1,000 at 4, 2,500 at 8, its joules 17x to 13x |
|
||||
| 3. Scheduled family epochs every 180 days | None of the chips in the table is fixed-function; the controller chip runs any program and the SRAM chip's shadow core is a sequencer | 0 |
|
||||
| 4. The (c''') floor and the F8 uniformity test per era | Keeps the hot-set cache at the record's 1.067x ceiling; the L2 hit at 2.4 nJ on the 5090 against 0.2 to 0.5 on a chip means the uniformity test is what stops a small cache from being the chip's edge | 1.067x at the ceiling (the record) |
|
||||
| A fifth: the denominator | The honest tier's own operating point is the resistance: the 5090 at the lock reads 1.67 microjoules, the M5 Max 0.78; every chip edge halves to thirds against them | the SRAM die 5x and the base die 3.6x to 4.4x against the M5 Max at zero shadow; about 3x with the shadow at k = 0.5 |
|
||||
|
||||
## 4. The lines in detail
|
||||
|
||||
### 4.1 Processing-in-memory and processing-near-memory
|
||||
|
||||
Samsung's HBM-PIM (Aquabolt-XL, February 2021) places a programmable computing unit inside each bank of an HBM2 stack
|
||||
and reports 2.5x system performance and over 60 percent energy saved on a Xilinx Alveo host (claimed, Samsung, 24
|
||||
August 2021). SK hynix's GDDR6-AiM (February 2022) adds compute to a 16 Gbps GDDR6 die at 1.25 V, claims up to 16x on
|
||||
some AI operations and 80 percent less power, and the AiMX card (2023) carries 32 GB of it (claimed, SK hynix). Samsung's
|
||||
LPDDR5X-PIM (Hot Chips, 25 August 2026) reaches 614 GB/s inside the package against 76.8 GB/s over the external
|
||||
interface at LPDDR5X-9600, 3x the tokens per second on Llama 3.1 8B, with mass production "as early as 2027" and
|
||||
LPDDR6-PIM in JEDEC work (claimed, TrendForce and Sammyfans, 25 to 26 August 2026). UPMEM's DPU-in-DRAM ships: 128
|
||||
DPUs per 8 GB DIMM, 64 MB per DPU, 350 MHz, MRAM reached only by DMA with a fixed cost plus a per-byte cost and at most
|
||||
628 MB/s per DPU for 2,048-byte transfers, and "there is no direct communication channel among DPUs" (claimed, the PrIM
|
||||
characterisation paper, arXiv 2105.03814, read 8 October 2026). The academic line (IMPICA, ICCD 2016: a pointer-chasing
|
||||
engine on a 3D stack's logic layer, 1.2x to 1.9x and 10 to 41 percent less energy on linked lists, hash tables and
|
||||
B-trees, claimed) puts the chaser on the base die, not in the bank, for the same reason this hash defeats the bank
|
||||
units: the next address is anywhere.
|
||||
|
||||
What it does to the chip's cost per dependent read: nothing good for the attacker at the bank level. The PIM unit's
|
||||
arithmetic is the wrong kind (FP16 SIMD, not 32-bit integer ARX) and the wrong place: with uniform addresses the chain
|
||||
leaves the bank after one read with probability 1 minus bank bytes over dataset bytes (98.4 percent at 2 GiB on a 32 MB
|
||||
bank), and in UPMEM's case leaves the DPU with no path but the host. The near-memory version (a chaser with its own
|
||||
controller on the base die) is the record's `f = 1` chip, and that is where PNM becomes real: see 4.2.
|
||||
|
||||
Blunted by: layer 2, by the dataset's size alone. k: none (no path).
|
||||
|
||||
### 4.2 HBM3E, HBM4 and the custom base die
|
||||
|
||||
JEDEC's JESD270-4 (16 April 2025; read via eeNews Europe, 18 April 2025, and the search summaries, the Business Wire
|
||||
and All About Circuits pages refusing the fetch) doubles the channel count to 32 with two pseudo-channels each on a
|
||||
2,048-bit interface at up to 8 Gbps, 2 TB/s per stack, 4 to 16-high stacks of 24 or 32 Gbit dies up to 64 GB, VDDQ 0.7
|
||||
to 0.9 V and VDDC 1.0 to 1.05 V (claimed). TSMC builds the standard HBM4 base die on N12 at 0.8 V for "roughly 1.5x"
|
||||
efficiency and the custom C-HBM4E base die on N3P at 0.75 V for "2x the power efficiency of today's DRAM
|
||||
manufacturing", and "the custom base die will integrate memory controllers and PHY components typically housed
|
||||
separately"; Micron targets 2027 production, SK hynix a first tailored HBM4E in the second half of 2026 with 12 nm for
|
||||
mainstream and 3 nm for NVIDIA and Google premium designs, Samsung 4 nm now and 2 nm for custom HBM (claimed,
|
||||
TrendForce, 1 December 2025 and 23 January 2026). Prices: HBM2e USD 120 per 16 GB, HBM3 200 per 24, HBM3E 300 per 36,
|
||||
HBM4 about 550 per 36 (estimate), factory gate, with contract about 2x and 20 to 26 week leads (claimed, Silicon
|
||||
Analysts, October 2026); Samsung is asking "mid-to-high $4 per gigabit" for 2027 HBM4 against about 1.5 for HBM3E
|
||||
(claimed, Sammyfans, 2 October 2026).
|
||||
|
||||
What it does to the chip's cost per read: the row activation (909 pJ per 1 KB row, HBM2, O'Connor Table 3) does not
|
||||
move; the movement and I/O terms fall with the voltage and the base-die node; a 32-channel stack doubles the activate
|
||||
parallelism the record's HBM rows are bound by (10.7 G reads per second per HBM3 stack, unmeasured; 2.3 G at the JEDEC
|
||||
tFAW). The arithmetic on the record's method: HBM4 1.0 to 1.1 nJ per read and 21.3 G reads per second per stack (166
|
||||
MH/s at about 36 W, 0.22 microjoules, 11x; at the JEDEC-tFAW ceiling 36 MH/s at 19 W, 0.53, 4.5x); the custom base die
|
||||
0.9 to 1.0 nJ with its controller inside (166 MH/s at 29 W, 0.18, 14x; 6.5x at the low ceiling). The latency stays in
|
||||
the 45 to 50 ns class; the capacity (36 to 64 GB) is 4x to 8x any floor layer 2 could set without retiring the honest
|
||||
cards.
|
||||
|
||||
Who can buy it: through 2027 the stacks are allocated to AI accelerators at Tier-1 volume terms; a custom base die is a
|
||||
per-customer engagement with the DRAM maker. A chip maker of Bitmain's or Canaan's size (the history's rows: tape-outs
|
||||
on 7 nm-class nodes, R&D in the tens of millions of dollars a year) could commission one from 2028 (approximate); a
|
||||
USD 5 M startup cannot. The brake is money and queue, not physics, and it expires.
|
||||
|
||||
Blunted by: nothing among the four. The shadow at `k` = 0.5 holds 4.4x, at `k` = 1 2.6x; the shadow core is logic on
|
||||
the N3P base die and sits at the band's low end against a GPU on an older node.
|
||||
|
||||
### 4.3 LPDDR6
|
||||
|
||||
JESD209-6 (2025) gives 10,667 to 14,400 MT/s on a 24-bit channel split into two 12-bit sub-channels, a 32-byte minimum
|
||||
access with 32 and 64-byte bursts, 4 to 64 Gb dies, lower voltages than LPDDR5, a dynamic efficiency mode and on-die
|
||||
ECC (claimed, JEDEC via PCWorld, HotHardware and MicrocontrollerTips, 2025). The bank count per channel and the tFAW
|
||||
are not in the public summaries read (unverified); LPDDR5's 16 banks per channel and a tFAW near 20 ns are the
|
||||
assumption (approximate). A 32-channel, 64-sub-channel controller chip then reads about 12.8 G dependent reads per
|
||||
second (approximate), 100 MH/s, at 1.5 to 2.0 nJ per read: about 4x to 5x the 5090 per joule, the GDDR7 board's class
|
||||
at a lower price per channel. The dies are the cheapest random-access memory a non-hyperscaler can buy outside the
|
||||
2026 price cycle (USD 3 to 4 per GB in 2024, approximate; USD 8 to 21 in 2026 LTAs, claimed).
|
||||
|
||||
The point of this line is not the chip. The M5 Max already is an LPDDR5X part with a GPU, at 0.78 microjoules per hash
|
||||
measured at the GPU-plus-DRAM meter, 3.1x the 5090 per joule; the Windows-class LPDDR5X SoCs (NVIDIA's and Qualcomm's
|
||||
desktop parts, approximate) and the LPDDR6 generation after them are the honest tier's floor. A controller chip on the
|
||||
same memory beats that tier by 1.3x to 1.6x at zero shadow. The resistance of the chain is set by this tier, and v6
|
||||
should say so (finding 3).
|
||||
|
||||
Blunted by: none needed.
|
||||
|
||||
### 4.4 3D DRAM
|
||||
|
||||
Samsung's VS-DRAM (VLSI 2023), 16 stacked layers demonstrated against Micron's 8, VCT 4F2 cells as the stepping stone
|
||||
with prototypes in 2025 and commercial 3D DRAM "by approximately 2030"; Neo Semiconductor's 3D X-DRAM at 230 layers
|
||||
and 128 Gbit per die as a concept (claimed, heise 29 May 2024, Yole, ComputerBase). No source read claims a latency or
|
||||
activate-rate change; the gain is capacity per area (about 3x). For a chip that reads 1 to 8 GiB at random, capacity
|
||||
per die is not the constraint (channels are), so 3D DRAM lowers the honest card's and the chip's cost per GB alike and
|
||||
changes no row. Outside the five-year edge.
|
||||
|
||||
### 4.5 CXL memory pools
|
||||
|
||||
CXL 2.0 expanders on Xeon 6 measure 100 to 160 ns against 75 ns local DDR5; a CXL 3.1 controller adds about 70 ns;
|
||||
pooled DDR5 was USD 4 to 7 per GB before the 2026 cycle (claimed, Introl, 1 February 2026, and the search summaries).
|
||||
A dependent chain through a host CPU and a PCIe-class link at 64 bytes per transaction is bound by the link's
|
||||
transaction rate (about 500 M per second per x8 device at about 25 W, approximate): 0.4x the 5090 per watt. A capacity
|
||||
tool. No edge, no layer needed.
|
||||
|
||||
### 4.6 Wafer-scale
|
||||
|
||||
The WSE-3 holds 44 GB of SRAM at 21 PB/s across 900,000 cores on 46,225 mm^2 of N5; the CS-3 draws about 23 kW and
|
||||
costs "maybe $2.5 million" (claimed, Cerebras and The Next Platform, 14 March 2024; the power from the search summaries).
|
||||
A 2 GiB dataset spread over the wafer is read by a dependent chain whose next address is uniformly random across 215
|
||||
mm of mesh: the traffic is all-to-all, the mesh is bisection-bound, and the average reply crosses about 140 mm of wire.
|
||||
At about 0.1 pJ per bit per mm (approximate) a 512-bit reply costs about 7 nJ before routers, 3x the GDDR7 board's
|
||||
2.0; at about 950 links across the bisection at 32 bits per cycle and about 1 GHz (approximate), the wafer completes
|
||||
about 110 G reads per second, 500 G with the dataset replicated twenty times in regions, 5 to 22 M reads per second per
|
||||
watt against the 5090's 54 M. Per joule 0.1x to 0.4x, per dollar one thousandth. The wafer is a streaming machine; this
|
||||
hash is not streaming. No layer needed; layer 2 removes the replicas as the dataset grows.
|
||||
|
||||
### 4.7 Chiplets, interposers and optical I/O
|
||||
|
||||
UCIe gives 0.25 to 0.5 pJ per bit by package type (claimed, UCIe consortium via SNIA SDC 2022 and 2025 pages); BoW 0.5
|
||||
to 0.7; CoWoS-S is USD 600 to 900 per H100-class package and a one-stack interposer about USD 200 (the record). A
|
||||
read that crosses a die boundary pays about 0.27 nJ (544 bits at 0.5 pJ) and 5 to 10 ns, which is what makes the
|
||||
multi-die SRAM chip of 4.9 cost 1.1 to 1.3 nJ per read instead of 1.0. Optical I/O (Ayar Labs' TeraPHY at about 5 pJ
|
||||
per bit, UCIe-compliant, a USD 500 M round on 3 March 2026; Celestial AI's Photonic Fabric at 6.2 pJ per bit and about
|
||||
120 ns round trip; claimed) is 6x to 8x the interposer's energy per bit and adds latency: it pools memory across
|
||||
packages, which this traffic never wants. No row moves.
|
||||
|
||||
### 4.8 FPGA with HBM
|
||||
|
||||
The Alveo V80 (Versal XCV80, 32 GB HBM2e as two 16 GB stacks, 820 GB/s, 190 W, USD 9,495 MSRP, May 2024) and the U55C
|
||||
(USD 4,747) are what a non-hyperscaler can buy today with HBM on it; Altera's Agilex 7 M-series carries the same two
|
||||
HBM2e stacks at 820 GB/s (claimed, AMD, Wccftech, The Next Platform). The record's reading stands: an HBM2 stack under a
|
||||
soft controller reaches the JEDEC tFAW ceiling, 2.3 to 2.4 G random reads per second (Shuhai, FCCM 2020, Figure 7),
|
||||
so two stacks give about 4.8 G, about 37 MH/s at about 150 W (approximate), 0.6x the 5090 per joule at 5x its price. No
|
||||
HBM3E FPGA was found announced in the pages read (unverified). No layer needed.
|
||||
|
||||
### 4.9 SRAM at 2 nm: the full store on one reticle
|
||||
|
||||
TSMC's N2 reads 38 Mb/mm^2 of high-density SRAM, 11 percent over N3E (claimed, IEEE Spectrum, 12 December 2024;
|
||||
volume from 2025); wafers are about USD 30,000 and "booked to 2028" (claimed, tech-insider, 2026). The arithmetic: 2
|
||||
GiB is 17,180 Mbit, 452 mm^2 of macro; with periphery, a controller, the lanes' registers and a shadow core, a 550 to
|
||||
650 mm^2 die under the 858 mm^2 reticle; about 95 gross dies per wafer, 55 to 70 percent good with row and column
|
||||
repair (approximate), USD 400 to 600 of silicon. No DRAM, no interposer, an organic package. Under class v5 the
|
||||
dataset's items are leaves of the chain state refreshed per window; the chip rewrites 2 GiB per window at on-die
|
||||
bandwidth, the same 32 ms every GPU pays (the record, chip-model-v3 5.10), and holds a node or shares one across a farm
|
||||
as the record prices.
|
||||
|
||||
Energy per read: the macro's 64-byte read, about 0.1 nJ (approximate), plus the global wire. The record took 0.6 pJ per
|
||||
bit of wire across a 128 mm^2 array; scaling with the side of the die gives about 1.3 pJ per bit across 600 mm^2, 0.68
|
||||
nJ for 512 bits, so about 1.0 nJ per read with the controller, range 0.5 to 2.0 (the sensitivity of every SRAM figure
|
||||
here). Latency 10 to 20 ns. No activate ceiling, no tFAW, no refresh: the rate is power-bound. At 300 W with 30 W of
|
||||
static and controller power: 270 W over 128 nJ per hash is about 2,100 MH/s per die, 0.14 microjoules per hash, 17x
|
||||
the 5090 at zero shadow (13x at 1.3 nJ, 8x at 2.0, 30x at 0.5); USD 0.25 per MH/s of silicon, about 0.4 with the
|
||||
board. Two dies at 4 GiB: half the reads cross one UCIe hop, 1.14 nJ, 15x, USD 1,000. Four dies at 8 GiB: 1.3 nJ, 13x,
|
||||
USD 2,000 to 2,500, a 4-die organic package. The honest cards hold 8 GiB fine (16 GB and up); a floor that pushed the
|
||||
dataset past what a package can hold (16 to 32 GiB, eight to sixteen reticles at USD 5,000 to 10,000 and 1.5 to 2.0
|
||||
nJ per read, still 7x to 9x) would retire every honest card under 32 GB first. Layer 2 therefore sets this chip's
|
||||
capex and die count, not its joules, and the floor's number is a card-lifetime decision, not a chip decision.
|
||||
|
||||
The project: the history's IBS figures (5 nm USD 416 M to 542 M, 3 nm 590 M) price an SoC; an SRAM array with a
|
||||
controller and a sequencer core is simpler and the startup figure ("$50M to $75M" for 7 nm, SemiAnalysis) is the
|
||||
better guide, so USD 100 M to 500 M at N2 (approximate) and 18 to 24 months to a first chip (approximate). That is the
|
||||
clock of Counter ASIC 3.0 item 4 and the daily-issuance threshold of the history (chips at USD 20 K to 50 K of daily
|
||||
issuance for compute-bound hashes; 32 months for Ethash at the largest prize): the SRAM chip arrives when the prize
|
||||
pays for an N2 project, and nothing in the hash moves that date.
|
||||
|
||||
Blunted by: layer 2 on capex only (USD 500 to 2,500 across the floor's range); the shadow at `k` on joules: with the
|
||||
shadow core on the same N2 die against a GPU on N3 or N4 the ALU band sits at 0.3 to 0.5; at `F` = 1.10 the chip reads
|
||||
4.8x at k = 0.5 and 2.7x at k = 1; at the 5090's whole latency shadow (about 330,000 ops per hash, `F` about 2.0,
|
||||
575 W) 3.7x and 2.0x; against the M5 Max's joule about 3x at k = 0.5.
|
||||
|
||||
### 4.10 RLDRAM and the activate-free line
|
||||
|
||||
RLDRAM 3 (Micron, 2011; ISSI second-sourced) has a tRC under 10 ns, 16 banks per device, no separate activate
|
||||
command and "SRAM-like random access" (claimed, Micron's product page and the 2011 announcements), in 576 Mb and 1.125
|
||||
Gb devices at up to 2,133 Mb/s (approximate). One device completes about 2 G random reads per second (16 banks over 8
|
||||
ns), six times a GDDR7 device's share of the 5090 board's 21.3 G; sixteen devices, 2 GiB, about 32 G reads per second,
|
||||
250 MH/s per board. The energy per read is not published in anything read (a power calculator exists); a full
|
||||
small-row access per read at an old node reads 2 to 4 nJ (approximate), so about 5x per joule, at USD 400 to 800 per
|
||||
GB (approximate, a low-volume networking part). It is the proof that an activate-free DRAM exists, and AMD's "Folded
|
||||
Banks" (ISCA 2025, with AMD Research: 8x the activate parallelism in a 3D-stacked HBM gives 6.7x the irregular
|
||||
bandwidth, claimed from the abstract; the PDF refused the fetch) is the same idea on the HBM roadmap. If a DRAM maker
|
||||
ships it in HBM, the activate ceilings in the record's HBM rows rise 6x to 8x and the HBM chip's rate per stack with
|
||||
them; its energy per read falls by the row-size term (O'Connor's FGDRAM: 51 percent). That is the DRAM-on-logic row.
|
||||
|
||||
Blunted by: layer 2 on RLDRAM's capacity cost (USD 3,000 to 6,000 at 8 GiB); nothing on the HBM version.
|
||||
|
||||
## 5. The k bands for the research lane's chip rows
|
||||
|
||||
For `docs/design/class-v6-rotating-family.md`. The shadow `k` is the chip core's energy per forced op over the GPU's
|
||||
at the same operating point (the record's ALU band 0.3 to 0.8 on the 5090's measured 6.2 to 11.3 pJ per op); the
|
||||
memory `k_read` is the technology's energy per dependent read over the 5090's measured 10.9 nJ. "Edge" is per joule at
|
||||
zero shadow; "with the shadow" is at the class v4 premium `F` = 1.10 microjoules on the 5090 (`E_chip` = `E_mem` + `k F`).
|
||||
|
||||
| Chip row | Year | `E_read` nJ | Rate per chip, MH/s | `E_hash` at zero shadow, microjoules | Edge over the 5090 (2.40) | Edge over the M5 Max (0.78) | `k_read` | Shadow `k` band | With the shadow, k = 0.5 / 1 | Silicon and memory, USD per MH/s |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| GDDR7 board, 28 nm controller (the record) | now | 2.0 | 166 | 0.466 | 5.1x | 1.7x | 0.18 | 0.5 to 0.8 (28 nm core: the high end) | 3.3x / 2.1x | 2.8 (2025 memory), about 7 in the 2026 cycle |
|
||||
| HBM3E, one stack | now | 1.2 | 84 (ceiling unmeasured) | 0.321 | 7.5x | 2.4x | 0.11 | 0.3 to 0.8 | 3.8x / 2.4x | 6.6 |
|
||||
| HBM3E, eight stacks | now | 1.2 | 666 | 0.262 | 9.2x | 3.0x | 0.11 | 0.3 to 0.8 | 4.1x / 2.5x | 4.0 |
|
||||
| HBM4, one stack, N12 base die | 2027 to 2028 | 1.0 to 1.1 | 166 (36 at the JEDEC tFAW) | 0.22 (0.53) | 11x (4.5x) | 3.6x (1.5x) | 0.10 | 0.3 to 0.8 | 4.4x / 2.6x | about 5 |
|
||||
| Custom HBM4E base die, N3P, controller in the stack | 2028 or later | 0.9 to 1.0 | 166 (36) | 0.18 (0.37) | 14x (6.5x) | 4.4x (2.1x) | 0.09 | 0.3 to 0.5 (an N3P core) | 4.4x / 2.6x | about 5 |
|
||||
| DRAM on logic, FGDRAM-class rows, hybrid bonded | 2029 to 2031 | 0.5 to 0.7 | 125 per stack, 1,000 for eight | 0.15 | 16x (12x to 20x) | 5.2x | 0.05 | 0.3 to 0.5 | 5.1x / 2.8x | about 4 (approximate) |
|
||||
| SRAM full store, one N2 reticle, 2 GiB | 2027 to 2028 (an N2 project) | 1.0 (0.5 to 2.0) | about 2,100 at 300 W | 0.14 | 17x (8x to 30x) | 5.6x | 0.09 | 0.3 to 0.5 (an N2 core) | 4.8x / 2.7x | 0.25 to 0.4 |
|
||||
| SRAM full store, two N2 dies, 4 GiB | the same | 1.14 | about 1,850 | 0.16 | 15x | 4.9x | 0.10 | 0.3 to 0.5 | 4.6x / 2.7x | 0.55 |
|
||||
| SRAM full store, four N2 dies, 8 GiB | the same | 1.3 | about 1,600 | 0.185 | 13x | 4.2x | 0.12 | 0.3 to 0.5 | 4.5x / 2.6x | 1.3 to 1.6 |
|
||||
| LPDDR6 controller chip, 32 channels | 2027 | 1.5 to 2.0 | about 100 | 0.25 to 0.30 | 4x to 5x | 1.3x to 1.6x | 0.15 to 0.18 | 0.3 to 0.8 | 3.1x / 2.0x | about 3 (approximate) |
|
||||
| Per-bank PIM, UPMEM, FPGA with HBM2e, wafer-scale, CXL, optical | | | | | under 1x or no path | | | | | |
|
||||
|
||||
Arithmetic, the SRAM row: 270 W over (128 x 1.0 nJ) = 2.11 G hashes per second; 300 W over 2.11 G = 0.142
|
||||
microjoules; 2.40 over 0.142 = 16.9x; with the shadow at k = 0.5: (0.142 + 0.55) over (2.26 + 1.10) = 0.692 over 3.36 =
|
||||
4.86x; at k = 1: 1.242 over 3.36 = 2.71x. The base-die row: 166 M x 128 x 0.95 nJ = 20.2 W plus 4 static plus 5
|
||||
controller = 29.2 W; 29.2 over 166 M = 0.176 microjoules; 2.40 over 0.176 = 13.6x. The M5 Max column divides 0.78 by
|
||||
the same `E_hash`. Every chip-side figure is modelled; the GPU-side figures are the record's measurements.
|
||||
|
||||
## 6. Consequences per tier
|
||||
|
||||
| Tier | What this file means | What is being done |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB card | Nothing changes today: no chip exists, and the first one in this file (an N2 SRAM die or an HBM4 base-die chip) is a USD 100 M-class project with a 2028-class date. When one lands it runs at 0.14 to 0.22 microjoules per hash against this card's 10 to 20; this tier is the first out, as the record says. The dataset's floor decides this tier's life more than any chip does: 4 GiB at year 4 (the spec's schedule) retires it then | the shadow sizing and the floor are the founder's numbers to set (section 7); the share-pattern detector (Counter ASIC 3.0 item 4) is what tells this miner a chip has arrived |
|
||||
| One 16 GB card (9070 XT class) | Holds 8 GiB with room; AMD's 2.4 G reads per second at 304 W is 7x behind the 5090 per joule and 50x to 100x behind the chips here | the vendor-share metric; nothing in the hash moves AMD's dependent-read rate |
|
||||
| One 24 or 32 GB card, the 5090 at the lock | 1.67 microjoules at the 1,300 MHz lock; the chips here are 8x to 12x ahead per joule at zero shadow and 2.5x to 4x with the class v4 shadow at k = 0.5 | the Ember knob carries the lock rows; the shadow's upper bound at the full ALU budget is the lever this file sizes |
|
||||
| The unified-memory SoC (M5 Max, LPDDR5X and LPDDR6 desktops) | 0.78 microjoules at the GPU-plus-DRAM meter: the honest tier the chips beat least (1.7x to 5.6x at zero shadow, about 3x with the shadow). Capex-poor (27 MH/s per USD 4,000 machine) but joule-rich | finding 3: v6 scores the floor and the shadow per tier with this tier as the reference joule; the dataset stays inside 16 GB unified memory |
|
||||
| A rig | A rig's cost is electricity; against an N2 SRAM chip at USD 0.3 per MH/s and 0.14 microjoules it earns 1/10 to 1/17 of a chip per watt and leaves when chips hold the hashrate | the issuance trigger: the bounty and the benchmark live before daily issuance crosses about USD 50 K (the record) |
|
||||
| A pool user | A chip fleet is a few operators; the share-pattern detector is the warning | the detector on the observer, before the public testnet (the record) |
|
||||
| The public claim | "Under 2x" is not reachable against any chip in this file at a `k` under 1. The honest sentence is: the strongest chip five years out beats a 5090 by 2.6x to 2.8x per joule at k = 1 and about 4.5x at k = 0.5 with the shadow on, and the best honest SoC by about 3x; and it costs an N2 project | the research lane's synthesis carries the number; nothing from this file goes to the site or the devnet |
|
||||
|
||||
## 7. Decisions this raises for the founder
|
||||
|
||||
Each carries a default and a deadline; silence means the default.
|
||||
|
||||
1. **Size the shadow against the SRAM chip, not the GDDR7 board.** Layer 1's program-length draw gets a lower bound at
|
||||
the length that holds the record's 2.1x on the GDDR7 chip today and an upper bound at the honest cards' full latency
|
||||
shadow (the 5090's about 330,000 ops unlocked, about 150,000 at the lock; the M5 Max about 290,000; the 9070 XT
|
||||
about 650,000; approximate from the record), re-based at every family epoch on the cards then mining. Default: the
|
||||
research lane writes the draw with these bounds into the synthesis; the hash lane measures the rate and the watts at
|
||||
the upper bound on the 5090 and the M5 Max before the first v6 era is cut. Deadline: 20:00 UK today (the synthesis).
|
||||
2. **The floor (layer 2).** 2 GiB at genesis is one N2 reticle; 4 GiB two; 8 GiB four. The floor moves the chip's
|
||||
capex (USD 500 to 2,500), not its joules (17x to 13x), and 8 GiB is the last size inside 16 GB unified memory and
|
||||
the 16 GB card tier. Default: the spec's schedule stands (2 GiB plus 0.5 GiB a year, doubling at year 4), chosen on
|
||||
card lifetime; the floor is not a chip lever and this file does not ask to raise it. Deadline: none; a note in the
|
||||
synthesis.
|
||||
3. **The reference joule (a fifth layer, or a rule).** Resistance is stated against the best honest joule (the
|
||||
unified-memory SoC tier, then the 5090 at the lock), not the unlocked 5090. Default: the research lane adopts it in
|
||||
the synthesis's chip rows (section 5's M5 Max column) and the public text, when there is one, carries the edge over
|
||||
the best honest joule. Deadline: 20:00 UK today.
|
||||
4. **The clock.** The chips in this file are USD 100 M-class projects with 2028-class dates; the issuance trigger, the
|
||||
benchmark and the share-pattern detector of Counter ASIC 3.0 item 4 are what decide when they are built. Default:
|
||||
unchanged from the record. Deadline: none.
|
||||
|
||||
## 8. Unverified and owed
|
||||
|
||||
- Every chip-side energy figure is modelled on the record's method (O'Connor's HBM2 breakdown, Samsung's pJ per bit
|
||||
roadmap, the 909 pJ row activation standing in for HBM3, HBM4 and GDDR7); the SRAM wire figure (0.6 pJ per bit at 128
|
||||
mm^2, scaled with the die's side) is from memory and moves the SRAM rows by 2x either way; the UCIe hop (0.5 pJ per
|
||||
bit) is the consortium's claim; the FGDRAM row size (256 bytes) and its 51 percent are the paper's simulation.
|
||||
- HBM4's banks per pseudo-channel and tFAW per channel are behind the JEDEC paywall (the Business Wire and All About
|
||||
Circuits pages refused the fetch); the "2x the activate ceiling" rests on 32 channels with tFAW per channel, as the
|
||||
record's HBM rows rest on the same assumption at 16; both unmeasured. The AWS F2 hour the record names (chip-model-v3
|
||||
5.3) is still the one measurement that would settle the HBM2 figure, and an HBM3 or HBM4 part is not rentable at the
|
||||
controller level by anyone outside a hyperscaler today.
|
||||
- LPDDR6's bank count and tFAW are not in the summaries read; the row is LPDDR5's structure (approximate).
|
||||
- RLDRAM 3's energy per read and its 2026 price are not published in anything read; the row is approximate.
|
||||
- The Cerebras mesh figures (link width, clock, bisection) are approximate; the fabric bandwidth was not on the pages
|
||||
read (The Next Platform gives only its change against WSE-2).
|
||||
- Dates: the DRAM-on-logic and hybrid-bonded HBM timelines ("2028 to 2031") are approximate; the sources read give
|
||||
3D DRAM "from 2030" and C-HBM4E production in 2027; the hybrid-bonding pages were not fetched (the TrendForce page
|
||||
refused). The N2 project cost and time are approximate.
|
||||
- The web-search budget of this session ran out at about 11:3x UK after 26 searches; the remaining facts were read by
|
||||
direct fetch of the pages named in section 9. Pages that refused (403): JEDEC's Business Wire release, All About
|
||||
Circuits, ACM's Folded Banks page, OC3D's GDDR7 capacity note, All About Circuits' HBM-PIM note. Their figures are
|
||||
carried from the search summaries and marked claimed.
|
||||
- Nothing was run on the Mac; nothing was built or benchmarked anywhere. The one measurement this file would want next
|
||||
is on the record's queue already: the 5090 and the M5 Max at the shadow's upper bound (decision 1).
|
||||
|
||||
## 9. Sources (URL and the date read; all read 8 October 2026 unless a file is named)
|
||||
|
||||
Repository: `docs/analysis/chip-model-v3.md` (sections 5.1 to 5.11, 6); `docs/analysis/counter-asic-4-research.md`
|
||||
on branch counter-asic-4 at fb61ed4b (sections 1, 15.1a, 20.4); `docs/analysis/asic-resistance-history.md` (2.5, 2.6);
|
||||
`docs/plans/counter-asic-3-status.md` (7c, the clock grid); `docs/spec/01-lottery-hash.md` (1.8, 1.13.3, 1.14);
|
||||
`docs/analysis/latency-shadow-2026-10-06.md`; `docs/analysis/sram-mirror.md`.
|
||||
|
||||
- JEDEC HBM4 (JESD270-4, 16 April 2025): https://www.eenewseurope.com/en/hbm4-standard-doubles-channel-count-for-ai-boost (18 April 2025); https://hothardware.com/news/jedec-finalizes-hbm4-spec; https://www.businesswire.com/news/home/20250416843598/en (refused)
|
||||
- TSMC HBM4 and C-HBM4E base dies: https://www.trendforce.com/news/2025/12/01/news-tsmc-unveils-custom-c-hbm4e-details-n3p-logic-dies-reportedly-target-2x-efficiency-gain/ (1 December 2025); https://www.trendforce.com/news/2026/01/23/news-samsungs-custom-hbm4e-design-reportedly-aimed-for-mid-2026-parallels-sk-hynix-and-micron/ (23 January 2026)
|
||||
- HBM prices: https://siliconanalysts.com/data/hbm-pricing (October 2026); https://www.sammyfans.com/2026/10/02/samsung-seeks-more-than-3x-hbm3e-pricing-for-hbm4/ (2 October 2026); https://www.trendforce.com/news/?p=62555
|
||||
- GDDR7 prices and roadmap: https://www.trendforce.com/news/2026/09/24/news-micron-reportedly-ends-2gb-gddr7-narrowing-supply-options-for-nvidias-rtx-50-series/ (24 September 2026); https://www.guru3d.com/story/micron-fiveyear-roadmap-shows-24gb-36gbps-gddr7-in-2026; https://overclock3d.net/?p=314548 (refused; the 4 and 6 GB parts in 2027 to 2028 from the search summary)
|
||||
- DRAM prices 2026: https://wccftech.com/mobile-dram-prices-expected-to-increase-by-100-quarter-over-quarter-as-long-term-agreements-now-getting-signed-at-prices-as-high-as-21-gb/ (4 May 2026); https://tech-insider.org/ddr5-ram-prices-2026/
|
||||
- Samsung HBM-PIM: https://news.samsung.com/global/samsung-brings-in-memory-processing-power-to-wider-range-of-applications (24 August 2021); https://www.allaboutcircuits.com/news/beyond-high-bandwidth-memory-samsung-breaks-processing-in-memory-into-AI-applications/ (refused)
|
||||
- SK hynix AiM and AiMX: https://news.skhynix.com/developed-processing-in-memory (2022); https://www.hc2024.hotchips.org/assets/program/conference/day1/11_HC2024.SKhynix.GuhyunKim.rev920240822.pdf
|
||||
- Samsung LPDDR5X-PIM and LPDDR6-PIM: https://www.sammyfans.com/2026/08/25/samsung-unveils-lpddr5x-pim-dram/ (25 August 2026); https://www.trendforce.com/news/2026/08/26/news-samsungs-4nm-gaia-could-mark-first-pim-commercialization-in-ai-pcs-mass-production-as-early-as-2027/ (26 August 2026); https://en.fnnews.com/news/202609060934024282
|
||||
- UPMEM: https://arxiv.org/pdf/2105.03814 (the PrIM characterisation, text extracted with pdftotext: Table 1, section 3.2, the inter-DPU statement); https://www.nextplatform.com/2020/02/04/putting-in-memory-processing-through-the-paces/ (4 February 2020); https://old.hotchips.org/hc31/HC31_1.4_UPMEM.FabriceDevaux.v2_1.pdf
|
||||
- IMPICA: https://ghose.cs.illinois.edu/papers/16iccd_impica.pdf (ICCD 2016)
|
||||
- Fine-grained DRAM: https://www.cs.utexas.edu/~skeckler/pubs/MICRO_2017_Fine_Grained_DRAM.pdf (text extracted with pdftotext: 3.92 pJ per bit per HBM2 access, 1.21 of it activation, the 256-byte row, tFAW "effectively eliminated", 51 percent)
|
||||
- Folded Banks (ISCA 2025): https://dl.acm.org/doi/10.1145/3695053.3731111 (refused today; the abstract as the record read it on 6 October 2026); https://wantongli.ucr.edu/news/announcement22-ISCA%202025
|
||||
- LPDDR6: https://www.pcworld.com/article/2845759/lpddr6-memory-standard-announced-as-ddr5-dram-takes-over.html; https://hothardware.com/news/jedec-lpddr6-standard-released; https://www.microcontrollertips.com/what-is-jesd209-6-and-why-is-it-important-for-edge-ai/
|
||||
- CXL: https://introl.com/blog/cxl-memory-expansion-pooling-disaggregated-memory-ai-data-center-2025 (1 February 2026); https://www.snia.org/sites/default/files/2025-09/SNIA-SDC25-Peethambaran-CXL-as-scalable-cost-effective-Memory.pdf
|
||||
- Cerebras: https://www.cerebras.ai/chip; https://www.nextplatform.com/2024/03/14/cerebras-goes-hyperscale-with-third-gen-waferscale-supercomputers/ (14 March 2024); https://sacra.com/c/cerebras-systems ("a couple million per system")
|
||||
- FPGA with HBM: https://wccftech.com/amd-announces-mass-production-of-the-alveo-v80-compute-accelerator-9495-price-tag/ (17 May 2024); https://www.amd.com/en/products/accelerators/alveo/u55c/a-u55c-p00g-pq-g.html; https://www.nextplatform.com/2022/03/08/a-cornucopia-of-memory-and-bandwidth-in-the-agilex-m-fpga; https://arxiv.org/abs/2005.04324 (Shuhai)
|
||||
- 3D DRAM: https://heise.de/en/news/Huge-RAM-3D-DRAM-with-multiple-layers-planned-from-2030-9738064.html (29 May 2024); https://www.yolegroup.com/industry-news/samsung-reveals-16-layer-3d-dram-plans-with-vct-dram-as-a-stepping-stone/
|
||||
- RLDRAM 3: https://www.micron.com/products/memory/dram-components/rldram-memory; https://newelectronics.co.uk/content/news/micron-unveils-third-generation-rldram-technology (2011); https://arxiv.org/pdf/1810.07059
|
||||
- SRAM at N2: https://spectrum.ieee.org/tsmc-n2-2670436570 (12 December 2024); https://marklapedus.substack.com/p/intel-tsmc-tout-sram-breakthroughs; wafer prices https://tech-insider.org/tsmc-2nm-wafer-price-2026/
|
||||
- UCIe and BoW: https://www.snia.org/sites/default/files/2025-05/SNIA-SDC22-Sharma-Universal-Chiplet-Interconnect-Express.pdf; https://www.3dtested.com/news/new-ucie-chiplet-standard-supported-by-intel-amd-and-arm
|
||||
- Optical: https://www.theregister.com/2026/03/03/ayar_labs_500m/ (3 March 2026); https://www.allpcb.com/allelectrohub/photonic-interconnects-aim-to-solve-ai-memory-bottlenecks (the Celestial AI figures)
|
||||
- H100 pointer-chase latency (353 ns, claimed): https://arxiv.org/pdf/2608.15764
|
||||
333
docs/analysis/class-v6/history.md
Normal file
333
docs/analysis/class-v6/history.md
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
# Class v6 research lane A, history: every ASIC-resistant proof of work, how it fell or held, and what that binds on the four layers
|
||||
|
||||
8 October 2026, branch `class-v6-history`, the Counter ASIC coordinator's history lane. The founder's word at 11:1x UK: class v6 is declared with four layers as its spine, and the research opens "see if anything can be optimised, added or invented". This file extends and corrects `docs/analysis/asic-resistance-history.md` (5 October 2026, the deep dive: 31 rows, 24 papers, ten lessons, seven ranked additions); it does not repeat that file's rows. What is new here: the exact MECHANISM of every chip (what it specialised: the memory, the hash core, the instruction mix, the parameter fixity), what each design missed, the timeline from announcement to chip to response, and for each one the mapping to class v6's four layers with "does v6 close it" in one sentence and one number. Every figure about another chain cites a URL with the date it was read, or is labelled approximate. Every Igneum figure names the repo file. Reading public research is in-house; nothing is paid or asked of anyone outside.
|
||||
|
||||
First cut landed on the mirror's master at 11:26 UK on 8 October (merge 4c58ad65: the table, the mechanisms, the three lessons), ahead of the 15:00 clock; this revision folds in the synthesis lane's reading of the six items (`docs/design/class-v6-rotating-family.md` section 7b) and cross-cites lane B's hardware file; the full report by 09:00 UK on 9 October carries any row the other lanes move. A line goes to the coordinator and to the synthesis lane at each landing.
|
||||
|
||||
## 0. One page
|
||||
|
||||
**The four layers, as declared, against the history's chip classes.** Every chip that ever beat a resistant hash belongs to one of five classes; the table says which layer of v6 answers each class, and with what number. "Closes" means the chip's measured or claimed edge falls under 2x per joule by a mechanism the layer supplies; "does not close" means the layer does not touch the chip's edge, and the number stands.
|
||||
|
||||
| Chip class (the rows of section 1 it covers) | What the chip specialised | Its best measured edge | Which v6 layer answers it | Does v6 close it | The number after v6 |
|
||||
|---|---|---|---|---|---|
|
||||
| A. Fixed-function pipeline on a compute-bound hash (X11, Blake-256, Blake2b, Blake2s, kHeavyHash, Eaglesong, Blake3, SHA512/256d, NexaPow, X16R's FPGA) | the hash's round function unrolled in silicon; the instruction mix fixed at genesis | 33x to 1,280x per joule (Antminer KA3, D3, KS5 Pro) | layers 1 and 3 (the op mix, program length and families drawn or unlocked per era; already closed since class v2 by the per-epoch random program) | yes, and it was already closed: a per-epoch program has no round function to unroll | 0 of 128 loads and 0 of 512 program ops are a fixed function; what remains is class E |
|
||||
| B. SRAM-scale memory (Scrypt's 128 KB, CryptoNight's 2 MB, Lyra2REv2's sponge, Cuckatoo's edge bitmap, Equihash's 144 MB) | the hash's whole working set on die or across a few dies, with a time-memory trade-off the designer had not drawn | 19x to 1,100x (Scrypt), 40x to 50x (CryptoNight X3), 12x to 100x (Equihash Z9 to Z15), 4x (Cuckatoo G1) | layer 2 (the dataset above any SRAM die, growing with chain state with a floor; the 256 MiB cache growing with it) | yes: the recompute chip that holds the cache on die reads 0.92x at the op budget and 1.86x per joule, and the cache's size forces it onto a 7 nm or better node | `chip-model-v3.md` section 2 and 5.4: 0.31x bare, 0.92x with the 3x factor, 1.86x per joule at f = 0; the curve from f = 0 to 1 is monotone and the partial-store chip is worse than both ends |
|
||||
| C. The memory system without the GPU (the Ethash chips: Linzhi Phoenix, Jasminer X4, Antminer E9; the f = 1 chip of the model) | a controller and PHY for the same DRAM the card carries, a 32-byte atom per read, no shader, no scheduler, no clock tree; 55 W of memory without the 271 W of GPU | 2.1x to 4.8x per joule measured (Ethash, 2020 to 2022); 5.1x (GDDR7) to 7.5x (one HBM3 stack) modelled for Igneum at zero premium | none of the four directly: every per-era draw is firmware to a chip that stores the dataset; layer 2 only when the dataset passes the chip's board | **NO.** The f = 1 chip keeps 5.1x per joule on GDDR7 at zero premium and 2.1x with the class v4 shadow at k = 1; v6's draws move neither figure. What moves it is the honest card's own joules (the operating point, 3.6x at the 1,300 MHz knee) and the shadow's premium | 5.1x / 3.6x / 2.1x (zero premium unlocked / knee / knee with the shadow at k = 1), `counter-asic-4-research.md` sections 0 and 20.4; the chip's USD 2.8 per MH/s against the card's 14.7 |
|
||||
| D. The firmware-survivable chip against periodic change (Monero's chips across four forks, Vorick's "survives forks at under a 5x hit", the X16R FPGA, the Equihash chip "able to follow parameter forks") | a programmable sequencer over the hash's op set; the per-block or per-fork change read as a configuration | chips back at 85 percent of Monero's hashrate four months after the v8 fork; 1.3x on X16R | layers 1 and 3 are the automatic form of the change those chains made by hand; they cost no governance event | partly: v6 closes the GOVERNANCE failure (no fork, no reset of the hashrate to a rentable size) and taxes die area for the reserve; it does not close the chip, because a reserve and a draw band readable at genesis are built in from day one | the draw's cost to the chip is a recompile per epoch and a new dataset mapping per era; the number is the "GPU without graphics" of ledger M1, which is class C's chip with a sequencer beside it: 5.1x less the shadow's k |
|
||||
| E. The hot set and the steered address (Kik's 64-bit seed on ProgPoW, Dinur-Nadler on MTP, AP-F8-1 on class v4, the weak-day MUL draw) | a small SRAM serving the reads a biased draw or a cooperating node concentrates | 1.067x at v4's ceiling (0.52 to 4.6 percent of reads on 0.1 percent of items); a memory skip on ProgPoW 0.9.3; MTP from 2 GB to under 1 MB | layer 4 (the (c''') floor and the F8-form uniformity test generalised to every era's draw, with a redraw on failure) | yes, for the class the test models: the floor at 0.995 refuses every live hot set the in-house pass found (nine, 0.9809 to 0.9919) at 0.7 percent of candidates | `docs/spec/01-lottery-hash.md` 1.4.7.2 (class v5); the open residue is the shadow-written concentrations at 0.9992 to 0.9997, worth about 1.0004x to a chip |
|
||||
|
||||
**The three lessons that bind on v6** (section 3 has the evidence):
|
||||
|
||||
1. **The chip that stores the dataset is firmware-immune to every draw; only joules and memory growth move it.** Every per-era parameter (mixer rounds, op weights, read width, program length, shadow placement) and every family epoch is read by the f = 1 chip's sequencer as a configuration, as Monero's chips read four forks and X16R's FPGA read the per-block order. The number v6 inherits is 5.1x per joule at zero premium (2.1x with the shadow at k = 1), and the lever is the honest card's operating point and the shadow's premium, not the draw.
|
||||
2. **Automatic change beats the human fork only where it costs the chip a redesign, and the one parameter that does is the memory.** Grin's six-monthly tweaks held because each was a new algorithm the lane was scheduled to retire; Monero's forks lost on the second lap; Ethash's DAG growth is the one scheduled change in the record that killed a shipped chip (the E3, when the DAG passed its 4 GB of DDR3). Layer 2 is that lesson made a rule, and its rate decides everything: at 2 GiB plus 0.5 GiB a year a 32 GB chip board outlives the chain, so layer 2 as declared ages out the honest 8 GB card before any chip unless the floor is set against DRAM cost per gigabyte, not against chain state.
|
||||
3. **A steered or biased address pattern is always found after launch unless the test lives in the acceptance rule, and every new draw needs its own null.** ProgPoW's seed, MTP's blocks and class v4's lossy sources were the same attack three times; layer 4 puts the test where it must be, and its cost is one census per era draw on the node (2.2 s per candidate at 2^20 today) with the null re-derived for every drawn parameter, because the window model that defines "uniform" changes with the read width and the program length.
|
||||
|
||||
**What the history says to add** (section 5): a layer 2 floor and a per-tier ceiling stated before its rate (the synthesis lane has since made the ceiling layer 2's first constant); the issuance clock and the share-pattern detector (unchanged from the 5 October ranking, still unbuilt); the read width's draw band stopped where the honest card stops being latency-bound ({4 B, 16 B} on the measured warrant, nothing wider); the op-mix band bounded by the per-vendor energy table; the reserve ordered by what a sequencer chip cannot fold into firmware.
|
||||
|
||||
## 1. The chips, one row per mechanism
|
||||
|
||||
Columns: the hash; the chip (vendor, model); the mechanism (what it specialised); what the design missed; the timeline (hash live, to the chip, to the response); the v6 layer that answers it; does v6 close it; the number (the chip's measured or claimed per-joule edge, and what the layer leaves). Every ratio is arithmetic on the cited rate and watt figures of the chip and the best consumer GPU of its year, approximate by construction; section 2 carries the sources. "Closed at v2" means the per-epoch random program already removed the mechanism and v6 inherits it.
|
||||
|
||||
| Hash (chain) | Chip | Mechanism: what it specialised | What the design missed | Timeline | v6 layer | Closed by v6 | The number |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Ethash (Ethereum) | Bitmain Antminer E3 | 18 chips with 4 GB of commodity DDR3; "a memory interface connected to a small compute engine" | the hash needs only a memory interface; and the chip's memory was under-sized | live Jul 2015; chip announced Apr 2018, shipped Jul 2018; died by DAG growth Mar to Oct 2020 | 2 (the only layer that reached it) | class C: no | 1.0x to 1.1x per joule; the DAG passed its 4 GB at 20 to 27 months |
|
||||
| Ethash | Innosilicon A10, A10 Pro, A11 Pro | the same with faster, larger DRAM (6 to 8 GB) | as above | Jul 2018 to Dec 2021 | none | no | 1.4x to 2.5x |
|
||||
| Ethash | Linzhi Phoenix (E1400) | 64 compute units and 72 mixers per board beside 4.4 GB of "decentralized memory inside its ASIC"; sized to the DAG | memory energy per bit is the whole cost; many small memories by many small cores cut it | announced Sep 2018; tapeout Sep 2019; samples Dec 2020; no mass production | none | no | 2.1x to 2.2x |
|
||||
| Ethash | Jasminer X4 (Sunlune) | DRAM dies hybrid-bonded (wafer-to-wafer DBI) onto a 40/45 nm logic die, 5 GB per unit, 40 chips per server | off-package DRAM at 3 to 4 pJ per bit was the cap; bonding takes it under 1 | Jun 2021 chip; Oct 2021 ship; the Merge 11 months later | none | no | 5.1x (about 7.3 pJ per bit all in) |
|
||||
| Ethash | Bitmain E9, E9 Pro | conventional, 6 to 7 GB | as the E3, four years of DRAM later | Jul 2022 (8 weeks before the Merge); Feb 2023 | none | no | 3.0x, 4.1x |
|
||||
| ProgPoW (never on Ethereum; KawPow, FiroPoW, ProgPowZ, Quai) | none; Linzhi claimed 3x to 8x with no derivation; one academic VU35P FPGA at 5.6 MH/s | a sequencer over eleven ops reads the per-period program as configuration (unbuilt); the on-die cache chip priced at "<< 0.1x" energy (unbuilt) | the light-evaluation chip was left as a suggestion; a 64-bit seed (Kik) | EIP May 2018; audits Sep 2019; exploit Mar 2020; dead Mar 2020; no chip on any adopter in 6 years | 1, 3 (the period made an era draw), 4 (the seed class) | class D and E yes; the Rao chip is class C, no | claimed 1.1x to 1.2x for the compute chip; the memory-system chip never priced; Igneum's reads 5.1x |
|
||||
| RandomX (Monero) | Bitmain Antminer X5 | many RISC-V chips on a board over commodity DRAM: an in-order machine for a fixed VM spec; insides never published | CFROUND's cost on x86, a scratchpad-only AES circuit, the latency slack of a fixed 256-instruction program (v2's own list) | live Nov 2019; private mining from about 2021; X5 Sep 2023; X9 announced Dec 2025 and withdrawn May 2026 (zero shipped); v2 released Mar 2026, activation pending | 1 (program length and op mix drawn, which v2 had to fork to change) | class D: yes for the class; the gap itself has no GPU analogue | 1.46x (X5, measured); 3.8x (X9, claimed, never shipped); 5.4x (Pinecone R1X, claimed, undelivered) |
|
||||
| CryptoNight (Monero) | Bitmain Antminer X3 (180 x BM1700); Baikal Giant N; secret chips from early 2017 | one or two 2 MB scratchpads in on-die SRAM with a hardware AES round per chip (inference from 1.2 kH/s at 2.6 W per chip) | the working set was a 2014 L3, which is a die | live Apr 2014; secret chips about 33 months; X3 announced at 47 and bricked by the v7 fork before delivery; chips back inside 4 months of v8; none in CN-R's 9 months | 2 (the dataset above any die); 1 and 3 (CN-R's per-block program is Igneum's per-epoch one) | yes | 40x (X3 against a Vega 64); Igneum's f = 0 chip 1.86x per joule |
|
||||
| Cuckatoo31 (Grin) | Obelisk GRN1 (cancelled) | one TSMC 16 nm die with 512 MiB of SRAM holding the node bits, lean mining; 150 GPS at 800 W per chip | the resource was SRAM size at 320 MB, and a die was designed for it | hash live Jan 2019; announced Jan 2019; sold Apr; cancelled Jul 2019 | 2 | yes (closed at v3) | 50x planned; the 2025 bounty: memory trades for time, not energy |
|
||||
| Cuckatoo32 (Grin) | iPollo G1 (30 x 12 nm chips) | node bits on die, the 512 MB edge bitmap in serial DRAM (the design's geometry; the G1's own split unpublished) | half the working set stayed in DRAM | Dec 2020, 23 months | 2 | yes | 3x |
|
||||
| Equihash 200,9 (Zcash) | Bitmain Z9 mini, Z9 (BM1740); A9; Z11; Z15 Pro | the whole 144 MB working set on one die ("about 128 MB", eDRAM or SRAM); the Fudan NDSS 2019 design: an on-die linear sorter with the lists in off-chip DDR4, parameter-independent | the 1,000x halving penalty had no proof; 144 MB fit a die; a chip was designed to follow any (n, k) | live Oct 2016; secret chips before May 2018; Z9 mini at 18 months; Z15 Pro at 80 | 2 (a 2 GiB set is not a die); the parameter-following chip is layer 1's warning | yes by layer 2 | 12x (Z9 mini) to 110x (Z15 Pro); the Fudan design 13x in simulation |
|
||||
| Scrypt (Litecoin) | Innosilicon A2; KnC Titan; Antminer L3+ (BM1485); L7 (BM1489) | one 128 KB scratchpad of on-die SRAM per core, 12 cores per 28 nm chip (L3+), 7 nm with 480 chips (L7); Percival's own lookup gap (half the memory for 25 percent more work) | the scratchpad was a 2011 cache; the time-memory trade favoured the chip at 2x to 4x by the designer's note | live Oct 2011; A2 Apr 2014 (30 months); L3+ Jun 2017; L7 Nov 2021 | 2 | yes | 70x (A2) to 1,300x (L7); Igneum's f = 0 chip 1.86x |
|
||||
| Scrypt-N (Vertcoin 2014) | none; dropped pre-emptively | an automatic N doubling a chip follows with more SRAM or a wider lookup gap | public and slow growth sizes the chip for years | Jan 2014 to Dec 2014 | 2 (the precedent) | n/a | no chip shipped |
|
||||
| Lyra2REv2 (Vertcoin) | FPGA bitstreams (2018), then Dayun Zig Z1 | a sponge of about 200 KB per core on die (approximate); 28 nm | SRAM-scale memory inside a hash chain | live Aug 2015; FPGA 2018; Z1 Sep 2018 (37 months); fork Feb 2019 | 2 | yes | 20x |
|
||||
| X16R (Ravencoin) | CVP-13 FPGA bitstreams; OW1 and SKC Turing R1 | sixteen fixed cores with per-block routing | the draw changed the order, not the hardware | live Jan 2018; FPGA named Sep 2018; 45 percent of blocks by Jul 2019; fork Oct 2019; chips "evident" again Jan 2020 | 1 (the warning) | the fixed-function lane closed at v2; the draw is governance | 0.75x (OW1) to 5x (R1) |
|
||||
| MTP, Argon2d (Zcoin) | none; Dinur-Nadler's attack before launch | the prover steers the data-dependent addresses into under 1 MB at a 170x penalty | the attacker controlled the memory's contents | 2017 attack; live Dec 2018; replaced Oct 2021 | 4 | yes (the day key is a VDF of chain state; the floor refuses hot sets) | 2 GB to under 1 MB; Igneum's residue about 1.0004x |
|
||||
| Yescrypt, yespower | none | L2-latency-bound sequential work; small prizes | n/a | 2014 on | 2 | n/a | no chip |
|
||||
| KawPow, Autolykos v2, Octopus, Verthash, FishHash | none | DRAM-scale random reads on small prizes | untested at a prize that pays for a controller project | 2020 to 2024 | 2 | class C: not tested | no chip; the Ethash record is their test |
|
||||
| kHeavyHash (Kaspa) | IceRiver KS0 to KS5L; Antminer KS3, KS5 Pro | a fixed 64 x 64 nibble matrix pipeline | compute with a matrix in it is the cheapest silicon there is | live Nov 2021; KS0 Sep 2023 (22 months); GPU share gone by late 2023 | closed at v2; the mm8 reserve's warning | yes | 170x to 720x |
|
||||
| Eaglesong (Nervos) | Toddminer C1; Antminer K5, K7 | a fixed sponge pipeline | compute | 4 months | closed at v2 | yes | about 100x |
|
||||
| Blake3 (Alephium); Blake2s (Kadena); Blake-256 (Decred); Blake2b (Sia); X11 (Dash); SHA512/256d (Radiant) | the 5 October file's rows | fixed pipelines | compute | 4 to 31 months | closed at v2 | yes | 33x to 6,500x |
|
||||
| NexaPow (Nexa) | DragonBall A21 | a secp256k1 Schnorr signature per nonce in hardware | a wide multiplier chain is already what the GPU does well | live 2023; chip Dec 2024 | closed at v2 | yes | 2.2x: the one compute row where the chip's edge stayed small |
|
||||
|
||||
What the table says, sorted by class: class A (fixed pipelines) 33x to 6,500x, closed at v2; class B (SRAM-scale sets) 12x to 1,300x, closed at v3 by the dataset and the drawn curve; class C (the memory system) 1.0x to 5.1x measured on Ethash, modelled 5.1x to 9.2x for Igneum, NOT closed by any layer; class D (firmware-survivable sequencers) 0.75x to 5x on the program side, closed as governance and open as a chip; class E (steered addresses) closed by layer 4 for the class it models.
|
||||
|
||||
## 2. The chips, in depth
|
||||
|
||||
One section per proof of work. Each carries the mechanism, the miss, the timeline, the layer mapping and the number. Where a lane's return says "not found" the row says so.
|
||||
|
||||
### 2.1 Ethash (Ethereum, July 2015): the E3, the Innosilicon line, Linzhi, Jasminer and the E9
|
||||
|
||||
**The mechanism of the hash.** A DAG of 1 GB growing 8 MB per epoch of 30,000 blocks (about 0.7 GB a year), 64 random 128-byte reads per hash mixed by FNV, keccak at each end; bandwidth-bound by design. EIP-1057 states the miss in one sentence: Ethash "requires external memory due to the large size of the DAG. However that is all that it requires - there is minimal compute ... a custom ASIC could remove most of the complexity, and power, of a GPU and be just a memory interface connected to a small compute engine" (https://eips.ethereum.org/EIPS/eip-1057, read 8 October 2026).
|
||||
|
||||
**The chips, and what each specialised.**
|
||||
|
||||
| Chip | Announced, shipped | The mechanism, as far as any source states it | MH/s, W, MH per joule | Against the best GPU of its year (derived, approximate) | Source (read 8 October 2026) |
|
||||
|---|---|---|---|---|---|
|
||||
| Bitmain Antminer E3 | leaked March 2018, announced 3 to 4 April 2018 at USD 800 (five per customer), shipped 16 to 31 July 2018; later batches USD 1,800 | 18 Ethash chips on three boards with 4 GB of commodity DDR3; chip, node, controller and read width never published; Bitmain's own support called it "a 4G video card" whose DDR "is up to the upper limit"; Rao's audit classes it as the conventional strategy (compute in silicon, memory off chip) | 180 claimed, 190 to 200 shipped, at 800 W: 0.24 MH/J | 1.0x to 1.1x against a tuned GTX 1080 Ti (45 MH/s at about 200 W, 0.225); under Rao's best overclocked 2019 GPU (0.40) | https://cryptoslate.com/bitmain-e3-asic-ethereum-miner/ ; https://2miners.com/blog/asic-miners-for-ethereum-antminer-e3-vs-innosilicon-a10-eth-master-comparison/ ; https://coingeek.com/memory-limitations-prompt-bitmain-antminer-e3-to-halt-etc-support/ ; https://www.kryptex.com/en/hardware/nvidia-gtx-1080-ti/reviews |
|
||||
| Innosilicon A10 ETHMaster, A10 Pro (6 GB), A10 Pro+ (7 GB), A11 Pro (8 GB) | A10 announced 23 July 2018 at 365, 432 and 485 MH/s (USD 3,800 to 5,000); A10 Pro June 2020; A10 Pro+ January 2021; A11 Pro presold March 2021 at 2,000 MH/s and 2,500 W, shipped December 2021 at 1,500 MH/s and 2,350 W ("20 percent less efficient"; broker quotes about USD 27,000) | the same conventional chip with a larger and faster memory system; the DRAM type is not stated on any page read (GDDR6 is the trade's assumption, approximate); node and read width not found | A10 485 at 850 W: 0.57; A10 Pro 500 at 860: 0.58; A10 Pro+ 750 at 1,350: 0.56; A11 Pro 1,500 at 2,350: 0.64 (0.80 claimed) | A10 2.5x against the 1080 Ti; A10 Pro 1.4x and A11 Pro 1.6x (2.0x claimed) against a tuned RTX 3090 (120 MH/s at about 295 W, 0.41) | https://www.criptonoticias.com/mineria/nuevo-minero-asic-innosilicon-procesa-485-mh-ethereum ; https://www.theblock.co/post/125871/innosilicon-ethereum-miner-a11-pos ; https://whattomine.com/coins/151-eth-ethash/asics |
|
||||
| Linzhi Phoenix (E1400) | announced 13 to 14 September 2018 by Chen Min (ex-Canaan) at 1,400 MH/s and 1 kW for April 2019; planned tapeout December 2018, actual September 2019; sample rollout 21 December 2020; no mass production; price never published; the company now "studying new opportunities" | two E1400 boards, each 64 compute units and 72 "mixers" with 4.4 GB of "decentralized memory inside its ASIC design" (The Block); the DRAM type never published (one 2019 forum post says an interposer and stacked HBM dies, unverified); the 4.4 GB sized to the DAG rather than to a commodity 6 or 8 GB, which reads as memory sized per chip (inference); Linzhi claimed a ProgPoW chip would reach 3x to 8x | 2,600 claimed, 2,733 measured by F2Pool, at about 3,000 W: 0.87 to 0.91 | 2.1x to 2.2x against the tuned 3090 | https://www.theblock.co/post/88622/questions-new-ethash-asic-ethereum ; https://www.coindesk.com/tech/2020/12/21/linzhi-begins-rollout-of-long-awaited-ethereum-miner-phoenix ; https://bitcoinmagazine.com/business/new-mining-manufacturer-linzhi-announces-ethereum-asic-miner ; https://linzhi.io/ ; https://github.com/Souptacular/linzhi |
|
||||
| Jasminer X4 (Sunlune) | chip announced 6 June 2021; X4 server announced 11 October 2021, first batch shipped 29 October 2021; launch price not found (USD 497 used today) | **the only Ethash chip that moved the memory**: TechInsights found "the first ever DRAM-to-Logic hybrid-bonding" (wafer-to-wafer DBI), DRAM dies bonded face to face onto a 32 mm by 21 mm logic die on XMC's planar 40/45 nm node; Jasminer's words: "3DIC technology, by integrating the data storage unit and the computing unit on the same chip"; 5 GB per unit, 40 chips per X4 server; the DRAM vendor, capacity per die and node not found | 2,500 at 1,200 W: 2.08 (the X4-1U 520 at 240 W: 2.17) | 5.1x against the tuned 3090; a physics check: 64 reads of 128 bytes is 65,536 bits a hash, so 2.08 MH/J is about 7.3 pJ per bit all in, under any off-package DRAM | https://www.techinsights.com/ko/node/51986 ; https://www.techinsights.com/ko/node/52149 ; https://semiconductor-digest.com/?p=22931 ; https://miningnow.com/asic-miner/jasminer-x4-2500mh-s/ |
|
||||
| Bitmain Antminer E9, E9 Pro | teased 27 April 2021 as "3 GH/s, the work of 32 GPUs"; shipped July 2022 at 2.4 GH/s, eight weeks before the Merge; E9 Pro February 2023, Classic only | conventional off-chip DRAM at larger scale: E9 (model 240-E) 6 GB in six bins 2,100 to 2,400 MH/s; E9 Pro (260-E) 7 GB; memory type not found on any page read; no teardown | E9 2,400 at 1,920 W: 1.25; E9 Pro 3,680 at 2,200: 1.67 | 3.0x and 4.1x against the tuned 3090 | https://d-central.tech/antminer-e9-family/ ; https://www.asicminervalue.com/miners/bitmain/antminer-e9-2-4gh ; https://www.coindesk.com/tech/2021/04/27/bitmain-to-release-antminer-e9-asic-for-ethereum-mining |
|
||||
|
||||
**Why the cap sits at 2x to 5x.** Rao's audit (6 September 2019): every DAG read is random at about 40 ns of latency "completely independent of the memory bandwidth or the computation engine"; "typical DRAM energy dissipation is 3 to 4 pJ per bit" and "the energy expended to move data from DRAM to compute are the same for GPU or ASIC", so the shipping chips showed "about 1.6x hashrate per watt over GPUs" (E3 0.24, A10 0.57, the best overclocked GPU 0.40 MH/W in his table); integrating memory with logic cuts the movement energy "much more than 10x" to "under 0.3 pJ per bit", "the looming threat" (https://github.com/ethcatherders/progpow-audit, the PDF's text, read 8 October 2026). Ren and Devadas (TCC 2017) give the bound: memory hardness bounds area, not energy; the energy of a memory access is comparable on a chip and a CPU, so bandwidth hardness is the only energy lever (https://eprint.iacr.org/2017/225). The derived ladder at 65,536 bits a hash: pure memory energy caps Ethash at about 0.76 MH/J on DDR3, 2.8 on GDDR6 and 3.9 on HBM2 (O'Connor et al., MICRO 2017: HBM2 3.92 to 3.97 pJ per bit, GDDR5 14.0); the shipped chips sit at 0.24 (E3), 0.6 (Innosilicon), 0.9 (Linzhi), 1.25 to 1.67 (E9, E9 Pro) and 2.1 (Jasminer, by leaving commodity packaging). The cap was the memory's own energy per bit, and the one chip that beat it moved the memory onto the die's face.
|
||||
|
||||
**The timeline.** Hash live July 2015; the E3 at 32 months (announced) and 36 (shipped); the first chip over 2x at 65 months (Linzhi, December 2020); 5x at 75 months (Jasminer, October 2021); the Merge at 86 (15 September 2022). The E3's death by DAG growth: Classic first, at epoch 328 (DAG about 3.56 GB, March 2020), then Ethereum, with a 30 March 2020 firmware stretching the DDR to about block 11.4 million (about October 2020): 20 to 27 months after shipping. Ethereum's responses: Zamfir's April 2018 poll (57 percent for an anti-chip fork); EIP-1057 created 2 May 2018, a 93 percent community vote in April 2019, audits delivered September 2019, "accepted" on 21 February 2020, EIP-2538's opposition on 25 February, then stagnant; the share claim in the EIP's own text: "as much as 40 percent of the Ethereum network may now be secured by ASICs" (undated inside a 2018 to 2020 document; no year-by-year series exists). After the Merge: Classic's hashrate went 64 to 183 TH/s in one day; today Classic reads 129.9 TH/s and ETHW 2.15; at USD 0.10 per kWh every Ethash chip in the table loses money (E9 Pro minus USD 5.28 a day), and a later wave (iPollo V1 3.6 GH/s at 3,100 W, June 2022; Jasminer X16-P 5.8 GH/s at 1,900 W, August 2023) holds Classic (https://hashrateindex.com/blog/how-much-ethereum-mining-hashrate-can-other-blockchains-absorb/ ; https://2miners.com/etc-network-hashrate ; read 8 October 2026).
|
||||
|
||||
**The mapping to v6.** Ethash is class C in full, and its chips are the f = 1 chip of `chip-model-v3.md` section 5 at three points on the packaging ladder: commodity DRAM on a board (E3, E9: 1x to 4x), memory sized and placed per chip (Linzhi: 2x), DRAM bonded to the logic (Jasminer: 5x, the model's HBM3 row). None of v6's four layers touches a chip of this class: the program, the mixer, the op mix, the family schedule and the acceptance floor are all firmware or configuration to a controller that stores the dataset; the only layer that reaches it is layer 2, and only when the dataset passes the chip's board, which at 2 GiB plus 0.5 GiB a year is year 60 for a 32 GB board (section 4.2). Does v6 close it: **no**. The number: 5.1x per joule on GDDR7 and 7.5x on one HBM3 stack at zero premium against the 5090 unlocked, 3.6x at the 5090's 1,300 MHz knee, 2.1x at the knee with the class v4 shadow at k = 1 (`counter-asic-4-research.md` section 0); the history's measured band for exactly this chip class is 1.0x (E3) to 5.1x (Jasminer), and Jasminer's number is the model's HBM-class row reached in 2021 on a 40 nm logic die. The forward line of the same mechanism is lane B's (`docs/analysis/class-v6/hardware-future.md`, master 34f63b3c, section 4.4 and its table row for fine-grained hybrid-bonded DRAM on logic: 13x to 17x per joule at zero shadow, modelled, on the 2028 to 2030 roadmaps); the Jasminer X4 is that row's shipped precedent, five years early and at 5x on a planar node, which is the reason to read lane B's 13x to 17x as a ceiling a first product will not reach and a second one will approach. The one thing Igneum has that Ethash did not: the honest card is latency-bound at 4-byte reads, not bandwidth-bound at 128, so the chip's energy per read is the activate's 909 pJ plus a 32-byte atom (2.0 nJ on GDDR7 against the card's measured 8.7 to 10.9 nJ marginal), which is where the 5.1x comes from, and the shadow is the only term on the card's side of that ratio.
|
||||
|
||||
### 2.3 RandomX (Monero, 30 November 2019): the chips, RandomX v2, and the X9's withdrawal
|
||||
|
||||
**The mechanism of the hash.** A VM running 8 chained programs of 256 instructions, 2,048 iterations each, over a 2 MiB scratchpad in three tiers (16 KiB, 256 KiB, 2 MiB) and a 2,080 MiB dataset derived from a 256 MiB cache by SuperscalarHash, a random superscalar program of about 450 instructions with 155 64-bit multiplies per function, tuned to a 170-cycle latency to match DRAM; double-precision floating point in all four rounding modes; the light-mode chip (cache on die) pays 760 cycles and 1,240 multiplies per item, "energy comparable to loading 64 bytes from DRAM" (https://github.com/tevador/RandomX/blob/master/doc/design.md and doc/specs.md, read 8 October 2026). Its DRAM argument: "DRAM cannot do more than about 25 million random accesses per second per bank group", about 1,500 H/s per bank group.
|
||||
|
||||
**The chips, and what each specialised.**
|
||||
|
||||
| Chip | Date | What is known of the inside | Rate, watts | Per joule against the best CPU | Source (read 8 October 2026) |
|
||||
|---|---|---|---|---|---|
|
||||
| Bitmain Antminer X5 | announced 27 August 2023, shipped September 2023 | "Bitmain's first RISC-V architecture CPU" (the reseller's only line); Spagni: not an ASIC but "a board containing multiple RISC-V CPU chips"; SChernykh: the chips were likely in use from about 2021, two years before sale, and do not beat Ryzen rigs per joule; core model, count, node, DRAM type and amount: NOT FOUND, no teardown | 212 kH/s at 1,350 W (157 H/W) | 1.46x over a Ryzen 9 7950X (107.5 H/W on Kryptex); a 100 W-capped Ryzen 9 9950X at 199 H/W beats it | https://criptonoticias.com/mineria/bitmain-lanza-antminer-x5-mineria-monero-asic ; https://bt-miners.com/products/bitmain-antminer-x5-monero-miner-212k-bt-miners/ ; https://pool.kryptex.com/en/device/cpu/amd/ryzen-9-7950x |
|
||||
| Bitmain Antminer X9 | sales opened 26 December 2025 at USD 5,600, shipping scheduled July 2026; WITHDRAWN by mid-May 2026, refunds within hours, zero units shipped, "technical adjustments and a new strategy" through resellers, no Bitmain statement | "custom RISC-V cores specifically optimized for RandomX" (Bitmain's claim as relayed in Monero issue 10270); nothing else | 1,000 kH/s at 2,472 W (404 H/W), claimed | about 3.8x over the 7950X, claimed, never measured | https://bitmain.com.vc/news/bitmain-launches-antminer-x9 ; https://github.com/monero-project/monero/issues/10270 ; https://oneminers.com/blogs/news/whatever-happened-to-the-antminer-x9-bitmain-monero-miner (2 October 2026) |
|
||||
| Pinecone INIBOX R1X | launched March 2026; shipping windows slipped from August to 10 to 18 October 2026; USD 3,200 to 4,950 | "built from ground up silicon", "optimized memory architecture for RandomX"; cores, node, DRAM: NOT FOUND; no delivered unit tested | 1,200 kH/s at 2,055 W (584 H/W), claimed | about 5.4x over the 7950X, claimed, undelivered | https://pineconebox.com/product/3 ; https://millionminer.com/news/monero-mining-guide-2026-antminer-x5-x9-pinecone-r1x-randomx |
|
||||
| tevador's own "possible ASIC design" (24 December 2018, pre-release, marked outdated) | | 4 GiB of HBM for the dataset, 64 MiB of SRAM for 256 parallel 256 KiB scratchpads, 256 decoder and scheduler cores, 28 single-instruction workers; about 120,000 programs a second at about 300 W, "9 times more power efficient than a CPU" | | 9x, by the designer's own estimate of the pre-release design | https://github.com/tevador/RandomX/issues/11 |
|
||||
|
||||
**What the design missed, in its authors' words.** RandomX v2 (PR 317 by SChernykh, merged 17 February 2026; v2.0 released 25 March 2026; Monero mainnet activation in PR 10038, open since August 2025, no date) names the three gaps a chip or a "specially designed CPU" took: (1) CFROUND, the rounding-mode switch, "costs up to 10 percent of hashrate on Ryzen CPUs" and "this is where an ASIC or a specially designed CPU can get an easy advantage"; v2 switches rounding 16 times less often; (2) the scratchpad initialisation was the only AES, so "a dedicated circuit for scratchpad initialization" paid off; v2 puts 16 AES operations per iteration in the main loop; (3) "while CPU cores got faster over the years, RAM latency stayed basically the same", about 50 to 55 ns from tuned DDR4 in 2019 to tuned DDR5 in 2026, so the fixed 256-instruction program left latency slack a faster core could not fill; v2 lengthens the program to 384 and prefetches two iterations ahead. Work per hash rises 52.9 percent; measured CPU hash rates move from minus 12.9 percent (a 100 W-capped 9950X) to plus 8 percent (a 28 W laptop part) (https://github.com/tevador/RandomX/blob/master/doc/design_v2.md and /pull/317, read 8 October 2026). The press reading: v2 "doesn't seem to be an attempt to eliminate every form of specialization", it removes "unintended advantages that benefited hardware in version 1.0" (https://www.coinpro.ch/en/?p=42081, 31 March 2026).
|
||||
|
||||
**The timeline.** Live 30 November 2019; X5 at parity hardware 46 months later (September 2023), on chips SChernykh believes mined privately from about 2021 (21 months after launch); the X9 announced at 73 months and withdrawn at 78; v2 released at 76 months with a 52.9 percent work increase and no activation date; the R1X undelivered at 82 months. The X9's withdrawal is read by the trade press as Bitmain waiting for v2 rather than shipping a part the fork would hit (https://oneminers.com/blogs/news/antminer-x9-cancelled-what-bitmain-pulling-the-model-means-for-monero-mining, 31 July 2026). Corrections to the 5 October file's row 17: the X9 was not delivered in July 2026 (withdrawn, zero units), and "no fork as of October 2026" is now "v2 released, activation pending".
|
||||
|
||||
**The mapping to v6.** RandomX is class D (the firmware-survivable machine): every X5 claim is a many-core RISC-V board over commodity DRAM, a better CPU for a fixed VM spec, and the v2 fixes are exactly the parameters class v6 layer 1 draws (program length, the op mix's cost on the honest machine, the memory latency slack). Two readings bind: first, RandomX's whole gap is the CPU's out-of-order overhead against an in-order many-core board, and Igneum's honest machine is already the in-order many-lane design, so the X5's 1.46x has no Igneum analogue; the Igneum analogue of "a better machine for the fixed spec" is class C's memory chip at 5.1x. Second, the v2 changes show what a per-era draw of program length and op mix buys: it closes the slack a faster honest core leaves (the latency-shadow argument of class v4 in RandomX's words), and it costs the honest machine up to 12.9 percent of rate at the power-capped point, which is the Igneum premium question in another chain's numbers. Does v6 close it: yes for the class (layers 1 and 3 draw what v2 had to fork to change), and the number is the X5's 1.46x, which becomes the shadow's premium arithmetic on Igneum (2.1x at k = 1 at the knee).
|
||||
|
||||
**The Qubic episode (2025) as the detector's lesson.** Qubic's pool reached an average of 22.09 percent of Monero's hashrate over the campaign and 23 to 34 percent during ten withholding periods, with six-hour windows near 50 percent and never a daily 51 percent (Lee and Kim, arXiv 2512.01437, AFT 2026, read 8 October 2026); an 18-block reorg on 14 September 2025 invalidated 117 to 118 transactions. Detection rested on things the adversary controlled: one payout wallet, extra-nonce signatures in its coinbase, its own pool API; when Qubic encrypted its job messages and rotated keys those signals went, and Rucknium's warning stands that a miner split across addresses and solo-mining leaves only the orphan rate and double spends as signals. Qubic ran stock CPU miners, so the nonce-distribution method that found the 2018 and 2019 chips (MoneroCrusher: nonces clustered under about 1.34 billion of 4.3; 85.2 percent of the hashrate, about 5,400 machines at 128 kH/s) did not apply. For Igneum's detector (the 5 October addition 4) this means two instruments, not one: the per-program rate spread and nonce pattern for a chip, and a share-by-key-and-template pattern for a concentrated honest fleet; neither survives an adversary who randomises both.
|
||||
|
||||
### 2.2 ProgPoW (EIP-1057, May 2018): the independent review, the exploit, and the adopters
|
||||
|
||||
**The mechanism.** A random program re-drawn every PROGPOW_PERIOD (50 blocks in 0.9.2, 10 blocks, about 2 minutes, in 0.9.3) from the block number, so miners compile ahead; 16 lanes, a 32-register file per lane, 64 outer iterations each with 4 uint32 DAG loads per lane (256 bytes per lane-group read), 11 cache accesses into a 16 KB cache and 18 random math ops drawn from eleven (add, mul, mulhi, min, rotl, rotr, and, or, xor, clz, popcount) with KISS99 as the generator and FNV1a for merging; keccak-f800 with 32-bit words at both ends "to reduce impact on total power"; the stated aim is that "the algorithm's requirements match what is available on commodity GPUs", the stated chip gain "minimal, roughly 1.1 to 1.2x", with the remaining chip levers named as removing the graphics pipeline, the floating-point units and minor merge-function tweaks (https://eips.ethereum.org/EIPS/eip-1057 and https://github.com/ifdefelse/ProgPOW, read 8 October 2026).
|
||||
|
||||
**The independent review, exactly.** Least Authority (report version 9 September 2019): no issues, five suggestions. Suggestion 2, the light-evaluation attack, in the report's words: "on-die scratchpad memory of around 100 MB is possible in ASICs, that we can fetch at least 128 bytes during a single read, and that such a read might have a latency in the range of one to some tens of cycles"; a chip replaces every DAG read with calc_dataset_item(cache, i) over an on-die cache, at a latency near DATASET_PARENTS x k1, "as low as about 300 cycles"; "the energy expended per bit to access DRAM is about 3 pJ per bit, but when the memory access is on-chip, it decreases to 0.3 pJ per bit, which is a 10x improvement"; conclusion: "efficient light-evaluation attacks may become possible within a few years. This is also an issue that applies to Ethash"; the mitigation offered: raise DATASET_PARENTS from 256 to 512 (which 0.9.4 did), and "for details on the related hardware advancements, please see Bob Rao's corresponding audit report". Suggestion 5: "hardware targeting machine learning is also useful for ProgPoW mining" (the PDF at https://leastauthority.com/static/publications/LeastAuthority-ProgPow-Algorithm-Final-Audit-Report.pdf, text extracted, read 8 October 2026). Bob Rao (6 September 2019): "the only meaningful metric is Energy per Hash"; Ethash chips "about 1.6x hashrate per watt over GPUs"; "10/7 nm processes provide up to 25 Mbits per mm^2 of SRAM and 100 M transistors per mm^2"; "with sufficient on-chip memory available, ProgPOW ASICs with << 0.1x E/H over GPUs can be built"; three chip approaches costed (the whole DAG on die, "possible in 2025+"; a custom stacked memory; the cache only, "about 51 MB as of 30 August 2019", with the item recomputed, "512 MB SRAM" on the slide); the DAG-on-die economics on a three-year Moore cadence: a single die holding a 6.49 GB DAG in 2024 to 2025 at 532 mm^2 and USD 221 per good die (USD 34 per GB), or sixteen dies of 33 mm^2 at USD 6.51 each; "a die that can hold the logic and entire DAG at any point in time becomes cost effective at around 2025 and beyond"; the 16-die split "becomes cost effective today": 16 x USD 6.62 of silicon plus USD 16 of package plus USD 25 of PCB plus USD 25 of heatsink and interface, "about USD 172 total", against "about USD 240" for a GPU board with "8 GB GDDR6 about USD 150"; a 10 nm-class chip "USD 20 M+" and "1+ year to develop, can be done if there is a 150-day ROI to miners" (the PDF at https://github.com/ethcatherders/progpow-audit, text extracted, read 8 October 2026).
|
||||
|
||||
**The exploit.** Kik, 4 March 2020: the 64-bit seed carried between the two keccak passes is too small; fix a seed and compute its mix once, grind an extra-nonce in the header to meet the difficulty on the final keccak, then scan nonces until keccak_progpow_64(header_hash, nonce) equals the seed; the memory path runs once per 2^64 nonces and the rest is keccak, "ASICs benefit most when network difficulty exceeds 2^50"; 0.9.4 widened the carried state from 64 to 256 bits (the digest of the first keccak plus the mix plus padding) (https://github.com/kik/progpow-exploit ; https://github.com/ifdefelse/ProgPOW ; read 8 October 2026).
|
||||
|
||||
**Linzhi's claim.** 8 January 2019: "shocked" by ProgPoW with USD 4 M invested, and a stated intention "to study the feasibility, and then build, ProgPoW ASICs"; the repository recording their claim puts a ProgPoW chip at 3x to 8x (https://github.com/Souptacular/linzhi ; https://forklog.com/proizvoditel-majnerov-linzhi-vystupil-protiv-realizatsii-predlozheniya-progpow/ ; read 8 October 2026). No ProgPoW chip was ever shown.
|
||||
|
||||
**The timeline and the adopters.** EIP created 2 May 2018; a 93 percent vote of 2.93 M ETH in April 2019; both audits September 2019; "accepted" on the 21 February 2020 call; EIP-2538's opposition 25 February; the 6 March 2020 call with "frustration but little progress"; stagnant since; the Merge 15 September 2022. KawPow (Ravencoin, 6 May 2020), FiroPoW (26 October 2021), ProgPowZ (Zano), Sero and Quai (January 2025) run ProgPoW variants; no chip is listed for any of them on WhatToMine or asicminervalue as of 8 October 2026 (https://whattomine.com/coins/234-rvn-kawpow/gpus ; https://www.asicminervalue.com/), on prizes that never reached the market caps at which the 2018 chips appeared (the 5 October file, section 2.5).
|
||||
|
||||
**The mapping to v6.** ProgPoW is class D (a sequencer over eleven ops reads the per-period program as configuration) and class E (Kik). Its review is the one piece of the record that priced Igneum's own chips before Igneum did: the light-evaluation attack is the f = 0 recompute chip (M16, 0.92x at the op budget with the mixer at x8), and Rao's 16-die DAG holder at USD 172 is the f = 1 chip at USD 470 of memory and board. Does v6 close it: the class D half yes (layers 1 and 3 are ProgPoW's period change made an era draw, with no fork), the class E half yes (Igneum's seed is 256 bits through the VDF; layer 4 is the acceptance-side test the audits said to add), and the Rao half no (class C, section 2.1). The number: ProgPoW claimed 1.1x to 1.2x against a conventional compute chip and never priced the memory-system chip; Igneum's model gives that chip 5.1x.
|
||||
|
||||
### 2.4 Cuckoo Cycle (Grin, January 2019): the GRN1, the G32, the iPollo G1 and the 2025 bounty
|
||||
|
||||
**The mechanism of the hash.** Find a 42-cycle in a random bipartite graph of 2^31 or 2^32 edges from siphash; the lean solver keeps "1 bit per edge and 1 bit per node in one partition", bottlenecked by random node-bit access, which "requires tons of SRAM, which is lacking on CPUs and GPUs, but easily implemented in ASICs"; the mean solver keeps 33 bits per edge and is bandwidth-bound, about 4x faster; Tromp: "our primary PoW of Cuckatoo31+ is intended to be mined by ASICs"; the family is "a Proof of SRAM" (https://github.com/tromp/cuckoo ; https://forum.grin.mw/t/cuckatoo31-im-mutability/2442 ; read 8 October 2026). The memory geometry a chip needs (the 13 November 2018 feasibility thread): the 512 MB edge bitmap is accessed sequentially and can sit in external DRAM (96 GB/s with 128 MB on chip); the node bitmap must be SRAM; Cuckatoo31 fits one die at "256 + 64 = 320 MB of on-chip memory", Cuckatoo32 needs "at least 640 MB" or 512 MB of SRAM plus 512 MB of serial DRAM; "trimming is over 99 percent of the effort" (https://forum.grin.mw/t/cuckatoo32-feasibility/1199 ; https://forum.grin.mw/t/advice-on-cuckatoo-hardware-implementation/12042). The original "several orders of magnitude" time-memory claim fell to Andersen's edge trimming on 31 March 2014, two months after publication, and the design took it as its baseline.
|
||||
|
||||
**The chips.**
|
||||
|
||||
| Chip | Dates | Mechanism | Rate, watts, price | Against a GPU | Source (read 8 October 2026) |
|
||||
|---|---|---|---|---|---|
|
||||
| Obelisk GRN1 (Cuckatoo31) | announced 17 January 2019; chip details 20 March; sale 9 April (Mini 70 GPS at 400 W for USD 2,000; GRN1 420 GPS at 2,200 W for USD 10,000; Immersion 840 at 4,400 W for USD 20,000; shipping October 2019); CANCELLED 19 July 2019 with full refunds | one die, TSMC 16 nm, "a full 512 MiB of memory on board" (SRAM), 150 GPS at 800 W per chip, "thousands of hashing cores and memory banks", siphash plus blake2b, two sorter types; cancelled for the Cuckatoo31 phase-out, Grin under USD 2 in May 2019 and funding | 420 GPS at 2,200 W planned | about 50x per joule against a GTX 1080 Ti at about 0.9 GPS and 250 W on C31 (approximate), planned, never built | https://forum.grin.mw/t/obelisk-grn1-chip-details/4571 ; https://forum.grin.mw/t/obelisk-grn1-full-sale/4773 ; https://forum.grin.mw/t/grn1-cancellation-announcement/5624 |
|
||||
| Innosilicon G32 (C31+ and C32+) | announced 17 April 2019 (G32-Mini 21.5 GPS at 140 W for USD 788; G32-1800 328 GPS at 1,800 W for USD 9,388; delivery from August 2019); CANCELLED 16 January 2020 citing foundry delays | about 100 chips at about 1.8 to 1.9 GPS each on C32 (an investor's figure) | never shipped | | https://forum.grin.mw/t/innosilicon-grin-miner-g32-preliminary-specification/4842 ; https://forum.grin.mw/t/innosilicons-grin-asics-canceled/6932 |
|
||||
| iPollo G1 (Cuckatoo32) | December 2020 | 30 chips at 12 nm; whether the node bits sit in SRAM or the edge bitmap in DRAM is not published; Tromp doubts it is multi-chip in Innosilicon's sense | 36 GPS at 2,800 W, USD 9,000 | about 3x per joule against an RTX 4060 at 0.45 GPS and 110 W | https://pool.kryptex.com/device/asic/ipollo/g1 ; https://miningboard.com/algorithms/Cuckatoo32 |
|
||||
|
||||
**The 2025 bounty.** Stephan Theisgen claimed the USD 10,000 linear time-memory trade-off bounty on 11 April 2025 (a solver at N/k bits at most 10k times slower, any k at or above 2), paid 30 April; the measured penalty about half an order of magnitude past linear; Tromp's reading: a chip with N/k bits must hash each edge "roughly (k + 1000) times" against under 6 for the lean miner, so memory can be traded for time but not for energy, and Cuckatoo "remains a Proof of SRAM" (https://forum.grin.mw/t/another-cuckatoo-bounty-succesfully-claimed/11739, read 8 October 2026).
|
||||
|
||||
**The mapping to v6.** Class B. Cuckoo's resource was SRAM size at 320 to 640 MB per die, and a 16 nm die with 512 MiB of SRAM was designed, priced and sold before the economics killed it; the one chip that shipped sat at 3x because half its working set stayed in DRAM. Layer 2 answers it the way the chip model already does: Igneum's 256 MiB cache is the GRN1's die (128 mm^2 at N5, `sram-mirror.md`), and the 2 GiB dataset above it is what the lean solver never had to hold; the time-memory curve Cuckoo got wrong by 50x and then bounded in 2025 (linear in time, not in energy) is the curve `chip-model-v3.md` section 5 draws for Igneum (monotone; the f = 0 end pays 6.3 nJ and 9,360 ops per item against 1.2 to 2.0 nJ for a stored one), and its verdict is the same as Tromp's: memory can be traded for time, not for energy. Does v6 close it: yes, and it was closed at class v3. The number: 50x planned on C31 and 3x shipped on C32; Igneum's f = 0 chip 1.86x per joule and the f = 1 chip, which Cuckoo did not have because its memory was never a DRAM-scale random-read set, 5.1x.
|
||||
|
||||
### 2.5 Equihash 200,9 (Zcash, October 2016): the Z9 and the parameter-following chip
|
||||
|
||||
**The mechanism of the hash and its claim.** Wagner's generalised birthday problem with algorithm binding; the paper's claim: a PoW needing "700 MB of RAM" that "increases the computations by the factor of 1000 if memory is halved" (https://eprint.iacr.org/2015/946, read 8 October 2026). Zcash chose (200, 9), which solvers run in about 144 MB (approximate); no trade-off-resistance bound was ever proved for Equihash (Alcock and Ren, 2017, cited through the Fudan paper below).
|
||||
|
||||
**The chips and the mechanism.** Bitmain's BM1740 (Z9 mini, announced 3 May 2018 at 10 kSol/s and 300 W, shipped June; Z9 September 2018, 42 kSol/s at 970 W on 48 chips) is "a single-chip Equihash miner", "presumably with around 128 MB of memory", eDRAM or SRAM "an open question" (Tromp, 7 June 2018, https://forum.z.cash/t/let-s-talk-about-asic-mining/27353/3332): the whole (200, 9) working set on one die, which the 1,000x claim had assumed impossible at that size. A (144, 5) solver needs over 1.6 GB and cannot fit; Tromp "physically inspected the product and did not find enough memory to handle (144, 5)". Vorick's architecture (13 May 2018) is the other route: "a basic architecture for equihash ASICs that would be able to successfully follow a hardfork that chose any set of parameters", with "massive speedups and efficiency gains over GPUs", because on a chip "you can merge the memory and computation together ... do most of your manipulating in-place" (the essay, through https://steemit.com/crypto/@waraa/the-state-of-cryptocurrency-mining). The published design of that route: Bai, Gao, Hu and Zhang, NDSS 2019, an adversary solver whose sort step is a linear-time insertion sorter of 2,048 "smartcell" flip-flop chains feeding merge stages buffered in off-chip DDR4 (list 1,600 Mib, pairs 2,016 Mib for (200, 9)), pair generation and XOR on small MCUs; simulated at SMIC 28 nm: 40.6 Sol/s at 500 MHz for 0.75 to 0.78 W, 52 to 54 Sol/J against about 4 for the best GPU software, "at least 10x" and parameter-independent (https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_09-5_Bai_paper.pdf, read 8 October 2026). Later chips: Innosilicon A9 (June 2018, 50 kSol/s at 620 W, USD 9,999); Z11 (April 2019, 135 kSol/s at 1,418 W, 12 nm); Z15 (420 kSol/s at 1,510 W); Z15 Pro (June 2023, 840 kSol/s at 2,780 W). Against a GTX 1080 Ti at 735 to 785 Sol/s and 250 to 305 W (about 2.7 Sol/J): Z9 mini 12x, Z9 16x, A9 30x, Z11 35x, Z15 Pro 110x (https://www.asicminervalue.com/miners/bitmain/antminer-z9 ; https://www.asicminervalue.com/miners/bitmain/antminer-z15-pro ; https://en.wikibooks.org/wiki/ZCash_mining_GPU_Comparison/GPU_Mining ; read 8 October 2026).
|
||||
|
||||
**The timeline.** Hash live 28 October 2016; three groups mining on secret chips before the Z9 announcement (Vorick); the Z9 mini at 18 months; the Zcash Foundation's statement of 8 May 2018 asked whether chips "could handle different parameters of Equihash" and the community vote of June 2018 went 45 to 19 against prioritising resistance; no fork; proof of stake announced November 2021; the Z15 Pro at 80 months.
|
||||
|
||||
**The mapping to v6.** Class B, with a lesson for layer 1: Equihash's parameters (n, k) were the knob the forks turned (Bitcoin Gold to 144,5; Beam to 150,5; Flux to 125,4), and a chip was designed to follow "any set of parameters" by keeping the memory off die and the sort on it. A per-era draw of a parameter a chip can follow is a configuration to it; a draw of the memory SIZE is the one that forced the Z9's single die to fail on (144, 5), which is layer 2's mechanism, not layer 1's. Does v6 close it: yes by layer 2 (a 2 GiB set is not a die), and the parameter-following chip is the honest warning for layer 1's draws. The number: 12x at the first chip, 110x by 2023, against a hash whose 1,000x penalty claim never had a proof; Igneum's claim for its curve rests on a drawn curve (`chip-model-v3.md` 5.4) and the in-house pass's exact pebbling optimum (adv-cache-3), and still has no proof.
|
||||
|
||||
### 2.6 Scrypt and Argon2: the Litecoin chips, the lookup gap, and MTP
|
||||
|
||||
**Scrypt (Tenebrix and Litecoin, 2011; N = 1,024, r = 1, p = 1: a 128 KB scratchpad).** The mechanism of every scrypt chip is one 128 KB of on-die SRAM per hashing core and many cores per die: Watkins (2014) "there only needs to be 128 KB of memory per processor ... one core driving a 128 KB cache", SRAM chosen over every other memory as fastest, salsa20/8 about 60 percent of the runtime and memory access 38 percent (https://arxiv.org/pdf/2208.02160); the BM1485 of the L3+ (June 2017, 504 MH/s at 800 W): 12 cores per chip, "every BM1485 integrates on-die SRAM to hold that scratchpad", 28 nm, 288 chips per unit, about 1.7 MH/s per chip (so about 1.5 MB of SRAM per chip, arithmetic) (https://d-central.tech/mining-glossary/bm1485/ ; https://www.asicminervalue.com/miners/bitmain/antminer-l3-504mh); the L7 (November 2021, 9.5 GH/s at 3,425 W, USD 15,000, 0.36 W per MH against the L3+'s 1.58): the BM1489 at TSMC 7 nm, 480 chips (https://d-central.tech/mining-glossary/bm1489/ ; https://cryptoage.com/en/2550-bitmain-antminer-l7-is-a-new-asic-miner-for-litecoin-and-dogecoin.html); the Innosilicon A2 (21 April 2014, 28 nm, 1.6 to 1.8 MH/s per chip at 10 W, about 150 MH/s per box at 1 kW: https://www.design-reuse.com/news/34403/innosilicon-28nm-litecoin-asic-reference-miner.html); the KnC Titan (March 2014, 250 MH/s at 800 to 1,000 W, USD 9,995, "4 chips x 2,284 cores": https://www.coindesk.com/markets/2014/03/28/kncminer-updates-titan-spec-promises-250mhs/), whose 2,284 cores cannot each hold 128 KB on a 2014 die, so it shared scratchpads or took the time-memory trade-off (not confirmed). The trade-off itself is in the designer's record: Percival (18 November 2012) on storing every other scratchpad entry: memory halved for about 25 percent more BlockMix work, area-time about 0.625x, the trade favouring the attacker at 2x to 4x reductions, "already in the paper's cost estimates" (https://mail.tarsnap.com/scrypt/msg00092.html; all read 8 October 2026). Against an R9 280X at 700 to 740 kH/s and 340 to 450 W at the wall: A2 about 70x, L3+ about 300x, L7 about 1,300x per joule (derived, approximate). Scrypt-N (Vertcoin 2014: N doubling by timestamp up to 2^30) was dropped on 13 December 2014 for Lyra2RE "as a proactive defense against emerging Scrypt-N capable ASICs", because raising N "simply involves doing more iterations" and more SRAM or a larger lookup gap (https://vertcoinproject.org/vertcoin_whitepaper.pdf ; https://coincentral.com/what-is-vertcoin-a-beginners-guide/).
|
||||
|
||||
**Argon2 as a proof of work (MTP, Zcoin, 10 December 2018).** Argon2d over 4 GB with a Merkle tree; Dinur and Nadler (2017): malicious proofs with under 1 MB, 1/3,000 of the honest memory, at a computation penalty of 170, "more than 55,000 times faster than what is claimed by the designers", with a 2^64 one-time precomputation, by injecting chosen blocks that steer Argon2d's data-dependent addresses (https://eprint.iacr.org/2017/497); MTP 1.2 patched it before launch; no Argon2 chip was ever built; Firo replaced MTP with FiroPoW on 26 October 2021 (https://firo.org/2021/10/01/firopow-and-instantsend-release.html). Yescrypt and yespower (GlobalBoost-Y 2014; Yenten, Cranepay, Tidecoin on yespower from 2018; L2-latency-bound sequential work: https://www.openwall.com/yespower/): no chip found, small prizes.
|
||||
|
||||
**The mapping to v6.** Class B throughout, and the parameter-growth lesson for layer 2 in its oldest form: Scrypt-N's automatic growth was abandoned because a chip follows a scratchpad that grows by doubling the SRAM it already has, and Percival's own 2012 note says the time-memory trade favours the chip at 2x to 4x. Does v6 close it: yes by layer 2 (no die holds 2 GiB; the curve is monotone against partial stores); the lookup-gap lesson is the reason the dataset's chained cache has a drawn pebbling optimum (adv-cache-3) rather than a claim. The number: 1,300x for scrypt by 2021; Igneum's f = 0 chip at 1.86x per joule. MTP is class E (section 4.4).
|
||||
|
||||
### 2.7 to 2.9 KawPow, Autolykos, Octopus: the no-chip hashes, and why
|
||||
|
||||
| Hash | Live | Mechanism | Chip status, 8 October 2026 | Why no chip (the honest reading) | Source |
|
||||
|---|---|---|---|---|---|
|
||||
| KawPow (Ravencoin; Neoxa, Clore, Meowcoin, Neurai) | 6 May 2020 | ProgPoW 0.9.4 with a per-block program; "no additional future algorithm forks are envisaged" | none listed on WhatToMine or asicminervalue | class D on a small prize: Ravencoin's cap never reached the 2018 cluster's | https://whattomine.com/coins/234-rvn-kawpow/gpus ; https://github.com/RavenProject/Ravencoin/blob/master/roadmap/README.md |
|
||||
| Autolykos v2 (Ergo) | February 2021 (v1 July 2019) | a k-sum (k = 32) over a Blake2b table of 2^26 elements of 31 bytes (2.08 GB) growing about 5 percent per 51,200 blocks from block 614,400 to a cap of 2,143,944,600 elements at block 4,198,400; v1's non-outsourceable puzzle removed because "large players could bypass this resistance using smart contracts" | none | class C territory (a table read per hash) on a small prize; its growth rule is the one automatic schedule in the record untested by a chip; f2pool and Ergo's own docs call it resistant with no chip named | https://docs.ergoplatform.com/mining/autolykos/ |
|
||||
| Octopus (Conflux) | October 2020 | Ethash-style DAG (the "dense matrix step" unverified in the source) | none | as above; f2pool: "cannot be efficiently mined with FPGAs or ASICs" | https://f2pool.io/mining/guides/how-to-mine-conflux/ |
|
||||
| Verthash (Vertcoin), FishHash (Iron Fish from April 2024, Karlsen) | January 2021; April 2024 | a 1.2 GB table from the chain's headers; a 4,608 MB constant dataset with Blake3 and 512 iterations | none | the same class as Ethash's chips, on prizes under the 2018 cluster | https://fips.ironfish.network/fips/fip-3-memory-hard-mining-algorithm |
|
||||
|
||||
The reading for v6: "no chip" on a DRAM-scale random-read hash is an economic fact, not a design one (the 5 October file, section 2.5: compute-bound hashes got chips at USD 20 K to 30 K of daily issuance, Ethash at USD 7.6 M). Every hash in this table is class C and none of them has been tested at a prize that pays for a controller project; the Ethash record is the test, and it read 1x to 5x.
|
||||
|
||||
### 2.10 kHeavyHash (Kaspa, November 2021): the KS chips
|
||||
|
||||
**The mechanism.** cSHAKE256 of the header and nonce, a 64 x 64 matrix of 4-bit values generated from the pre-PoW hash, a nibble-wise matrix-vector multiply, XOR, a final cSHAKE (https://github.com/kaspanet/rusty-kaspa/blob/master/consensus/pow/src/lib.rs, read 8 October 2026); designed for optical and specialised hardware; the chip wires the multiply as a fixed pipeline. IceRiver KS0 (September 2023, 100 GH/s at 65 W), KS1 (1 TH/s at 600 W), Antminer KS3 (August 2023, 9.4 TH/s at 3,550 W), KS5 Pro (March 2024, 21 TH/s at 3,150 W), KS5L (April 2024, 12 TH/s at 3,400 W); against an RTX 4090 at 2.08 GH/s and 226 W: KS0 about 170x, KS5 Pro about 720x per joule (https://www.asicminervalue.com/miners/bitmain/antminer-ks5-pro-21th ; https://www.kryptex.com/overclocking/nvidia-rtx-4090-micron-24gb-medium-overclock ; read 8 October 2026). Timeline: 17 to 20 months to the first chip; the GPU share negligible by late 2023; seven forks left Kaspa to re-resist (the 5 October file, row 23). Node: not found.
|
||||
|
||||
**The mapping to v6.** Class A. The matrix multiply is a warning for the reserve's mm8 family, not for the hash: a fixed 64 x 64 nibble multiply is the cheapest thing silicon does, and the measured rows agree (the 5090's int8 MAC at 1.4 to 4.1 pJ against a 5 nm array's claimed 0.04 to 0.4, `counter-asic-4-research.md` 15.1a). Does v6 close it: yes, closed since class v2 (no fixed function to unroll); the number, 720x, is what a fixed pipeline does to compute-bound work, and the reserve's ordering (section 4.3) keeps mm8 last for exactly this reason.
|
||||
|
||||
### 2.11 CryptoNight (Bytecoin 2012, Monero 2014): the X3, the secret chips, and four forks
|
||||
|
||||
**The mechanism of the hash.** 524,288 iterations of an AES round plus an 8-byte multiply over a 2 MB scratchpad sized to a 2014 per-core L3; latency-bound at SRAM scale.
|
||||
|
||||
**The chips.** Bitmain Antminer X3, announced 15 March 2018 at 220 kH/s and 550 W (465 to 470 W measured), 180 BM1700 chips on three boards, USD 11,999 for batch 1 falling to USD 1,900; no teardown or vendor description of the BM1700 exists (node, SRAM, AES units not found). The arithmetic is the mechanism: 1.2 kH/s and about 2.6 W per chip is one or two 2 MB scratchpads in on-die SRAM with a hardware AES round per chip, and nothing else reaches that rate in that power (approximate, inference). Baikal Giant N, March 2018, 20 kH/s at 60 W. Against a Vega 64 at 2,009 H/s and about 200 W card power (about 10 H/W) the X3 is about 40x per joule; against a Threadripper 1950X at about 1,000 H/s and 185 W, about 75x (approximate) (https://www.asicminervalue.com/miners/bitmain/antminer-x3-220kh ; https://www.asicminervalue.com/miners/baikal/bk-n ; https://hothardware.com/reviews/monero-mining-with-amd-ryzen-threadripper ; read 8 October 2026).
|
||||
|
||||
**The secret chips.** Monero's own 2018 review: by early 2018 "it was estimated that 80 to 90 percent of the network was specialized hardware" (https://web.getmonero.org/2019/02/12/2018-year-in-review.html); about half the hashrate (about 500 MH/s of 1,000) left at the v7 fork on 6 April 2018; Krawiec-Thayer's nonce study (24 November 2018) found half of all blocks with nonces in the lowest 0.002 percent of the space, patterns that "evaporated abruptly" at the fork (https://www.hackernoon.com/utter-noncesense-a-statistical-study-of-nonce-value-distribution-on-the-monero-blockchain-f13f673a0a0d). Vorick (13 May 2018, through secondary coverage): secret Monero ASIC mining "since early 2017, making up 50 percent of the hashrate". The timeline from the hash to the first secret chip is therefore about 33 months (April 2014 to early 2017), not the 43 the 5 October file gives (which counted to the fork); the announced chip came at 47.
|
||||
|
||||
**The four forks and what each cost a chip.** v7 (6 April 2018): a one-byte tweak to the main loop; half the hashrate left; "a precaution and deterrent". v8 (18 October 2018, height 1,685,555): a whole-cache-line shuffle (4x the bandwidth demand) plus a 64:32 division and a 64-bit square root per iteration, 5 to 10 percent off CPU rate; the hashrate went from about 320 MH/s to just under 1,000 MH/s before March 2019 with chip nonce patterns visible from December: chips back inside two months, dominant inside four (https://decrypt.co/14421/ ; https://en.cryptonomist.ch/2019/05/08/mining-monero-hashrate-pow-change/). CryptoNight-R (9 March 2019, brought forward from April after the detection): a per-block random sequence of 60 to 69 integer instructions (63 on average; MUL 40 percent, XOR 23, SUB 12, ADD 12, ROR 8, ROL 6) over 9 registers, seeded by height so miners compile ahead; SChernykh's claim is a chip's minimum latency for the random math "at least 2.5 times higher" than the DIV plus SQRT it replaced (a chain of 15 multiplies against 6), with up to 1.5x more for an out-of-order chip; a hardware engineer in the PR thread estimated a chip could still do about 18 ns per iteration, comparable to a CPU (https://github.com/SChernykh/CryptonightR ; https://github.com/monero-project/monero/pull/5126). The hashrate fell from about 1 GH/s to 140 MH/s and settled at 300 to 350 MH/s; no CN-R chip is documented in its nine months. RandomX followed on 30 November 2019.
|
||||
|
||||
**The mapping to v6.** CryptoNight is class B (SRAM-scale memory: layer 2 closes it, the 2 GiB dataset is 1,000 mm^2 of SRAM even at N5) and class D (the forks: v7 and v8 kept the machine's shape and the chips returned; CN-R changed what the machine had to be, a per-block random program, and no chip came in nine months, which is the per-epoch random program Igneum ships). The number: 40x per joule for the X3 against a GPU; the Igneum analogue, the f = 0 recompute chip that holds the 256 MiB cache on die, reads 0.92x at the op budget and 1.86x per joule (`chip-model-v3.md` 5.4), and CN-R's 2.5x latency claim is the fixed-shape mixer's 3x factor in the other direction. The honest residue: Monero's chips were found by nonce pattern four months after a fork at 85 percent of the hashrate; Igneum has no detector yet.
|
||||
|
||||
### 2.12 X16R (Ravencoin, January 2018): a drawn order over fixed functions
|
||||
|
||||
**The mechanism and the chips.** Sixteen hash functions in an order set by the previous block hash, a per-block automatic change with no fork. The sequencing changed; the sixteen primitives did not, so one large FPGA holding all sixteen cores needs only per-block routing: BittWare and SQRL's CVP-13 (Xilinx VU13P) was announced on 7 September 2018 naming "X17r, X16r and TimeTravel10" (https://www.cryptoninjas.net/2018/09/07/squirrels-research-labs-and-bittware-launching-new-fpga-crypto-mining-hardware/ ; https://www.bittware.com/cvp-13 ; read 8 October 2026). An unknown pool held 10 to 20 percent of blocks in February 2019, 30 to 40 in March, 45 by 11 July; the OW Miner OW1 (September 2019, 182 MH/s at 1,500 W, about USD 500) and SKC Turing R1 (680 MH/s at 800 W, about USD 1,500) were sold as ASICs, their insides unconfirmed; against a P102-100 at 35 MH/s and 219 W the OW1 is 0.75x and the R1 about 5x per joule (https://cryptoage.com/en/1782-asics-ow-miner-ow1-and-skc-miner-turing-r1-for-the-x16r-algorithm-exist.html ; https://miningboard.com/algorithms/X16R). X16Rv2 (1 October 2019) swapped one hash in; on 30 January 2020 Tron Black called the chips' return "evident"; KawPow followed in May 2020; Ravencoin's roadmap records "ASICs have been developed for X16R (and X16Rv2)" (https://cointelegraph.com/news/ravencoin-community-clash-over-mining-algorithm-continues ; https://github.com/RavenProject/Ravencoin/blob/master/roadmap/README.md).
|
||||
|
||||
**The mapping to v6.** Class D in its purest form, and the warning for layer 1: a draw over a FIXED set is a configuration to a chip that holds the set. Igneum's op-mix draw is over twelve families a chip holds from genesis; what the draw does cost a chip is nothing, and what it costs the honest card is the per-vendor energy table. Does v6 close it: the fixed-function lane is closed by the per-epoch program (there is no sixteen-core pipeline to route), and the draw itself is a governance device. The number: 5x for the one X16R box with a plausible chip inside; the GPU-without-graphics sequencer's gain on Igneum's program side is bounded by that kind of figure, and its memory side by class C's 5.1x.
|
||||
|
||||
### 2.13 Lyra2REv2 (Vertcoin, August 2015): FPGA first, then the Zig Z1
|
||||
|
||||
A memory-hard sponge (Lyra2 at T = 1, R = 8, C = 256, p = 1: a matrix small enough for on-die SRAM per core, on the order of 200 KB, approximate) inside a chain of hashes. The FPGA came first: an academic Lyra2 core on 16 July 2018 and a full FPGA miner at 2.6 to 3.7 MH/s and 323 to 432 nJ per hash, "significantly more energy efficient than both a GPU and a commercially available FPGA-based miner", which confirms commercial bitstreams before the chip (https://arxiv.org/abs/1905.08792); the Dayun Zig Z1 on 19 September 2018: 6.8 GH/s at 1,200 W, 28 nm, USD 8,000, "equivalent to 100 GeForce GTX 1080 Ti", about 20x per joule against a 1080 Ti at about 68 MH/s and 250 W (https://cryptoage.com/en/1231-first-asic-miner-lyra2rev2-dayun-zig-z1.html; read 8 October 2026). Vertcoin 0.14.0 forked at block 1,080,000 (1 February 2019) "to rid the network of the current generation of Lyra2REv2 ASICs and FPGAs" with Lyra2REv3 (R = 32, p = 4: 16x the memory), then Verthash in January 2021 (https://github.com/vertcoin-project/vertcoin-core/releases/tag/0.14.0). The 22 reorgs of October to December 2018 and the December 2019 attack came through rented hash on a hashrate the forks had reset (the 5 October file, row 7). Mapping: class B (layer 2 closes it) and lesson 5 (the fork reset the hashrate to a rentable size, which v6's layers 1 and 3 never do). The number: 20x; Igneum's f = 0 chip 1.86x.
|
||||
|
||||
### 2.14 Eaglesong (Nervos, November 2019) and Blake3 (Alephium, November 2021): compute, embraced
|
||||
|
||||
Eaglesong: a new sponge; the Toddminer C1 in February 2020 at 4 months, the Antminer K5 (April 2020, 1.13 TH/s at 1,580 W), the K7 at 63.5 TH/s and 3,080 W; about 100x per joule against an RTX 2080 Ti at about 1.5 GH/s and 220 W (approximate) (https://www.asicminervalue.com/miners/bitmain/antminer-k5-1130gh ; https://miningboard.com/algorithms/Eaglesong). Blake3: double Blake3, chosen as chip-friendly; the Goldshell AL-BOX (May 2024, 360 GH/s at 180 W), Antminer AL1 (15.6 TH/s at 3,510 W) and AL1 Pro (August 2024, 16.6 TH/s at 3,730 W), AL3 (8 TH/s at 3,200 W); about 100x to 220x per joule against an RTX 4090 at 6.0 GH/s and about 300 W (https://www.asicminervalue.com/miners/goldshell/al-box ; https://whattomine.com/asics/294-bitmain-antminer-al1-pro). Blake2s on Kadena: the Goldshell KD5 (March 2021, 18 TH/s at 2,250 W) and Antminer KA3 (166 TH/s at 3,154 W) against a GTX 1660 Super at 633 MH/s and 79 W: about 1,000x and 6,500x (https://www.asicminervalue.com/miners/goldshell/kd5 ; https://miningboard.com/algorithms/Blake%20%282s-Kadena%29). NexaPow (SHA-256 plus a secp256k1 Schnorr signature per nonce, "useful ASICs"): the DragonBall A21 (December 2024, 3.4 GH/s at 1,800 W, USD 6,999) is only about 2.2x per joule against an RTX 4090 at 320 MH/s and 380 W, because big-integer elliptic-curve arithmetic leaves a fixed pipeline little to strip (https://spec.nexa.org/mining/NexaPOW/ ; https://www.asicminervalue.com/miners/dragonball-miner/a21 ; all read 8 October 2026). Mapping: class A, closed since class v2; the numbers (100x to 6,500x) are what a fixed function costs, and NexaPow's 2.2x is the one compute-bound row where the chip's edge stayed small, because the work was already a wide multiplier chain, which is the shape of the ALU shadow's k band (0.3 to 0.8) in another chain's numbers.
|
||||
|
||||
## 3. The three lessons, with the evidence
|
||||
|
||||
**Lesson 1. The chip that stores the dataset is firmware-immune to every draw; only joules and memory growth move it.** Evidence: the five Ethash chips (section 2.1) never touched Ethash's compute and never needed to; Rao's audit said in 2019 that "the energy expended to move data from DRAM to compute are the same for GPU or ASIC" and that the only lever a chip has is the memory's own energy per bit, which Jasminer took by bonding the DRAM to the die; the Least Authority audit's conclusion that "the random math core likely prohibits the build of a light-evaluation based ASIC" was about the f = 0 chip and said nothing about the f = 1 chip, which is the one that shipped on Ethash in three forms. On Igneum's side the identity of `counter-asic-4-research.md` section 2 says it in one line: at zero premium the edge is E_card over E_mem and no hash change touches it. What v6 inherits: 5.1x per joule on GDDR7 at zero premium against the 5090 unlocked, 3.6x at its 1,300 MHz knee, 2.1x with the class v4 shadow at k = 1, and USD 2.8 against 14.7 per MH/s. What binds on v6: every per-era draw of layer 1 and every family epoch of layer 3 must be priced against this chip as a configuration change (a recompile per epoch, a new mapping per era) and never claimed as a cost to it; the public text's "under 2x" stays worded against the recompute chip, as the 6 October verdict already requires.
|
||||
|
||||
**Lesson 2. Automatic change beats the human fork only where it costs the chip a redesign, and the one parameter that does is the memory.** Evidence: Grin's three Cuckaroo tweaks (17 July 2019, 15 January 2020, 16 July 2020) each changed the edge function and no chip ever shipped for the lane, but each was a hard fork with a new solver and the lane was scheduled to die; Monero's v7 and v8 kept the machine's shape and the chips were back inside four months, CN-R changed the machine and no chip came in nine months, RandomX changed it again and the X5 took 46 months; X16R's per-block order cost the FPGA nothing; Scrypt-N's public, slow growth was abandoned before a chip because the chip could be sized for years of it; Ethash's DAG growth is the one automatic rule in the record that killed a shipped chip, and it killed the one chip whose memory was sized to the card fleet's own limit (4 GB), 20 to 27 months after shipping. The compile and design cycles bound the race: Bitmain built the A3 in about 5 months and Halong the B52 in 9 (Vorick), a full Vivado compile on a mid-size part runs 42 to 160 minutes and hours on a large one (PRflow, FPT 2019), so an hourly program outruns every compile and a six-monthly change outruns no chip. What binds on v6: layers 1 and 3 close the governance failure (no fork, no hashrate reset to a rentable size, which cost Vertcoin two 51 percent attacks) and tax a sequencer chip die area, not architecture: Rao's own figure is about 1 M gates and 0.025 mm^2 at 10 nm for ProgPoW's whole inner loop, so a lane array carrying every reserve family is the class v4 shadow core's 30 mm^2 of N5 and USD 25 to 40, not a wall. Layer 2 is the one real lever, and its value is its floor and ceiling, not its tracking: the floor keeps class B closed (2 GiB is 1,000 mm^2 of SRAM at N5), and a ceiling under the honest tiers' memory is a requirement, because any rate that ages out a 32 GB chip board retires the 8 GB card first.
|
||||
|
||||
**Lesson 3. A steered or biased address pattern is always found after launch unless the test lives in the acceptance rule, and every new draw needs its own null.** Evidence: Kik's exploit came five months after two audits that named the seed's keccak as a thing to scrutinise and three days after the EIP was declared dead; Dinur and Nadler found MTP's under-1 MB proof before launch only because the construction was published and reviewed, and the fix was a construction change; AP-F8-1 was found by the attack-pass lane one day after class v4 reached the devnet, in 96.6 percent of the class's programs, and took three sub-versions and class v5's floor to close; the in-house pass then found a program that passed every part of the rule and still read a live hot set, which is why the floor sits at 0.995 and not 0.98. What binds on v6: layer 4's test is a per-site ratio against the window model, and the window model is a function of the dataset size, the windows, the era stride and the read width, so a draw of any of those changes the null and the census must be re-derived per era (2.2 s per candidate at 2^20 on one box core; 0.7 percent of candidates refused at the floor); the residue the floor cannot reach without refusing most clean programs (the shadow-written concentrations at 0.9992 to 0.9997, about 1.0004x) moves with any draw of the shadow placement and needs its own ceiling per era, with the F8 gate's 1.2x-of-window shape.
|
||||
|
||||
## 4. The four layers against the history, layer by layer
|
||||
|
||||
### 4.1 Layer 1: per-era draws of the class parameters
|
||||
|
||||
What is declared: the parameters now fixed by release (the mixer round count within the tested margin, the op-mix weights within the measured safe band, the read width, the program length, the shadow placement) are drawn per era from chain state like the program. What the history says about each:
|
||||
|
||||
| Parameter drawn | The precedent | What the draw costs a chip | What it costs the honest card and the verifier | Reading |
|
||||
|---|---|---|---|---|
|
||||
| Mixer round count (within the tested margin) | RandomX made the item derivation itself a random program so a chip could not hard-wire it (SuperscalarHash); CryptoNight-R's random math raised chip latency 2.5x | nothing on the f = 1 chip (it derives no item); on the f = 0 recompute chip the fixed shape is the 3x factor, and a drawn ROUND COUNT keeps the shape: the chip builds the widest count and gates the rest | the verifier's 10 ms gate caps the count (x8 is 2.1 ms per warp on the reference core, x16 about 3.7); the daily build 23 to 77 ms at x8 | a draw of the count within a margin the chip already covers is firmware; the lever against the recompute chip is a drawn SHAPE (the 5 October addition 2, reserve), and the recompute chip is not the one anyone builds (chip-model-v3 5.6) |
|
||||
| Op-mix weights (within the measured safe band) | X16R drew the ORDER of sixteen fixed hashes per block and an FPGA served it at 1.3x within 20 months; ProgPoW drew the math per period and no chip exists on its adopters in eight years, on small prizes | a sequencer chip over the twelve families covers any weight table; the measured GPU cost per family is the real constraint (shfl 55.8 pJ against add 11.3 on the 5090: a shuffle-heavy draw taxes the card up to 5x per instruction with no better k) | the per-program hash-rate spread must stay under the 5 percent rule on every vendor across the band (the six-era spread was 1.3 to 3.2 percent on the era layout) | right as a governance device (no fork), neutral as a chip device; the band must be bounded by the per-vendor cost table of `counter-asic-4-research.md` 15.1a, not only by the rate spread |
|
||||
| Read width | w16 moved the honest denominator 2.7 percent and the chip's cost not at all; w64 made the 5090 bandwidth-bound (71.9 MH/s); the 9070 XT pays a 64-byte line at every width | the chip pays the same 32-byte atom at w4 and w16; wider reads hand a custom controller the Ren-Devadas bandwidth lever (the Ethash chips' whole edge) | a 47 percent loss on the 5090 at w64 | the one parameter whose draw can move the memory physics the wrong way: keep the allowed set at {1} (as 1.13.1 already does) unless a wider width is measured latency-bound on all three vendors; a draw over {4 B, 16 B} is harmless and worthless |
|
||||
| Program length | ProgPoW's loop count and Ethash's 64 accesses were fixed; RandomX's 8 chained programs of 256 instructions fixed; no chain drew its program length | a longer program is more shadow work per hash: the class v4 lever (the latency ladder) priced at 2.1x at k = 1; a chip builds the longest rung's core and idles it on short eras | the verifier rung 3 is inadmissible on the reference core (latency-ladder section 5); the card's premium per op 6.2 to 11.3 pJ measured | the draw must stay inside the admissible rungs (0 to 2); its value is the governance one (the ladder stepped by draw instead of by 90 percent signal), and the honest card pays the premium on every era |
|
||||
| Shadow placement | no precedent in any chain; the per-load placement (16 blocks of 16 after every load) was DEAD as drawn on 7 October (acceptance in execution order accepts 1.4 percent of candidates; `counter-asic-4-research.md` 20.2a-close) | the per-load form would force the chip's ALU core inside every read's dependency (the USD 200 M break-even row, 16.2) if a sound form existed | compile-ahead at 16 sites; a class change | draw only over placements shown sound (today: the one block after the loads); the per-load form is the research item, not a draw value |
|
||||
|
||||
### 4.2 Layer 2: the dataset's size tracking chain-state growth with a floor
|
||||
|
||||
What is declared: the state-derived dataset's size tracks chain-state growth, with a floor, so fixed-memory silicon ages out. The history has exactly two scheduled memory-growth rules that ran against shipped hardware, and one of them killed a chip:
|
||||
|
||||
| Precedent | The rule | What it did to chips | What it did to honest cards | Source |
|
||||
|---|---|---|---|---|
|
||||
| Ethash DAG growth | +8 MB per epoch of 30,000 blocks (about 0.7 GB a year; Rao's audit, slide "DAG size"); 1 GB at launch (July 2015), 3.0 GB by July 2019, 3.94 GB by November 2020 | the Antminer E3 (shipped July 2018, 4 GB of DDR3) ran out of DAG room on Classic at epoch 328 (about 3.56 GB, March 2020) and on Ethereum at about block 11.4 million (about October 2020) after a 30 March 2020 firmware stretched its DDR use: 20 to 27 months after shipping, 57 to 63 months after the hash went live; the A10 Pro (6 GB) and every later chip carried more memory than any card of its year and never aged out | the same rule retired 3 GB cards in 2018 and 4 GB cards by December 2020; Ethereum Classic cut the DAG to 2.47 GB (Thanos, ECIP-1099, block 11,700,000, 28 November 2020) to keep the 4 GB cards after the August 2020 51 percent attacks, and its own text says the fork kept "competitive mining across both GPU and ASIC hardware" | https://ethereumclassic.org/blog/2020-11-27-thanos-hard-fork-upgrade/ ; https://coingeek.com/memory-limitations-prompt-bitmain-antminer-e3-to-halt-etc-support/ ; https://cointelegraph.com/news/bitmains-antminer-e3-to-continue-mining-ether-with-new-update (all read 8 October 2026); Rao's audit, the DAG-size slide (https://github.com/ethcatherders/progpow-audit) |
|
||||
| Autolykos v2 table growth | N = 2^26 elements of 31 bytes (2.08 GB) until block 614,400; then about 5 percent every 51,200 blocks (N doubles every 102,400 blocks, about 142 days at Ergo's 2-minute block) to a cap of 2,143,944,600 elements at block 4,198,400 (about 66 GB) | no chip has shipped for Autolykos as of today, on a small prize; the rule has never been tested against one | the table passes 8 GB in about 2 years of growth and 16 GB a year later (arithmetic on the rule); Ergo's GPU fleet thins by card memory on a schedule it chose | https://docs.ergoplatform.com/mining/autolykos/ (read 8 October 2026) |
|
||||
|
||||
The arithmetic that binds. A chip's memory is bought by the board, and today's prices are the chip model's: GDDR7 about USD 20 per 2 GB device, so 32 GB on a 512-bit board is USD 320; one HBM3 stack 24 GB about USD 200 (`chip-model-v3.md` 5.1, September to October 2026 prices). Igneum's schedule as specified (2 GiB plus 0.5 GiB a year, doubling steps at years 4, 12, 28; spec 1.13.3) reaches 4 GiB at year 4 and 8 GiB at year 12. Against that schedule:
|
||||
|
||||
| Memory the chip or card holds | Years until the dataset passes it | Who it is |
|
||||
|---|---|---|
|
||||
| 8 GB | 12 | the honest 8 GB card (the first tier out, `card-lifetime-2026-10-05.md`) |
|
||||
| 12 GB | 20 | the honest 12 GB card |
|
||||
| 16 GB | 28 | the 9070 XT class |
|
||||
| 24 GB | 44 | one HBM3 stack; the 4090 and the M5 Max class |
|
||||
| 32 GB | 60 | the f = 1 GDDR7 chip's board; the 5090 |
|
||||
|
||||
So layer 2 as a rate "tracking chain state" ages out fixed-memory silicon only if the dataset grows faster than a chip generation's memory headroom, and every rate that does that retires the honest small cards first by the same table. The E3 is the only case in the record where a growth rule beat a chip, and it beat a chip that had under-provisioned memory by a factor the card fleet also hit (4 GB). The rule that would hurt the f = 1 chip is one that keeps the dataset above what one board of commodity DRAM holds at the chip's price point, and that rule is unaffordable for the honest fleet. The honest reading: layer 2 is the right lever class (the memory is the one parameter a stored-dataset chip cannot read as firmware), and its value is set by the floor and the ceiling, not by the tracking: a floor keeps the dataset above every SRAM die (class B stays closed: 2 GiB is 1,000 mm^2 of SRAM even at N5), and a ceiling keeps it under the honest tiers' memory. Between those two lines the chip's board holds whatever the card holds, and the growth rate changes nothing for it. What "tracking chain state" adds over the fixed schedule is governance (no release decides the size) and the class v5 link (the dataset is built from the state, so the size follows the state's record count naturally); it is not an anti-chip rate. Resolved by the synthesis lane (11:2x UK, `docs/design/class-v6-rotating-family.md` section 7b on `counter-asic-4`): layer 2's rule carries a per-year ceiling as its first constant, the fixed schedule's power-of-two step for that year, so the chain's state can bring a step forward and never add one; "retires cards before chips" is recorded as the reason the ceiling exists. Under that rule the table above is the ceiling's own schedule, and the honest 8 GB tier's year-12 line stands whatever the state does. What this file adds for the record: if the chain's state grew the way Ethereum's did (approximate, from memory: the account and storage state passed 100 GB in its eighth year), a dataset tracking it with no ceiling would have outgrown every consumer card inside the first decade; the ceiling is what makes layer 2 a governance rule and not a fleet-retirement rule.
|
||||
|
||||
### 4.2a One schedule, priced (main's ask of 11:2x UK, for the 20:00 reading)
|
||||
|
||||
Main asked for one dataset-floor schedule priced per tier and against the chips, so the 20:00 reading carries a decision: 6 GiB at the v6 epoch, 10 GiB two years on, 14 GiB at four years, each step by height like a class epoch, the schedule a consensus field. The method is `card-lifetime-2026-10-05.md`'s: a card's room for the dataset is its usable memory (75 percent of card memory, the standing rule "the whole working set under 6 GB on an 8 GB card"; 50 percent of Apple unified memory) less the non-dataset working set (best: 32 KiB scratch, the cache freed after the build, 254 to 479 MiB by tier; worst: 128 KiB scratch, the cache resident, 600 to 1,500 MiB). The population is the bench table's measured cards (`site/miner-bench.json`, 8 October 2026: 32 distinct consumer cards, one Apple part, eight datacentre parts); the fleet's own card census is not a file this lane could find and is owed, so the shares below are by count of distinct measured cards, not by hashrate.
|
||||
|
||||
**What the memory arithmetic says about main's three steps.** A step fits a tier when the dataset plus the working set is under the tier's usable memory.
|
||||
|
||||
| Step | 8 GB (usable 6,144 MiB) | 10 GB (7,680) | 11 GB (8,448) | 12 GB (9,216) | 16 GB (12,288) | Apple 16 GB unified (8,192) | 24 GB (18,432) | 32 GB (24,576) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 6 GiB (6,144 MiB) | **does not fit the rule**: 6,398 best, 6,744 worst, 78 to 82 percent of the card; fits only if the budget rule moves to about 85 percent (a headless rig with no display) | fits (6,404 best, 63 percent) | fits | fits | fits | fits (6,432 MiB, 79 percent of its usable half) | fits | fits |
|
||||
| 10 GiB (10,240 MiB) | out | out | out | **out**: 10,506 best, 10,888 worst, 86 to 89 percent | fits (10,518, 64 percent) | out | fits | fits |
|
||||
| 14 GiB (14,336 MiB) | out | out | out | out | **out**: 14,614 best, 15,032 worst, 89 to 92 percent | out | fits (14,752, 60 percent) | fits |
|
||||
|
||||
So main's schedule as stated retires, under the standing budget rule: at 6 GiB the 6 GB tier and, unless the rule is loosened to about 85 percent, the 8 GB tier (22 percent of the measured consumer cards: the RTX 3070, 3070 Ti, 3060 Ti, 4060, 4060 Ti 8 GB, 5060, 2070 Super) on day one; at 10 GiB the 10, 11 and 12 GB tiers and Apple 16 GB (another 31 percent: the RTX 3080, 1080 Ti, 2080 Ti, 4070, 4070 Ti, 4070 Super, 5070, 3080 Ti, 3060, Arc B580) at year 2; at 14 GiB the 16 GB tier (28 percent: the RTX 5060 Ti, 5070 Ti, 5080, 4080, 4080 Super, 4070 Ti Super, 4060 Ti 16 GB, RX 9070 XT, RTX A4000) at year 4, leaving 24 GB and above (16 percent of the measured consumer cards, plus every datacentre part). That is not the tier order main's note describes, so the schedule that drops the tiers in that order is priced beside it:
|
||||
|
||||
| Step | Dataset | Year | Who falls off (share of the 32 measured consumer cards) | Who holds, and at what share of card memory (best / worst) |
|
||||
|---|---|---|---|---|
|
||||
| v6 epoch | 5.5 GiB (5,632 MiB) | 0 | the 6 GB tier (RTX 2060: 3 percent) | 8 GB at 72 / 76 percent (the worst case one point over the rule); 10 GB 77 percent; Apple 16 GB unified at 72 percent; everything larger |
|
||||
| +2 years | 8 GiB (8,192 MiB) | 2 | the 8 GB tier (22 percent), the 10 GB RTX 3080 (3 percent: 8,452 of 7,680), Apple 16 GB unified (8,480 of 8,192) | 11 GB at 100 percent of usable (the 1080 Ti and 2080 Ti: out in practice, 6 percent); 12 GB at 69 / 72 percent; 16 GB; 24 GB; 32 GB; Apple 32 GB at 52 percent of its usable half |
|
||||
| +4 years | 11 GiB (11,264 MiB) | 4 | the 12 GB tier (22 percent) | 16 GB at 70 / 73 percent; 24 GB at 48 percent; 32 GB; Apple 32 GB at 71 percent of its usable half |
|
||||
| the fixed schedule's own steps beyond | 16 GiB | the year the state brings it forward, else year 28 | the 16 GB tier (28 percent); Apple 32 GB | 24 GB at 68 percent; 32 GB |
|
||||
|
||||
Reading, per tier, in the form main asked for (the year each falls off, and the share of today's measured cards that is): on the priced schedule the 6 GB tier falls at the v6 epoch (3 percent), the 8 GB tier and the 10 GB RTX 3080 and the Apple 16 GB laptop at year 2 (25 percent of the consumer cards plus the base Apple laptop), the 11 GB Pascal and Turing parts in practice at year 2 (6 percent), the 12 GB tier at year 4 (22 percent), the 16 GB tier at the 16 GiB step (28 percent), and 24 GB and above hold through every step priced. On main's schedule as stated the same tiers fall two years earlier each, and the 8 GB tier falls on day one unless the 75 percent rule moves. Either way this is the fleet-retirement rule the ceiling exists to bound (section 4.2): a step every two years retires a quarter of today's measured consumer cards each time.
|
||||
|
||||
**Against the chips, with the number.** Three chips, by what the step does to each:
|
||||
|
||||
| Chip | What a step costs it | The number |
|
||||
|---|---|---|
|
||||
| The hybrid-bonded DRAM chip sized at launch (the Jasminer X4's shape: DRAM dies bonded to the logic at tapeout, 5 GB per unit in 2021; the E3's shape in commodity form, 4 GB of DDR3 soldered to the board) | its memory is fixed at tapeout, so it dies at the first step it cannot carry, as the E3 did 20 to 27 months after shipping. On main's schedule a chip sized to the 6 GiB floor with 8 GB of bonded DRAM dies at the 10 GiB step (year 2); on the priced schedule a chip sized to 5.5 GiB with 8 GB dies at the 8 GiB step (year 2); a chip sized with 16 GB holds through year 4 on both and dies at the 16 GiB step. But the schedule is a consensus field read at genesis, so a maker sizes to the step it wants to survive: the X4's 5 GB was Ethash's DAG plus a year, chosen off a public schedule; the E3 died because its memory was sized to the CARD FLEET's limit, not to the schedule | kills only the chip that under-sizes; sizing to 16 GB costs the X4 shape about 3x its 2021 memory die area (approximate) and the E3 shape USD 160 more of GDDR7 (8 x USD 20, chip-model-v3 5.1) on a USD 470 part |
|
||||
| The f = 1 GDDR7 chip on a board (the record's class C; 16 x 2 GB devices, 32 GB) | nothing until the dataset passes 32 GB: every step priced here fits its board; a bigger step buys more devices at USD 20 per 2 GB | USD 0 per step through 16 GiB; its 5.1x per joule and USD 2.8 per MH/s stand at every step |
|
||||
| The SRAM store (the f = 0 recompute chip holding the cache on die) | the dataset's size costs it nothing (it derives every item); the CACHE's doubling under option C costs it capex only: 128 mm^2 and USD 46 per good die at 256 MiB, 255 mm^2 and USD 111 at 512 MiB (year 4 on the fixed schedule, or the year the state brings the doubling forward), 510 mm^2 and USD 306 at 1 GiB; and the mirror's size is what forces it onto a 7 nm or better node (the 5 October file, section 2.5) | USD 46 to 111 per die at the year-4 doubling; its edge stays 0.92x at the op budget and 1.86x per joule (`chip-model-v3.md` 5.4), unmoved by the dataset's steps |
|
||||
|
||||
The honest sentence for the 20:00 reading: the dataset-floor schedule is a fleet-retirement rule with a chip tax of USD 0 to 160 per unit on the chips that matter (classes C and B), and it kills only a chip whose maker ignores a public consensus field; the one precedent of a growth rule killing a chip (the E3) is a precedent of a maker sizing to the fleet, not to the schedule. If the schedule is adopted, the 75 percent budget rule and the 6 GiB floor cannot both stand for the 8 GB tier; 5.5 GiB keeps the 8 GB card inside the rule in the best case and one point over in the worst, and the headless-rig reading (about 85 percent) is what makes 6 GiB fit. The fleet's hashrate-weighted census is owed before the share column is read as a hashrate share.
|
||||
|
||||
### 4.3 Layer 3: scheduled family epochs by height, every 180 days, no release
|
||||
|
||||
What is declared: a new instruction family goes live on a height schedule, every 180 days by default, with no release (the reserve of spec 1.13.2, ordered at genesis). The record of scheduled change against chips:
|
||||
|
||||
| Chain | The change and its cadence | Human release needed | What it cost the chip | Outcome | Source |
|
||||
|---|---|---|---|---|---|
|
||||
| Grin, Cuckaroo lane | a new tweak of the edge function every six months (Cuckarood July 2019, Cuckaroom January 2020, Cuckarooz July 2020), each a hard fork; the lane's reward share scheduled from 90 percent to zero by January 2021 | yes, every time | a new edge function per tweak: a redesign, not a configuration | no chip ever shipped for the tweaked lane; the chip lane (Cuckatoo31+) got the iPollo G1 at about 4x in 23 months | the 5 October file rows 18 and 19, [S61] to [S66] |
|
||||
| Monero | four algorithm forks in 20 months (v7 April 2018, v8 October 2018, CN-R March 2019, RandomX November 2019) | yes, every time | v7 and v8: a re-spin (85 percent of the hashrate vanished at v7 and chips were back at 85 percent four months after v8); CN-R: random math per block, chip latency up 2.5x; RandomX: a new class of machine, parity hardware after 46 months | the forks were events; the chips survived the ones that kept the shape and died on the one that changed the machine | rows 16 and 17; Vorick on a fork-surviving chip, [S47] |
|
||||
| Ravencoin X16R | the ORDER of sixteen fixed hashes drawn per block from the previous block hash; no fork needed | no | nothing: a sixteen-core sequencer reads the order as a configuration | an FPGA at 1.3x within 20 months; the X16Rv2 fork (one hash swapped) was answered by bitstreams within weeks | row 9 |
|
||||
| Ethereum ProgPoW | the random math re-drawn every PROGPOW_PERIOD (50 blocks in 0.9.2, 10 blocks in 0.9.3, about 2 minutes); the op set fixed (add, mul, mulhi, min, rotl, rotr, and, or, xor, clz, popcount) | no | a sequencer over the eleven ops and a 32-register file; the audit priced the conventional compute chip at little gain and the on-die cache chip at "<< 0.1x" energy per hash | never deployed on Ethereum; no chip on KawPow or FiroPoW in five to six years on small prizes | https://eips.ethereum.org/EIPS/eip-1057 and https://github.com/ifdefelse/ProgPOW (read 8 October 2026) |
|
||||
| Igneum class v2 to v6 | a new program every epoch; era draws every 180 days; one reserve family per era | no | a recompile per epoch; the reserve families are in the shipped generator from genesis, so a chip that reads the reserve at genesis carries every family's datapath from day one | the governance failure is closed; the chip is not | this file |
|
||||
|
||||
The clocks that bound the race (all read 8 October 2026): Bitmain built the A3 "in about 5 months" and Halong the B52 "in about 9" (Vorick, through https://www.nextbigfuture.com/2018/05/obelisk-explains-the-state-of-asics-and-crypto-mining.html); KnC taped out a 20 nm part "only 3 months after starting the project" in 2014 (https://www.design-reuse.com/news/34090/20nm-asic-for-bitcoin-mining.html); ASICMiner went from founding in July 2012 to 64-chip boards on 31 January 2013 (Taylor, IEEE Computer 2017, https://michaeltaylor.org/papers/Taylor_Bitcoin_IEEE_Computer_2017.pdf); Linzhi from founding (February 2018) to tested boards (December 2020) took 27 months; Rao's 10 nm-class project "1+ year". A full Vivado compile on a mid-size Xilinx part (ZCU102) runs 42 minutes typical and 160 worst (PRflow, FPT 2019, https://ic.ese.upenn.edu/abstracts/prflow_fpt2019.html) and "several hours" on a large device (a 2021 Paderborn talk); the Least Authority audit called a 2-minute period "impractical" for a bitstream; the one KawPow FPGA on record is an academic VU35P build at 5.6 MH/s (NTU 2022, https://tdr.lib.ntu.edu.tw/handle/123456789/84103?locale=en), under an eighth of a 2022 GPU. So an hourly program outruns every compile and every design cycle; a 180-day family epoch outruns no chip's design cycle and does not need to, because the family is in the generator from genesis.
|
||||
|
||||
Reading. Scheduled change beat chips in exactly one shape: Grin's, where each change was a new function nobody could know in advance, and even then only because the lane was built to die. Every change a chip could read at genesis (X16R's order, ProgPoW's period, Igneum's reserve) became firmware. Layer 3 as declared is X16R's and ProgPoW's shape, automated and spaced at 180 days: it removes the fork (lesson 5 of the 5 October file, and the one thing that cost Vertcoin two 51 percent attacks), and it taxes a sequencer chip die area for families not yet live. The honest number for that tax is small: the reserve's candidates are integer ALU operations (shifts, bit-field extract, andn, byte permute, popcount, select, the second shuffle form; mm8 last), each a few thousand gates per lane; against the f = 1 chip's USD 470 of memory and board, a lane array carrying every reserve family is the same 30 mm^2 of N5 the class v4 shadow already forces (`counter-asic-4-research.md` 16.1: USD 25 to 40 of die). The one family that is not cheap for a chip to carry idle is one whose unit is large (mm8's tile engine), and that is exactly the family the measured rows say not to use for forcing (the 5090's int8 MAC at 1.4 to 4.1 pJ against a 5 nm array's claimed 0.04 to 0.4: `counter-asic-4-research.md` 15.1a). So layer 3 is right for governance and neutral for the chip; what would make a family epoch cost a chip a redesign is a family whose semantics are not knowable at genesis, which is the random item-derivation program of the 5 October addition 2 (a per-day SuperscalarHash-style derivation), the one RandomX idea Igneum has not taken, and it acts on the f = 0 chip only.
|
||||
|
||||
### 4.4 Layer 4: the (c''') floor and the F8-form uniformity test per era draw
|
||||
|
||||
What is declared: the (c''') acceptance floor (the per-site distinct-index ratio at or above 0.995 against the window model, spec 1.4.7.2) and the F8-form uniformity test generalised to each era's parameter draw, with a redraw on failure. The history has three attacks of this one class, and in every case the test that would have caught it lived outside the acceptance rule:
|
||||
|
||||
| Attack | The steer | Found when | What the fix was | Source |
|
||||
|---|---|---|---|---|
|
||||
| Kik's ProgPoW exploit (4 March 2020) | the 64-bit seed between the two keccak passes: fix a seed, compute its mix once, then grind nonces until keccak_progpow_64(header, nonce) equals the seed; the memory path is run once per 2^64 nonces, so a chip that never touches the DAG wins once the difficulty passes 2^50 | after two tentative approvals and five months after both audits (September 2019) | ProgPoW 0.9.4 widened the carried state from 64 to 256 bits (the digest of the first keccak, plus the mix, plus padding) | https://github.com/kik/progpow-exploit and https://github.com/ifdefelse/ProgPOW (read 8 October 2026) |
|
||||
| Dinur and Nadler on MTP (2017) | the prover controls the memory's contents, so by injecting blocks it steers Argon2d's data-dependent addresses into a set it can hold in under 1 MB at a 170x compute penalty, in place of 2 GB | before launch, by cryptanalysis, not by the design's own test | MTP 1.2 patched the construction | the 5 October file [P18] [S38] |
|
||||
| AP-F8-1 on class v4 (7 October 2026) | a load site whose source register was last written by a lossy op (or, mul, mulhi) saturates to all-ones or zero at a known rate, and the era map sends the constant to one item; 96.6 percent of class v4 programs carried a lossy-sourced load; the worst seed read 29x the window model on one item | in the attack-pass lane's census, one day after the class shipped to the devnet | sub-versions 1 to 3 (the freshness fixpoint, the executed shadow block, the (c'') ratio at 0.98), then class v5's (c''') at 0.995 | `docs/plans/counter-asic-3-status.md` section 7c; spec 1.4.7.2 |
|
||||
|
||||
Reading. Layer 4 is the right answer to this class and the only one of the four layers that acts on a mechanism the record shows beating hashes after launch. Two things bind on it. First, the test can only see what its null models: (c''') measures distinct-index ratios against the window model, which is a function of the dataset size, the per-site windows, the era stride and the read width; a draw of the read width or the program length changes the null, so "generalised to each era's draw" means the window model is re-derived per era and the census re-run per draw, not one floor reused. The cost is known: 2.2 s per candidate at 2^20 on one box core, once per epoch draw on a node, and 0.7 percent of candidates refused by the floor (spec 1.4.7.2). Second, the residue the floor cannot reach without refusing most clean programs is the shadow-block-written concentrations at 0.9992 to 0.9997, worth about 1.0004x to a chip (spec 1.4.7.2), and a per-era draw of the shadow placement moves that residue, so the redraw rule needs its own ceiling stated (the 1.2x-of-window gate of F8 is the right shape; the number per era is the census's to set). The Kik lesson is separate and already closed: Igneum's seed is 256 bits through the VDF and the program is the epoch's, so there is no 64-bit state to grind; the header-locality search (the 5 October check 1) remains the nearest analogue and was measured by adv-accept-2 in the in-house pass.
|
||||
|
||||
## 5. What to add, optimise or invent, ranked (first cut; the full report re-ranks with the other lanes' findings)
|
||||
|
||||
| Rank | What | Why the history says so | Cost to the honest card | Where |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **State layer 2's floor and ceiling in gigabytes per tier, before its rate.** The floor above every SRAM die (today's 2 GiB holds); the ceiling under the honest tiers' memory on a stated glide (the 8 GB tier's life is the first number) | the E3 is the only chip a growth rule ever killed and it was the chip with the fleet's own memory limit; Scrypt-N was abandoned because its growth was public and slow; Autolykos's growth is untested; a dataset that tracks chain state literally outgrows every card if the state grows the way Ethereum's did (approximate) | none at the floor; everything at the ceiling | genesis rule, with the card-lifetime table |
|
||||
| 2 | **The clock and the detector**, unchanged from the 5 October ranking and still unbuilt: the per-program rate spread and nonce pattern on the observer (the method that found Monero's chips at 85 percent), plus a share-by-key-and-template instrument (what found Qubic until it randomised), plus the issuance trigger at about USD 50 K a day | every chip in the record was on its chain before it was announced (Monero 2017, Zcash's three groups, SChernykh's 2021 reading of the X5) | none | before the public testnet |
|
||||
| 3 | **The read width's draw band stops where the honest card stops being latency-bound**: the synthesis lane keeps {4 B, 16 B} on the measured warrant (w16 latency-bound within 2.7 percent on the 5090 and the 9070 XT, within 1 percent on the M5 Max); w64 is out (the 5090 bandwidth-bound at 71.9 MH/s); the draw costs every chip nothing (the same 32-byte atom at either width) and is a governance value only | the Ethash chips' whole edge was the bandwidth lever Ren and Devadas name; a width that makes the card bandwidth-bound hands the chip that lever | within 2.7 percent at w16; 47 percent at w64 | the band {4 B, 16 B} in the class v6 spec; nothing wider without a per-vendor latency-bound measurement |
|
||||
| 4 | **Bound the op-mix draw by the per-vendor energy table, not only by the rate spread**: the 5090 pays 55.8 pJ per shuffle against 11.3 per add, so a shuffle-heavy era taxes the honest card up to 5x per instruction for no better k | X16R's drawn order cost the chip nothing and the fleet nothing; Igneum's draw can cost the fleet watts | up to 2x the premium per instruction at the band's edge | the band's definition in the class v6 spec |
|
||||
| 5 | **Order the reserve by what a sequencer cannot fold into firmware**, mm8 last; and name the random item-derivation program (the 5 October addition 2) as the one reserve item whose semantics are not knowable at genesis | the kHeavyHash chips and the 5090's own 1.4 to 4.1 pJ per int8 MAC; RandomX's SuperscalarHash is the one idea Igneum has not taken, and it acts on the f = 0 chip only | none at launch | reserve ordering, genesis |
|
||||
| 6 | **Generalise layer 4 with a null per drawn parameter**: the window model re-derived per era, the census per draw, and a stated ceiling for the shadow-written residue per shadow placement | lesson 3 | 2.2 s per candidate once an epoch on a node | the class v6 acceptance rule |
|
||||
| 7 | **The per-load shadow placement as a research item, not a draw value**, until a sound construction is drawn (the 16 x 27 form accepted 1.4 percent of candidates) | it is the one placement that would force the chip's ALU core inside every read's dependency (the USD 200 M break-even row) | compile-ahead at 16 sites | `counter-asic-4-research.md` 20.2a |
|
||||
|
||||
## 6. Consequences per user tier
|
||||
|
||||
| Tier | What this history means for it | What is being done |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB card | Layer 2 is the one layer whose rate reaches this tier first: at 0.5 GiB a year the 8 GB card is out at year 12, and any rate fast enough to age out a 32 GB chip board is out of this card's life inside two years. No chip of class A, B or E touches this miner under v6; the class C chip (the memory system without the GPU) reaches it as it reached Ethash's 4 GB miners: by price per MH/s, 5x | the layer 2 ceiling stated per tier (section 4.2) before the rate; the issuance clock and the detector (section 5) |
|
||||
| One 12 GB or 16 GB card | as the 8 GB tier with 20 and 28 years on the schedule; the 9070 XT's 10.6 microjoules per hash is 23x behind the GDDR7 chip per joule on the model, so this tier's card is the first the class C chip displaces | nothing in the hash fixes AMD's dependent-read rate; the vendor-share metric is the warning |
|
||||
| One 24 or 32 GB card (5090, M5 Max) | the honest best: 1.69 microjoules at the 5090's knee against the chip's 0.47; 3.6x at zero premium, 2.1x with the shadow at k = 1; the M5 Max at 0.78 microjoules is 1.7x behind the GDDR7 chip with no shadow at all | the operating point as the shipped default (Ember); the shadow's rung |
|
||||
| A rig | the Ethash precedent in full: chips at 2x to 5x per joule and 5x per dollar took the hashrate over four years, and the ASIC share stayed small only while the chips were not cheap enough at scale; the model says this chip is (USD 2.8 against 14.7 per MH/s) | the break-even cap row (USD 100 M in years 1 to 2 with the N5 shadow core) is the real wall; the clock |
|
||||
| A pool user | MoneroCrusher found chips at 85 percent of Monero's hashrate by the share pattern, four months after a fork; Igneum's detector is the same method on the observer, unbuilt | the detector before the public testnet (section 5) |
|
||||
| Every tier, on governance | no fork, ever, for a draw or a family: the history's clearest lesson (Vertcoin's two 51 percent attacks after forks, Monero's four, Ethereum's two-year ProgPoW fight) is the one v6's layers 1 and 3 close outright | nothing further |
|
||||
|
||||
## 7. Unverified and owed
|
||||
|
||||
- The research lanes' returns for the chip mechanism columns (section 1 and 2) are the first cut's owed content; where a lane reports "not found" the row says so and carries the 5 October file's figure.
|
||||
- The Vorick post (13 May 2018) is read through secondary coverage (davidgerard.co.uk, zycrypto.com, nextbigfuture.com, a steemit copy; read 8 October 2026): the secret-Monero-ASIC claim ("since early 2017, making up 50 percent of the hashrate"), the three Zcash groups, the Equihash fork-following architecture, "about 5 months" for Bitmain's A3 and "about 9 months" for Halong's B52, the A3 under USD 10 M with USD 20 M of orders in eight minutes, the manufacturer withdrawal that cost Obelisk "north of USD 2 million". The 5 October file's "13 months for a startup" and "a chip able to survive Monero's forks at under a 5x hit" were NOT found on any fetched page and are carried here as unverified; the Monero fork-survival fact that is verified is the record itself (chips back inside four months of v8).
|
||||
- The KawPow fork block and its 3-block period are approximate (the minerstat and Tron Black pages answered 403).
|
||||
- The Ethereum DAG date of passing 4 GB on the main chain (about December 2020) is approximate; the Classic figures (3.94 GB at epoch 376, 27 November 2020) are cited.
|
||||
- The Ethereum state-size figure in section 4.2 is from memory, approximate, and is the open question handed to the synthesis lane.
|
||||
- Every per-joule ratio for a chip against a GPU is arithmetic on the cited rate and watt figures of both and is approximate by construction.
|
||||
- Nothing here is a measurement; the Igneum figures are the repo's measured rows as cited, and the chip figures are the chip model's, modelled.
|
||||
|
||||
## 8. Sources
|
||||
|
||||
Every URL is cited inline at the row that uses it, with "read 8 October 2026" at the row or the section; the 5 October file's [S], [P], [E] and [L] lists are cited by their tags and not repeated. The primary documents read in full by this lane (text extracted where the page is a PDF): EIP-1057 (https://eips.ethereum.org/EIPS/eip-1057); the ifdefelse ProgPOW README (https://github.com/ifdefelse/ProgPOW); the Least Authority audit (https://leastauthority.com/static/publications/LeastAuthority-ProgPow-Algorithm-Final-Audit-Report.pdf, report version 9 September 2019); Bob Rao's hardware audit (https://github.com/ethcatherders/progpow-audit, 6 September 2019); Kik's exploit (https://github.com/kik/progpow-exploit); RandomX design.md, design_v2.md, specs.md, PR 317, release v2.0 and issue 11 (https://github.com/tevador/RandomX); Tromp's README and the Grin forum threads named in 2.4 (https://github.com/tromp/cuckoo ; https://forum.grin.mw); the Ergo Autolykos docs (https://docs.ergoplatform.com/mining/autolykos/); the Thanos post (https://ethereumclassic.org/blog/2020-11-27-thanos-hard-fork-upgrade/); the TechInsights Jasminer notes (https://www.techinsights.com/ko/node/51986 and /52149); the Fudan NDSS 2019 paper (https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_09-5_Bai_paper.pdf); Percival's lookup-gap note (https://mail.tarsnap.com/scrypt/msg00092.html); Lee and Kim on Qubic (https://arxiv.org/html/2512.01437v2); PRflow (https://ic.ese.upenn.edu/abstracts/prflow_fpt2019.html). Papers of 2024 to 2026 found: Blocki and Smearsoll, "Provably memory-hard proofs of work with memory-easy verification", ePrint 2025/1456 (Omega(N^2 / log N) cumulative memory with polylog verification: https://eprint.iacr.org/2025/1456); Condrey, PoSME, arXiv 2604.15751; Yang et al., PHICOIN, arXiv 2412.17979 (a resistance claim with no algorithm in the abstract). Pages that refused every lane (403, 404, DNS): Vorick's original post on Medium and sia.tech and its archive copy; Linzhi's and ifdefelse's Medium posts; MoneroCrusher's Medium post (figures taken from criptonoticias coverage); bitmain.com's product list; support.bitmain.com's Z9 page; minerstat; Tron Black's posts; innosilicon.global; jasminer.com (an empty shell); cryptomining-blog.com; the Yole DBI report. The session's web-search budget ran out at 11:0x UK; everything after that is direct fetches of known URLs, and "not found" in this file means not found on a fetched page.
|
||||
256
docs/analysis/class-v6/invention.md
Normal file
256
docs/analysis/class-v6/invention.md
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
# Class v6 invention lane: layers beyond the four, each priced against the chip that stores the dataset
|
||||
|
||||
8 October 2026, first cut 11:1x to 13:xx UK (BST, the Mac's clock; the boxes print CEST, one hour ahead, and nothing below is stated in box time), branch `class-v6-invention` from the mirror's master at 17d61d52, research lane C under the Counter ASIC coordinator. The founder's word of 11:1x UK: class v6 is declared with four layers as its spine (per-era draws of the released parameters; the state-derived dataset's size tracking chain state with a floor; scheduled family epochs every 180 days with no release; the acceptance floor and the F8-form uniformity test generalised to each era's draw), and deep research opens: "see if anything can be optimised, added or invented". This file is the invention lane's answer: every candidate layer beyond the four as one paragraph, one known-failed test stated as a harness run, and one chip-model row, then the ranked list of what v6 should add. The synthesis is the research lane's `docs/design/class-v6-rotating-family.md` (branch `counter-asic-4`), which reads this file.
|
||||
|
||||
Status: a research document and a gate plan. No consensus code this week; nothing here touches the devnet, the testnet object, the spec or any served page. Every number carries a label: **measured** (a run on a named box or card, the log named), **modelled** (arithmetic on the chip model's cited figures, `docs/analysis/chip-model-v3.md` section 5), **claimed** (a vendor's or an author's figure, URL and date), **approximate** (from memory or a scaling). The rule every candidate is held to: **reject what costs GPUs more than it costs chips, and say so with the number; keep what raises a chip's k or capex or shortens its useful life more than it raises every GPU tier's cost.**
|
||||
|
||||
## 0. One page
|
||||
|
||||
The frame is last night's identity (`docs/analysis/counter-asic-4-research.md` section 2): against the chip anyone builds, a GPU's memory system without the GPU (chip-model-v3 section 5.5, the `f = 1` chip), the per-joule edge is `(E_card + F) / (E_mem + k F)`, with `F` the premium per hash the card pays for forced work and `k` the chip core's energy per forced op over the card's. Zero premium is zero forcing. No hash-side design reaches a chip edge of about 2x at zero premium; the premium-free floor is the card's own whole-card energy over the chip's memory energy, 3.6x on a 5090 at its knee (measured card, modelled chip), and the class v4 shadow buys 2.1x at `k = 1` for 82 to 90 W on a 5090. The four layers of v6 render a fixed-function chip useless on its release day and move nothing in the identity for the GPU-like chip. So an invented layer can do one of four things, and only four: raise `k` (nothing on measured rows reads `k` above 1, section 15.1a of the research file); raise the chip's capex or project cost (the one lever the identity does not see); shorten the chip's useful life (layers 1 and 3 already do this for fixed silicon); or cost the honest cards less at the same `F`. Every candidate below was read against those four.
|
||||
|
||||
What was measured today (all on igneum-build-1, the counter-asic-4 crate at 5984ffab, logs under `/srv/builds/v6-invention/`):
|
||||
|
||||
| Reading | Number | Label |
|
||||
|---|---|---|
|
||||
| The sound per-load shadow (one pass of a 256-instruction sub-block after every load, `mx8+shl4096x1`), the form 20.2a-close named and never drew | 234 of 256 seeds accepted within the 32-attempt cap, 0.927 rejection per candidate, mean accepted attempt 9.7; the iterated 16 x 27 form on the same seeds 76 of 256, 0.989 per candidate | measured (section 3.1) |
|
||||
| The same at class v4's instruction count (`mx8+shl2304x3`: 16 sub-blocks of 144, three passes, 55,296 shadow instructions per hash) | 224 of 256 seeds, 0.935 per candidate | measured |
|
||||
| The verifier on the sound forms, cold warp on core 40 with core 88 loaded (the ladder's method) | shl4096x1 8.28 to 8.55 ms; shl2304x3 8.44 to 8.82; class v4's shape 8.33 to 8.63 on the same core in the same minutes; all under the 10 ms gate | measured (section 3.2) |
|
||||
| The per-load prototype's acceptance under a drawn era | 0 of 256 seeds on every per-load form: 5,536 of 7,862 bias rejections name index bit 26 or 27 "set in 0 or 16,384 of 16,384", the era WINDOW's fixed top bits, an instrument fault; so 20.2a-close's "1.4 percent accepted, 42 of 64 seeds exhaust" (run across drawn eras) measured the instrument, not the construction | measured; a correction for the research file (section 3.3) |
|
||||
| The two packs for the card row | `mx8_shl4096x1` (id 75ca9547da21b200) and `mx8_shl2304x3` (id bbfdfc1dcdda0b46), OVERALL PASS, in the hash lane's PC 1 job after the AMD grid (about 12:45 UK) | exported; the card row owed |
|
||||
|
||||
**The ranked list of what v6 should add beyond the four** (section 4 has every column):
|
||||
|
||||
| Rank | Layer | What it does to the chip | What it costs the cards (5090 / 5070 Ti / M5 Max) | Status |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **Layer 5: the shadow placed per load, one pass of a long sub-block** (the capex lever: the chip's core must sit inside every read's dependency, so controller, lanes and PHY share one N5-class die or an interposer) | energy edge unchanged (`k` is `k` whichever die the core sits on); project cost about USD 30 M to about 60 M, the break-even market cap about USD 100 M to about 200 M (the mission lane's model, modelled); capex per MH/s 3.0 to 4.3 USD (modelled) | 5090: the 16 x 27 form measured 13 to 14 W UNDER the whole block at the same instruction count (448 against 462 W unlocked; 283 against 296 at the 1,300 lock), rate within 1.2 percent; the 256 x 1 form's row in today's PC 1 job / no row, the 4070's 30 W premium at its tune point as the proxy / the 16 x 27 form -0.5 percent of rate measured; the 256 x 1 form's Metal footprint OWED | acceptance measured today (0.927 per candidate, P(exhaust at 256) about 4 x 10^-9); the sub-version 3 dataflow rule in execution order is the fix that brings it toward v4's 0.68 (gate plan, section 2.1) |
|
||||
| 2 | **Layer 6: register-file width drawn per era (8 to 32 registers per lane)**, the link tax on layer 5 | with layer 5, the lane state crossing the controller twice per read grows from 64 B to 128 to 256 B: 2.2 to 9 TB/s of die-to-die traffic at the 5090's read rate, past any one-stack interposer, which closes the "or an interposer" branch and forces the single N5 die (modelled); alone, nothing | 0 rate on every card while latency-bound (a GPU lane holds up to 255 registers; 7,262 lanes x 256 B is 1.9 MB against the 5090's 43 MB of register file, approximate); the verifier's register-major arrays 4x (unmeasured, under 0.1 ms by the op law); no pack form today (the register count is a generator constant) | modelled; the pack form is a generator change with its own census (gate plan) |
|
||||
| 3 | **Layer 7: warp-uniform data-dependent block selection** (which of B shadow sub-blocks runs next is chosen by a warp-reduced register value, uniform across the 32 lanes, so no divergence) | nothing for the GPU-like chip (a sequencer already); an FPGA overlay or a fixed pipeline must hold all B blocks for one block's throughput: B x the shadow's LUT area (approximate); shortens a per-epoch bitstream's worth | one uniform indirect branch per iteration: about 0 (the `sel` register already does this for the immediates); verifier 0 | modelled; a generator change behind a pack (gate plan); rank 3 because it moves the FPGA lane only |
|
||||
| 4 | **The reserve ordered by hardware orthogonality** (layer 3 as it stands, with the order fixed: shuffle-crossbar families, then byte-permute, then popcount and priority encoder, the int8 tile last) | a chip pre-wires every family for about USD 4 of N5 (modelled); the order makes the first unlocks the ones a 12-op datapath lacks most | 0 at 4 points (measured step costs under 1 percent of ALU time on every vendor) | an ordering rule inside layer 3, not a new layer; the history's addition 6 |
|
||||
|
||||
Rejected with the number, each in section 2: per-lane data-dependent branches (divergence costs the card, a chip nothing); reads tied to the shard proof per block (a refresh per block is 0.3 to 1 W on a chip, 1.3 percent of a 4090's hash time); randomised memory topology (a chip's address decoder permutes its lines for nothing; the stride and interleave are already drawn); the VRAM-size ratchet as a lever (a chip buys 24 to 32 GB that the 8, 12 and 16 GB tiers cannot: it retires cards first); proof-carrying hashes sampled by the pool (the chip holds everything the witness proves); prover-gated eligibility (proving is 1.6 kW network-wide at any hash rate, 0.7 percent of the hash's energy at 100 GH/s); time-locked parameter commitments beyond the era VDF (the drawn band is firmware; the 2-hour lead already denies the fixed chip 180 days); a fraction of reads derived from the cache (the 5090 loses about 30 percent of rate, the chip 7 percent of energy); row-straddling reads (the w64 regime, 47 percent of the 5090's rate); a refresh per block (dead by arithmetic, the research file's row 7).
|
||||
|
||||
Per tier, in one line each: a home miner on any card sees no change from anything here today (nothing ships; the devnet pays nothing); the 5090 tier's one number is that the per-load placement costs it LESS than class v4's whole block at the same work (13 to 14 W measured on the 16 x 27 form, the 256 x 1 form's row due about 12:45 UK); the 5070 Ti has no measured row in this lane (the 4070's rows are the proxy, approximate); the Apple tier's open question is the inline footprint of a 4,096-line block per iteration (the 1,024-line block cost the M5 Max 17 percent on 6 October, measured), which decides whether rank 1 needs a block-shape cap for Apple; a pool user sees nothing; a node verifies the per-load forms in the same 8.3 to 8.8 ms as class v4 (measured); a chip maker sees its project forced onto one advanced die by rank 1 and its interposer escape closed by rank 2.
|
||||
|
||||
## 1. The frame, and what a candidate must do
|
||||
|
||||
| Term | Value, 5090 at the 1,300 MHz knee | Label | Source |
|
||||
|---|---|---|---|
|
||||
| `E_card`, class v3 | 1.67 microjoules (127.3 MH/s at 213.0 W; 134.6 at 223.3 on the efficiency pass) | measured | research file 20.3 and the status file's efficiency pass |
|
||||
| `E_mem`, the `f = 1` GDDR7 chip | 0.466 microjoules (16 devices, 166 MH/s at 78 W); one HBM3 stack 0.321 | modelled | chip-model-v3 5.4 |
|
||||
| `F`, the class v4 shadow's premium | 0.652 microjoules (82.8 W over 126.9 M hashes x 102,100 ops: 6.4 pJ per counted op) | measured | research file 20.3 |
|
||||
| The edge at zero premium | 3.6x (GDDR7), 5.2x (one HBM3 stack) | measured card, modelled chip | section 2 of the research file |
|
||||
| The edge with the shadow | 2.1x at `k = 1`, 2.9x at `k = 0.5`, 3.5x at `k = 0.3`; the honest `k` band for an ALU-shaped core 0.3 to 0.8 | modelled on measured rows | 20.4 |
|
||||
| The chip's capex | USD 470 of memory, controller and board per 166 MH/s: 2.8 USD per MH/s; plus the shadow core USD 25 to 40 (3.0 to 3.1); plus an interposer USD 200 (4.3); a 5090 at MSRP 14.7, at the 2026 street price about 29 | modelled; the card price measured | 16.1 |
|
||||
| The project cost and the break-even cap | a 28 nm controller USD 5 M (cap about 17 M); plus an N5 shadow core USD 30 M (cap about 100 M); the core forced onto the controller's die or an interposer USD 60 M (cap about 200 M) | modelled (the mission lane's model, s = 0.30) | 16.2 |
|
||||
|
||||
The four doors, and the one each candidate must walk through:
|
||||
|
||||
1. **Raise `k`.** Measured on the 5090 (15.1a): the int32 ALU op 6.2 to 11.3 pJ against a 5 nm SIMD array's 2 to 5 (k 0.3 to 0.8); the shuffle 29 to 56 pJ against a crossbar's about 20 (k 0.4 to 0.7, and the card pays 5x the add per op); the int8 tile 0.8 to 4 pJ per MAC against an array's 0.04 to 0.4 (k 0.03 to 0.3); an L2 hit 1.4 to 2.4 nJ against on-die SRAM's 0.2 to 0.5 (k 0.1 to 0.3). Nothing reads above 1. A candidate that claims to raise `k` must name the block and the measured GPU cost per op it rests on.
|
||||
2. **Raise the capex or the project.** The hash's dataset is a card's worth of DRAM and its work a fraction of a card's logic, so per-unit capex cannot pass about 4.3 USD per MH/s (16.1); the project cost is the lever that moves the break-even cap, and the only mechanism found for it is forcing the shadow core into every read's dependency (16.2). A candidate here is priced by which die it forces.
|
||||
3. **Shorten the useful life.** Layers 1 and 3 kill fixed silicon at the first draw outside its wired value; a GPU-like chip's life is its memory's and its node's. A candidate here must move the GPU-like chip, or it is layer 1 again.
|
||||
4. **Lower the honest card's cost at the same `F`.** The operating point (the knee lock) is the miner's lever, not the protocol's; a protocol lever here is a shape that runs cheaper per instruction on the card (the 16-instruction block effect, 13 to 14 W measured) at the same chip cost.
|
||||
|
||||
## 2. The candidates
|
||||
|
||||
Each: the paragraph, the known-failed test as a harness run, the chip row, the verdict. The harness names: `v6inv-census.sh` is `/srv/builds/v6-invention-census.sh` on build-1 (a copy sits in this lane's scratch and lands under `tools/attack/v6-invention/` with the 09:00 report), the counter-asic-4 crate's `igneum-pow accept --class <form>` over seeds `igneum-v6inv/<i>`; the F8 census is `tools/attack/f8-uniform` (`attack-f8 warps`, master); the chip row is the model's arithmetic with the GPU side measured where a pack ran.
|
||||
|
||||
### 2.1 Layer 5: the shadow placed per load, one pass of a long sub-block (KEEP, rank 1)
|
||||
|
||||
The class v4 shadow runs 256 instructions 27 times after instruction 63 of every iteration, where the next iteration's 64 base instructions and 16 loads stand between the block and every load, and a chip may run it on a second die with 64 B of lane state crossing once per iteration (140 GB/s at the 5090's read rate, a PCB link; 16.2). Placed inside every read's dependency the same work makes the lane state cross twice per read, 2.2 TB/s, an interposer-class link or one N5 die carrying controller, lanes and PHY, which the mission lane's model prices at a project of about USD 60 M against 30 M and a break-even cap of about 200 M against 100 M. The 16 x 27 form (16 sub-blocks of 16, each iterated 27 times) was built on 7 October and closed the same night: 27 passes of a 16-instruction map right before a load collapses or biases the load's address register before any base instruction can re-randomise it, and the acceptance rule in execution order refused it. The sound form named there and never drawn is one pass of a long segment per load: 16 sub-blocks of 432 instructions, each run once, the same 6,912 instructions per iteration. The experimental class caps the per-load block at 4,096 instructions, so today's census takes the two forms inside the cap that bracket it: `mx8+shl4096x1` (16 x 256, one pass, 59 percent of class v4's work per hash) and `mx8+shl2304x3` (16 x 144, three passes, class v4's exact count), and the constant-work ladder between them (16 x 256 x 1, 32 x 128 x 2 ... 16 x 16 x 16) that reads how the acceptance rate depends on the sub-block length against the pass count. The 432 x 1 form itself needs the cap raised, a one-line change in a research-only parser, which this lane does not make this week; its number is bracketed by the 256 x 1 and 144 x 3 rows and the ladder's flatness between them.
|
||||
|
||||
Known-failed test, as a harness run: `ERA=none SEEDS=256 /srv/builds/v6-invention-census.sh` on build-1 under `lease pool 48`. Fail: a form whose per-candidate rejection is 0.98 or above (P(exhaust) at the 256-attempt cap above 0.5 percent, an epoch without a program every few months). The 16 x 27 form reads 0.989 and fails (section 3.1). Pass: a form under 0.95 (P(exhaust) under 2 x 10^-6). The 256 x 1 form reads 0.927 and passes; so does every form with a sub-block of 36 instructions or longer (0.927 to 0.949). The second known-failed case is the instrument's own: `ERA=era` on the same seeds reads 0 of 256 accepted on every per-load form, with the window's fixed top bits named as biased index bits (section 3.3); the fixed instrument (bits at or above `28 - k_off_s` excluded from the value-level test) must accept per-load forms under drawn eras at the no-era rate within the binomial band, and still refuse the per-load record of 7 October (candidate 0 of the first export, 1,482 duplicate lanes).
|
||||
|
||||
Chip row:
|
||||
|
||||
| Column | Value | Label |
|
||||
|---|---|---|
|
||||
| A fixed-function chip's `k` | unchanged (the work is the same ALU mix); its capex: the controller cannot be a 28 nm part with the core elsewhere, so the project moves from USD 5 M (no core) or 30 M (a core on its own die) to about 60 M (one N5-class die or a 2.5D package); break-even cap about USD 200 M in years 1 to 2 (s = 0.30) against about 100 M | modelled (16.2, the mission lane's N3 single-die row; a GDDR7 PHY on N5 is unpriced) |
|
||||
| A GPU-like chip's per-joule edge | unchanged: 2.1x at `k = 1`, 3.5x at `k = 0.3` at the knee; its capex per MH/s 3.0 to 4.3 USD against 2.8 | modelled |
|
||||
| RTX 5090 | the 16 x 27 v2 export: 135.90 MH/s at 448.3 W unlocked against the whole block's 137.51 at 462.2 (the premium 137 against 151 W, -14 W); at the 1,300 lock 126.04 at 282.9 against 126.93 at 295.8 (-13 W); the 256 x 1 and 144 x 3 forms in today's PC 1 job (the hash lane, after the AMD grid, about 12:45 UK) | measured (research file 20.3); the sound forms' rows owed |
|
||||
| RTX 5070 Ti | no row (no card in this lane); the 4070's class v4 premium at its tune point, 30 W for no rate, less the block effect, is the proxy | approximate |
|
||||
| Apple M5 Max | the 16 x 27 v2 export 26.88 MH/s against 27.01 (-0.5 percent, Metal packbench, 7 October); the 256 x 1 form's inline text is 4,096 shadow lines per iteration where the 1,024-line block cost the M5 Max 17 percent (6 October, measured), so its footprint is the open Apple number: OWED (a Mac measurement under the measure lock, which this lane does not run; the hash lane's or the shipper's Metal row) | measured for 16 x 27; the sound form's row owed |
|
||||
| The verifier | 8.28 to 8.55 ms cold with the sibling loaded (256 x 1), 8.44 to 8.82 (144 x 3), against class v4's 8.33 to 8.63 on the same core in the same minutes; the acceptance's dynamic test 35 ms per candidate (1,111 ms for 32), 13.7 candidates per seed on average: about 0.5 s of one core per epoch | measured (section 3.2) |
|
||||
| The acceptance | 0.927 per candidate (256 x 1), P(256 consecutive rejections) 0.927^256 about 4 x 10^-9 per seed; class v4 sub-version 3 reads 0.681 and 2 x 10^-43; the gap is the missing dataflow rule (the per-load class is not the class v4 shape, so (a'), (c') and (c'') do not run on it; the value-level bias test catches the same population: 1,563 of 2,329 bias rejections name index bit 0 at a one-count near 4,096 or 12,288 of 16,384, the product's low-bit law) | measured (section 3.1) |
|
||||
|
||||
Verdict: KEEP as layer 5, the first thing v6 adds beyond the four, because it is the only mechanism found that moves the project cost, it costs the 5090 less than class v4's own block (measured on the 16 x 27 form; the sound form's row today), and its acceptance is now a measured 0.927 with a named fix (the sub-version 3 dataflow fixpoint run over the real execution order, base and sub-blocks interleaved, as the generator's draw rule) that the research file's 20.2 already asked for. What it does not do: move the energy identity by one joule. The founder's "useless as soon as it dropped" is layer 1's and 3's sentence; layer 5's sentence is "the chip that can be built costs twice as much to start".
|
||||
|
||||
### 2.2 Layer 6: the register-file width drawn per era, the link tax on layer 5 (KEEP, rank 2)
|
||||
|
||||
The hash runs on 8 registers per lane, a prototype value to be fixed at gate 1 (spec 1.4). The lane state a chip must carry is those 8 words plus the nonce and counter, 64 B, which is why the chip's 1,172 lanes are 73 KB of SRAM and why, under layer 5, the per-read crossing is 128 B at 17.5 G reads per second, 2.2 TB/s: an interposer carries that (a one-stack HBM package moves about 0.8 to 1.2 TB/s of memory traffic and a die-to-die link of a few TB/s is a 2.5D product; approximate, from memory), so the chip has an escape at USD 200 of package instead of one die. Draw the register count per era from {8, 16, 32} (the acceptance rule's part (b) over every register; the program length scaled so that every register is written, or registers above 8 initialised and read by the shadow alone) and the crossing is 128 to 256 B per lane per read: 4.5 to 9 TB/s, past the interposer class, so the single die is forced and the project's USD 60 M row has no cheaper branch. A GPU pays nothing in rate while latency-bound: a CUDA lane holds up to 255 registers, the 5090's 7,262 lanes in flight at 256 B are 1.9 MB against about 43 MB of register file (170 SMs x 256 KB; approximate), and occupancy at 32 live registers plus the kernel's temporaries fits the 64K-register SM at full residency (approximate, unmeasured). The verifier's register-major arrays grow 4x (4 KB per warp) and the interpreter's cost per op does not move.
|
||||
|
||||
Known-failed test, as a harness run: the generator with `REGISTERS` as a class field (a research-only change behind a pack name, `mx8+r32`), then `v6inv-census.sh` over the per-load forms at 8, 16 and 32 registers. Fail: the 32-register form's per-candidate rejection above the 8-register form's by more than the binomial band (more registers, more cold registers, more (b) rejections unless the program length scales). Pass: rejection at or under the 8-register form's; the F8 census at 2^24 on 64 seeds within 1.2x of the window model (the top 0.1 percent of items); the cold verify on core 40 with core 88 loaded under 10 ms. Then the card: one pack per register count on the 5090, rate within 1 percent of the 8-register pack at both states (the occupancy claim measured, not argued).
|
||||
|
||||
Chip row:
|
||||
|
||||
| Column | Value | Label |
|
||||
|---|---|---|
|
||||
| A fixed-function chip | with layer 5: the lane state per read 128 to 256 B, 4.5 to 9 TB/s of die-to-die traffic at the 5090's read rate; the interposer branch (USD 200 of package) closed, the single N5-class die forced; the project about USD 60 M either way, but with no cheaper escape; alone (without layer 5): nothing, the state crosses once per iteration | modelled, the link figures approximate |
|
||||
| A GPU-like chip's per-joule edge | unchanged; its lane SRAM 73 KB to 300 KB (nothing) | modelled |
|
||||
| RTX 5090 / 5070 Ti / M5 Max | 0 rate while latency-bound (approximate: the occupancy arithmetic above; a measurement is the pack); watts: the same `F` (the same ops) | approximate until the pack runs |
|
||||
| The verifier | 4x the register arrays per warp (4 KB); cost per op unchanged (0.1 ns per lane-instruction, the shadow's law) | modelled |
|
||||
| The acceptance | part (b) over 16 or 32 registers needs the base program to write every register: at 64 instructions over 32 registers about 13 percent of registers are never written (approximate, e^(-64 x 0.75 / 32)), so either the base length scales with the register count (the verifier's 10 ms gate holds to about 330,000 ops) or the extra registers belong to the shadow alone and part (b) reads the base's 8 | modelled; the census decides |
|
||||
|
||||
Verdict: KEEP as layer 6, conditional on layer 5 (alone it moves nothing). Its value is one sentence in the chip's project plan: no interposer saves the second die.
|
||||
|
||||
### 2.3 Layer 7: warp-uniform data-dependent block selection (KEEP, rank 3, small)
|
||||
|
||||
A program whose control flow depends on the data it reads is the brief's first candidate. Per-lane branches are dead on arrival: a divergent branch costs a GPU warp both paths and a chip with per-lane sequencers nothing (the history's "placed nowhere" table; RandomX's one predictable branch targets speculative CPUs, which Igneum does not have). The form that survives is warp-uniform: at the end of each iteration a value reduced across the 32 lanes by shuffles (xor-fold of `r0`, say, which costs 5 shuffles) selects which of B drawn sub-blocks runs next, the same block for every lane of the warp, so the GPU takes one uniform indirect branch per iteration (as `sel` already takes one per iteration for the immediates) and the FPGA overlay or the fixed pipeline must hold all B blocks and pay B times the shadow's area for one block's throughput. For the GPU-like chip, a sequencer that already runs the hour's program, it is one more jump. What it buys: the per-epoch bitstream (the FPGA lane, history addition 5) holds B blocks instead of one, so a mid-size part's compile (42 to 160 minutes, PRflow, claimed in spec 1.13.1) carries B times the logic; at B = 4 a part that fitted one block does not fit, and at B = 8 the overlay must time-multiplex. Nothing in the energy identity moves. The acceptance must run every reachable path (B blocks per iteration, each judged by (a') in its own order) and the uniformity census must read the selection's bias (a selection that favours one block is a block that runs more).
|
||||
|
||||
Known-failed test, as a harness run: a research-only class `mx8+sh256x27+sel<B>` (the generator draws B blocks, the interpreter selects per iteration from the warp-folded `r0`), then `igneum-pow accept` over 256 seeds with every path judged, and `attack-f8 warps` at 2^24 on 64 seeds. Fail: the block-selection histogram over the 2^24 nonces outside 6 sigma of uniform (a plant: select from lane 0's `r0` bit 0 alone, which the fold is meant to prevent), or any path's (a') verdict differing from the whole-program verdict. Pass: within the band, 60 of 64 seeds under 1.2x on the hot-set test, as class v4 reads.
|
||||
|
||||
Chip row:
|
||||
|
||||
| Column | Value | Label |
|
||||
|---|---|---|
|
||||
| A fixed-function chip or an FPGA overlay | B x the shadow's logic for one block's throughput, or time-multiplexing at 1/B the rate; a bitstream compiled per epoch carries B blocks | approximate (LUT area scales with the straight-line block; no FPGA row exists in the repo) |
|
||||
| A GPU-like chip | nothing: one jump per iteration on a sequencer | modelled |
|
||||
| RTX 5090 / 5070 Ti / M5 Max | about 0: one uniform branch per iteration, 5 shuffles per iteration for the fold (5 x 8 = 40 shuffles per hash at 29 to 56 pJ: 0.002 microjoules, 0.3 percent of `F`); the compile-ahead carries B x 256 instructions of text (B = 4: the 1,024-line footprint that cost the M5 Max 17 percent on 6 October) | modelled on measured per-op costs; the Apple footprint is the cap on B |
|
||||
| The verifier | B x the acceptance's dynamic test per candidate (every path); the hash's cost unchanged | modelled |
|
||||
|
||||
Verdict: KEEP, rank 3, with B capped by the Apple footprint (B = 2 or 4 at the 256-instruction block, or B = 4 at 64-instruction blocks, which the 6 October measurement says run 2.5 to 3.5 percent faster anyway). It is the only candidate that moves the FPGA lane, which the history ranks as the first adversary of a per-hour program (Lyra2REv2, X16R) and which no measured row in the repo has priced (the HBM FPGA row is 0.30x to 0.39x of a 5090 per watt, chip-model-v3 5.3, the soft-overlay case unmeasured).
|
||||
|
||||
### 2.4 The reserve ordered by hardware orthogonality (KEEP as an ordering rule inside layer 3)
|
||||
|
||||
Layer 3 unlocks reserve families by height and rotates after exhaustion. The order is Open in spec 1.13.2 except R1. The history's addition 6 said: families that force a full 32-bit datapath per lane first, the int8 tile last. Today's measured rows (research file 15.1a; the research lane's layer 3 table) sharpen it: a shuffle crossbar is the one block where the GPU's cost per op is highest (29 to 56 pJ) and a chip's is near it (about 20 pJ, approximate), so `shfla` (lane plus delta, a second crossbar form) is the family a 12-op chip lacks most and gains least on; byte permute and popcount next (small adders a chip adds for 0.1 pJ, but a datapath without them loses 4 points of the mix); the int8 tile last (k 0.03 to 0.3: a chip's MAC array is cheaper than the GPU's tensor core, so the tile is kept for datapath diversity and never for joules). The chip row is layer 3's: about USD 4 of N5 pre-wires all eight. Cost to the cards at 4 points: under 1 percent of ALU time on every vendor (measured step costs: shfla 1.91x on Apple, 1.53x NVIDIA, 0.75 to 0.84 AMD). Known-failed test: the layer 3 gate's own (a kernel built without the live family refused at packcheck; the fast-time harness crossing one family epoch with a stale miner, 0 accepted blocks after it). Verdict: not a new layer; an ordering rule, stated so the synthesis fixes it at genesis.
|
||||
|
||||
### 2.5 Data-dependent program graphs, per-lane (REJECT)
|
||||
|
||||
The brief's form: the program's control flow drawn from the data it reads, per lane. A GPU warp executes a divergent branch as both paths with lanes masked, so a branch taken by half the lanes doubles the ALU work of that span; a chip with a sequencer per lane pays the taken path only. Number: a shadow of 55,296 instructions per hash with one two-way branch per 64 instructions at 50 percent divergence costs the card up to 2x the shadow's premium (165 W instead of 83 at the knee on the 5090, modelled on the measured 6.4 pJ per op) for a chip cost of 1x; `k` on the branched work falls to 0.15 to 0.4. Costs GPUs more than chips. The warp-uniform form (2.3) is what survives.
|
||||
|
||||
### 2.6 Latency-bound reads tied to the shard proof (REJECT)
|
||||
|
||||
The hash's reads sampled from the state the miner is proving: class v5 already keys every item to a leaf of the execution state at the epoch's cut and refreshes per epoch (spec 1.8.6; proof of following). Tying the reads to the segment being proved means a refresh per block (every second) from the touched leaves. The research file's row 7 priced the refresh as a cost: a 1 GiB rebuild is 157 G ops, 13.4 ms on a 5090 and 32 ms on a 4090 (measured), 0.16 to 0.5 J on a chip core (1 to 3 pJ per op, approximate); per block that is 0.3 to 1 W against 78 W of chip hashing (0.4 to 1.3 percent) and 1.3 to 3.2 percent of a GPU's hash time (the rebuild stalls the hash on the card; the chip's rebuild runs on its core beside the memory). A delta refresh (only the touched leaves, a few KB) costs both sides nothing. Either way the GPU pays more or equal. What the tie would buy is liveness (a chip must follow the chain per block, not per epoch), which class v5's per-epoch refresh already gives at the WAN line of 2a.2. Number: GPU 1.3 to 3.2 percent of rate against a chip's 0.4 to 1.3 percent of energy. Rejected.
|
||||
|
||||
### 2.7 Randomised memory topology per era (REJECT)
|
||||
|
||||
The dataset's address map and stride family drawn per era: class v3 draws the stride multiplier `M`, the rotation `R` and the interleave `pos` per era already (spec 1.13.1, Counter ASIC 2.0 layers 4 and 8, decided IN at a six-era hash-rate spread of 1.3 percent on the 5090, 3.2 on the 9070 XT, 0.8 on the M5 Max, measured). The plan said then what still holds: a chip whose address decoder can permute its address lines pays nothing. Drawing a richer family (a per-era permutation polynomial over bank and row bits, a drawn item size, a drawn line interleave across devices) costs the chip's decoder a few hundred gates and costs the honest card whatever the mapping does to its own DRAM's bank parallelism: a mapping that concentrates consecutive dependent reads into one bank group hurts the side with fewer lanes in flight, which is the chip (1,172 against 7,262), but the chip adds lanes at 64 B each (lane state is free, chip-model-v3 5.5), so the asymmetry closes at no cost. The one topology lever that would have moved the chip, the hot region above the window model, was read by adv-cache-2 as the diffuse era-stride excess (a product's low bits placed at address bit `R`; 8 of 27 drawn-era programs over 1.2x), which is a FAULT the next class's value-level test removes, not a lever to keep. Number: 0 to the chip, 0 to 3.2 percent to the cards. Rejected; the existing draws stand.
|
||||
|
||||
### 2.8 VRAM-size ratchet (REJECT as a lever; layer 2's floor stands)
|
||||
|
||||
A floor that rises with chain state by rule is layer 2. The ratchet form (the floor tracking the modal miner's VRAM minus the prover footprint, or rising on a calendar faster than the schedule) was read against the card-lifetime table (`docs/analysis/card-lifetime-2026-10-05.md`, option (b) steps): the 4 GB tier ends at the 4 GiB step, 8 GB at 8 GiB, 12 and 16 GB at the 16 GiB step; a chip holds 24 GB (one HBM3 stack, about USD 200, modelled) or 32 GB (the 5090's own 16 devices, USD 320), so every step retires a card tier before it touches the chip, and at 32 GiB and beyond the chip adds devices and its activate-bound rate RISES with the bank count (chip-model-v3 5.7, row "dataset size": "not a lever against this chip"). Number: at the 16 GiB step the 8, 12 and 16 GB tiers are out (3 of 6 card tiers) and the chip's energy per hash moves 0. Rejected as a lever; layer 2's rule (the schedule as the floor, the state above it, a ceiling at the next cache doubling) is kept exactly as the synthesis writes it, with its honest line that it retires cards before chips.
|
||||
|
||||
### 2.9 Proof-carrying hashes sampled by the pool (REJECT)
|
||||
|
||||
A fraction of hashes carries a verifiable execution witness. Three witness forms were read. (i) The hash's own 128 item values: the stored-dataset chip has every item; the recompute chip derives them; cost 0 to both, 512 B per share on the wire. (ii) A Merkle witness of the state leaves under the window's root: the chip's node has it (one node serves a farm, class-v5 2a.2); cost 0 to both. (iii) A witness that the item was DERIVED (a transcript of the 8 dependent cache reads and the mixer's 72 applications): a stored-dataset chip cannot produce it without the cache and the mixer core, so this form forces the `f = 0` chip's silicon (the 256 MiB SRAM mirror, USD 46 of die and an N5 project) onto the `f = 1` chip for the sampled fraction `g`; but the honest GPU must produce the same transcript, and deriving an item on the card is 8 dependent 64-byte cache reads at the mixer's 9,360 ops (the inline kernel measured 4.8x slower than the honest kernel on the M5 Max, spec 1.8.5), so at `g = 1/128` (one item per hash) the card pays about 4 percent of its rate and at `g = 1/16` about 30 percent; the chip derives on an SRAM-resident cache at 6.3 nJ per item (modelled) for 7 percent of its energy at `g = 1/16`. Number: GPU 4 to 30 percent of rate against the chip's 1 to 7 percent of energy. Rejected on form (iii); forms (i) and (ii) force nothing.
|
||||
|
||||
### 2.10 Prover-gated eligibility (REJECT)
|
||||
|
||||
The block's eligibility tied to the miner's proving (a key must have proved its share of segments in the last window to claim a block), so a chip farm must carry provers. The bound is the gas bound the research file's section 7 found: the chain needs 2 shards per block at the v1 budget, about 1.6 kW of 5090 proving network-wide at 1 block per second (measured prover rows), independent of the hash rate. Against the hash: at 1 GH/s the hash draws 2.4 kW (2.4 microjoules per hash, measured), so proving is 67 percent of it; at 100 GH/s 0.7 percent; at 10 TH/s 0.007 percent. A chip farm at share `s` must prove share `s` of 1.6 kW: six 5090s per farm at any scale, which is the node it already runs. Redundant proving (each segment proved by `m` provers) raises the forcing `m` times and is the useful-work gaming the history records (Aleo, Boundless). Number: at mainnet scale the forcing is under 0.01 percent of the chip's energy; the honest card already proves. Rejected; the 80/20 split stands.
|
||||
|
||||
### 2.11 Time-locked parameter commitments (REJECT beyond the era VDF)
|
||||
|
||||
An era's parameters committed under a VDF so a chip cannot be built ahead: the era VDF of 7 October (`docs/analysis/era-vdf-2026-10-07.md`) already makes the era draw's input unknowable for 517 s on the fastest prover measured (chiavdf's GMP path, 208,800 squarings per second, against the production T of 108 million) and the era lead is 2 hours (`pow_era_lead`), so a chip taped out against era `n` knows era `n + 1`'s draw 2 hours before it runs, against a 5-month (Bitmain) to 13-month (a startup) design cycle (the history's lesson 5, Vorick). Lengthening the delay or the commitment changes nothing a chip can use: the GPU-like chip holds the whole drawn band as firmware (the synthesis's section 7), and the fixed-function chip is dead at the first draw outside its wired value whether it learns the draw 2 hours or 2 days ahead. The one party for whom 2 hours matters is the FPGA fleet (a bitstream compiles in 42 to 160 minutes on a mid-size part, claimed), and layer 7 (2.3) and the epoch length (spec 1.13.1, the 600 s floor) are the levers for it, not the lock. Cost of a longer lock: one honest node core for the VDF's hour per era (today) rising linearly with T; the 2019-class verify gate already missed by 2.2x (26 ms against 10, measured, proxy). Number: 0 to the chip at any delay above 2 hours; the honest node's core-hours rise with T. Rejected.
|
||||
|
||||
### 2.12 A fraction of reads derived from the cache in the hash (REJECT)
|
||||
|
||||
The brief's spirit of "tie the hash to what the chip must hold": a fraction `g` of the 128 reads per hash derived on the fly from the 256 MiB cache (8 dependent cache reads and 72 mixer applications) instead of read from the dataset, so the stored-dataset chip must carry the recompute chip's cache and core for that fraction. This is 2.9 form (iii) without the witness and the same arithmetic: the card's derived read is 8 dependent DRAM reads (the cache does not fit L2 at 256 MiB, and the cache doubling keeps it so), so at `g = 1/16` the card's dependent-read count per hash rises from 128 to 184 and its rate falls about 30 percent (modelled on the latency-bound rule; the inline kernel's 4.8x at `g = 1` is the measured anchor); the chip with the cache on die derives at 6.3 nJ per item (4.0 nJ of SRAM reads, 2.3 of mixer; modelled) for 0.466 to 0.50 microjoules per hash (+7 percent) and buys the USD 46 mirror and the N5 project it already needs for the shadow core. Number: GPU -30 percent of rate at `g = 1/16`, chip +7 percent of energy and +USD 46 of die. Rejected.
|
||||
|
||||
### 2.13 Row-straddling and double-activation reads (REJECT)
|
||||
|
||||
The chip and the card share the DRAM's physics (the research file's section 5: the same tRC, the same activate window, the same 32-byte atom). A read that opens two rows (an item straddling a row boundary, or two independent 32-byte sectors per read) costs the chip's memory +0.9 nJ per read (a second 909 pJ activation; modelled) and the card +1 sector of traffic, which at 128 reads per hash is the w64 regime (64 B per read): the 5090 fell to 71.9 MH/s, bandwidth-bound, 47 percent of its rate (measured, read-width). Number: chip +45 percent of `E_mem` (0.466 to 0.58), card -47 percent of rate and about +9 percent of energy per hash on the memory side alone. The edge moves from 3.6x to about 3.1x at the knee (modelled) at the price of half the card's rate. Rejected.
|
||||
|
||||
### 2.14 Per-era lane-state and scratch draws (REJECT)
|
||||
|
||||
Per-lane live state across the hash (a scratch with read-modify-write) was measured out in Counter ASIC 2.0 (layer 3: the recompute chip's gain at every share 2.4x, the cards -12 to -48 percent) and bounded in `docs/analysis/scratch-soundness.md` (the live state sits in a chip's SRAM at under 5 percent of its mirror). A drawn scratch size per era draws from a dead family. Number: the cards -12 to -48 percent of rate (measured), the chip +picojoules per access. Rejected. (The register-file width of 2.2 is the live form of this idea: state that costs the GPU nothing because its register file is already there, and costs the chip a link, not an SRAM.)
|
||||
|
||||
### 2.15 A refresh per block (REJECT; the research file's row 7)
|
||||
|
||||
Dead by arithmetic: a 1 GiB rebuild is 0.16 to 0.5 J on a chip core and 13 to 32 ms of a card's hash time; per block that is 0.4 to 1.3 percent of the chip's energy and 1.3 to 3.2 percent of the card's rate. The refresh cadence is a liveness tool (class v5's proof of following), not an energy lever.
|
||||
|
||||
## 3. The measured rows (igneum-build-1, 8 October 2026, 11:0x to 11:2x UK)
|
||||
|
||||
The crate: `igneum-pow` of branch `counter-asic-4` at 5984ffab, built on build-1 through `tools/build-remote.sh --no-fetch --box 1` from the detached worktree `igneum-wt-v6-inv-ca4` (RESULT rc=0, 11 s, sccache); the binary `/srv/builds/igneum-wt-v6-inv-ca4/igneum-pow/target/release/igneum-pow`. Every run under `/srv/builds/_bin/lease` (pool 48 at class measure for the censuses, `cores 40,88` for the benches), owner `class-v6-invention`; the box at load 15 to 24 on 96 threads from other lanes throughout; logs under `/srv/builds/v6-invention/` (`census-none.tsv`, `census-era.tsv`, `logs/<era>-<form>-<seed>.log`, `census-run-*.log`), copied into `docs/analysis/class-v6/logs/` with the 09:00 report.
|
||||
|
||||
### 3.1 The acceptance census (`v6inv-census.sh`, 256 seeds `igneum-v6inv/0..255`, every candidate's verdict through `igneum-pow accept --class <form>`, the class's own 32-attempt cap)
|
||||
|
||||
No era (`ERA=none`; 2,816 rows in 80 s on 48 cores):
|
||||
|
||||
| Form (sub-blocks x length x passes) | Shadow instructions per iteration | Seeds accepted of 256 | Seeds exhausting 32 attempts | Candidates | Rejection per candidate | Mean accepted attempt | First failing part, the top four |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `mx8+sh256x27` (class v4's shape, the whole block after instruction 63) | 6,912 | 256 | 0 | 268 | 0.045 | 0.05 | (a) 6, (b) 5, (c) 1 (the v2/v3 rule only: the crate's `accept` does not take the class v4 parts on this spelling, so the row is the control's shape, not sub-version 3's 0.681) |
|
||||
| `mx8+shl256x27` (16 x 16 x 27, the 7 October form) | 6,912 | 76 | 180 | 7,043 | 0.989 | 15.9 | per-load bias 6,081; (c) 545; (b) 212; (a) 129 |
|
||||
| `mx8+shl256x16` (16 x 16 x 16) | 4,096 | 69 | 187 | 7,117 | 0.990 | 15.4 | bias 6,294; (c) 406; (b) 215; (a) 133 |
|
||||
| `mx8+shl576x12` (16 x 36 x 12) | 6,912 | 215 | 41 | 3,815 | 0.944 | 10.6 | bias 3,367; (b) 98; (a) 72; (c) 63 |
|
||||
| `mx8+shl512x8` (16 x 32 x 8) | 4,096 | 209 | 47 | 4,120 | 0.949 | 11.5 | bias 3,686; (b) 106; (a) 82; (c) 37 |
|
||||
| `mx8+shl1152x6` (16 x 72 x 6) | 6,912 | 226 | 30 | 3,428 | 0.934 | 9.9 | bias 3,036; (b) 97; (a) 62; (c) 7 |
|
||||
| `mx8+shl1024x4` (16 x 64 x 4) | 4,096 | 231 | 25 | 3,487 | 0.934 | 10.6 | bias 3,097; (b) 81; (a) 73; (c) 5 |
|
||||
| `mx8+shl2304x3` (16 x 144 x 3, class v4's count) | 6,912 | 224 | 32 | 3,457 | 0.935 | 9.9 | bias 3,076; (b) 89; (a) 67; (c) 1 |
|
||||
| `mx8+shl2048x2` (16 x 128 x 2) | 4,096 | 228 | 28 | 3,423 | 0.933 | 10.1 | bias 3,041; (b) 83; (a) 65; (c) 6 |
|
||||
| **`mx8+shl4096x1` (16 x 256 x 1, the sound form inside the cap)** | 4,096 | **234** | **22** | 3,217 | **0.927** | 9.7 | bias 2,823; (b) 94; (a) 64; (c) 2 |
|
||||
| `mx8+shl4096x2` (16 x 256 x 2) | 8,192 | 233 | 23 | 3,273 | 0.929 | 9.9 | bias 2,875; (b) 95; (a) 67; (c) 3 |
|
||||
|
||||
What the ladder says: the iterated 16-instruction map is the fault (0.989 to 0.990 whatever its pass count), and from a 36-instruction sub-block up the rejection is flat at 0.927 to 0.949 whatever the pass count or the work per hash (4,096 or 6,912 or 8,192 instructions per iteration). The remaining 0.93 is not the placement: the `(c)` lane-constant and distinct rejections fall to 1 to 7 per form (they were 406 to 545 on the 16-instruction forms), and the bias rejections are the product's low-bit law at the load's source, which under class v4 the sub-version 3 dataflow rule (a') removes at the draw and which the per-load class, not being the class v4 shape, never applies. The value-level reading of the 256 x 1 form's 2,329 bias rejections: index bit 0 in 1,563 (one-counts clustered at 3,584 to 4,608 of 16,384, the 1/4 law, 680 of them; and at 11,776 to 12,288, the 3/4 complement, 212), bits 26 and 27 in 502 (one-counts 7,168 to 7,680: a mild low bias of the top address bits just outside the 6-sigma band of 384, the high bits of small products through `mulhi`, unattributed), the other 26 bits 264 in all; by site, site 0 takes 788 of 2,329 (its source is written last by the previous iteration's sub-block 15 and by the base instructions before instruction 1, where the draw's redraw covers the sub-block's last writer of the next load's source and not a product rotated into place by a later `rotl`). The named fix is one rule, the research file's own ask of 20.2: the sub-version 3 freshness fixpoint and the shared-operand rule run over the real execution order (base instruction, the load, its sub-block, the next base instructions), and a load whose source is not fresh at that point refused at the draw, which class v4 pays at 0.568 of candidates and which should bring the per-load forms from 0.93 toward 0.68.
|
||||
|
||||
The acceptance's own cost: 35 ms per per-load candidate on one box core (1,111 ms for 32 candidates; the control's v2/v3 rule 2.8 ms), so an epoch's draw at 13.7 candidates is about 0.5 s of one core; the (c'') ratio at 2^20 would add class v4's 2.8 s per chosen candidate.
|
||||
|
||||
### 3.2 The verifier (core 40 of the EPYC 9454P at nice 19, core 88 its SMT sibling held busy by a 100,000-warp class v4 bench for the whole run, killed by its pid at the end; the ladder's method; `igneum-pow bench --seed igneum-genesis --day 2026-10-03 --class <form> --warps 50`)
|
||||
|
||||
| Form | Cold, sibling loaded, warp base 0 / 4,096 / 1,000,000 (ms) | Avg of 50, loaded (ms) | Cold, core alone, earlier in the same minutes (ms) | Label |
|
||||
|---|---|---|---|---|
|
||||
| `mx8+sh256x27` (class v4's shape) | 8.63 / 8.33 / 8.33 | 8.29 | 5.90 / 6.34 / 5.24 | measured; the ladder's run 2 read 8.77 / 5.14 at load 25 on 6 October |
|
||||
| `mx8+shl4096x1` | 8.55 / 8.28 / 8.28 | 8.24 | 5.44 / 5.21 / 5.17 | measured |
|
||||
| `mx8+shl2304x3` | 8.82 / 8.54 / 8.44 | 8.43 | 5.31 / 5.13 / 5.09 | measured |
|
||||
| `mx8+shl4096x2` | not run loaded | | 5.97 / 5.33 / 5.29 | measured, alone only |
|
||||
|
||||
The per-load forms verify in class v4's time within the run's noise (the same instruction count, the same items derived, 4,096 per warp on every row). A first pass of the loaded column was discarded: its sibling run (300 warps) ended before the measured warps began, the ladder script's own known-failed case, and read the quiet-core figures; the second pass held the sibling for the whole run.
|
||||
|
||||
### 3.3 The instrument fault under drawn eras, and the correction it forces
|
||||
|
||||
The same 2,816 rows with `--era igneum-era-test/<seed mod 16>` (`ERA=era`): every per-load form 0 of 256 seeds accepted, 8,192 candidates per form, rejection 1.000; the class v4 shape 256 of 256. Of the 256 x 1 form's 7,862 bias rejections, 5,536 name index bit 26 or 27 "set in 0 of 16,384" or "set in 16,384 of 16,384" at a load site, which is the era's working-set window (spec 1.13.1: `k_off = below(3)` per site puts the site on the whole dataset, a half or a quarter by fixing the top `k` bits of the index to the drawn offset `o`); the prototype's `BiasedIndexBit` test loops bits 0 to 27 and does not exclude bits at or above `28 - k_off_s`, so under any era it refuses every program with a half- or quarter-window site, which is nearly every program. The research file's 20.2a-close ("the 16 x 27 per-load class accepts 22 of 1,621 candidates over 64 seeds, 1.4 percent; 42 of 64 seeds exhaust the chain's 32 attempts") was read across drawn eras and therefore measured the instrument on most of its rows; the no-era census above is the construction's own figure (the 16 x 27 form 0.989 per candidate, 76 of 256 seeds accepted, which still fails the test of 2.1 and keeps that form dead). Owed to the research file (the Counter ASIC coordinator): the instrument fix (skip the window bits per site) and the 20.2a-close figures re-read with it; neither is made this week by this lane, which changes no code in the crate.
|
||||
|
||||
### 3.4 The packs
|
||||
|
||||
`igneum-pow export --seed igneum-genesis --day 2026-10-03 --class <form> --out <dir>` on build-1: `mx8_shl4096x1` (attempt 7, id 75ca9547da21b200, OVERALL PASS, kernel.cu 245,267 bytes, 4,308 lines) and `mx8_shl2304x3` (attempt 8, id bbfdfc1dcdda0b46, OVERALL PASS, kernel.cu 142,010 bytes, 2,516 lines), under `/srv/builds/v6-invention/packs/`, tarred as `v6inv-perload-packs.tgz` (sha256 ca1986b7fc5fab20a643fc37151a55e01f91edbfacc6f1a22a7384ae87cc11bc). Handed to the hash lane at 11:1x UK, taken into its v6 PC 1 job (the 5090 alone, the packs beside today's class v3 x8 and x16, the two re-weighted shadow packs, and the controls `mx8-genesis` and `mx8_sh256x27`; self-test, the 2^24 fingerprint, MH/s and W unlocked and at the 1,300 MHz lock), queued after the AMD grid, publish about 12:00 UK, close about 12:45 UK. Nothing of the packs' output goes to a served page or the spec.
|
||||
|
||||
## 4. The ranked list, every column
|
||||
|
||||
| Rank | Layer | Door (section 1) | Chip: fixed-function `k` / capex | Chip: GPU-like per-joule edge | Cost: RTX 5090 | Cost: RTX 5070 Ti | Cost: Apple M5 Max | Verifier | Harness state |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | Layer 5: the shadow per load, one pass of a long sub-block | capex and project (door 2) | `k` unchanged; project about USD 60 M against 30 M, cap about 200 M against 100 M (modelled); capex 3.0 to 4.3 USD per MH/s | unchanged (2.1x at `k = 1`) | -13 to -14 W against class v4's block at the same work (16 x 27 form, measured); the sound forms' rows in today's PC 1 job | no row; the 4070's 30 W premium as the proxy (approximate) | -0.5 percent (16 x 27 form, measured); the 4,096-line footprint OWED | 8.28 to 8.82 ms loaded (measured) | acceptance 0.927 measured; the dataflow rule in execution order is the fix (gate plan) |
|
||||
| 2 | Layer 6: the register-file width drawn per era | capex (door 2), with layer 5 | closes the interposer branch: 4.5 to 9 TB/s of die-to-die traffic (modelled, approximate link figures) | unchanged | about 0 rate (occupancy arithmetic, approximate; the pack measures it) | the same | the same | 4x the register arrays, cost per op unchanged (modelled) | no pack form; a generator constant (gate plan) |
|
||||
| 3 | Layer 7: warp-uniform block selection | life of a bitstream (door 3, the FPGA lane) | B x the shadow's area for a fixed pipeline or an overlay (approximate) | unchanged | about 0 (one uniform branch and 40 shuffles per hash: 0.3 percent of `F`) | the same | the compile footprint caps B (the 1,024-line block cost 17 percent, measured) | B x the dynamic test per candidate | no pack form (gate plan) |
|
||||
| 4 | The reserve's order | life of fixed silicon (door 3) | USD 4 pre-wired | unchanged | under 1 percent at 4 points (measured steps) | the same | the same | 0 | layer 3's gate |
|
||||
|
||||
What the list does not contain, and why it is honest to say so: no candidate moves the per-joule identity. The chip that stores the dataset keeps 3.6x at zero premium and 2.1x at `k = 1` under every layer here, as under the four. What the list adds is the project cost (rank 1 doubles it on the mission lane's model, rank 2 removes its cheaper branch), the FPGA lane's cost (rank 3), and the order of the reserve (rank 4). The founder's line "render an ASIC useless as soon as it dropped" is true of the fixed-function chip under layers 1 and 3 and stays false of the GPU-like chip under everything; the honest public sentence is the research file's: the price per joule of the honest card's operating point and the shadow's premium are what hold the general chip, and the layers decide which chip can be built and what it costs to start.
|
||||
|
||||
## 5. Consequences per tier (the standing rule of 5 October 2026)
|
||||
|
||||
| Tier | What this file means today | What is being done |
|
||||
|---|---|---|
|
||||
| A home miner, one 8, 12 or 16 GB card, any vendor, any OS | nothing changes: no layer here ships this week, no class moves, the devnet pays nothing; if rank 1 lands in v6 the card runs the same shadow work in a different place at the same or lower watts (the 16 x 27 form's measured 13 to 14 W under the whole block on a 5090; a 4070-class card's premium at its tune point is 30 W today, measured) | the sound forms' 5090 rows today; a small-card row (the 4070) by job when the hash lane's queue allows |
|
||||
| One 24 or 32 GB card (5090 class) | the measured rows of section 3; the per-load placement is cheaper for this card than class v4's block at the same work | the PC 1 job, about 12:45 UK |
|
||||
| RTX 5070 Ti | no card in the project; every row is the 4070's or the 5090's scaled, approximate; the hash lane's default (the stock pair plus the 5090's lock slope) stands | stated as approximate wherever it appears |
|
||||
| Apple (M-series) | the one open number: the inline footprint of a 4,096-line per-load block (the 1,024-line block cost 17 percent on 6 October); if it costs rate, rank 1 takes a block-shape cap for the Apple tier (144 x 3 at 2,516 lines, or a 64-instruction sub-block form) and the synthesis says so | a Metal packbench row under the measure lock by the lane that runs the Mac (not this one) |
|
||||
| A rig | watts per card as the 5090 row; a rig's bill under rank 1 is at or under class v4's | the same rows |
|
||||
| A pool user | nothing: no share, payout or template changes in any candidate kept; the rejected 2.9 (pool-sampled witnesses) is the only one that would have touched the pool protocol | nothing |
|
||||
| A node operator (the verifier) | the per-load forms verify in class v4's time (8.3 to 8.8 ms loaded, measured); layer 7 at B blocks multiplies the acceptance's dynamic test per candidate, not the hash | the 2019-class core measurement (O-1.14) decides rung and block caps as before |
|
||||
| A chip maker | under rank 1 the controller and the shadow core share one advanced die or an interposer (project about USD 60 M, modelled); under rank 2 the interposer no longer suffices; under rank 3 a per-epoch bitstream carries B blocks; the per-joule edge is unchanged | the gate plan of section 6 |
|
||||
| The public claim | nothing moves; the chip texts rest on the research file's close (2.1x at `k = 1` for 82 to 90 W on a 5090 at the knee, measured four times) | the Counter lane's texts |
|
||||
|
||||
## 6. The gate plan (hours, never weeks; nothing this week)
|
||||
|
||||
| Gate | What runs | Pass line | Known-failed case |
|
||||
|---|---|---|---|
|
||||
| G5-draw (layer 5) | the generator's dataflow fixpoint and shared-operand rule over the real per-load order; the 4,096 cap raised so 16 x 432 x 1 draws; `v6inv-census.sh` at 256 seeds, no era and drawn eras (with the instrument's window bits excluded) | rejection per candidate at or under 0.75 on every sound form, 0 exhaustions, drawn-era rate equal to the no-era rate within the binomial band; the F8 census at 2^24 on 64 seeds: 60 of 64 under 1.2x, the four tail seeds' sites read against their own windows | the 7 October per-load record (candidate 0 of 854050a4293f0615) refused; the 16 x 27 form at 0.989 refused by the line |
|
||||
| G5-card (layer 5) | one pack per sound form on the 5090 (today), the 4070 and the 9070 XT by job, Metal packbench on the M5 Max | rate within 1 percent of class v4's shape at both states on NVIDIA and AMD; watts at or under class v4's; the Apple footprint within 5 percent or the block-shape cap set | a pack whose fingerprint differs from the Rust verifier's on any vendor |
|
||||
| G6 (layer 6) | the register count as a class field; the census at 8, 16, 32; the card packs | 2.2's pass line | 2.2's fail line |
|
||||
| G7 (layer 7) | the selection class; every path judged; the selection histogram at 2^24 | 2.3's pass line | the lane-0 plant |
|
||||
| G-order (the reserve) | layer 3's gate with the order fixed | layer 3's line | layer 3's case |
|
||||
|
||||
Hours: the dataflow rule in execution order 2 to 3 (the research file's own estimate) plus the re-export and census 1; the register class 3 to 4; the selection class 4 to 6; the card jobs are queue time. Nothing is coded this week; the first code is the founder's call after the synthesis.
|
||||
|
||||
## 7. Unverified and owed
|
||||
|
||||
- The sound forms' card rows (the 5090 today through the hash lane; the 4070 and the 9070 XT later; the M5 Max footprint by the lane that runs the Mac).
|
||||
- The 16 x 432 x 1 form itself: bracketed by the 256 x 1 and 144 x 3 rows (0.927 and 0.935) and the flat ladder between 36 and 256; not drawn (the cap).
|
||||
- The instrument fix (the window bits) and the re-read of 20.2a-close: owed to the research file's owner, not made here.
|
||||
- The bits 26 and 27 mild bias (502 rejections at one-counts 7,168 to 7,680 of 16,384): unattributed; a trace of the site's source writers is the next read.
|
||||
- The register-width and block-selection classes: modelled only; no pack exists.
|
||||
- Every chip figure is the model's (chip-model-v3 section 5 and the research file's sections 2, 15.1a, 16 and 20.4); the die-to-die link figures of 2.2 are approximate, from memory, uncited; no chip has been measured.
|
||||
- The web search budget of this session was exhausted before this lane's reading; every external figure here is one already cited in the repo's files, with its URL and date there (the research file's section 14, the history's section 6, chip-model-v3 5.1).
|
||||
|
||||
## 8. Sources
|
||||
|
||||
Internal: `docs/spec/01-lottery-hash.md` (1.4.3 to 1.4.7, 1.8.5, 1.8.6 on branch class-v5, 1.13); `docs/analysis/chip-model-v3.md` (sections 1 to 3, 5.1 to 5.11, 6); `docs/analysis/counter-asic-4-research.md` on branch counter-asic-4 (sections 0, 2, 4, 7, 9, 15.1a, 15.1b, 16, 17, 20.2 to 20.4); `docs/plans/cryptanalysis/in-house-pass.md` (sections 12 to 14); `docs/plans/counter-asic-3-status.md` section 7c; `docs/analysis/asic-resistance-history.md` (sections 1.2, 2.4 to 2.6, 3, 4.3); `docs/analysis/era-vdf-2026-10-07.md`; `docs/design/class-v5-stored-state.md` on branch class-v5 (2a, 3); `docs/design/latency-ladder.md` on branch ladder (2 to 5); `docs/analysis/card-lifetime-2026-10-05.md`; `docs/analysis/latency-shadow-2026-10-06.md`; `docs/analysis/scratch-soundness.md`; `docs/design/class-v6-rotating-family.md` on branch counter-asic-4 (the synthesis's outline at 5984ffab); the harness runs of section 3 (logs on build-1 under `/srv/builds/v6-invention/`).
|
||||
|
||||
External, as cited in those files (read there on the dates they state): O'Connor et al., Fine-Grained DRAM, MICRO 2017; Li, Reddy, Jacob, MEMSYS 2018; Folded Banks, ISCA 2025; Horowitz, ISSCC 2014; Dally, Hot Chips 2023; the mlsysbook energy table citing Horowitz and Dally; the RandomX design document; the ProgPoW audits (Least Authority and Rao, 2019); Condrey, PoSME, arXiv 2604.15751 (April 2026); the Ethash, RandomX, Equihash and Cuckatoo chip rows of the history; the GDDR7 price rows (TrendForce, September 2026).
|
||||
2817
docs/analysis/class-v6/logs/census-era.tsv
Normal file
2817
docs/analysis/class-v6/logs/census-era.tsv
Normal file
File diff suppressed because it is too large
Load diff
2817
docs/analysis/class-v6/logs/census-none.tsv
Normal file
2817
docs/analysis/class-v6/logs/census-none.tsv
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
86
docs/analysis/era-vdf-2026-10-07.md
Normal file
86
docs/analysis/era-vdf-2026-10-07.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -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%;
|
||||
|
|
|
|||
231
docs/analysis/horizon-2026-10.md
Normal file
231
docs/analysis/horizon-2026-10.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
# 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 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 founder's rule: Claude-side work takes hours, never weeks).
|
||||
|
||||
## The lanes
|
||||
|
||||
| Lane | File | State |
|
||||
|---|---|---|
|
||||
| 1 consensus-security | `docs/analysis/horizon/consensus-security.md`, and the paper `docs/analysis/51-percent.md`; models `sim/horizon/consensus-security/` | landed, commit eec2cd7 |
|
||||
| 2 algorithm | `docs/analysis/horizon/algorithm.md`; model `sim/horizon/algorithm/model.py` | landed, commit 5ff7393 |
|
||||
| 3 finality-and-weight | `docs/analysis/horizon/finality-and-weight.md`; models `sim/horizon/finality-and-weight/` | landed, commit c3aa502 |
|
||||
| 4 economy-and-utility | `docs/analysis/horizon/economy-and-utility.md`; models `sim/horizon/economy-and-utility/` | landed, commit 4617a01 |
|
||||
| 5 network | `docs/analysis/horizon/network.md`; the experiment `docs/analysis/block-rate-devnet2.md` | landed, commits 3777014 and b965b64 (runs A and B; A2 dropped) |
|
||||
| 6 polish | `docs/analysis/horizon/polish.md` | landed, commit ac4cc93 (95 ledger rows) |
|
||||
| 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 founder
|
||||
|
||||
Closed 6 October 2026, 22:3x UK, every lane landed.
|
||||
|
||||
**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.
|
||||
|
||||
2. **Tonight's two-hour finality pause.** 42.7 percent of the frozen voter table left in three minutes and the frozen table held the pause a window (30 days on mainnet). Fix: a signed `leave` item, the key out of every denominator an hour later. **In 0.3.16** (finality-pause-node 766e70ca, finality-pause 27f82ec). The slices-under-10-percent rule: **done**.
|
||||
|
||||
3. **A 51 percent attacker buys a 90-second reorder window and nothing past a certificate.** **Done:** the peer-driven unwrap class (8e2f5cbe, 0.3.16), the receive-side version gate (f1ea7a38, 0.3.15). Next: weight-gated deep fork choice and vote-or-burn.
|
||||
|
||||
4. **The chip is settled in kind, open in degree**: 5.7x per joule at class v3, 2.1x at v4. The lever is the latency-shadow size N as a genesis ladder, each step by 90 percent signal. Text (M32, M33): **done**. **Decision owed:** the N ladder at genesis.
|
||||
|
||||
5. **Fees stay tiny for about ten years; emission carries security, with no end date.** USD 450 a day at launch and 4,200 in year 5, against 54,800 and 13,700 of emission. Text (E19, E20, E21, P24, P25): **done**. Next: unproven credit rolled forward, the job price decoupled from `f_p`.
|
||||
|
||||
6. **The signalling window could be bought for a day.** Seven consecutive daily windows at 95 percent, the floor a week past publish: **in 0.3.16** (ca3-v4-0316 0760b844). The thresholds in one sentence (G15): **done**.
|
||||
|
||||
7. **New proof of work, measured.** Scheme A (mining is proving): never. Scheme B (tensor shadow): never as class content; kept as reserve R8. Scheme C (dataset from stored state): the class v5 candidate; hash rate and watts unchanged, build +1.4 ms, verifier +0.11 to 0.21 ms per unit.
|
||||
|
||||
8. **Block rate: 1 a second for the testnet and launch.** 10 a second on Devnet 2 ran 77.6 percent red on node cost. 10 waits behind three gates: per-block CPU under 50 ms on a laptop core, finality constants in DAA seconds, vote aggregation. **Done.**
|
||||
|
||||
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 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 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 founder's decision owed**. The files sit on their branches and are not merged; read them there.
|
||||
|
||||
### Emission: the tail
|
||||
|
||||
`docs/analysis/tail-emission.md` on branch `tail-emission` (e1e28d2), ledger E22. The one page at the top of the file, 598 words:
|
||||
|
||||
> **The problem.** Today's schedule pays miners USD 210,513 a day in year 1 at a flat USD 0.10, 6,877 in year 11, 427 in year 20, 13 in year 30. A 24-hour 51 percent rental costs 11.8 days of budget at any price and any hardware, so the attack costs USD 2.5 M in year 1, 81,106 in year 11, 5,042 in year 20, 158 in year 30. Year 20 pays 0.2 percent of year 1.
|
||||
>
|
||||
> **Not a fix.** Fees (USD 450 a day at launch, 4,195 in year 5, under 1 percent of it to a miner; Bitcoin's fee share is 0.69 percent), a four-year halving (the same cliff later), a treasury (emission with an owner).
|
||||
>
|
||||
> **The safe baseline.** A tail denominated in supply: p percent of scheduled supply a year for ever, from the first month the curve pays less. Security becomes a fixed share of market cap in every year: at p = 1 a 24-hour attack costs 0.032 percent of market cap, the 20-day veto 0.6 percent, at any price, no oracle. The holder gives **0.99 percent of their share a year** (9.5 percent over ten years), nothing else. No rent, no forced movement, no cost to inactivity.
|
||||
>
|
||||
> **The revolutionary candidates, tested.** A hash thermostat (E) beats its withholding game only with presence pay and reads hardware as money: a 10x efficiency jump halves its budget for two years. Settled-value targeting (F) needs 8.5 percent inflation at velocity 1 and a wash trader pins it to its ceiling for one base fee. Security as a product (H) is 1 percent of the subsidy in year 1; dormant-coin rent (G) is refused by ruling. The hybrid (I) holds under every modelled attack and adds nothing to the floor.
|
||||
>
|
||||
> **The supply question.** The coin count is cosmetic (18 decimals); the unit price is social. Kaspa and Dogecoin paid blocks in the hundreds of coins and drew miners. A solo 100 MH/s card on a 100 GH/s network earns 2,190 IGN a day today, 6,912 at 100 a block. A monthly glide loses 2.9 percent a month and halves nobody's income overnight.
|
||||
>
|
||||
> **Recommendation, one coherent emission, every field a genesis parameter:** 100 IGN a block at one block a second; a monthly glide with a two-year half-life (Kaspa's shape at half its pace); a 90-day ramp from 10 percent; a 1 percent tail from the month the glide first pays under 1 percent of supply (year 11.4). Supply: 8.64 billion at the switch, 9.5 billion in year 20, 11.6 billion in year 40. The budget never falls under 1 percent of supply, tested to year 200.
|
||||
>
|
||||
> **The public sentence** (replaces "4 billion, approached and never reached"): *Igneum has no hard cap. Emission starts at 100 IGN a block and falls 2.9 percent a month for ten years, then runs at 1 percent of supply a year for ever, every coin to the miners and provers who secure the chain. A holder's share falls 1 percent a year, and a 24-hour attack costs about twelve days of emission at any price, in any year.*
|
||||
>
|
||||
> **What moves.** The emission constants of `igneum.rs` become one parameter set, `EmissionSchedule::CURRENT` (section 5 names each); the testnet carries `TESTNET_1`. Coded on fork branch `tail-emission-node`; the devnet digest is pinned unchanged by test; the testnet digest moves. A genesis decision for `igneum-testnet-1`, nothing live touched.
|
||||
>
|
||||
> **Risks.** "No cap" is a sentence critics quote (E1 already concedes Monero's trade). The 1 percent is a judgement inside the peer range (Monero 0.85, Bitcoin 0.83, Ethereum about 0.5). Year 1 stays front-loaded (26 percent; 47 in two).
|
||||
|
||||
### Vote or burn
|
||||
|
||||
`docs/analysis/vote-or-burn.md` on branch `vote-weigh` (e816f28). The verdict, 498 words:
|
||||
|
||||
> **Neither mechanism closes the line. Both price silence. Only the bonus passes the 95 percent test, and only at genesis.**
|
||||
>
|
||||
> 1. **Honest silence, measured.** 10.4 percent of the 83 staying keys' 45,176 key-checkpoints over 24 hours sat under participation 0.5; 79 percent of that is 15 fleet keys the Devnet 2 gate swapped off the live chain and back, mining nothing while away. The 19 always-on desktop keys dipped under 0.5 in 3.4 percent of checkpoints, longest dip 34 minutes, which on mainnet's 2-hour window never reaches 0.5. Tonight's 20 silent keys mined nothing while silent, so the burn would not have shortened the pause.
|
||||
>
|
||||
> 2. **History.** Every reward cut was fought and lost by the miners who stayed (Ethereum three times, Zcash three times, Ergo at 92 percent); where a cut removed a class's subsidy, the class left (Decred: two thirds of hash gone in a month). Offline penalties are accepted only as the reward forgone (Ethereum 0.625 against 0.844 earned; Cosmos 0.01 percent). No proof-of-work chain takes coins from a found block for a liveness fault.
|
||||
>
|
||||
> 3. **The 95 percent test.** Respect: 7-day hash at least 95 percent of the week before, no fork over 5 percent at 30 days, 95 percent of weight signing. Estimates: burn 70 to 80 percent (a 20 percent cut of a found block for an outage, without precedent, and for ever from `--no-vote` miners and pool members without a verifier). Bonus at genesis 93 to 97 percent (nobody loses what they had). Bonus by signal after launch 85 to 90 percent. A bonus from the proving pool: 97 percent, deters nothing.
|
||||
>
|
||||
> 4. **Security.** Weight is blue blocks (W2); an unpaid block is still weight, so a silent third keeps the veto under every mechanism. Base: the producer share. The burn prices a 34 percent set's silence at 6,206 IGN an hour (51-percent.md's 7,757 was 20 percent of the whole subsidy), the bonus at 3,103: USD 372 or 186 per 12-hour pause at USD 0.005; a deposit worth a double spend covers either. Weight-gated deep fork choice removes the prize; LEAVE ends tonight's class of pause.
|
||||
>
|
||||
> 5. **Recommendation.** No burn: `vote_burn_bps` stays never and leaves the tree before the testnet code is public. The bonus, `signing_bonus_bps` 1,000 (node tree ca3-v4-0316 10db4b61), on from the testnet genesis as the schedule, never by signal: silence = a voter of the table at the latest checkpoint in the block's past with no vote in the presence window (7,200 s, 240 indices); a young key and a key under dust read as present; the unsigned tenth to the proving pool. Gate before the switch leaves never: the replay test of section 5, then the owner's word. The miner's sentence: **"Igneum pays 80 percent of each block to its miner. 8 of those 80 points are for signing finality, which your miner does by itself every 30 seconds. Miss two hours of signing and your next blocks pay 72 until you sign again. Nothing is burned."**
|
||||
|
||||
### Open questions, being implemented
|
||||
|
||||
- The N ladder list (the era-draw ladder of the algorithm lane): being implemented; the one page's owed item is its values at genesis.
|
||||
- 18 decimals: being implemented.
|
||||
|
||||
## 2. The ranked list: top 25 across every lane
|
||||
|
||||
Rank is payoff over cost across lanes, with safety first, then liveness, then money, then text. "L1 r3" means lane 1's own rank 3; the lane file holds the full evidence row.
|
||||
|
||||
| Rank | Item | Lane | Evidence | Model | Hours | Consequence per tier | What to build | Gate |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | Verify the aggregated segment proof in consensus; a record whose proof fails against the pinned aggregator key is invalid | L1 r1 | spec 07 7.7 items 3 and 4; P21; a producer captures its block share of the 20 percent pool with fake records | capture = pool x (H inside the window + most outside): 11,636 IGN/h at 51 percent; SP1 light-verifier cost per record 1.8 to 2.4 s measured (bench-log) | 8 to 12 | prover: paid only for proofs; holder: the pool is real; node operator: one verify per record on the proof pool thread; miners: nothing | the verify call in the record check of `consensus/core` proving, the v0 per-shard record payout-only and capped at the exclusive window | fast-time: a fake-proof record is rejected by every node and the producer loses the block; a true record pays |
|
||||
| 2 | The departure announcement: a `leave` item (key, DAA score, signature) in blocks; D = 1 h later the key is in no denominator, sliding or frozen, and its votes are invalid; app Stop and the fleet library send it | L3 r1, L1 r8 | tonight's pause (L3 3.1): 42.7 percent left in 3 min, the frozen table held a window; sim T: first lock 1 h after a 34, 45 or 50 percent departure, 0 conflicts in every partition, eclipse and equivocator row | `finality_horizon.py` seeds 7, 11, 13 | 6 (+4 for F5's trusted certificate) | holder: a 30-day mainnet pause becomes 1 h when leavers are honest; a silent leaver still costs a window; pool and rig: one message on a clean stop; attacker: buying keys to leave them gains nothing (w + L must still reach 2/3) | the item in the coinbase finality section, the denominator rule in `finality.rs`, the send in Ember and `tools/fleet/lib` | harness: 45 percent leaves with leaves, lock within D + 1 checkpoint; 0 conflicts in 50/50, 60/40 and the 34 percent eclipse |
|
||||
| 3 | Signalling over 7 consecutive daily windows at 95 percent, the floor no nearer than 7 days past the publish, the stale-box list empty before the floor | L1 r6, L4 4.5 | the P2 one-day window is buyable: 19 N of hash for 24 h (USD 534k at 100 GH/s) forces a flip onto an unready fleet; a 6 percent holdout buys delay for nothing | `signalling.py`, `signal_game.py` | 3 | pool and rig: a week more before a class change and a week of visible share; home miner: a week to update; attacker: the bill x7 in public | the window count in the P2 rule, spec 5.7 text, one sentence carrying 60 / 90 / 95 in spec, CLAUDE.md and the litepaper | fast-time: 6 of 7 days at 95 percent does not flip; 7 does; the harness's failed case |
|
||||
| 4 | Weight-gated deep fork choice: a tip whose fork point is older than D (10 min of past-median time) is a candidate only if the blocks on it since the fork were produced by keys holding at least 1/3 of the weight table at the fork point | L1 r2 | during a pause or the first 20 days a renter with fresh keys double-spends at the 12-h depth for USD 146 at 1 GH/s | `cost_model.py`; deterministic over the block's past | 10 to 16 | holder and exchange: rented hash cannot reorg past 10 min even while nothing locks; honest miners: nothing (their keys hold the weight); new chain: the first 20 days gain a bound they lack today | the candidate filter in GHOSTDAG tip selection over the certified weight table | fast-time: a 51 percent fresh-key fork from 15 min back is refused by every honest node; a 1/3-weight fork is accepted; partition heal unchanged |
|
||||
| 5 | Vote-or-burn: a block whose producer key has participation under 0.5 over the presence window in its own past burns 20 percent of its producer share | L1 r3 | the pause as a liveness attack has zero marginal cost once the veto is held | 0.2 x A x 0.8 x subsidy: 7,757 IGN/h at 34 percent (USD 155/h at 0.02) | 6 to 8 | attacker: a pause costs; honest home miner: nothing while signing (the official client signs every checkpoint); pool user: the pool's participation; partition-safe because the test is the block's own past | the participation read in coinbase validation, the burn in the subsidy split | fast-time: a 34 percent silent set's blocks pay 80 percent; a 50/50 partition burns nothing on either side |
|
||||
| 6 | Roll unproven shard credit into the next proven segment's pool instead of the escrow for ever | L4 r2 | a ten-day prover refusal strands 547,570 IGN a day (5.5 M IGN) with no rule; spec 5.3, 7.7 item 3, 7.8 item 7 are silent | `stress.py` refuse scenario, `stranded_share` | 8 | prover: credit is never lost to the market's slow days; holder: no undecided burn; rollup customer: nothing | the roll-forward in the pool credit split, spec 5.3 text | fast-time: 100 unproven then 10 proven segments return the escrow to zero |
|
||||
| 7 | Decouple the job price from `f_p`: reserve = measured proving electricity per pgas at the published settlement rate (spec 5.10.3), the 1.5 premium becomes a bid | L4 r1 | at the adopted floor a billion-cycle job is 15 IGN = USD 0.075 / 0.30 / 1.50 against Boundless's 0.21 (approximate); above USD 0.014 per IGN the chain overprices by rule | `utility.py` sections 1 to 2 | 16 | rollup customer: a price that can clear; prover: still paid above electricity; holder: more jobs, more burn | the reserve rule in spec 5.4 and 5.11, the bid field on the job | a simulated job book clears within 20 percent of Boundless's median at all three prices |
|
||||
| 8 | Operational rule, no protocol change: a standing box never leaves the live chain for an experiment; any orchestrated departure over 10 percent of weight is staged in slices under 10 percent an hour; the fleet library refuses to swap a standing box's chain | L3 r2 | the rehearsal took 36.5 percent at once; sim L2 and M4: a gradual departure costs nothing because every lock re-freezes the table | spec 3.7 item 2 | 1 | fleet operator: finality stays on; every tier: no pause from our own experiments | a refusal in `tools/fleet/lib` plus a `--staged` departure | the next rehearsal keeps `finality_active` true |
|
||||
| 9 | Peer floor and mesh, and no `unwrap` on a peer-driven path with a sync-request fuzz gate (tonight's pruned-node crash: `consensus/src/processes/sync/mod.rs:87`, plus 27 sibling sites listed in L1) | L1 r4 | run A's star (321 tips, 77 percent red); the hub crash at 19:57Z: any peer can crash any pruned node at zero hash cost | a star with its hub down is n islands; the sibling list by file:line | 9 to 11 | node operator: no crash from a peer's request; home miner and rig: the app alarms under 3 outbound peers or 2 checkpoints without a vote; fleet: no star | the fleet lib dials 3 boxes beside the hands; the node alarm and app line; the unwrap sweep; the fuzz in `tools/ci` | the fuzz runs the sync request space against a pruned node with no panic; the alarm fires on a known-cut case and stays quiet on a healthy one |
|
||||
| 10 | N (the latency-shadow size) as a genesis ladder per era {100k, 130k, 200k, 330k, 650k, 1.0M}, floor and ceiling fixed at genesis (10x verifier headroom on the 2.5x rule; O-1.14 sets the ceiling), one era draw consumed as for `epoch_len`, each step by 90 percent miner signal over 7 days, never unconditional | L2 r5, L7 finding 1 | HBM4 raises the stored-dataset chip's bare edge (4.9x to 12x depending on tFAW, L2 and L7 disagree and both are labelled); N = 100k holds the chip at 2.1x on GDDR7 at k = 1, N = 330k at 1.3x; an unconditional doubling retires the M5 Max at era 1 (-10.5 percent at 200k) | `model.py --section ladder` (L2 5.3a, the reconciled table) | 6 to 8 | 100k to 130k: the M5 Max -3.3 points, nobody else; 130k to 200k: M5 Max -6 more, the 5090 -2.7 at its cap, the 4070 +21 W; 200k to 330k: compute-bound on every capped NVIDIA card; pool users nothing at any step; verifier +0.17 to 0.56 ms per warp | the ladder in the era-draw table of spec 1.13, the step rule on P2's mechanism | per step: every public-benchmark card within 5 percent of its previous-step rate, bit-exact on three vendors, verifier under 10 ms cold on the O-1.14 core |
|
||||
| 11 | Aggregate-first vote verification and aggregated in-block carriage as the mainnet default (spec 3.4.2 item 2, decided) | L3 r3 | per-vote verification is 1.6 s per checkpoint per node at 1,000 voters and about 13 s at 8,192 (approximate); measured lock delay p50 0.86 to 1.26 s at 4 to 93 voters on node 1 | L3 4.3 table; blst 0.3.17 `fast_aggregate_verify` already in the fork | 8 | node operator on a laptop: a checkpoint costs one pairing, not a thousand; every tier: lock delay stays under 3 s at 8,192 voters | batch votes per (index, hash), verify once, bisect on failure; the in-block aggregate | fast-time with 1,000 and 8,000 synthetic voters: p50 lock delay under 3 s, CPU under 25 percent of a core, 0 conflicts |
|
||||
| 12 | The finality weight table carried inside the recursive segment proof, updated one mergeset per segment: a consensus proof at mergeset cost | L7 r1 | phase two's hardest item (P4) becomes incremental on code that exists; about one BLS verify per 30 s (one shard's budget, approximate, unmeasured) | `frontier_model.py` | 60 (first 10: measure) | phone wallet and bridge: finality from one proof, no node trusted for the voter set; rollup customer: one object; prover: one more public-value block per segment | the W2 transition in the aggregator guest, the GHOSTDAG colouring check in-guest | first: BLS verify and colouring cycle counts inside the SP1 guest on a 24 GB card within 2x of the estimate; then a proof per checkpoint under 30 s on a proving-only 5090 |
|
||||
| 13 | Reproducible-build attestations in a registry contract; Ember refuses a release under N of M attestations | L7 r3 | ledger G7, G13: the update key is an operator channel; Bitcoin's guix.sigs is the precedent with the chain as the sigs repo | text and contract | 12 | every tier: an update needs N independent builders, not one key; fleet: the box's reproducible Windows and Linux builds (CLAUDE.md 6 Oct) are the inputs | the contract, the builder tool, the Ember check | a release with 1 of 3 attestations is refused by Ember; 3 of 3 installs; the known-failed case |
|
||||
| 14 | Close O-1.14 with a real 2019 laptop run; adopt the half-core proxy as the standing stand-in; dr736 out, dr368 in as R0 | L2 r1 | box proxy: dr736 10.51 ms cold and 15.49 half-core; class v4 5.06 and 8.23 | L2 5.5 | 2 | miners 0; pool verifier cores x1.3 at dr368 instead of x2.4; a 2019 node keeps 1.8 ms under the gate on class v4 | Windows cross build on the box, relay to the laptop, bench, log | ms per warp under 10 cold on the real core for class v4 and dr368 |
|
||||
| 15 | The vote signs the execution root too (chain id, index, block hash, post_root); a certificate pins the state; snapshots check against the last certificate | L1 r5 | snapshot poisoning: a wrong state above the pin is undetectable today | every voter executes natively (spec 07) | 8 to 12 | holder: a lock is a state lock; node joining from a snapshot: cannot be poisoned; cost: exec lag enters lock latency | the vote message, the pin rule, the snapshot check | fast-time: a poisoned snapshot node cannot join the quorum; honest lock delay rises by the measured exec lag only |
|
||||
| 16 | `finality_provisional` reported beside `finality_active`, never as a lock, plus a detector-driven `security_alert` (correlated group over 1/3 of a day's blue blocks; pause; conflict) shown by wallets and the explorer as "confirm at 12 h" | L3 r4, L1 r7 | sim P: provisional conflicts in every partition (516 to 1,062 per 360 min), final 0; the exchange guidance exists only as text today | L3 4.1; the detector of counter-asic-3 | 3 + 4 to 6 | exchanges: a third row in the guidance; holder: told when to wait; home miner: a line in Ember | the RPC fields, the explorer and wallet copy, spec 3.9 row | forced pause: explorer and wallet show provisional and the alert; a healthy day shows neither |
|
||||
| 17 | Every miner-signalled execution parameter enters the consensus digest the same release, with a `tools/ci` check that fails a `Params` field marked signalled and absent from the digest | L4 r5 | signal-then-defect on an execution parameter is a silent state fork unless the handshake refuses the defector; `Params.fees` is in the digest, nothing guarantees the next | `signal_game.py` section 3 | 6 | node operator: no quiet fork; every tier: a defector is refused at the handshake | the check, the digest rule in spec 5.8 | the check fails on a planted field and passes on the live set |
|
||||
| 18 | The 80/20 split as a 60 percent parameter inside a hard band [10, 30] percent | L4 r4 | not load-bearing at launch traffic; 30 percent buys backlog relief at 100 shards a block (economy-2026-10-04 5.3) | `stress.py` | 10 | prover and miner: a split the market can move inside a band nobody can break; holder: the cap and the halving untouched | the band in spec 5.3 and 5.7 | harness: a 60 percent vote reaches 30 percent; a 100 percent vote cannot pass the band |
|
||||
| 19 | A WebAssembly verifier of the wrapped block proof in the tab, with the millisecond count shown | L7 r4 | three working precedents (Helios WASM, ziren-wasm-verifier, wasm-groth16-verifier); the certificate half already runs at 58 to 68 ms warm, 139 to 155 ms cold (bench-log round 6, P3) | text | 16 | holder and rollup customer: verify in the browser; the P3 wrapper measurement comes forward | the Groth16 or Plonk wrapper, the WASM verifier on the site | a block proof verifies in the tab under 500 ms on a laptop; the measurement labelled on the page |
|
||||
| 20 | Ember as node, wallet and light client for everyone | L7 r5 | 1,000 testnet miners become 1,000 verifying nodes; Monero shows about 5,000 peers (monero.fail), Ethereum 8,136 execution nodes (ethernodes), approximate | text | 20 | home miner: one app; node count = miner count; Sybil counts of nodes irrelevant by design; node cost per lane 5's tiers | the node and wallet surfaces inside Ember | a fresh Windows install mines, verifies and shows a balance in the first 60 s with no second download |
|
||||
| 21 | Work-stake: vote weight as the external-job bond, with the spec sentence "no coin stake; the only thing at stake is 30 days of public work" written first | L7 r2 | a 0.1 percent key has about 16,427 IGN of 30-day pool income plus its vote at risk against a designed 0.0015 IGN coin bond per job | `frontier_model.py` section 3 | 24 (prototype) | prover: a bond nobody can buy; rollup customer: a griefing cost that scales with the prover's standing; holder: "no stake" stays true as "no coin stake" | the bond rule in the job claim | a griefed job costs the griefer its weight for 30 days in the fast-time harness; the spec sentence lands before the prototype |
|
||||
| 22 | Client-shipped certified checkpoint (index, hash, voter-table digest) refused if missing from the DAG; exec generations spaced geometrically to the finality depth (about 16, 1.8 GB today) | L1 r9, r10 | long-range and seed attacks; a pause-time deep reorg needs a peer's snapshot today | assumevalid's shape; 114.8 MB per snapshot measured | 3 + 3 | every tier: a fresh install cannot be bootstrapped onto a private DAG; node operator: disk for generations, no blocked executor after a deep reorg | the checkpoint in the release, the generation schedule | a cold node refuses a DAG missing the checkpoint; a 5,000-block reorg re-executes from a generation with no snapshot request |
|
||||
| 23 | Public-text corrections, one bundle: the prover's price as a formula with network hash as the input, never a number; the era draw and the reserve described as schedule changes against fixed datapaths, not unpredictability against a chip, with the chip's USD per MH/s-hour beside the honest cards'; F19's "day 19 to 20" is a v2 number (v3: a full window); funding.md section 4's dev-fee ceiling is 1 percent of the producer share (38,520 / 154,080 / 770,400); the dev fee is "default-on, switchable", not "optional"; "proofs at the cost of power" conditioned on hash; the three signalling thresholds in one sentence | L4 r3, r7; L2 r3; L3; L1 | each row cites its lane section | text | 1 to 3 each | holders and critics read claims that survive review | the litepaper, the customer brief, spec 5.7, funding.md, the ledger F19 | the site's forbidden-strings check and a reviewer's read |
|
||||
| 24 | Measure the FPGA lane on AWS F2 (one VU47P, HBM2, about USD 1.98 an hour) and replace the ceiling row | L2 r2 | the measured 2.4 G reads/s equals the JEDEC tFAW ceiling; the old 1.9x row rests on a 12 ns tFAW the JEDEC HBM2 table does not give (28 ns); the soft overlay reads 0.30x to 0.47x of the 5090 per watt | L2 5.1 | 8 to 10 plus USD 2 to 8 | none today; the public FPGA claim becomes a measured number | the overlay bench on F2 | reads per second per watt at 1 GiB; the row replaced |
|
||||
| 25 | Ember tune as the shipped default per card model, and the two fleet measurements every price rests on: the miner's hash loss while each tier proves, and a full 30 M-cycle shard beside the miner on 12 and 16 GB cards | L2 r7, L4 r8 | the 4070 at 3.65 uJ untuned and 2.57 tuned (-30 percent); the hybrid row is the only one that undercuts the market and rests on one 5090 measurement; the 4.7 M fixture is 16 percent of a full shard (linear scaling says 240 s on a 3060, outside the 120-s claim timeout) | L2 5.1; `utility.py hybrid_hash_loss` | 2 + 6 (fleet) | every NVIDIA tier gains 10 to 30 percent per joule; the 12 GB tier learns whether it can claim a full shard in time | the defaults table in the app from the fleet priors; eleven rows with both numbers in `prover-tiers-real-cards.md` | the rows land; the claim timeout is set from them |
|
||||
|
||||
Lanes 5, 6 and 8 landed after this table was ranked; their rows are in the lane files (network.md section 6, polish.md section 1, new-pow.md section 7) and in the one page above. The table itself is not re-ranked: rank 1 and rank 2 are in 0.3.16, rank 3 is in 0.3.16, rank 8 is done, rank 9's unwrap half is in 0.3.16, rank 23's text bundle is done.
|
||||
|
||||
### Not recommended, with the reason (as valuable as the list above)
|
||||
|
||||
| Item | Lanes | Why not |
|
||||
|---|---|---|
|
||||
| Prover attestations as a second finality leg | L1 r12, L3 r8 | provers are the miners (same vote keys), so no new party; proof coverage is 2.4 to 4.7 percent of blocks tonight with lag p99 62 s, so every lock would wait on proofs; revisit only after 99 percent of blocks are proven within 60 s for 7 days with 3 provers per block, and even then it adds about a minute of lock delay |
|
||||
| Time-locked (vesting) weight | L1 r13 | a bought key transfers vested weight, so the acquired-keys bound is unchanged; honest new cohorts wait longer |
|
||||
| Any automatic re-lock after an abrupt departure | L1 r14, L3 r7 | departure and partition are the same observation in one view; the decaying denominator produces 467 to 473 conflicting locks in a 360-minute 50/50 honest split, the hysteresis floor brings the 13.3 percent equivocator bound back (565 to 597 conflicts at 20 percent); the two-tier rule is a report, never a lock |
|
||||
| Mining is proving (the lottery's work as a proving step) | L8 scheme A, L7 3.10 | dead on bytes (2.9 MB of openings per block), on the verifier (32 to 40 ms against 10), and on sampleability (Ball et al. 2017, Ofelimos 2022); re-opens Aleo's fastest-prover-wins |
|
||||
| Proving others' chains as the main income | L7 3.11 | all of Ethereum L1's proving is about USD 36 a day at the Sep 2026 tracker cost against USD 13,700 a day of year-1 emission at USD 0.005; demand must grow 1,000x against a falling cost curve |
|
||||
| Burn-redirect audit bounties, a review escrow by 60 percent signal | L7 3.6, L4 task 3 | a dev fund with a veto, the switch spec 5.5 removed; the honest payer for a second audit is the entity's own provers |
|
||||
| Proof verification on a hardware wallet's secure element | L7 3.9 | a bn254 pairing on a Cortex-M-class element is seconds to minutes (approximate); do the companion verify |
|
||||
| A general compute market for unverifiable work (rendering, inference) | L7 3.12 | an escrow without a verifier is a trust-me payment with lower fees; the verifiable subsets are named and kept |
|
||||
|
||||
## 3. What a 51 percent attacker can and cannot do
|
||||
|
||||
The paper is `docs/analysis/51-percent.md` (lane 1). In one table, at the measured USD 11.7 per GH/s-hour:
|
||||
|
||||
| Hash share | What it buys | For how long | Cost at 1 GH/s / 1 TH/s of network | What it earns | Net |
|
||||
|---|---|---|---|---|---|
|
||||
| 20 percent | reorders only the last k = 18 blocks; nothing past a certificate | per attempt | subsidy forgone while withholding | its block share minus reds | loss |
|
||||
| 34 percent | wins the selected-chain race only inside 60 s (40 percent of the time); holds the veto (blocks every lock) after 20 days of mining at 51 percent, or 30 days at 52 percent of the network's hash | 20 to 30 days to acquire, then free to hold while silent | about USD 6k / 5.8 M to acquire (half earned back as subsidy); a pause then costs nothing | subsidy; nothing from the pause itself unless it double-spends at the 12-h depth during the pause (USD 146 to 146k) | the one line vote-or-burn (rank 5) and weight-gated fork choice (rank 4) close |
|
||||
| 45 to 51 percent | wins the selected-chain race over a 90-s hold 70 to 85 percent of the time (32 to 46 blocks at 1 bps); turns 26 to 28 percent of honest blocks red; lifts its weight share to about 56 percent, never 2/3 | the 63 to 93 s between a checkpoint block and its lock | USD 11.7 / 11,700 per hour of rented hash; the market could not supply a TH/s on 6 October (RunPod: 0 pods for 20 asks) | a quarter of honest subsidy while withholding; the pool capture of rank 1 (11,636 IGN/h) until the in-consensus verifier lands | loss on the chain; gain only through the pool, which rank 1 closes |
|
||||
| 67 percent of weight | locks alone | 2.03 x N of hash for 30 days: USD 17,100 per GH/s of network | | subsidy | at rental equilibrium about 33 percent of 30 days of subsidy net |
|
||||
| any share | a rule change | 95 percent of blue-block weight over the window, or the floor | 19 N for 24 h today (USD 534k at 100 GH/s); x7 under rank 3 | | |
|
||||
|
||||
What no share buys: a block the nodes do not re-execute (the native-execution veto), a state the aggregated proof chain does not commit to, a lock past a certificate under two thirds of weight, a parameter change without the signal.
|
||||
|
||||
The residual risks stated plainly: the first 20 days (no weight table yet); a pause after a sudden departure of a third of weight (30 days under v3 until rank 2 lands); the Sybil count of keys (X5's definition adopted, the measurement scheduled); the proving pool until rank 1; the 2/3-of-total rule under long churn (the window bound: a 50/50 honest partition locks on both sides from day 10).
|
||||
|
||||
## 4. Per lane: the three biggest findings
|
||||
|
||||
### Lane 1, consensus-security
|
||||
1. The lock bounds a majority; the k-cluster does not (45 to 51 percent wins a 90-s race 70 to 85 percent of the time; the lock at 63 to 93 s is the bound).
|
||||
2. The veto is cheap (20 days of 51 percent: USD 6k at 1 GH/s, 5.8 M at 1 TH/s, half earned back) and the pause is then free.
|
||||
3. The proving pool is capturable today (11,636 IGN/h at 51 percent) with a correct-statement fake-proof record; the only attack that earns more than it costs.
|
||||
|
||||
### Lane 2, algorithm
|
||||
1. Class v4's verifier measured on a 2022 server core: 4.90 / 5.06 / 8.23 ms (steady / cold / half-core); dr736 9.76 / 10.51 / 15.49, out; R0 is dr368.
|
||||
2. The f = 1 GDDR7 chip: 5.7x per joule against the 5090 at v3, 2.1x at v4 with k = 1; USD 0.00021 per MH/s-hour against USD 0.0117 rented; the FPGA soft overlay 0.30x to 0.47x per watt.
|
||||
3. The reserve and the era draw buy about nothing against a chip; the N ladder in the era draw at genesis is the lever, and lane 7's HBM4 column is reconciled with three named disagreements.
|
||||
|
||||
### Lane 3, finality-and-weight
|
||||
1. Tonight's pause: the 2/3 rule at checkpoint 6843 (53.1 percent of total after a 42.7 percent departure), then the frozen table (Q5) holding it for a window; locks formed without the hub; topology refuted.
|
||||
2. Only the departure announcement passes the line (0 conflicting locks everywhere, the pause under an hour); the decaying denominator, the hysteresis floor and the two-tier rule fail or are reports.
|
||||
3. Weight capture costs USD 8,424 x N x W/(1 - W) to rent for the window: the veto 0.52 N for 30 days (USD 4,300 per GH/s of network), a lock alone 2.03 N (USD 17,100); lock delay measured p50 0.86 to 1.26 s at 4 to 93 voters; the path breaks near 1,000 voters on per-vote verification.
|
||||
|
||||
### Lane 4, economy-and-utility
|
||||
1. The proving price is h/N: 100 to 300x Boundless at 1.16 GH/s, 0.2 to 0.4x at 100 GH/s beside the miner; the adopted floor overprices above USD 0.014 per IGN.
|
||||
2. Fees are not a security budget for a decade (USD 450 a day at launch, 4,200 in year 5, against 54,800 and 13,700 of emission); sustained honest hash costs USD 24.8 per GH/s-day against USD 281 rented, so the 20-day veto costs 11.8x the honest fleet at every price and year.
|
||||
3. The 80/20 survives every stress but a ten-day prover refusal, which strands 547,570 IGN a day of pool credit with no rule to return it.
|
||||
|
||||
### Lane 7, frontier
|
||||
1. HBM4 raises the stored-dataset chip's edge (lane 2 bounds the figure at 4.9x to 12x bare by tFAW); the N schedule belongs in the era draw at genesis.
|
||||
2. Vote weight is already a slashable, non-purchasable bond: work-stake for external jobs, with "no coin stake" written into the spec first.
|
||||
3. The consensus proof can be incremental: the weight table inside the recursive segment proof, one mergeset per segment; the first measurement is the in-guest BLS verify and colouring cycle counts.
|
||||
|
||||
### Lane 5, network
|
||||
1. Reds come from node cost, not topology: run A at 10 blocks a second on a star ran 77.6 percent red because the hub needed 61 to 345 ms per block; the propagation model predicts under 0.1 percent red in every bps x delay cell with an ideal hub.
|
||||
2. The controller reads blue work only, so a star or a withholder eases difficulty; the whole-DAG estimator holds the rate in every cell.
|
||||
3. The recommendation: 1 block a second for the testnet and launch; 10 behind three gates (per-block CPU under 50 ms on a laptop core at mergeset 248, finality constants in DAA seconds, vote aggregation). The experiment is `docs/analysis/block-rate-devnet2.md`.
|
||||
|
||||
### 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 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.
|
||||
2. Scheme B (tensor shadow) is free for the honest card and so nearly free for the chip (0.056 to 0.70 pJ per multiply-add against 11 pJ per ALU op); never as class content, kept as reserve R8.
|
||||
3. Scheme C (dataset from stored state) is the class v5 candidate: the 4090's rate and watts equal within noise (63.083 against 63.088 MH/s), build +1.4 ms, verifier +0.11 to 0.21 ms per unit, bit-exact on 1,024 items and 128 lanes.
|
||||
|
||||
## 5. What was not run, and why
|
||||
|
||||
| Item | Lane | Why |
|
||||
|---|---|---|
|
||||
| The in-guest BLS verify and GHOSTDAG colouring cycle counts (rank 12's first gate) | 7, 3 | no 24 GB card free tonight: PC 2 and the fleet were on class v4 and Devnet 2 |
|
||||
| The real 2019-class core (O-1.14) | 2 | the US laptop was not on the relay; the box's half-core proxy stands in |
|
||||
| The FPGA soft overlay on real HBM2 | 2 | no FPGA in the fleet; AWS F2 plan written |
|
||||
| The AMD watts at every N | 2 | the ADLX sampler row is owed on the runner's `--cards-off` mechanism (counter-asic-3-status 6a) |
|
||||
| A `cargo bench` of `fast_aggregate_verify` at 93, 1,000 and 8,192 keys | 3 | the BLS figures are arithmetic on blst's published timings, approximate |
|
||||
| The 10 bps mesh run A2 | 5 | dropped: run B (the 1 bps control) landed clean and the model attributes run A's reds to node cost, which a mesh does not change |
|
||||
| The full 30 M-cycle shard beside the miner on any tier; the hash loss while proving on Ampere and Ada | 4 | the fleet measured the 4.7 M fixture only |
|
||||
| Market prices for Taiko-class batch proving and Bonsai | 4 | not published; marked approximate |
|
||||
| The observed end of tonight's pause | 3 | expected at DAA 216,402, about 20:40Z; the timeline closes when the observer rows show the lock |
|
||||
|
||||
## 6. Rules and corrections for main
|
||||
|
||||
| # | Rule or correction | Lane | State |
|
||||
|---|---|---|---|
|
||||
| 1 | P21 is a priced economic hole until the in-consensus verifier lands; the 20 percent pool is not "paid to provers" without the caveat | 1 | relayed |
|
||||
| 2 | The one-day P2 signalling window is buyable for a day; seven consecutive windows | 1, 4 | relayed |
|
||||
| 3 | The DAG controller reads blue work only, so a withholder or a star eases difficulty 16 to 32 percent (run A's loop) | 1, 5 | relayed; lane 5 owns the fix |
|
||||
| 4 | No `unwrap` on a peer-driven path; the sibling list; a sync-request fuzz in `tools/ci` | 1 | relayed |
|
||||
| 5 | A standing box never leaves the live chain for an experiment; orchestrated departures over 10 percent staged under 10 percent an hour | 3 | relayed; the fleet library refusal is rank 8 |
|
||||
| 6 | Under rule v3 any sudden departure of a third of weight is a 30-day mainnet pause; the leave item is the one safe way to shorten it | 3 | relayed |
|
||||
| 7 | F19's "stalls until day 19 to 20" is a v2 number; under v3 a full window | 3 | relayed, ledger text owed |
|
||||
| 8 | "No coin stake; the only thing at stake is 30 days of public work" into spec 03/05 and the litepaper before any work-stake prototype | 7 | relayed; routed to the site-miner agent |
|
||||
| 9 | The litepaper's "proving: a second income" carries the USD 36 a day arithmetic | 7 | relayed; routed |
|
||||
| 10 | Ember's updater installs nothing while finality is paused | 7 | relayed; sent to the Ember agent for 0.3.16 |
|
||||
| 11 | The customer brief's "priced in dollars per proof" and the litepaper's "proofs at the cost of power" are conditioned on network hash | 4 | relayed |
|
||||
| 12 | The three signalling thresholds (60 / 90 / 95) in one sentence across spec 5.7, CLAUDE.md and the litepaper | 4 | relayed |
|
||||
| 13 | The spec is silent on pool credit nobody claims; today it is a burn nobody decided | 4 | relayed |
|
||||
| 14 | funding.md section 4 overstates the dev-fee ceiling by a quarter | 4 | relayed |
|
||||
| 15 | Any future miner-signalled execution parameter outside the consensus digest makes signal-then-defect a quiet state fork | 4 | relayed |
|
||||
| 16 | The era draw and the reserve are not unpredictability against a chip; the public text should say what they buy | 2 | relayed |
|
||||
350
docs/analysis/horizon/algorithm.md
Normal file
350
docs/analysis/horizon/algorithm.md
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
# Horizon lane 2: the shipped hash and its class system, refined
|
||||
|
||||
6 October 2026, evening UK. Lane 2 (algorithm) of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon`, branch `horizon` on master 3f4f719. Scope: the shipped lottery hash and its classes (v2, v3 live, the v4 candidate `mx8+sh256x27`, the reserve R0 to R8, the era draw, the dataset schedule). No new puzzle is proposed here (lane 8 owns that). Every chip figure is arithmetic on cited figures and is approximate; every GPU figure names its bench-log entry or analysis file; the one new measurement is the verifier proxy on igneum-build-1 (section 2).
|
||||
|
||||
What was read, in full unless marked: `docs/spec/01-lottery-hash.md`, `docs/spec/04-seeds-and-vdf.md`, `docs/analysis/chip-model-v3.md`, `docs/analysis/asic-resistance-history.md`, `docs/analysis/latency-shadow-2026-10-06.md`, `docs/analysis/m16-recompute-attacker-2026-10-05.md`, `docs/analysis/sram-mirror.md`, `docs/analysis/int8-matrix-family.md`, `docs/analysis/weak-program-census-2026-10-03.md` (section 1), `docs/analysis/scratch-soundness.md` (section 0), `docs/analysis/card-lifetime-2026-10-05.md`, `docs/plans/counter-asic-3-status.md`, `docs/plans/counter-asic-3-reserve.md`, `docs/plans/counter-asic-3-derivation.md`, `docs/plans/counter-asic-2.md`, `docs/plans/epoch-length.md` (sections 11 and 12), `docs/plans/era-layout.md`, `docs/plans/mixer-x4.md` (sections 6.2 to 6.5), `docs/plans/read-width.md`, `docs/fud-ledger.md` (M1 to M31 headings; M9, M16, M17, M18 in full; M28 heading), `docs/bench-log.md` (entries "Counter ASIC 2.0, the numbers", "the 9070 XT on the eGPU", the 6 October item 8, item 6 and gate-run entries, "Rental cost of hash, 6 October 2026"), `igneum-pow/src/generator.rs` (the class constants, `ShadowClass`, `LoadClass::{V2, MX4, MX8, DR736}`), `igneum-pow/src/memhard.rs` (`Shape`, the growth rule, `round_key`, `derive_items_mask`), `igneum-pow/src/main.rs` (`bench`), `docs/analysis/prover-tiers-real-cards.md` (the 11 rented cards), and `/Users/joshm/Projects/igneum-wt-gpu-fleet/docs/analysis/block-rate-devnet2.md` (RUN_A and RUN_B are still placeholders at 21:30 UK; nothing from it is used).
|
||||
|
||||
## 1. The six questions and the one-line answers
|
||||
|
||||
| # | Question | Answer in one line |
|
||||
|---|---|---|
|
||||
| 1 | Chip model on the 6 October numbers; the FPGA lane | The stored-dataset chip (f = 1, GDDR7) reads 5.7x per joule against the 5090 bench row and 1.7x against the M5 Max at class v3; at class v4 and k = 1 those fall to 2.1x and 0.9x. Per dollar it is 56x under rented hash and 5.4x under an owned 5090 per MH/s-hour. The FPGA soft overlay tightens to 0.30x to 0.47x per watt: the measured 2.4 G reads/s equals the JEDEC tFAW ceiling of a 2-stack HBM2 part, so the 1.9x bank-bound row is unreachable on any FPGA that exists. AWS F2 at USD 1.98 an hour carries the exact HBM2 subsystem and can measure it |
|
||||
| 2 | Reserve R0 to R8 | Every reserve family together costs a chip about 8 to 14 adders per lane, about USD 4 of N5 silicon on a 14,000-lane array; none moves the per-joule edge by over 10 percent. The reserve's value is obsolescence of a datapath taped out against class v3, and since the order is public that value is zero against a chip that ships with every block. Recommended order: R0 derive at dr368 (dr736 fails the gate proxy), R1 shfla, R2 perm, R3 popc and clz, R4 bfe, R5 shifts, R6 sel, R7 andn, R8 mm8 at era 8 by the rule, no exception |
|
||||
| 3 | A class v5 from the shadow | Option (i) is void: the v4 shadow already consumes loaded data (its registers hold the dataset words of the iteration), so a chip precomputes nothing today. Option (ii), a shuffle-heavy shadow mix, raises the attacker's k floor from about 0.32 to 0.46 (approx), not to 1. Option (iii): the one N every owned card holds within 5 percent is 130,000 counted ops (the M5 Max's point); it buys 2.1x to 1.7x against the 5090 at k = 1 for 4.8 percent of the Mac's rate; the verifier at 130,000 is 4.9 ms on the box proxy and 8.5 ms on the half-core proxy, inside the gate |
|
||||
| 4 | The era draw's randomness | Forging the certified checkpoint the era VDF reads costs 20 days of 100 percent hash: USD 5,600 at 1 GH/s, USD 5.6M at 1 TH/s rented, and buys one draw of a space whose spread is 0.8 to 3.2 percent of hash rate per card and 0 percent for a chip. Re-rolling by withholding needs a 1,800x faster VDF. The draw buys nothing against a chip; the public reserve order means a chip is taped out with every block. What would cost a chip is work (N) and the honest card's own watts, not unpredictability |
|
||||
| 5 | The 2019-class verifier | Measured proxy tonight: a Zen 4 core at 3.8 GHz with server DRAM reads 2.0x to 2.2x the M5 Max core (v4 4.90 ms steady, 5.06 cold; dr736 9.76 steady, 10.51 cold: FAIL); with both SMT siblings busy (the pessimistic bracket) v4 8.23 ms, dr368 8.16, dr736 15.5. The 2.5x rule holds within 15 percent. The gate protects a node at 1 to 10 percent of one core per block rate, an 18-minute IBD, and a header-flood cost of 100 bogus headers per second per core |
|
||||
| 6 | Dataset growth to 2030 | Hold option (b): 2 GiB at genesis, 4 GiB at year 4, 8 GiB at year 12. The 8 GB tier (26.7 percent of Steam today, about 0 by 2030 on the trend) mines to year 12 anyway; the 12 GB tier loses mine-and-prove compressed at the year-4 step and the 8 GB tier loses core-only at genesis, and both are the prover's footprint, not the dataset's. Growth is for the SRAM reticle (1.6 GiB per reticle at N5) and GPU L2, not for the HBM chip |
|
||||
|
||||
## 2. Method
|
||||
|
||||
| Item | What was done | Where |
|
||||
|---|---|---|
|
||||
| The model | One Python script, every table in this file; inputs listed with source and label | `sim/horizon/algorithm/model.py`, `README.md` beside it |
|
||||
| Verifier proxy | `igneum-pow` built on igneum-build-1 through `tools/build-remote.sh` from the crate directory (`IGNEUM_AGENT=horizon`, build slot build-0, 10 s wall, sccache miss 1, artefact 991,352 bytes, sha256 6d2867...); `igneum-pow bench --seed igneum-genesis --day 2026-10-03 --class <c> --warps 50` for v2, mx8, mx8+sh256x27, dr368, dr736 under `nice -n 19 taskset -c 40` (one core), then the same on cores 40 and 88 at once (the two SMT siblings of one physical core: `thread_siblings_list` 40,88); load average 1.3 before, 2.3 after; `scaling_cur_freq` read 3,799,885 kHz during a run (the governor is schedutil with `scaling_max_freq` 2,750,000 but the core boosted to 3.8 GHz; `cpupower` needs root, so no fixed low clock was possible); the cache fill 361 ms on that core | section 5.5 |
|
||||
| Not run | A Mac packbench ladder for v5 (the data-dependent and shuffle-heavy shadow variants do not exist as kernel text, so nothing could be measured; the plan is in 5.3); any PC job; any FPGA | section 7 |
|
||||
| External figures | Steam Hardware Survey September 2026 (store.steampowered.com/hwsurvey, read 6 October 2026); ICCAD 2021 "Demystifying the Characteristics of High Bandwidth Memory for Real-Time Systems" Table I (JEDEC HBM2 timings in cycles at 2,133 MT/s, text extracted with pdftotext); Intel HBM2 IP user guide (16 AXI pseudo-channel ports per stack); AWS F2 pricing (f2.6xlarge USD 1.98 an hour on-demand, USD 0.66 spot, one VU47P with 16 GB HBM2) | section 5.1 |
|
||||
|
||||
## 3. Evidence
|
||||
|
||||
### 3.1 Honest cards, measured (class v3 unless said)
|
||||
|
||||
| Card (tier) | MH/s | W | uJ per hash | Source |
|
||||
|---|---|---|---|---|
|
||||
| RTX 5090, PC 2 bench, 431 W cap, the control | 132.2 | 350 | 2.65 | `latency-shadow-2026-10-06.md` s5; bench-log item 8 |
|
||||
| RTX 5090, PC 1 app, Ember run 5 | 127.4 | 310 | 2.43 | `counter-asic-3-status.md` s7 item 1 |
|
||||
| RTX 5090, app 5 October | 124 | 290 | 2.34 | `miner-eff` record, cited in latency-shadow s3 |
|
||||
| RTX 5090, rented Vast pod, untuned | 98.5 | 258 | 2.62 | `prover-tiers-real-cards.md` |
|
||||
| RTX 4090, rented | 52.3 | 183 | 3.50 | same |
|
||||
| RTX 3090, rented | 37.8 | 229 | 6.05 | same |
|
||||
| RTX A5000, rented | 47.7 | 223 | 4.67 | same |
|
||||
| RTX 4070, PC 1, 1,860 MHz lock, 160 W cap | 30.95 | 79.5 | 2.57 | status item 8, 4070 rows |
|
||||
| RTX 4070, rented | 25.0 | 91 | 3.65 | prover-tiers |
|
||||
| RTX 5070, rented | 41.9 | 137 | 3.27 | prover-tiers |
|
||||
| RTX 3060, rented | 23.8 | 104 | 4.36 | prover-tiers |
|
||||
| RTX 3080, rented | 40.8 | 205 | 5.02 | prover-tiers |
|
||||
| RTX 4060 Ti 16 GB, rented | 17.6 | 72 | 4.11 | prover-tiers |
|
||||
| RTX 4060 Ti 8 GB, rented | 19.1 | 73 | 3.81 | prover-tiers |
|
||||
| RTX 4060, rented | 17.1 | no reading (0.0 W logged) | n/a | prover-tiers |
|
||||
| RX 9070 XT, PC 1 | 18.9 | 199 to 203 | 10.6 | bench-log 9070 XT telemetry; status 3a |
|
||||
| Apple M5 Max, GPU + DRAM channels | 27.08 | 21.0 | 0.78 | latency-shadow s3 |
|
||||
| Apple M5 Max, package (approx) | 27.08 | 38 | 1.40 | `ember-tune.md`, approximate |
|
||||
|
||||
At class v4 (`mx8+sh256x27`, measured): the 5090 131.95 MH/s at 431 W (3.27 uJ, the cap binds), the M5 Max 26.67 at 37.2 W (1.39), the 4070 31.08 at 109 W (3.51), the 9070 XT 19.29 at owed watts. Every other card's v4 energy below is modelled as the card's marginal ALU energy times N (NVIDIA 11 pJ per counted op measured on the 5090, Apple 6.9 measured on the M5 Max, AMD taken as NVIDIA's, approximate).
|
||||
|
||||
### 3.2 Chips (model, approximate; `chip-model-v3.md` s5.4, latency-shadow s6)
|
||||
|
||||
| Chip class | MH/s | W at v3 | uJ, v3 | uJ at v4, k = 0.3 / 0.5 / 1 / 1.5 | $ silicon + memory, v3 / v4 | $ per MH/s |
|
||||
|---|---|---|---|---|---|---|
|
||||
| f = 0 on-die recompute, 256 MiB SRAM, x8 | 41.7 | 53.7 | 1.29 | 1.62 / 1.84 / 2.39 / 2.94 | 700 / 740 | 16.8 |
|
||||
| f = 1 stored dataset, GDDR7, 16 devices | 166.4 | 77.6 | 0.466 | 0.80 / 1.02 / 1.57 / 2.12 | 470 / 510 | 2.8 |
|
||||
| f = 1, HBM3 one stack | 83.6 | 26.8 | 0.321 | 0.65 / 0.87 / 1.42 / 1.97 | 550 / 590 | 6.6 |
|
||||
| f = 1, HBM3 eight stacks | 666 | 174 | 0.262 | 0.59 / 0.81 / 1.36 / 1.91 | 2,650 / 2,690 | 4.0 |
|
||||
|
||||
`k` is the chip core's energy per counted op over the 5090's measured 11 pJ. The f = 0 chip's measured stand-in (M16 round 2, the inline kernel inside the 5090's L2) ran 33.9 MH/s at 431 W: 0.256x per chip and 5x worse per joule than honest, so the f = 0 row above is the model's ceiling for that class, not a measurement.
|
||||
|
||||
### 3.3 The verifier proxy, measured tonight (igneum-build-1, EPYC 9454P, one core at 3.8 GHz, nice 19)
|
||||
|
||||
| Class | M5 Max core, quiet | 2.5x rule (approx) | Box one core, steady / cold | Box over Mac | Box half-core (SMT sibling loaded) | 10 ms gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| v2 | 0.60 | 1.5 | 1.13 / 1.30 | 1.88x | not run | pass |
|
||||
| mx8 (class v3) | 2.06 | 5.2 | 4.52 / 4.67 | 2.19x | 7.56 | pass |
|
||||
| mx8+sh256x27 (class v4) | 2.33 | 5.8 | 4.90 / 5.06 | 2.10x | 8.23 | pass |
|
||||
| dr368 | 2.69 | 6.7 | 4.72 / 5.32 | 1.75x | 8.16 | pass, thin on the half-core |
|
||||
| dr736 | 4.88 | 12.2 | 9.76 / 10.51 | 2.00x | 15.49 | FAIL (cold run over 10 ms on one core; 15.5 on the half-core) |
|
||||
|
||||
Raw lines (the Mac figures are `counter-asic-3-derivation.md` 5.1 and the gate-run entry): box one core `CPU verify: 1.128 / 4.516 / 4.901 / 4.716 / 9.763 ms per 32-lane warp, avg of 50`; cold `warp base 0: 1.305 / 4.670 / 5.061 / 5.324 / 10.509 ms`; half-core `7.562 / 8.234 / 8.162 / 15.491` (cpu 40) and `7.556 / 8.227 / 8.168 / 15.479` (cpu 88); every lane-0 and lane-31 hash equal to the Mac's vectors (`42246ba99fc58e4f`, `19b56348bc85304d`, the dr736 `e23d389f3eea0c83`). Cache fill 360.5 to 364.7 ms on the box core against 175 to 181 on the M5 Max.
|
||||
|
||||
### 3.4 Installed base (Steam Hardware Survey, September 2026, cited)
|
||||
|
||||
| VRAM | Share | Trend (cited: 8 GB 35.03 percent in August 2025, 33.66 in September 2025; 12 GB 19.30 in August 2025) |
|
||||
|---|---|---|
|
||||
| 512 MB to 4 GB | 14.06% | falling |
|
||||
| 6 GB | 5.12% | falling |
|
||||
| 8 GB | 26.71% | about -7 points a year |
|
||||
| 10 to 11 GB | 2.74% | flat |
|
||||
| 12 GB | 13.06% | about -6 points a year |
|
||||
| 16 GB | 27.21% | rising; overtook 8 GB in August 2026 |
|
||||
| 20 to 24 GB | 7.93% | rising slowly |
|
||||
| 32 GB | 1.41% | rising slowly |
|
||||
| 64 GB | 0.50% | new row |
|
||||
|
||||
### 3.5 Rental and ownership cost (bench-log "Rental cost of hash, 6 October 2026", measured)
|
||||
|
||||
USD 0.0117 per MH/s-hour on RunPod community pods (1,748 MH/s for USD 20.44 an hour); the 8x 4090 rig USD 0.0129; a 5090 pod USD 0.41 to 0.74 an hour for 98 to 128 MH/s.
|
||||
|
||||
## 4. Model
|
||||
|
||||
| Formula | Inputs (label) |
|
||||
|---|---|
|
||||
| Energy per hash E = W / rate | measured watts and MH/s per card; chip W from `chip-model-v3.md` 5.4 (approx) |
|
||||
| Chip energy at class v4: E_v3 + N x 11 pJ x k | N = 100,000 counted ops (measured class v4), 11 pJ = the 5090's marginal per op (measured), k free |
|
||||
| Edge per joule = E_card / E_chip | both sides above |
|
||||
| Hourly cost per MH/s = price / (2 years x rate) + E x 3.6e9 x USD 0.10 per kWh | prices approximate (launch list from memory, labelled); chip $ from 5.4; electricity approx |
|
||||
| FPGA random-read ceiling = min(banks / tRC, channels x 4 / tFAW, channels / tRRD) | 2 stacks, 16 channels, 32 pseudo-channels, 16 half-banks each (approx); tRC 48 cycles, tFAW 30, tRRD 6 at 1,066 MHz (ICCAD 2021 Table I, JEDEC HBM2); latency 137.8 ns (Shuhai, measured) |
|
||||
| Reads in flight = rate x latency; per watt = rate / board W | U55C 115 to 150 W, U280 225 W (datasheets) |
|
||||
| Verifier add per warp = shadow instructions / 1,000 x slope | slope 3.2 us (M5 Max, measured), 7.0 us (box one core, this lane), 12.1 us (box half-core, this lane) |
|
||||
| Checkpoint forgery cost = network MH/s x 480 h x USD 0.0117 | 20 days of 100 percent hash to 2/3 weight (CLAUDE.md headline, from the finality sim) |
|
||||
| Tier fit at a dataset step: miner resident = dataset + 192 MiB; prover peak beside the miner from `prover-tiers-real-cards.md` scaled by the dataset's growth | usable VRAM 75 percent (mine-only), 98 percent headless (the prover rows) |
|
||||
|
||||
## 5. Results
|
||||
|
||||
### 5.1 Task 1: the chip model on the 6 October numbers, and the FPGA lane
|
||||
|
||||
Per joule and per dollar against every honest card (the full table with every card is the `chip` section of the model; the rows that decide things):
|
||||
|
||||
| Card (tier) | uJ v3 / v4 | f = 0 chip edge, v3 / v4 at k = 1 | f = 1 GDDR7 edge, v3 / v4 at k = 0.3 / 0.5 / 1 / 1.5 | f = 1 HBM3 one stack, v3 / v4 at k = 1 | $ per MH/s (card, approx) | USD per MH/s-hour owned |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RTX 5090 bench (32 GB) | 2.65 / 3.27 | 2.1x / 1.4x | 5.7x / 4.1x / 3.2x / 2.1x / 1.5x | 8.3x / 2.3x | 15.1 | 0.00113 |
|
||||
| RTX 5090 app, Ember (32 GB) | 2.43 / 3.53* | 1.9x / 1.5x | 5.2x / 4.4x / 3.5x / 2.3x / 1.7x | 7.6x / 2.5x | 15.7 | 0.00114 |
|
||||
| RTX 4090 rented (24 GB) | 3.50 / 4.60* | 2.7x / 1.9x | 7.5x / 5.8x / 4.5x / 2.9x / 2.2x | 10.9x / 3.2x | 30.6 | 0.00210 |
|
||||
| RTX 3090 rented (24 GB) | 6.05 / 7.15* | 4.7x / 3.0x | 13.0x / 9.0x / 7.0x / 4.6x / 3.4x | 18.9x / 5.0x | 39.7 | 0.00287 |
|
||||
| RTX 4070 PC 1 tuned (12 GB) | 2.57 / 3.51 | 2.0x / 1.5x | 5.5x / 4.4x / 3.5x / 2.2x / 1.7x | 8.0x / 2.5x | 17.7 | 0.00127 |
|
||||
| RTX 5070 rented (12 GB) | 3.27 / 4.37* | 2.5x / 1.8x | 7.0x / 5.5x / 4.3x / 2.8x / 2.1x | 10.2x / 3.1x | 13.1 | 0.00108 |
|
||||
| RTX 3060 rented (12 GB) | 4.36 / 5.46* | 3.4x / 2.3x | 9.4x / 6.9x / 5.4x / 3.5x / 2.6x | 13.6x / 3.8x | 13.8 | 0.00123 |
|
||||
| RTX 4060 Ti 8 GB rented (8 GB) | 3.81 / 4.91* | 3.0x / 2.1x | 8.2x / 6.2x / 4.8x / 3.1x / 2.3x | 11.9x / 3.5x | 20.9 | 0.00157 |
|
||||
| RTX 4060 Ti 16 GB rented (16 GB) | 4.11 / 5.21* | 3.2x / 2.2x | 8.8x / 6.5x / 5.1x / 3.3x / 2.5x | 12.8x / 3.7x | 28.4 | 0.00203 |
|
||||
| RX 9070 XT (16 GB AMD) | 10.6 / 11.7* | 8.3x / 4.9x | 22.8x / 14.7x / 11.5x / 7.5x / 5.5x | 33.2x / 8.3x | 31.7 | 0.00287 |
|
||||
| Apple M5 Max, GPU + DRAM | 0.78 / 1.39 | 0.6x / 0.6x | 1.7x / 1.8x / 1.4x / 0.9x / 0.7x | 2.4x / 1.0x | 129 | 0.00745 |
|
||||
| Apple M5 Max, package (approx) | 1.40 / 2.02 | 1.1x / 0.9x | 3.0x / 2.5x / 2.0x / 1.3x / 1.0x | 4.4x / 1.4x | 129 | 0.00752 |
|
||||
|
||||
`*` modelled v4 energy. The chip's hourly cost per MH/s (two-year amortisation plus electricity, approx): f = 1 GDDR7 USD 0.00021, HBM3 one stack 0.00041, eight stacks 0.00025, f = 0 recompute 0.00109; an owned 5090 0.00113; rented hash 0.0117. So the stored-dataset chip undercuts rented hash 56x and an owned 5090 5.4x per MH/s-hour, and the recompute chip matches the 5090 exactly, which is why nobody builds it.
|
||||
|
||||
What changed against the 5 October record: the honest denominators moved (the 5090 is 2.34 to 2.65 uJ by where it is measured, not 2.40), the marginal ALU energy is measured at 11 pJ (not the model's 5.5), the M5 Max at 0.78 uJ is the honest best per joule by 3x, and the rented fleet shows the untuned mid-tier (3060, 3080, 3090, A5000, 4060 Ti) at 3.8 to 6.1 uJ, 1.5 to 2.3x worse than the 5090 bench row: against those cards the GDDR7 chip reads 8x to 13x per joule at v3 and 3.1x to 4.6x at v4 with k = 1. The per-tier reading: the Apple tier is already inside 2x of the GDDR7 chip with no shadow and crosses under 1x at v4 and k = 1; the 5090 and the tuned 4070 reach about 2.1x to 2.2x at v4 and k = 1; the untuned mid-tier and AMD stay at 3x to 7.5x, which Ember tuning (the 4070 rows: 3.65 to 2.57 uJ) closes by about 30 percent and nothing in the hash closes further.
|
||||
|
||||
The FPGA lane. The brief's soft-overlay range was 0.30x to 0.39x per watt measured-basis and 0.7x to 1.9x at an unmeasured bank-bound ceiling. The reads-in-flight model with the JEDEC HBM2 timings:
|
||||
|
||||
| Ceiling | Formula | G reads/s per card | Reads in flight at 137.8 ns | Per W at 115 / 150 / 225 W (M/s/W) | Against the 5090 per W (53.7 M at 326 W) |
|
||||
|---|---|---|---|---|---|
|
||||
| Measured, Shuhai U280 default mapping (FCCM 2020, Fig 7) | 32 pc x 75 M | 2.4 | 331 | 21 / 16 / 11 | 0.39x to 0.30x (U55C); 0.20x (U280) |
|
||||
| tFAW-bound (JEDEC HBM2, ICCAD 2021 Table I: 30 cycles at 1,066 MHz, 4 ACT per channel) | 16 ch x 4 / 28.1 ns | 2.3 | 313 | 20 / 15 / 10 | 0.37x to 0.28x; 0.19x |
|
||||
| tRRD-bound (6 cycles) | 16 ch / 5.6 ns | 2.8 | 392 | 25 / 19 / 13 | 0.46x to 0.35x; 0.24x |
|
||||
| Bank-bound, no activate window (the epoch-length 12.2 ceiling row) | 32 pc x 16 banks / 45 ns | 11.4 | 1,567 | 99 / 76 / 51 | 1.84x to 1.41x; 0.94x |
|
||||
| O'Connor's HBM2 activate figure as carried by chip-model-v3 5.3 | 16 ch x 8 / 12 ns | 10.7 | 1,470 | 93 / 71 / 47 | 1.73x to 1.32x; 0.88x |
|
||||
|
||||
The reading: Shuhai's measured 2.4 G/s equals the tFAW ceiling at JEDEC timings (2.3 G/s). The measured row was read on 6 October as a mapping artefact ("the paper's point is that this mapping is the wrong one for random access"); the activate window says it is the DRAM's own limit, which a bank-interleaved mapping does not lift because tFAW is enforced per channel by the die. The 11.4 G bank-bound row needs tFAW gone; O'Connor's 12 ns figure (which the chip model carries for HBM2 and HBM3) is 2.3x shorter than the JEDEC HBM2 cycle count tabled by ICCAD 2021, and the difference is the whole 0.7x to 1.9x row. Tightened range for a 2-stack HBM2 FPGA (U55C, U280, F2's VU47P): 2.3 to 2.9 G reads/s, 0.30x to 0.47x of the 5090 per watt, in the RX 9070 XT's class (2.4 to 2.7 G/s measured). HBM2e parts (Versal HBM, Agilex 7 M) raise the pin rate, not tFAW in nanoseconds (approx), so they sit in the same band; no FPGA with HBM3 exists as a product. Approximate throughout: the 16 half-banks per pseudo-channel, the 1,066 MHz reading of the ICCAD table, the board watts under load.
|
||||
|
||||
Can a rented FPGA hour measure it? Vast.ai lists no FPGAs (GPU marketplace only, checked 6 October 2026). AWS F2 (f2.6xlarge: one Virtex UltraScale+ HBM VU47P, 16 GB HBM2 in 2 stacks, 32 pseudo-channels, USD 1.98 an hour on-demand in us-east-1, USD 0.66 spot) carries the same HBM2 subsystem as the U55C and U280, so yes. The gate, written as a measurement plan:
|
||||
|
||||
| Step | What | Hours (agent) | Pass line |
|
||||
|---|---|---|---|
|
||||
| 1 | Port the chase kernel of `docs/benchmarks/repro.md` 2.2 to a Vitis HLS AXI master over the HBM IP: 1 GiB working set across all 32 pseudo-channels, N dependent 4-byte-reads-in-flight lanes (N = 256, 1,024, 4,096), the HBM IP's address map set to bank-interleaved (RAMA or the IP's "random access" option), a second variant with 32-byte reads | 4 to 6 | the kernel reports reads per second and the chain's checksum equal to the CPU's |
|
||||
| 2 | Build the AFI (the F2 shell flow; the Vivado licence rides with the instance), 2 to 4 hours of F2 time at USD 2 to 8 | 2 (mostly waiting) | an AFI that loads |
|
||||
| 3 | Run the ladder; read board power through `xbutil examine --report electrical` (or the F2 shell's sensors) at 1 Hz; take the mean over each run | 1 | reads per second and watts per rung |
|
||||
| 4 | Write the row into `epoch-length.md` 12.2 in place of the ceiling row | 1 | the public claim carries a measured FPGA number |
|
||||
| Gate | reads per second per watt at 1 GiB | | expected 15 to 25 M/s/W (0.3x to 0.5x of the 5090); the alarm line is 27 M/s/W (0.5x); over 54 M/s/W (1.0x) the FPGA lane becomes a Counter ASIC 4.0 item |
|
||||
|
||||
Consequences per tier of the FPGA finding: none today (no FPGA mines); if the measured row holds, a soft-overlay FPGA at USD 4,000 to 5,000 a card (approx) mines at an RX 9070 XT's rate per watt for 7x the price, so no home or rig tier is displaced by it; the per-program bitstream lane stays answered by layer 9.
|
||||
|
||||
### 5.2 Task 2: the reserve R0 to R8
|
||||
|
||||
| Slot | Family | Chip block it adds (approx area in 32-bit adders per lane, `counter-asic-3-reserve.md` s3) | Chip datapath energy per op, N5 floor (pJ, approx) | Honest step cost, Apple / NVIDIA / AMD (measured, ratio to the add-xor-rotate chain) | Verifier cost | Chip edge per joule it moves (model) | Cost per tier |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| R0 | derive (per-day item program) | a sequencer: 60 KB instruction store, 16-register file, ALU with multiplier and rotator; removes the 3x fixed-function credit of the f = 0 chip | n/a (it is the f = 0 chip's item cost: 9,992 ops per item) | hash rate 0 on all three vendors; daily build +7 ms Mac, 0 on the 5090 and 9070 XT; compile +0.75 s Mac, +1.1 s NVRTC, +2.0 s AMD per day (a once-a-day module is a requirement) | dr736 +2.8 ms per warp on the Mac, +5.2 on the box core, over the gate cold; dr368 +0.6 Mac, +0.2 box, 8.16 on the half-core | f = 0 chip: 0.92x to 0.34x to 0.43x per chip; per joule from 1.86x to about 0.7x to 0.9x at the same allowance (approx); f = 1 chips: 0 | pool verifier cores x2.4 at dr736, x1.3 at dr368; every miner tier 0 |
|
||||
| R1 | shfla (lane + delta) | 32-lane x 32-bit crossbar per warp, 32,768 mux bits, 3 to 6 adders per lane | 1.00 | 1.91 / 1.53 / 0.75 to 0.84 | one op per instruction, under 0.01 ms per warp at W_new = 4 | shadow floor +21 percent at 5 percent of instructions (approx); the GPU pays 1.5x the step too, so k unchanged | Apple under 1 percent of ALU time (argued), NVIDIA and AMD 0 |
|
||||
| R2 | perm (byte permute) | 4x4 byte crossbar, 128 mux bits, 1 to 2 adders | 0.10 | 1.13 emulated / 1.30 / 1.73 to 1.93 emulated | negligible | under 1 percent | Apple and AMD emulate at 1.1x to 1.9x per op: under 1 percent at 4 points |
|
||||
| R3 | popc and clz | popcount tree and priority encoder, 1.5 to 3 adders | 0.10 | 0.87 and 1.01 / 1.50 and 1.63 / 0.91 to 1.01 and 1.19 to 1.30 | negligible | under 1 percent | 0 |
|
||||
| R4 | bfe | mask generator on the shifter, 0.3 adders | 0.06 | 0.77 / 1.54 / 1.00 to 1.09 | negligible | 0 | 0 |
|
||||
| R5 | shl, shr | barrel shifter beside the rotator, 0.2 adders | 0.06 | 0.85 and 0.86 / 1.27 and 1.28 / 1.00 to 1.02 and 0.91 to 1.02 | negligible | 0 | 0 |
|
||||
| R6 | sel | 32 muxes, 0.3 adders | 0.03 | 0.76 / 1.32 / 0.90 to 1.01 | negligible | 0 | 0 |
|
||||
| R7 | andn | 32 inverters, under 0.1 adders | 0.02 | 0.75 / 1.26 / 0.91 to 1.01 | negligible | 0 | 0 |
|
||||
| R8 | mm8 | 8x8x16 u8 MAC tile per warp, about 100 adders per lane; licensable IP at every node | 1.60 | owed (Metal 4 matmul2d) / 2.43 / 1.68 to 1.83 native, exactness unverified | 1,024 MACs per instruction per unit, about 10 us per warp at W_new = 4 | shadow floor +37 percent at 5 percent of instructions (approx); the GPU pays 2.4x the step: k unchanged or worse for the GPU | Apple pays the library path (owed); NVIDIA and AMD native |
|
||||
|
||||
The ordering argument. Against the f = 1 chip, which is the chip anyone builds, every family R1 to R8 moves the per-joule edge by under 10 percent, because the chip's cost at class v4 is N x k x 11 pJ and a family changes the per-op energy of 4 points in 79 of the shadow mix. Against the f = 0 chip only R0 matters (it is the item cost). So the reserve's function is not per-joule resistance; it is to make a datapath taped out against class v3's eleven families wrong at the first unlock. A chip maker who reads this document tapes out every block from day one: R1 to R7 together are about 8 to 14 adders per lane (a lane with a 32x32 multiplier is about 35), about 11 mm^2 on a 14,000-lane N5 array, about USD 4 of silicon (sram-mirror.md's USD 0.36 per mm^2, approx); R8 is a licensed tile. The era schedule therefore buys nothing against a chip and the order can be set by honest cost alone: the largest new structure first while every vendor's measured step cost is in (shfla: AMD measured cheap at 0.75 to 0.84, which the reserve document said was the one number that could move it to R1), the emulated families next, mm8 last at era 8 by the rule (no era-4 exception: mm8 removes no adversary class, `epoch-length.md` 12.4). R0 enters at dr368, not dr736: the box proxy puts dr736 over the gate on a cold run (10.5 ms) and at 15.5 ms on the half-core; dr368 reads 4.7 and 8.2.
|
||||
|
||||
Recommended text change to `counter-asic-3-reserve.md` section 5: R1 shfla, R2 perm, R3 popc and clz, R4 to R7 unchanged, R8 mm8 at era 8; R0 at `derive_len = 368` with 736 behind the O-1.14 measurement. The era schedule "family n at era n" stands; what it costs per tier at each unlock is the step-cost table above (Apple under 1 percent of ALU time per family, NVIDIA and AMD nothing measurable), and what it buys is stated honestly in the public text (section 6, proposal 4).
|
||||
|
||||
### 5.3 Task 3: a class v5 from the shadow
|
||||
|
||||
The three options, each priced:
|
||||
|
||||
(i) Shadow ops that depend on the loaded data. The v4 shadow block runs at the end of every iteration on the eight lane registers, and those registers hold the words the iteration's 16 loads XORed in (`generator.rs`: the block is executed after instruction 63 with the iteration's `sel`; `verify.rs` runs it with the same `step`). So every shadow op already consumes loaded data, and the next iteration's load addresses depend on the shadow's outputs. A chip cannot precompute any of it; what it can do is what the GPU does: overlap one lane's shadow with another lane's reads in flight. Option (i) therefore moves k by 0. A variant that draws the shadow's immediates from loaded words is M18's one-bit select at 32 bits: a chip wires the operand, and it is not a defence. Verdict: void; no measurement needed.
|
||||
|
||||
(ii) Shadow work that exercises GPU structures a chip lacks. Bank-conflict timing is not a value and cannot enter a bit-exact function. Register-file width (8 registers today) can be widened in the shadow to 16 or 32 (ProgPoW used 32): a chip's register file per lane in flight grows from 32 B to 128 B, 1.8 MB on a 14,000-lane array, trivial. Warp shuffles are the structure that costs: the xor-mask shuffle needs a 5-stage butterfly per warp (5,120 mux bits), the indexed shuffle a full crossbar (32,768). The chip datapath floor per op (N5, approx: add 0.06 pJ, mul 0.52, butterfly 0.30, crossbar 1.00, mm8 tile 1.60) against the GPU's measured step costs:
|
||||
|
||||
| Shadow op mix | Chip floor per op (pJ, approx) | k floor after the 2x pipeline and 8x register and wire overhead of latency-shadow s6 (approx) | GPU cost of the same mix on the 5090 (step ratios, measured) |
|
||||
|---|---|---|---|
|
||||
| Today's weights (add 32, mul 22, rot 13, shfl 8 of 75) | 0.221 | 0.32 | 1.0x to 1.5x per op |
|
||||
| Shuffle-heavy (shfl 14, shfla 8, the rest scaled) | 0.315 | 0.46 | the shuffles cost the 5090 1.49x to 1.53x the chain step, so its own energy per op rises about 10 percent (approx) |
|
||||
| With mm8 at 8 points | about 0.45 | about 0.65 | mm8 costs the 5090 2.43x the step |
|
||||
|
||||
So a shuffle-heavy shadow raises the attacker's claimed floor from about 0.3 to about 0.5 and costs the GPU about 10 percent more energy per shadow op; it does not reach k = 1. What a chip would need to add: the 32-lane crossbar per warp (R1's structure), nothing else new. The structure argument is bounded: a wide-SIMD array with a crossbar per 32 lanes is still a fixed datapath with no scheduler, and that is where the 0.3 came from.
|
||||
|
||||
(iii) One N for every card. Per-card bind points at the 5 percent rule (measured): M5 Max about 130,000 counted ops (interpolated between 102,100 at -1.5 percent and 150,800 at -7.3), RTX 5090 at its 431 W cap about 210,000 (199,600 at -2.7, 330,700 at -34.7), RTX 4070 at its 160 W cap about 250,000 (199,600 at +0.4, 330,700 at -12.3, approx), RX 9070 XT over 331,000 (holds at every rung; budget about 650,000). The consensus N is the minimum: 130,000, set by the Mac. The unmeasured rented cards by ALU budget (approx, cores x clock): 3060 about 270,000, 4060 about 440,000, 3080 about 360,000; their power caps are unmeasured and on NVIDIA the cap binds before the budget, so these are not pass marks. The ladder at every N the lane can compute:
|
||||
|
||||
| N counted ops | 5090 uJ (rate delta) | M5 Max uJ (delta) | 4070 uJ (delta) | f = 1 GDDR7 chip uJ at k = 0.3 / 0.5 / 1 / 1.5 | Edge over the 5090 | Edge over the M5 Max | Edge over the 4070 | Verifier add per warp: M5 Max / box core / box half-core (ms) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 930 (class v3) | 2.65 | 0.78 | 2.57 | 0.47 / 0.47 / 0.47 / 0.47 | 5.7x | 1.66x | 5.5x | 0 |
|
||||
| 102,100 (class v4) | 3.27 (-0.2%) | 1.39 (-1.5%) | 3.51 (+0.4%) | 0.80 / 1.02 / 1.58 / 2.14 | 4.1x / 3.2x / 2.1x / 1.5x | 1.74x / 1.36x / 0.88x / 0.65x | 4.4x / 3.4x / 2.2x / 1.6x | 0.18 / 0.39 / 0.67 |
|
||||
| 130,000 (the one-N candidate) | 3.27 (-0.3%) | 1.43 (-4.8%) | 3.78 (+0.4%) | 0.89 / 1.18 / 1.89 / 2.60 | 3.7x / 2.8x / 1.7x / 1.3x | 1.61x / 1.22x / 0.76x / 0.55x | 4.2x / 3.2x / 2.0x / 1.5x | 0.23 / 0.49 / 0.85 |
|
||||
| 150,800 | 3.27 (-0.3%) | 1.46 (-7.3%) | 3.98 (+0.4%) | 0.96 / 1.29 / 2.11 / 2.94 | 3.4x / 2.5x / 1.5x / 1.1x | 1.52x / 1.13x / 0.69x / 0.50x | 4.1x / 3.1x / 1.9x / 1.4x | 0.26 / 0.57 / 0.99 |
|
||||
| 199,600 | 3.35 (-2.7%) | 1.58 (-10.5%) | 4.45 (+0.4%) | 1.12 / 1.56 / 2.65 / 3.74 | 3.0x / 2.1x / 1.3x / 0.9x | 1.40x / 1.01x / 0.59x / 0.42x | 4.0x / 2.9x / 1.7x / 1.2x | 0.35 / 0.76 / 1.31 |
|
||||
| 330,700 | 4.99 (-34.7%) | 1.87 (-21.0%) | 5.89 (-12.3%) | 1.55 / 2.28 / 4.09 / 5.91 | 3.2x / 2.2x / 1.2x / 0.8x | 1.21x / 0.82x / 0.46x / 0.32x | 3.8x / 2.6x / 1.4x / 1.0x | 0.58 / 1.26 / 2.18 |
|
||||
|
||||
Per-tier watts at N = 130,000 (measured rungs interpolated): the M5 Max 37 W GPU plus DRAM (from 21), the 5090 431 W (its cap, from 350), the 4070 about 118 W (from 79.5), the 9070 XT owed; a 5090 rig pays about 23 percent more electricity for 0.3 percent less rate, an Apple miner 1.8x the GPU watts for 4.8 percent less rate, a pool user nothing, every verifier tier +0.23 to +0.85 ms per warp. The verifier at 130,000 on the half-core proxy is 8.5 ms (8.23 + 0.85 - 0.67), inside the gate with 1.5 ms spare; the gate does not bind N before the Mac does.
|
||||
|
||||
#### 5.3a The reconciled N ladder (this lane's measured cards, lane 7's HBM4 inputs; `--section ladder`)
|
||||
|
||||
Lane 7 (`docs/analysis/horizon/frontier.md` 2.3, `sim/horizon/frontier/frontier_model.py` model 1.4) adds HBM4: JEDEC JESD270-4 doubles channels per stack (16 to 32, cited), so its one-stack ceiling is taken as 2 x 10.7 = 21.4 G reads/s at 1.0 nJ per read (approximate, unsourced), 5 W static, 10 W controller: 167 MH/s at 0.218 uJ bare. The table below carries that chip beside the three of chip-model-v3 5.4, every chip at N = bare + (N - 930) x 11 pJ x k, and every honest card at its measured rung.
|
||||
|
||||
| N counted ops | 5090 uJ, W (rate delta) | M5 Max uJ, W (delta) | 4070 uJ, W (delta) | 9070 XT rate delta (W owed) | 12 GB and 8 GB rented cards | Chip edge over the 5090 per joule at k = 0.5 / 1 / 1.5: GDDR7 | HBM3 one stack | HBM3 eight stacks | HBM4 one stack | Verifier ms per warp: M5 Max core / 2019-class by the 2.5x rule / measured half-core proxy | Card that binds first (5 percent rule) |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 930 (class v3) | 2.65, 350 W (0) | 0.78, 21 W (0) | 2.57, 80 W (0) | 0 | measured at v3 only: 5070 3.27 uJ, 3060 4.36, 4070 untuned 3.65, 4060 Ti 8 GB 3.81 | 5.7x | 8.3x | 10.1x | 12.2x | 2.06 / 5.2 / 7.56 | none |
|
||||
| 49,700 | 3.21, 425 W (+0.1%) | 1.16, 31 W (-1.2%) | 3.01, 94 W (+0.4%) | +2.1% | not measured at any N | 4.4x / 3.2x / 2.5x | 5.5x / 3.7x / 2.9x | 6.1x / 4.0x / 3.0x | 6.6x / 4.3x / 3.1x | 2.15 / 5.4 / 7.88 | none |
|
||||
| 102,100 (class v4) | 3.27, 431 W (-0.2%) | 1.39, 37 W (-1.5%) | 3.51, 109 W (+0.4%) | +2.0% | not measured | 3.2x / 2.1x / 1.5x | 3.7x / 2.3x / 1.6x | 4.0x / 2.4x / 1.7x | 4.2x / 2.5x / 1.7x | 2.24 / 5.6 / 8.23 | none (M5 Max -1.5%) |
|
||||
| 130,000 | 3.27, 431 W (-0.3%) | 1.43, 37 W (-4.8%) | 3.78, 117 W (+0.4%) | +1.7% | not measured | 2.8x / 1.7x / 1.3x | 3.2x / 1.9x / 1.3x | 3.4x / 1.9x / 1.4x | 3.5x / 2.0x / 1.4x | 2.29 / 5.7 / 8.41 | M5 Max at its 5 percent point |
|
||||
| 150,800 | 3.27, 431 W (-0.3%) | 1.46, 37 W (-7.3%) | 3.98, 124 W (+0.4%) | +1.5% | not measured | 2.5x / 1.5x / 1.1x | 2.9x / 1.7x / 1.2x | 3.0x / 1.7x / 1.2x | 3.1x / 1.8x / 1.2x | 2.32 / 5.8 / 8.55 | M5 Max (-7.3%) |
|
||||
| 199,600 | 3.35, 431 W (-2.7%) | 1.58, 38 W (-10.5%) | 4.45, 138 W (+0.4%) | +1.0% | not measured | 2.1x / 1.3x / 0.9x | 2.4x / 1.3x / 0.9x | 2.5x / 1.4x / 0.9x | 2.6x / 1.4x / 1.0x | 2.41 / 6.0 / 8.87 | M5 Max (-10%), 5090 (-2.7%) |
|
||||
| 330,700 | 4.99, 431 W (-34.7%) | 1.87, 40 W (-21.0%) | 5.89, 160 W (-12.3%) | +3.6% | not measured | 2.2x / 1.2x / 0.8x | 2.3x / 1.3x / 0.9x | 2.4x / 1.3x / 0.9x | 2.5x / 1.3x / 0.9x | 2.64 / 6.6 / 9.74 | 5090 (-35%), M5 Max (-21%), 4070 (-12%) |
|
||||
|
||||
Sources per column: 5090 and M5 Max rungs `latency-shadow-2026-10-06.md` s3 and s5; 4070 and 9070 XT rungs `counter-asic-3-status.md` item 8 (the 9070 XT's watts owed: the ADLX sampler parsed 0 samples); the rented cards `prover-tiers-real-cards.md` (class v3 only); chip bare energies chip-model-v3 5.4 and lane 7 model 1.4; the 11 pJ unit latency-shadow s5; the verifier slopes 3.2 us per 1,000 shadow instructions (Mac, measured) and 12.1 us (box half-core, this lane); the 130,000 row interpolated. Every chip cell is approximate.
|
||||
|
||||
Disagreements with lane 7's model 1.4, named: (a) its honest card at N is the linear 326-to-575 W model of chip-model-v3 5.7 (2.95 uJ at N = 100,000, 3.50 at 200,000, 4.22 at 330,000); the measured 5090 under its 431 W cap reads 3.27, 3.35 and 4.99 uJ with -0.2, -2.7 and -34.7 percent of rate (the cap binds from 102,100 ops; `power.min_limit` is 400 W, so no cap below the one measured exists on a 5090). (b) Its shadow core is 150 W fixed at the 5090's 136 MH/s; on a 167 MH/s HBM4 chip that under-counts the core by 1.23x. Energy per hash is N x 11 pJ x k whatever the chip's rate, so HBM4 at N = 100,000 and k = 1 is 1.33 uJ and the edge 2.5x, not 1.11 uJ and 2.65x; at 200,000 1.4x (lane 7: 1.74x); at 330,000 1.3x (1.33x: agrees, because the 5090's own energy jumps to 4.99 at that rung). (c) Its HBM3 and HBM4 ceilings (10.7 and 21.4 G per stack) rest on the 8-activates-per-12-ns rate chip-model-v3 5.3 carried from O'Connor; the JEDEC HBM2 cycle table (ICCAD 2021 Table I) gives 4 per 28 ns per channel (section 5.1 of this file), and HBM3's own tFAW is behind the paywall. If HBM3 and HBM4 keep HBM2's window the one-stack ceilings are 2.3 and 4.6 G, HBM4's bare energy 0.55 uJ and its bare edge 4.9x, not 11x. The GDDR7 column is the one with a measured anchor (the 5090 reaches 82 percent of its ceiling) and is the column to quote in the summary; the HBM columns are the upper bound.
|
||||
|
||||
Verifier headroom for N (lane 7's "10x of headroom"): on the M5 Max core 10 - 2.33 = 7.67 ms buys 2.4 M shadow instructions, N about 4.5 M ops (19x); on the 2.5x rule 4.2 ms buys 525,000 instructions, N about 1.06 M (10x, lane 7's figure); on the measured half-core proxy 1.77 ms buys 146,000 instructions, N about 370,000 (3.7x). The 10x holds on the rule and not on the pessimistic measured bracket; O-1.14 decides which. Either way the cards bind first: M5 Max 130,000, 5090 at 431 W 210,000, 4070 at 160 W about 250,000, 9070 XT over 331,000. An unconditional doubling of N per era (lane 7's candidate) would take the Apple tier out at the first step (200,000: -10.5 percent) and the 5090 and 4070 at the second (400,000: compute-bound at their caps), which is why the proposal below steps N by signal, not by schedule alone.
|
||||
|
||||
The verdict on v5: the shadow lever is close to spent on the owned cards. Going from 100,000 to 130,000 buys 2.1x to 1.7x against the 5090 at k = 1 and costs the Mac its whole 5 percent allowance; a shuffle-heavy mix buys the k floor 0.3 to 0.5. Together they define one candidate, `v5 = mx8 + sh256x35 with the shuffle-heavy weight table`, worth measuring but not worth a cut on its own: the chip question is k, and no shadow design moves k past about 0.5 against a fixed-datapath array.
|
||||
|
||||
What measurement decides it (not run tonight; the shuffle-heavy weight table does not exist as a knob, and the Mac's miner state was not checked, so the plan stands in for the run): (1) add a shadow weight table to `ShadowClass` (`generator.rs`, 2 hours), draw the block from it, emit it in the three dialects as today; (2) export `mx8+sh256x35` at today's weights and at the shuffle-heavy table for seed igneum-genesis; (3) Mac: `with-lock.sh measure packbench --pack <dir> --batches 60 --batch-log2 24 --group 256` with the IOReport sampler, 2 packs plus the control, about 6 minutes, the miner paused first (`curl -s http://127.0.0.1:60030/.../api/state` from the app's `app.url`, then pause through the app, never from a script); (4) the same ladder as PC jobs on the 5090, 4070 and 9070 XT through the existing `tools/ca3-shadow` playbooks with the card off through the runner's `--cards-off`; (5) `igneum-pow bench` on the Mac core and the box proxy. Pass lines: every owned card within 5 percent of its class v4 rate; bit-exact fingerprints on Metal, CUDA and AMD OpenCL; verifier under 10 ms on the half-core proxy; the 5090's marginal pJ per op on the shuffle-heavy mix read on the three rungs under its cap.
|
||||
|
||||
### 5.4 Task 4: the era draw's randomness
|
||||
|
||||
How it picks. Era n's seed E_n is the 1-hour class-group VDF of `Hash(chain_id || n || blue block hashes of the day before C_era(n))`, where C_era(n) is the highest certified checkpoint at least 7,200 DAA s before the era (spec 04 s4.4). One SplitMix64 stream from E_n draws, in order: the ten op weights perturbed by -2..+2 points each, the output fold rotations, an unused draw for `epoch_len` (set by signal), then under class v3 a second stream draws the load width (pinned at 4 bytes: the draw is consumed), the stride multiplier M (odd) and rotation R, and the four interleave bit positions (spec 01 s1.13.1, `era-layout.md` 1.1). Not drawn: the load count (16), the mixer round count (8) and multiplier (8), the cache and dataset sizes, the item construction.
|
||||
|
||||
What an attacker can bias, priced. Two routes. (a) Forge the certified checkpoint: needs 2/3 of the 30-day blue-block weight, which is 20 days of 100 percent of the network's hash (the headline). Rented at the measured USD 0.0117 per MH/s-hour:
|
||||
|
||||
| Network hash | 20 days of 100 percent, rented | What it buys in the draw |
|
||||
|---|---|---|
|
||||
| 1 GH/s | USD 5,616 | one era's (weights +-2, fold, M, R, pos): a per-card hash-rate spread of 0.8 to 3.2 percent (six eras measured, bench-log "Counter ASIC 2.0, the numbers"), 0 chip effect |
|
||||
| 10 GH/s | USD 56,160 | the same |
|
||||
| 100 GH/s | USD 561,600 | the same; the rental market could not supply 20 pods of any card at 19:00Z on 6 October (bench-log), so a TH/s is not rentable at all |
|
||||
| 1 TH/s | USD 5.6M (not supplied) | the same |
|
||||
|
||||
(b) Re-roll without weight: the miner of the last blue block before C_era(n)'s cut withholds or publishes to change the input set; this costs one block's reward and needs the 3,600-s VDF evaluated inside the 2-s publish window, a 1,800x faster evaluator (spec 04 s4.6: a 300x evaluator beats the epoch's 600 s, not the era's 3,600 s). Even free, one re-roll is one more sample of the same space. So the draw is unbiasable at any price that matters, and that is the honest answer to "what can be biased": nothing worth having.
|
||||
|
||||
Weak corners. The op-weight perturbation can move the multiply share (mul, mad, mulhi: 22 of 75) to 16 or 28 of 75; at the N5 datapath floor that is 0.158 to 0.232 pJ per shadow op (0.195 at the base), about +-20 percent of the shadow's datapath energy, and the GPU's energy moves the same way (its IMAD is the chain's own op). The stride and interleave cost a chip two integer operations per load and an address-line permute, nothing per joule. The fold rotations are a wire mux. The measured six-era spread (1.3 percent on the 5090, 3.2 on the 9070 XT, 0.8 on the M5 Max) is the whole of what the draw moves. There is no corner that makes a chip easier, because no drawn parameter touches the memory bound, the item derivation or N.
|
||||
|
||||
Predictability against the 32-month lead time. Everything a chip needs is public at genesis: the eleven live families, the reserve order R0 to R8 and its era schedule, the mixer shape and x8, the dataset and cache schedule, the class v4 shadow's size and weights. The draw hides (M, R, pos, weights +-2, fold rotations) until 2 hours before each era, and none of those needs silicon: an address decoder that permutes lines, a programmable rotator, an immediate table. So a chip taped out in month 0 against this spec runs every era for the chain's life, and the era draw buys nothing against it. What the draw does buy: the fork-fatigue lesson of the history (no human release, no vote), and a per-program hard-datapath FPGA cannot amortise a bitstream across eras (layer 9 handles the within-era case). Said plainly for the public text: the era draw and the reserve are automatic schedule changes against fixed datapaths and governance, not unpredictability against a chip. What would be unpredictable and costly to a chip is not available in a genesis-fixed rule set: a per-era draw among K reviewed item constructions (K mixers or K derive forms) is still K public blocks a chip carries; a per-era draw of the load count or the dependent-read depth changes the memory bound per era and fails the 5 percent rule on the honest cards (read-width: a per-program width mix spread 5.5 to 22.3 percent). What costs a chip is N (its k), the memory system (f = 1 is the ceiling and is a commodity controller), and the honest card's own watts (the M5 Max at 0.78 uJ is inside 2x of the GDDR7 chip with no shadow at all).
|
||||
|
||||
### 5.5 Task 5: the 2019-class verifier gate
|
||||
|
||||
Measured tonight (section 3.3): on igneum-build-1 one EPYC 9454P core boosted to 3.8 GHz (not a low clock: the governor's cap is 2.75 GHz but the core read 3,800 MHz under load, and `cpupower` needs root) with server DDR5 behind it reads 1.9x to 2.2x the quiet M5 Max core across five classes, and 2.0x on the cache fill. The half-core proxy (both SMT siblings busy on the same class) reads 3.4x to 3.7x the Mac. A 2019 laptop core (Skylake-class at 3.5 to 4.5 GHz with DDR4 at about 80 ns) sits between these brackets on the arithmetic (lower IPC than Zen 4, lower DRAM latency than the server), so the 2.5x rule is about right and the two proxies bracket it. Against the gate:
|
||||
|
||||
| Class | One box core, cold run | Half-core | Verdict at 10 ms | Headroom left for shadow on the half-core (ms) |
|
||||
|---|---|---|---|---|
|
||||
| class v3 (mx8) | 4.67 | 7.56 | pass | 2.4 |
|
||||
| class v4 (mx8+sh256x27) | 5.06 | 8.23 | pass | 1.8 (about 150,000 more shadow instructions at 12.1 us per 1,000) |
|
||||
| dr368 | 5.32 | 8.16 | pass | 1.8 |
|
||||
| dr736 | 10.51 | 15.49 | FAIL on both proxies | none |
|
||||
|
||||
So dr736 is out as a genesis-live or near-term reserve length on measured evidence, not on the 2.5x rule; dr368 is in. The owed O-1.14 measurement on a real 2019 laptop (the US laptop's Windows `igneum-pow` build, main's decision 7) still closes the question; the box proxy is the stand-in until it lands, and the half-core row should be the standing pessimistic rule in place of "2.5x" (it is a measurement; 2.5x is a ratio from memory).
|
||||
|
||||
How to measure it tomorrow: `tools/cross-remote.sh` from `igneum-pow/` builds the Windows exe on the box (1 min 44 s measured for the node; the pow crate is one crate, under a minute), the relay carries it to the US laptop when it appears, `igneum-pow bench --seed igneum-genesis --day 2026-10-03 --class <c> --warps 50` for the five classes, ms per warp into `docs/bench-log.md` under O-1.14; 1 hour of agent work. A rented old CPU on Vast is the fallback (Vast lists CPU-only offers by core generation; a 2019 Xeon or i7 host at under USD 0.10 an hour, approx), same binary, same command.
|
||||
|
||||
What the gate protects, and what loosening it costs:
|
||||
|
||||
| | At 10 ms per warp | At 20 ms (loosened) | At 5 ms (tightened) |
|
||||
|---|---|---|---|
|
||||
| A node on a 2019 laptop at 1 bps | 1 percent of one core per block | 2 percent | 0.5 percent |
|
||||
| At 10 bps (the Devnet 2 experiment) | 10 percent of one core | 20 percent | 5 percent |
|
||||
| IBD over the 108,000-header pruning window, one core | 18 min | 36 min | 9 min |
|
||||
| Header flood (M15 class): invalid headers per second that saturate one core | 100 | 50 | 200 |
|
||||
| A pool core verifying shares | 100 per second | 50 | 200 |
|
||||
| What the lever buys the hash | the x8 mixer, dr368, the v4 shadow all fit with 1.8 ms spare on the half-core | dr736 fits (15.5 on the half-core: no, still out), x16 mixer fits | nothing of class v4 fits on the half-core |
|
||||
|
||||
Loosening to 20 ms would admit the x16 mixer (about 7 ms on the Mac, 15 on the half-core: still out) and not dr736 on the half-core, so it buys little against a chip (the f = 1 chip derives no item) and doubles the header-flood and IBD costs on the weakest node. Keep 10 ms; measure the laptop; use the half-core row until then.
|
||||
|
||||
### 5.6 Task 6: the dataset schedule to 2030
|
||||
|
||||
The schedule (spec 1.13.3 option (b), decided for the cache on 5 October, recommended for the dataset): 2 GiB at genesis, 4 GiB at year 4 (day 1,460), 8 GiB at year 12, 16 GiB at year 28; the cache 256 MiB, 512 MiB, 1 GiB, 2 GiB on the same days (`memhard.rs` `growth_doublings`). The devnet packs run 1 GiB today.
|
||||
|
||||
The installed base against it (Steam September 2026, cited; the trend is approximate):
|
||||
|
||||
| Year (approx calendar) | 8 GB share | 12 GB share | Dataset | Who falls out of mine-only | Who falls out of mine-and-prove (the prover's measured peaks, `prover-tiers-real-cards.md`) |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026 (devnet) | 27% | 13% | 1 GiB | nobody with 4 GB or more | 8 GB: compressed does not fit beside the miner (measured); core-only 2^25 fits with 1 GB spare |
|
||||
| 2027 (genesis, year 0) | about 20% | about 10% | 2 GiB | 4 GB cards hold with 0.5 to 0.8 GB spare (card-lifetime) | 8 GB loses core-only beside the miner (the miner's resident set grows 1 GiB: 7.35 + 1.0 GB over 8.19) and becomes prove-alone; 12 GB holds compressed on headless Linux (10.2 + 1.0 of 12.3) |
|
||||
| 2031 (year 4) | about 0% | about 0% | 4 GiB | 4 GB cards (5 percent of Steam today, about 0 by then) | 12 GB loses compressed (10.2 + 3.0 GB over 12.3), keeps core-only (7.2 + 3.0 of 12.3); 16 GB keeps compressed (9.2 + 3.0 of 16.4) |
|
||||
| 2039 (year 12) | 0 | 0 | 8 GiB | 8 GB cards (26.7 percent of Steam today; the trend says under 1 percent by 2030) | 12 GB loses core-only; 16 GB loses compressed, keeps core-only (7.4 + 7.0 of 16.4); 24 GB keeps compressed (11.0 + 7.0 of 24.6) |
|
||||
| 2055 (year 28) | 0 | 0 | 16 GiB | 12 and 16 GB | 24 GB loses compressed; 32 GB keeps everything |
|
||||
|
||||
Reading per tier: the 8 GB tier, a quarter of Steam today, is never a mine-and-prove card on the measured prover footprint whatever the dataset does, and it mines until year 12, by which time its share on the trend is nil; the 12 GB tier (13 percent, falling 6 points a year) mines to year 28 and loses mine-and-prove compressed at year 4 because the prover peaks at 10.2 GB beside a 1.4 GB miner; the 16 GB tier (27 percent and rising) mines and proves compressed to year 4 and core-only to year 12; 24 and 32 GB are unconstrained to year 28. The dataset is never the binding constraint on any tier before year 12; the prover's 5.6 to 10.7 GB footprint is. On the chip side the schedule changes nothing: one HBM3 stack holds 24 GB and the 5090's own board 32 GB (chip-model-v3 5.7), so f = 1 reads every step of the schedule to year 28 without a second stack.
|
||||
|
||||
What growth is for, then. Two things, both real and neither a chip: (1) the cache above every GPU's on-die cache (96 MB on the 5090, 128 MB on GB202; the spec's own rule), so the honest hash stays DRAM-latency-bound and no consumer GPU gains an L2 shortcut; (2) the dataset above one reticle of SRAM at the node of the day (1.6 GiB per reticle at N5 headline density, 1.9 at N2, sram-mirror.md s4), so an "f = 1 in SRAM" chip, which would read at SRAM latency and beat the DRAM activate ceiling by 10x, stays a multi-reticle part: at 2 GiB that is 2 dies at N2, at 4 GiB 3 dies, at 8 GiB 5 dies (approx, headline density; the lower-bound density halves these). Wafer-scale parts already hold more (a Cerebras WSE-3 carries 44 GB of on-wafer SRAM, approximate, from memory, unpriced here): the schedule does not price that device out and nothing in the hash can, but at the dependent-read pattern a wafer's cross-die hops cost latency that no one has measured for this hash (open, section 7).
|
||||
|
||||
Recommendation with numbers: hold the schedule as decided (2 GiB genesis, doublings at years 4, 12, 28). Do not slow it: slowing buys the 8 GB tier nothing (it mines to year 12 either way) and loses the SRAM-reticle margin (at a flat 2 GiB one N2 reticle holds 1.9 GiB today and about 3.4 GiB by 2036 on the 6 percent a year density trend, sram-mirror s6, so a flat dataset fits one reticle within a decade). Do not grow faster: the only tier a faster schedule costs is the 8 GB tier (year 12 to year 4) and the Apple 8 GB laptop, and it buys nothing against the HBM chip. One change: write the prover footprint, not the dataset, into the public card-lifetime sentence (litepaper line 560: "12 GB or more proves full shards" becomes "12 GB mines and proves on headless Linux until the year-4 dataset step, 16 GB until year 12, 24 GB beyond; 8 GB proves alone"), since that is the number that moves users.
|
||||
|
||||
## 6. Ranked proposals
|
||||
|
||||
| Rank | Proposal | Evidence | Model | Hours | Consequence per tier | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Close O-1.14 with a real 2019 laptop run and adopt the half-core proxy as the standing stand-in; dr736 out, dr368 in as R0's length | box proxy: dr736 10.5 ms cold, 15.5 half-core; v4 5.06 / 8.23 | section 5.5 | 2 (Windows cross build on the box, relay to the laptop, bench, log) | miners 0; pool verifier cores x1.3 at dr368 instead of x2.4; a 2019 node keeps 1.8 ms of headroom under v4 | ms per warp under 10 cold on the laptop for v3, v4, dr368; dr736 recorded as the figure that fails |
|
||||
| 2 | Measure the FPGA lane on AWS F2 (one VU47P, HBM2, USD 1.98 an hour) and replace the 12.2 ceiling row | the measured 2.4 G/s equals the JEDEC tFAW ceiling; the 1.9x row rests on a 12 ns tFAW the JEDEC cycles do not support | section 5.1 | 8 to 10 agent hours plus USD 2 to 8 of F2 time | none today; the public FPGA claim becomes a measured 0.3x to 0.5x per watt | reads per second per watt at 1 GiB; alarm at 27 M/s/W, Counter ASIC 4.0 at 54 |
|
||||
| 3 | Public-text correction: the era draw and the reserve are automatic schedule changes against fixed datapaths and against forks, not unpredictability against a chip; publish the chip's USD per MH/s-hour beside the honest cards' | section 5.4: every drawn parameter needs no silicon; the reserve is about USD 4 of N5 silicon on a chip | sections 5.1, 5.2, 5.4 | 1 | holders and miners read a claim that survives review; nothing on the devnet changes | `docs/evidence.md` row with the two numbers (USD 0.00021 chip, 0.00113 owned 5090, 0.0117 rented) and the draw sentence |
|
||||
| 4 | Reserve order: R1 shfla, R2 perm, R3 popc and clz, R4 to R7 unchanged, R8 mm8 at era 8 by the rule; R0 at dr368 | AMD step cost of shfla measured 0.75 to 0.84 (the one number the reserve document said could move R3); dr736 fails the proxy | section 5.2 | 1 (spec text in `counter-asic-3-reserve.md` s5 and s6) | Apple pays shfla's 1.91x per op first, under 1 percent of ALU time at 4 points (argued, measured at the unlock rehearsal); NVIDIA and AMD 0 | the family-live 5 percent run per vendor at each unlock rehearsal |
|
||||
| 5 | **N grows by the era draw at genesis, inside a verifier-bounded ladder, each step taken by 90 percent miner signal.** The shadow size N becomes a genesis ladder indexed per era, {100,000, 130,000, 200,000, 330,000, 650,000, 1,000,000} counted ops (the measured rungs, then doublings), floor 100,000 and ceiling 1,000,000 fixed at genesis (the ceiling is the 10x verifier headroom on the 2.5x rule; 370,000 on the half-core proxy until O-1.14 lands, which then sets it), the era stream consuming one draw for it as it does for `epoch_len`, and the step up or down set by 90 percent of blue blocks over 7 days at a day boundary (spec 5.7's mechanism, P2's signalling code), never unconditionally | lane 7: HBM4 doubles the f = 1 chip's rate per stack, so the bare edge rises 5.7x to 12x (upper bound) and only N answers it; this lane: the chip's edge over the 5090 at k = 1 falls 2.1x (100,000) to 1.7x (130,000) to 1.3x (200,000 and 330,000); an unconditional doubling takes the M5 Max out at the first step | section 5.3a; `--section ladder` | 6 to 8 (the ladder field in `ShadowClass` and the era stream, the signal rule shared with `epoch_len`, a fast-time run across one step, packs and vectors per rung) | At each step, measured: 100,000 to 130,000 costs the M5 Max 3.3 points of rate and 0 W more, the 5090 and 4070 nothing, the 9070 XT nothing, every verifier +0.05 ms; 130,000 to 200,000 costs the M5 Max 6 more points and the 5090 2.7 at its cap, the 4070 +21 W, every verifier +0.12 ms (Mac) to +0.5 (half-core); 200,000 to 330,000 is compute-bound on every NVIDIA card at its cap (5090 -35 percent) and is a step the signal would refuse until cards change; a pool user nothing at any step; a chip's shadow core grows with N at k x 11 pJ per op | ONE gate per step, published before the project recommends the signal: at the step's N, every card of the public benchmark set (the four owned plus the eleven rented models) within 5 percent of its rate at the previous step, bit-exact fingerprints on Metal, CUDA and AMD OpenCL, and the verifier under 10 ms per warp cold on the O-1.14 core (the half-core proxy until then) |
|
||||
| 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 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 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.
|
||||
|
||||
3. The honest statement about the draw and the reserve is owed before the public testnet. The project has said the era draw and the family reserve are "automatic anti-ASIC escalators"; against the chip that the model says anyone would build they escalate nothing, because every parameter they move is firmware or a USD 4 block. They are good design against forks and against a hard-datapath FPGA, and that is what the text should say. The chip's USD per MH/s-hour (56x under rental, 5.4x under an owned 5090) belongs beside it, because it is the number a miner will compute on the day a chip appears.
|
||||
|
||||
4. The reserve order changes only where the measurements moved: shfla's AMD cost came in cheap, so the largest structure goes first; dr736 failed the proxy, so R0 is dr368. mm8 keeps no exception because `epoch-length.md` 12.4 showed it removes no adversary class.
|
||||
|
||||
5. N as a genesis ladder is the one structural change this lane proposes, and it is lane 7's idea with the cards' measured bind points written into it. The memory generation it answers (HBM4, 2028) arrives on a two-to-three-year cadence; the chain must answer without a release, which the era stream and the `epoch_len` signal rule already provide the shape for. What the measured rungs add: the ladder's steps are the cards' own bind points, the step is taken by the miners who pay for it, and the ceiling is the verifier's measured core, not a 10x from a ratio. An unconditional doubling per era would retire the Apple tier at era 1 and every capped NVIDIA card at era 2, so the schedule alone is not the proposal; the schedule plus the signal is.
|
||||
|
||||
5a. v5 as a class (`mx8 + sh256x35` with a shuffle-heavy mix) is measurable in an evening and is not a cut. Its honest ceiling is the Mac's 5 percent and a k floor of about 0.5; it buys 2.1x to 1.7x against the 5090 at k = 1. The design item that decides more than v5 is k itself (proposal 8).
|
||||
|
||||
6. The dataset schedule is right as decided and the public sentence about cards is wrong in kind: it talks about the dataset when the prover is what ends a tier's mine-and-prove life.
|
||||
|
||||
7. The one lever that moves every chip row and costs no consensus change is the honest card's watts. The fleet showed the untuned mid-tier at 1.5 to 2.3x the 5090's energy per hash; Ember's measured tune on the 4070 took 30 percent off. Shipping it as the default is a miner-app change with a measured gate.
|
||||
|
||||
8. k is the whole chip question at class v4 and nobody in the project can measure it; the external brief can estimate it.
|
||||
|
||||
## 7. Open questions and what could not be run
|
||||
|
||||
| Item | Why not | What would close it |
|
||||
|---|---|---|
|
||||
| The v5 Mac packbench ladder | the shuffle-heavy shadow weight table does not exist as a knob (the block uses the program's weights), and the Mac's miner state was not checked; a run without the knob would have measured v4 again | proposal 5, 4 hours of code then the 6-minute measure session |
|
||||
| A fixed low clock on the box | `cpupower` needs root; the core boosted to 3.8 GHz under schedutil | the laptop measurement (proposal 1); the half-core row is the pessimistic stand-in |
|
||||
| The 9070 XT watts at class v4 and its per-joule row | the AMD watts job failed on 6 October and the re-run waits on the runner's `--cards-off` (status 6a) | the next cut's PC 1 job |
|
||||
| The RTX 4060's watts (logged 0.0 W on the rented box) | the sampler read nothing on that host | one re-rent |
|
||||
| HBM2 tFAW and half-bank count on the U55C and F2 parts | behind the JEDEC paywall; the ICCAD table is a simulator's configuration (DRAMSim3), not a datasheet, and its clock interpretation (1,066 MHz) is mine | the F2 hour (proposal 2) |
|
||||
| A wafer-scale SRAM dataset holder (Cerebras-class, 44 GB on-wafer, approximate) | not priced anywhere in the project; its cross-die hop latency on a dependent-read chain is unmeasured | a Counter ASIC 4.0 analysis item, not a lane 2 item |
|
||||
| The Steam trend to 2030 | linear extrapolation of two points per tier | the survey itself, yearly |
|
||||
| The era draw's cryptanalysis (the stride bijection, the ROT weak-key draw) | out of scope here and still open in spec 1.8.4 and era-layout s8 | the item 3 brief |
|
||||
| `block-rate-devnet2.md` RUN_A and RUN_B | placeholders at 21:30 UK | nothing in this lane depends on them |
|
||||
|
||||
## 8. Summary for the coordinator
|
||||
|
||||
Lane 2 refined the shipped hash and its classes on the 6 October numbers and one new measurement. The chip model's answer does not change in kind: the stored-dataset chip is the chip, it reads 5.7x per joule against the 5090 bench row and 1.7x against the M5 Max at class v3, 2.1x and 0.9x at class v4 and k = 1, and it undercuts rented hash 56x and an owned 5090 5.4x per MH/s-hour; the FPGA lane tightens to 0.30x to 0.47x per watt because the measured random-read rate of an HBM2 FPGA is the JEDEC activate ceiling and not a mapping artefact, and AWS F2 can measure it for USD 2 an hour. The reserve and the era draw buy nothing against a chip (every drawn parameter is firmware; every reserve block is about USD 4 of silicon) and the public text should say what they do buy. The verifier gate got its first measured proxies: class v4 passes a 2022 server core (5.06 ms cold) and the same core with its SMT sibling busy (8.23 ms); dr736 fails both (10.5 and 15.5 ms), so R0 is dr368. The dataset schedule holds; the tier constraint to 2030 is the prover's footprint, not the dataset.
|
||||
|
||||
1. Class v4 verifier on the box proxy 4.90 ms steady, 5.06 cold, 8.23 on the half-core; dr736 9.76 / 10.51 / 15.49: dr736 is out on measurement, class v4 keeps 1.8 ms under the gate on the pessimistic bracket (section 5.5; `sim/horizon/algorithm/model.py --section verifier`).
|
||||
2. The f = 1 GDDR7 chip's edge per joule: 5.7x (5090 bench), 1.7x (M5 Max) at v3; 2.1x and 0.9x at v4 with k = 1; 4.1x and 1.7x at k = 0.3; against the untuned rented mid-tier 8x to 13x at v3; USD 0.00021 per MH/s-hour against 0.0117 rented (section 5.1; `--section chip`).
|
||||
3. The HBM2 FPGA soft overlay: 2.3 to 2.9 G reads/s per 2-stack card by tFAW and tRRD (measured 2.4), 0.30x to 0.47x of the 5090 per watt; the 1.9x ceiling needs a tFAW of 12 ns that the JEDEC HBM2 table (28 ns) does not give (section 5.1; `--section fpga`).
|
||||
328
docs/analysis/horizon/consensus-security.md
Normal file
328
docs/analysis/horizon/consensus-security.md
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
# Horizon lane 1: consensus security. What a hash majority buys on Igneum, attack by attack, with the bound and the price
|
||||
|
||||
Date: 6 October 2026, evening UK (written 19:40 to 21:30 UTC). Lane: consensus-security. Worktree: `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon` at `3f4f719`). Companion paper: `docs/analysis/51-percent.md`. Models and runs: `sim/horizon/consensus-security/` (README at the end of this file, section 9).
|
||||
|
||||
What was read before modelling: `docs/spec/02-consensus.md`, `03-finality.md`, `04-seeds-and-vdf.md`, `06-open-items.md`, `07-execution.md`, `08-client-security.md`; `docs/fud-ledger.md` F1 to F25, M14, M15, M23, M24, P7, P9, P11, P12, X18 to X20, G8, G12, G13, C4, D6, E16; `docs/analysis/difficulty-2026-10-03.md` (section 11), `difficulty-2026-10-04-oscillation.md`, `sim/difficulty/attacks/README.md` (the seven attacked ways); `sim/results_v2.md` A to M; `docs/benchmarks/finality-v3-2026-10-04/*.md`, `docs/benchmarks/round4-consensus-2026-10-04/results-final2.md`; `tools/finality-attacks/README.md` and `lib/net.mjs`, `tools/harness/README.md` and scenarios, `tools/exec-attacks/README.md`; `docs/review/redteam-2026-10-04.md`, `docs/review/round-4-reddit-2026-10-06.md`; `docs/plans/counter-asic-3-node.md` section 6 (P2); `docs/analysis/security-budget.md`; `docs/bench-log.md` "Rental cost of hash, 6 October 2026"; `vendor/igneum-node/consensus/src/processes/ghostdag/protocol.rs` (main checkout); `infra/fast-time/README.md` and `override-60x.json`; CLAUDE.md's 6 October rules. Two facts from main during the lane (19:4xZ, the first later corrected): the live devnet's finality has been paused since 18:39:40Z (lane 3 confirmed the cause at c3aa502: a 42.7 percent departure held by the frozen table, not the hub outage); and run A on igneum-devnet-2 at 10 blocks/s in a star of 41 miners through one seed gave 12.4 blocks/s, 77 percent red blocks, 321 tips, max reorg 55, with the controller lowering difficulty on the low blue rate (cite as "main, 6 Oct 2026 19:4xZ, block-rate-devnet2.md run A" until the file carries the rows).
|
||||
|
||||
Price basis for every cost: USD 11.7 per GH/s-hour, measured on RunPod community pods on 6 October 2026 (`docs/bench-log.md`, "Rental cost of hash": 1,748 MH/s for USD 20.44 an hour; approximate above 2 GH/s because the market supplied no more; the live devnet was 1.16 GH/s). Costs are given per network size at 1, 10, 100 GH/s and 1 TH/s. Writing rules: no em dashes, numbers in tables, every figure labelled measured, simulated, cited or approximate.
|
||||
|
||||
## 1. Summary for the coordinator
|
||||
|
||||
A hash majority on Igneum buys the ordering race inside the lock latency and nothing a certificate covers, and that is measured here, not asserted. Three findings lead.
|
||||
|
||||
1. **The lock does the work the k-cluster cannot.** The DAG simulator (`ghostdag_sim.py`, GHOSTDAG as `protocol.rs` runs it) shows a 45 to 51 percent withholder wins the selected-chain race over a 90-second hold 70 to 85 percent of the time, reorganising 32 to 46 chain blocks at 1 block/s; a 34 percent withholder wins only inside 30 to 60 s (40 percent of attempts) and never at 90 s; a 20 percent one only the last k = 18 blocks (5 to 15 percent of attempts). Finality's lock lands about 63 to 93 s after a checkpoint block (spec 03 C1, 3.11.3), so the window a majority can reorder is the lock latency, measured at 90 to 120 s, not a block count. Below two thirds of weight, no hash share reaches past a certificate (spec 03 3.11.2, `sim/results_v2.md` H, I, L3, M5: 0 conflicts under 1/3 in every seed).
|
||||
2. **The pause is the residual, and it is cheap to buy and free to hold.** Reaching the veto (1/3 of 30-day weight) at 51 percent of blocks takes 20 days and costs USD 6k at 1 GH/s, USD 5.8M at 1 TH/s in rent (`cost_model.py`), of which the attacker earns 51 percent back as subsidy; once held, silence costs nothing (the silent key keeps mining and earning) and pauses finality for as long as it likes (`finality_horizon.py` S: at 34 to 90 percent silent, 0 locks for the whole silence, 0 conflicts). During a pause the chain is proof of work with a 12-hour depth, and a 12-hour 51 percent double spend costs USD 146 at 1 GH/s and USD 146k at 1 TH/s. Tonight's pause is the departure case, confirmed by lane 3 (`docs/analysis/horizon/finality-and-weight.md` 3.1): 20 keys holding 42.7 percent of the frozen table stopped mining in three minutes, the signing weight fell to 53.1 percent at checkpoint 6843, the frozen table (Q5) holds the pause for a window (2 h on the devnet, 30 days on mainnet) where rule v2 would have locked after 35 minutes; certificates formed while the hub was down, so the topology hypothesis is refuted. The signed departure (LEAVE, lane 3's rank 1) is the fix.
|
||||
3. **The proving pool is capturable by any block producer today, in proportion to its hash and up to most of it.** Consensus checks a proof record's statement against native execution and its signature, and NOT the proof (spec 07 7.7 item 4, 7.8 item 8); the first valid record carried pays. A producer that writes a correct statement with random proof bytes into its own block is paid the shard; at 51 percent of blocks it takes at least 51 percent of the 20 percent pool (11,636 IGN an hour at full subsidy) and, because its fake lands in its next block while honest records need 9 to 11 s of proving, most of the shards outside the 10-s exclusive window. This is ledger P21 priced: the only line in the table where a hash majority earns more than it spends. The fix is proof verification in consensus (section 6, rank 1).
|
||||
|
||||
The ranked proposals are in section 6. Two cost nothing in liveness and close whole classes: proof verification in consensus (rank 1) and weight-gated deep fork choice (rank 2: a chain forked deeper than D seconds is a candidate only if the keys that built it hold a third of the weight table at the fork, so rented hash cannot reorg past D even during a pause). Two cost liveness and are not recommended as asked: prover attestations as a second finality leg, and any rule that re-enables locks under the frozen table after an abrupt departure, because a view cannot tell a departure from a partition.
|
||||
|
||||
## 2. Method
|
||||
|
||||
| Model | File | What it does | Machine, lock, seeds |
|
||||
|---|---|---|---|
|
||||
| GHOSTDAG withholding | `sim/horizon/consensus-security/ghostdag_sim.py` | Abstract DAG: Poisson arrivals at 1 and 10 blocks/s, 8 equal honest miners publishing at once, one uniform one-way delay d, one attacker at share H withholding (hold T then release; or selfish: release when about to lose or at lead 6), GHOSTDAG coloring and selected parent exactly as `protocol.rs` (k-cluster with `blues_anticone_sizes`, topological mergeset, blue work), 10 parents. Reports, from honest miner 0: reorg depth in chain blocks against the chain it held at release, fork age, whether the private tip became the chain, attacker blue share, honest blocks turned red | Mac, `with-lock.sh run nice -n 19`, 20 seeds per cell; 1 bps k 18 (`ghostdag_results_1bps.md`, 11 s), 10 bps k 124 (`ghostdag_results_10bps.md`, 223 s); the star check inline (section 4.3) |
|
||||
| Finality sweep | `sim/horizon/consensus-security/finality_horizon.py` | Imports `sim/finality_v2.py` unchanged (1,000 Pareto keys, 3 regions, 2-s delay, 2.2 percent outage, no DAG, perfect retarget, keys free); adds six sweeps over the adversary's share 20, 34, 51, 67, 90 percent: renter, silent set, bought keys, poisoned eclipse, partition with an equivocator under v2 and v3, abrupt departure under v2 and v3 | Mac, run lock, seeds 7 and 11; `finality_horizon_results.md` |
|
||||
| Signalling game | `sim/horizon/consensus-security/signalling.py` | Arithmetic on P2 (95 percent of a one-day blue-block window, floor height): binomial noise, holdout cost, forced-flip cost, signal-then-defect | Mac, instant; `signalling_results.md` |
|
||||
| Cost table | `sim/horizon/consensus-security/cost_model.py` | Every attack's rented hash A/(1 - A) x N, its duration from the spec's arithmetic, its cost at 1 to 1,000 GH/s, what it earns at the three IGN price inputs of `security-budget.md`, and the equilibrium network where rent equals subsidy | Mac, instant; `cost_results.md` |
|
||||
| Node harness | `tools/finality-attacks/run.mjs s6 --fast-time` against the 0.3.14 Mac binary (`vendor/igneum-node/target-0314/release`, built 6 Oct 17:56), rule v3 forced on, SCALE 0.4, ports 29800+, suffix 980, `/tmp/igneum-horizon-fin` | The 3/3 and 4/2 partitions on a real DAG | Mac, run lock; result in section 4.5 (or the recorded runs if the node refused the file) |
|
||||
|
||||
Where a number is from a recorded run and not re-run tonight it is cited by file. Nothing live was touched; no tracked file outside this lane's three paths was edited.
|
||||
|
||||
## 3. Evidence: the measured and simulated numbers
|
||||
|
||||
### 3.1 Ordering: what a withholder does to the selected chain (simulated, `ghostdag_results_1bps.md` section 2, 20 seeds, d = 0.67 s = the cloud devnet's p99 propagation, ledger F7)
|
||||
|
||||
| attacker share H | hold 30 s | hold 60 s | hold 90 s | hold 120 s |
|
||||
|---|---|---|---|---|
|
||||
| 20% | won 10%, reorg med 0 / max 18, att blue 65% | 0%, 0 / 1, 38% | 0%, 0 / 1, 21% | 0%, 0 / 1, 18% |
|
||||
| 34% | won 40%, 0 / 18, 77% | 40%, 0 / 36, 55% | 5%, 0 / 39, 35% | 10%, 0 / 52, 28% |
|
||||
| 45% | won 70%, 10 / 17, 94% | 75%, 22 / 32, 84% | 70%, 33 / 46, 80% | 70%, 46 / 57, 80% |
|
||||
| 51% | won 85%, 10 / 15, 90% | 85%, 20 / 28, 88% | 80%, 32 / 44, 86% | 80%, 42 / 53, 84% |
|
||||
| 67% | won 100%, 8 / 13, 98% | 100%, 16 / 23, 98% | 100%, 26 / 31, 99% | 100%, 34 / 41, 99% |
|
||||
| 90% | won 100%, 2 / 4, 98% | 100%, 4 / 9, 99% | 100%, 6 / 13, 99% | 100%, 9 / 17, 99% |
|
||||
|
||||
"won" = the private tip became honest miner 0's selected chain; "reorg" = chain blocks removed from the chain it held at release (median / max over seeds); "att blue" = the attacker's blue blocks over its blocks in the honest view (its weight and subsidy kept). The fork age when it wins equals the hold (30 to 122 s). At d = 5 s (Kaspa's design bound) the same shares win more often at short holds (34% wins 100% at 30 s) and the same at long ones. Natural reorg depth with no attacker: p99 2, max 2 to 3 at d 0.35 to 0.67 s (the cloud devnet measured p99 3, max 5 over 37,113 removals, ledger F7); p99 15 at d = 5 s.
|
||||
|
||||
The arithmetic behind the table: the attacker's private chain merges honest blocks as blue until its first private block has k honest blues in its anticone, then every later honest block is red in that chain. So the private tip's blue work is R + min(k, N_h) against the honest tip's N_h, with R = H lambda T and N_h = (1 - H) lambda T: it wins whenever R + k > N_h, that is for every T when H >= 1/2 and for T < k H / ((1 - 2H) lambda) below it (6 s at 20%, 56 s at 34%, 180 s at 45%, with Poisson noise around each). The honest blocks it turns red (hon red: 25 to 46 percent of honest blocks at 45 to 51 percent and 60 to 120 s holds; 4 to 14 percent at 34 percent) pay their 80 percent to the attacker when its chain wins (spec 02 2.5, the red rule), so a sustained withholder at or above 45 percent takes honest subsidy and raises its weight share; at 20 to 34 percent its own blocks go red and it loses both.
|
||||
|
||||
Weight share a repeating withholder settles at, derived from the 60-s rows (the longest hold that beats the lock on every checkpoint, section 5.1), approximate: w = H x attblue / (H x attblue + (1 - H)(1 - honred)).
|
||||
|
||||
| H | 20% | 34% | 45% | 51% | 67% | 90% |
|
||||
|---|---|---|---|---|---|---|
|
||||
| weight share under sustained 60-s withholding | 9% | 25% | 48% | 56% | 77% | 95% |
|
||||
| blue rate the difficulty controller reads (share of true) | 86% | 76% | 79% | 80% | 86% | 94% |
|
||||
|
||||
So 51 percent of hash reaches about 56 percent of weight by red-flooding the honest side, still 11 points under two thirds; the honest miners lose about a quarter of their subsidy for as long as it lasts, and the chain reorganises every minute, in public.
|
||||
|
||||
### 3.2 Ordering at 10 blocks/s (simulated, `ghostdag_results_10bps.md`, k = 124)
|
||||
|
||||
| measure | d = 0.35 s | d = 0.67 s | d = 2 s |
|
||||
|---|---|---|---|
|
||||
| natural reorg depth p99 / max, no attacker | 11 / 15 | 22 / 26 | 99 / 109 |
|
||||
| 51% hold 30 s: won, reorg med / max | - | 100%, 50 / 58 | - |
|
||||
| 51% hold 90 s | - | 100%, 146 / 163 | - |
|
||||
| 34% hold 30 s / 60 s | - | 95% (59 / 64) / 0% | - |
|
||||
| 20% hold 10 s / 30 s | - | 45% (10 / 33) / 0% | - |
|
||||
|
||||
At 10 blocks/s the same shares reorganise ten times the chain blocks in the same seconds, and the natural reorg depth at a 2-s delay (99) is above the mainnet checkpoint depth d = 60. C1's rule that d scales with the rate (d = 60 B, ledger F7 round 2) is load-bearing; at 10 blocks/s with d = 600 the determination sits 60 s behind the checkpoint as at 1 block/s.
|
||||
|
||||
### 3.3 The star: run A reproduced (simulated inline, section 4.3 code; 10 bps, k 124, 8 miners, honest only, 120 s)
|
||||
|
||||
| uniform relay delay d, s | blocks in flight | honest blocks red | natural reorg p99 / max at miner 0 | reading |
|
||||
|---|---|---|---|---|
|
||||
| 0.67 | 7 | 0% | 19 / 23 | healthy |
|
||||
| 2 | 20 | 0% | 78 / 99 | reorgs grow with d |
|
||||
| 5 | 50 | 0% | 8 / 133 | still under k |
|
||||
| 10 | 100 | 31% | 0 / 0 | past k/lambda: miners stop switching, each on its own chain |
|
||||
| 15 | 150 | 50% | 0 / 0 | |
|
||||
| 20 | 200 | 60% | 0 / 0 | |
|
||||
| 30 | 300 | 67% | 0 / 0 | run A's 77% red sits beyond this row |
|
||||
|
||||
Once the effective delay passes k / lambda (12.4 s at k 124 and 10 blocks/s), the DAG stops converging: every miner's own tip is heaviest in its own view, red climbs to two thirds, and the "reorg 0" rows are the absence of consensus, not its presence (run A's 321 tips). Through one hub relaying 12 blocks/s to 41 peers the effective delay is the hub's validation and relay time, which the fleet measures and this lane does not; the model says 77 percent red needs 30 s or more of it, approximate. The controller then reads the blue rate as a third of the true rate and eases, which widens the DAG further (section 4.2 attack 8). Lane 5 owns the fix; the attack bound is in 4.2.
|
||||
|
||||
### 3.4 Finality weight over the adversary's share (simulated, `finality_horizon_results.md`, seeds 7 and 11; the table is inserted in section 3.5 from the run)
|
||||
|
||||
### 3.5 Finality sweep results
|
||||
|
||||
Condensed from `finality_horizon_results.md` (seeds 7 and 11; the full tables are there). A = the adversary's share.
|
||||
|
||||
| sweep | 20% | 34% | 51% | 67% | 90% |
|
||||
|---|---|---|---|---|---|
|
||||
| R renter, signing: day it reaches 1/3 / 2/3 (sim; formula 10/A, 20/A) | never / never | 30.0 / never | 20.0 / never | 15.0 / 30.0 | 12.0 / 23.0 (formula 11.1 / 22.2; the dust effect of B) |
|
||||
| S silent set keeps mining, 6 h: locks while silent, first lock after resume | 100%, 0 min | 0%, 0 min (720 stalled) | 0%, 0 min | 0%, 0 min | 0%, 0 min; 0 conflicts in every row |
|
||||
| K bought keys worth A, buyer mines 30%: veto held (days) / stalls if silent (of 86,400) | never / 816 to 1,045 | day 1 to 6 / 36k to 42k | day 1 to 24 / 74k to 76k | day 1 to 26 / 79k to 80k | day 1 to 27 / 82k; share at day 30 is 30% in every row; 0 conflicts |
|
||||
| E poisoned eclipse, 20% pool, 2 h: conflicting locks (eclipsed side holds 20% + A) | 0 (40%) | 0 (54%) | 67 to 70 from minute 2 (71%) | 30 to 32 from minute 4 (87%) | not run: the attacker alone is over 2/3 |
|
||||
| P 50/50 partition with an equivocator, 150 min, v2 and v3: conflicting locks, first at | 0 | 21 to 70, minute 14 to 78 (the knife edge) | 299 to 300, minute 0 | 301, minute 0 | 293 to 300, minute 0; every pre-heal lock kept, 0 post-heal stalls, v3 = v2 in every cell |
|
||||
| C abrupt departure (stops mining and signing): first lock, days, v2 / v3 (analytic v2 30(1 - 1/(3A))) | 0.00 / 0.00 | 0.8 / 30.0 (0.6) | 10.5 / 30.0 (10.4) | 15.2 / 30.0 (15.1) | 18 to 19 / 30.0 (18.9); 0 conflicts |
|
||||
|
||||
Readings. R: the formula holds to 0.1 day; 51 percent never reaches two thirds. S: from one third upward the pause is exactly the silence, free to the silent set. K: bought weight is worth its blocks and decays; a silent 51 percent buyer pauses finality for 24 days then loses the veto. E: an eclipse cannot produce a conflict below the one-third equivocator bound whatever the pool; above it the conflict is the equivocator's, not the eclipse's. P: the bound is one third in every view under both rules, as 3.11.2 says; at 34 percent the model's 2.2 percent outage makes it intermittent (21 to 70 of 300 indices). C: under v2 the pause after a departure ends when the survivors fill two thirds of the sliding table; under v3 (the live rule since 135,200) on day 30 whatever the share; on the devnet's 2-hour window those days are minutes: 2 h after the last lock under v3, which for tonight's 18:39:40Z lock is about 20:40Z (approximate, if the departed boxes hold over a third of the frozen table and do not return).
|
||||
|
||||
### 3.6 Signalling (arithmetic, `signalling_results.md`)
|
||||
|
||||
| fact | value |
|
||||
|---|---|
|
||||
| noise on a one-day window share at p = 0.95 | 0.07 points; a 94.5% fleet never flips, a 95.1% fleet flips on day one (P 0.91) |
|
||||
| 6% holdout rent per day at 1 / 10 / 100 / 1,000 GH/s | USD 18 / 179 / 1,792 / 17,923 (it earns 6% of the subsidy meanwhile) |
|
||||
| forced flip, 95% of one day's blue blocks (19 N for 24 h) | USD 5,335 at 1 GH/s, 53k at 10, 534k at 100, 5.3M at 1 TH/s; the market could not supply a TH/s on 6 Oct |
|
||||
| signal then defect | the defector's blocks fail PoW under the new program and are refused; cost falls on it alone |
|
||||
|
||||
### 3.7 Cost of every attack (arithmetic, `cost_results.md`, excerpt; the full table has 12 rows x 4 network sizes)
|
||||
|
||||
| attack | share, duration | rent at 1 GH/s | at 100 GH/s | at 1 TH/s | subsidy earned meanwhile (IGN) |
|
||||
|---|---|---|---|---|---|
|
||||
| win the lock-latency race (90 s) | 51%, 90 s | USD 0.3 | USD 30 | USD 304 | 1k |
|
||||
| 12-h double spend during a pause or the first 30 days | 51%, 12 h | USD 146 | USD 15k | USD 146k | 559k |
|
||||
| orphan an hour beyond merge depth during a pause | 51%, 1.5 h | USD 18 | USD 2k | USD 18k | 70k |
|
||||
| the veto, 1/3 of weight | 51%, 20 d | USD 6k | USD 585k | USD 5.8M | 22M |
|
||||
| lock alone, 2/3 of weight | 67%, 30 d | USD 17k | USD 1.7M | USD 17.1M | 44M |
|
||||
| long-range private DAG over the window (cold start) | 51%, 30 d | USD 9k | USD 877k | USD 8.8M | 34M |
|
||||
| hold a pause once the veto is held | 0 marginal | 0 | 0 | 0 | keeps earning |
|
||||
| take the hands or the 8 aggregators down | 0 hash | DoS cost only | | | |
|
||||
| fake proof records as a block producer | 0 extra hash | 0 | 0 | 0 | up to the whole 20% pool |
|
||||
|
||||
At the rental-market equilibrium (hash joins until rent equals subsidy: 39, 156 and 780 GH/s at USD 0.005, 0.02 and 0.10) the veto nets about 48 percent of 20 days of the chain's subsidy and locking alone about 33 percent of 30 days; the 12-hour pause-time double spend costs about 12.5 hours of subsidy.
|
||||
|
||||
## 4. The attacks, by layer: what each achieves, the defence, the bound, the price
|
||||
|
||||
The hash shares H run 20, 34, 51, 67, 90 percent in every table; "rent" is A/(1 - A) x N x hours x USD 11.7 and the four network sizes are 1, 10, 100, 1,000 GH/s.
|
||||
|
||||
### 4.1 GHOSTDAG ordering
|
||||
|
||||
| attack | what it achieves (sim) | defence | bound | rent (1 GH/s to 1 TH/s) | earns |
|
||||
|---|---|---|---|---|---|
|
||||
| Selfish mining on the DAG (release when about to lose, lead 6) | blue share equals hash share within 0.3 points at every H (`ghostdag_results_1bps.md` section 3): honest blocks are merged, not orphaned, so there is no relative gain; max reorg 1 to 3 chain blocks | GHOSTDAG merges parallel blocks; a red block pays the merger | 0 gain; the attacker's reds are its loss | 0 extra | nothing |
|
||||
| Withholding to reorder (double spend) | 20%: the last k blocks, 5 to 15% of attempts; 34%: 30 to 60 s, 40%; 45 to 51%: the whole hold, 70 to 85%, 32 to 46 chain blocks at 90 s; 67%+: every attempt | the lock: a candidate tip must pass through every certified checkpoint (spec 03 F1); the checkpoint block locks 63 to 93 s after it is mined | reorg depth = the lock latency, 90 to 120 s (spec 3.11.3), whatever H under 2/3 of weight; credit on the lock only (P17's four states) | 90 s at 51%: USD 0.3 / 3 / 30 / 304 | a deposit credited BEFORE the lock, which no conforming wallet does |
|
||||
| Sustained red-flooding of honest blocks (repeat 60-s withholds) | 45 to 51%: honest blocks 25 to 28% red, honest subsidy to the attacker, weight share 48 to 56%, chain reorganising every minute | the lock bounds each hold to under 63 s (the first checkpoint inside the hold locks by then); W2 counts blues | weight ceiling about 56% at 51% of hash, 77% at 67% (approx., 3.1): a 51% miner never reaches 2/3 | the ordinary cost of 51% | about a quarter of honest subsidy while it lasts |
|
||||
| Balance attack (keep two honest halves balanced) | needs network control, not hash: harness s3 and the redteam show a partition under merge depth heals to one chain (`redteam-2026-10-04.md` rows 2, 3) | merge depth 3,600 s merges the sides; beyond it, blue work and finality decide | one chain within merge depth; beyond it the 3.7 item 9 fork (section 4.3 finality) | 0 hash | nothing without a partition tool |
|
||||
| k-cluster poisoning (make honest blocks red) | the same as red-flooding: only a withholder can be in an honest block's anticone without being in its past; share bound as above | k = 18 at 1 bps (Kaspa's table, delay bound 5 s) | at d = 5 s a 34% withholder turns 29% of honest blocks red in a 30-s window (sim) | as 51% | redirected subsidy at 45%+ only |
|
||||
| Timestamp games on ordering | none on GHOSTDAG (order is by blue work and hash); on the clocks see 4.2 | 10-s future tolerance, parent minus 10 s (spec 02 2.3) | past-median time can run at most 10 s ahead of real time: nothing against a 30-day window | 0 | nothing |
|
||||
| Merge-depth games (release a chain forked over 3,600 s ago) | honest blocks of the hour become unmergeable and are abandoned if the released chain is heavier: the 229-block shape of 6 Oct (CLAUDE.md 6 Oct rules) | F1: a chain missing a certified checkpoint is not a candidate; any lock inside the hour kills it | only during a pause or the first 30 days; depth then bounded by the finality depth, 12 h | 1.5 h at 51%: USD 18 / 183 / 2k / 18k | an hour of honest subsidy orphaned, none gained |
|
||||
| Red-block flooding (publish blocks on stale parents) | the attacker's blocks are red, pay the honest merger, carry no weight; honest blues unaffected | W2, the red rule | pure loss to the attacker | | nothing |
|
||||
|
||||
### 4.2 The difficulty rule: the seven recorded ways and the ones to add
|
||||
|
||||
The seven of `sim/difficulty/attacks/README.md` and `results.md`, read not re-derived (Igneum rule v2 with the 4 October clock and floor):
|
||||
|
||||
| # | way | recorded bound | status |
|
||||
|---|---|---|---|
|
||||
| 1 | pool hopping (10 to 100% of the base, 24 h) | +1.5% blocks per hash at most, 0.7 points over Kaspa's rule; a 60-s dwell makes the 50 and 100% hoppers lose 1.7 to 4.0% | PASS under 5%, open by the letter |
|
||||
| 2 | pulsed rental (50x for 10 min hourly) | weight per hash 0.26 (Kaspa's rule 0.98): a pulse buys no weight; the base's blocks per hash fall 36% in the hour after | PASS (M14, F14 closed with the finality run: the renter never reaches a third) |
|
||||
| 3 | timestamp stretching (30 and 50% forger, earliest, latest, alternating) | +0.4 to +1.1% drift after an hour (worst seed +2.7%), difficulty ratio 1.00, worst gap 10 s; on 3 igneumd nodes a 50% forger moved nothing (0.82 to 0.88 blocks/s, 0 rejected) | FIXED (M23); before the fix the chain ran at a fifth of its rate at 9.9x difficulty |
|
||||
| 4 | short-lane oscillation (25% square wave every 120 blocks) | std 0.160 against 0.045 steady, 12% above the attacker's own square wave; the oscillator earns 1.4% less | FAIL by the letter, no past-only controller can pass, no change |
|
||||
| 5 | epoch games (hold dodger, hold flooders) | 0.0%, +0.7%, +0.3% (worst +2.4%) | PASS |
|
||||
| 6 | polluted window (10x joins and leaves at the lane switch) | settles 292 to 334 s, worst gap 17 s | PASS (Kaspa's rule 2,910 to 3,540 s) |
|
||||
| 7 | block flood (85 blocks/s of PoW-less input) | the target stops at 2^128 after about 2,630 blocks, no panic | FIXED (floor) |
|
||||
|
||||
Ways not in the seven, with the bound this lane gives:
|
||||
|
||||
| # | way | model | bound | who gains |
|
||||
|---|---|---|---|---|
|
||||
| 8 | Red-share gaming: a withholder turns honest blocks red, the estimator counts blue work only (spec 02 2.3 "what this section does not do"), the controller eases | the 60-s rows of 3.1: the blue rate read is 0.86 of true at 20%, 0.76 at 34%, 0.79 at 45%, 0.80 at 51%, 0.86 at 67% | the ease is at most 1/(blue share) - 1: 16 to 32% more blocks per real second for everyone (emission above schedule by the same factor, spec 02 2.5); no relative gain to the attacker beyond 4.1's redirected subsidy; the attacker's own reds cap it | nobody relatively; everyone's emission runs 16 to 32% fast while it lasts |
|
||||
| 9 | The star collapse (run A): effective delay past k / lambda, red to 67 to 77%, the controller reads a third of the rate and eases, the DAG widens | 3.3's table | a positive feedback with no attacker: the controller must read total work or the fleet must not be a star; lane 5 owns the rule, the fleet lib the topology | an attacker who can slow the hub (DoS) gets the collapse for free |
|
||||
| 10 | Clock trust after a pruning-proof sync: a header whose selected parent has no stored clock starts from the raw stamp (difficulty-2026-10-03.md section 11, Limits) | not measured | at most one window of bias after a sync; a forger needs to be the first blocks a syncing node sees | a stretcher against fresh nodes only |
|
||||
| 11 | Partition retarget: each side retargets to its share within 657 s (the 50x step-down figure), so each side keeps 1 block/s; at the heal the heavier side's targets rule and the lighter side's blocks carry less work | spec 02 2.3 measured steps; `finality_v2.py` +daa | consistent by construction; the minority's blocks merge red under merge depth | nobody |
|
||||
|
||||
### 4.3 The finality weight
|
||||
|
||||
| attack | what it achieves | defence | bound (sim) | rent | earns |
|
||||
|---|---|---|---|---|---|
|
||||
| Sybil (many keys) | nothing: weight is blue blocks, every draw is by weight (W6; harness s2: dust keys zero weight, sortition by weight PASS, F17 fixed) | W2, W3, F17 | 0 | 0 | 0 |
|
||||
| Weight capture by mining (the renter) | share (t/30) A: 1/3 on day 10/A, 2/3 on day 20/A, never under A = 2/3 (`results_v2.md` B to 0.04 points; sweep R) | the 30-day flat window | 51%: veto day 20, 2/3 never while honest miners stay; 67%: day 15 and 30; 90%: 11.1 and 22.2 | 20 d at 51%: USD 6k / 58k / 585k / 5.8M | 22M IGN of subsidy |
|
||||
| Weight capture by buying or renting keys | a bought key is worth its blocks and decays as the window slides: share = A (1 - t/30) + r t/30 (3.11.5; `results_v2.md` K; sweep K); keys worth 40% hold the veto from day 1 to 20, worth 20% never | W2 decay, W5 succession, equivocation strips a sold key the seller still holds | max(A, r) for a day, r after 30 days; a silent 40% buyer stalls 63k of 86k checkpoints then loses the veto on day 19 to 20 | the price of pools' keys, not hash | a pause of up to 20 days |
|
||||
| Long-range (private DAG from an old point) | a cold node with no certificate follows the heavier DAG (F5) | F5's trusted certificate (designed, not implemented); the client-shipped checkpoint (proposal 11) | needs more blue work than the public DAG over the window: 30 days of >50% | USD 9k / 88k / 877k / 8.8M | 34M IGN |
|
||||
| Eclipse (poisoned pool) | 0 conflicting locks, 0 locks on the eclipsed side at 1, 2, 4 h for a 34% attacker and a 20% pool (`results_v2.md` L3, F2); sweep E extends it to 51 and 67% | the 2/3-of-total floor binds whatever the presence window says (3.3.2) | the eclipsed side must hold 2/3 of total: a 47%+ attacker plus a 20% pool (sweep E, see 3.5) | the eclipse plus the weight | nothing under the bound |
|
||||
| Partition with an equivocator | 0 conflicts to 33%, conflicts from 34% (`results_v2.md` H at 2/3; M5 under v3); sweep P at 51, 67, 90 | two certificates need 4/3 of weight in signatures (3.11.2) | 1/3 of weight, every view, any partition length under one window since the last lock (v3) | the 20-day veto | two finalised histories across a partition, each side's deposits |
|
||||
| Equivocation alone | strips the key for 30 days, no coin penalty (F6); detection by any carrier block, agreed by every node (F23 fixed) | 3.6 | costs the attacker its weight, nothing else | | nothing |
|
||||
| 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 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)
|
||||
|
||||
| game | model | bound | price |
|
||||
|---|---|---|---|
|
||||
| 6% holdout blocks a change for ever | the window share has 0.07 points of noise: 94.9% flips with P 0.09, 94.5% never | the floor N6 ends it; nothing else does | USD 18 a day at 1 GH/s, 17.9k at 1 TH/s; the holdout earns 6% of subsidy meanwhile, so net about zero at equilibrium |
|
||||
| What the floor does | converts the signal into a fixed height at N6: the hazard of 6 October (DAA 198,000 crossed while boxes were still updating: two-sided chain, 229-block reorg) returns for every node not on the object at N6 | the floor should sit no nearer than a week past the publish on a network miners run, and the stale-box list (P1 pass rule) must be empty before it | |
|
||||
| Signal then defect | a defector's blocks fail PoW under the new program and are refused (`check_header_version` then PoW); a pool with stale workers loses their blocks | the defector pays, nobody else | |
|
||||
| The one-day window | a renter at 19 N for 24 h with a patched byte forces the flip; for v4 it hurts nobody (the signalling binary is the v4 binary), for a later object it forks every node still on the old one | the share of the fleet not yet on the object at the forced flip | USD 5.3k at 1 GH/s to 5.3M at 1 TH/s, and the market could not supply a TH/s |
|
||||
| Proposed | require 95% on each of 7 consecutive daily windows (7x the renter's bill, a week of visible share), keep the one-day tally for display | | 3 hours |
|
||||
|
||||
### 4.5 Proof records
|
||||
|
||||
| attack | today's rule | bound | who gains |
|
||||
|---|---|---|---|
|
||||
| Forgery of state | the native-execution veto: a record whose statement differs from the node's own execution of that segment along the carrying block's chain is ignored (7.2 item 5, P11 fixed) | state is never moved by a record; a soundness bug is a light-client problem (P7), a job-output problem for the precompile (D6, contained by R12) | nobody |
|
||||
| Forgery of the proof (correct statement, random bytes) | NOT checked in consensus (7.7 item 4, 7.8 item 8); the first valid record per shard or segment carried pays | a producer at share H takes at least H of the 20% pool and, since its fake rides its next block while an honest proof takes 9 to 11 s on a 5090 (P9 table), most shards outside the 10-s exclusive window; inside it only the H of slots it is assigned | any block producer: 11,636 IGN/h at 51% of a pool paying 22,815 IGN/h |
|
||||
| Withholding (an assignee sits on its window) | after 10 DAA s anyone may prove and be paid; a segment unproven after 600 DAA pays nothing (7.8 item 7); mandatory proofs off | one window of latency per absent assignee; nothing waits | nobody |
|
||||
| Grief (flood the pool with invalid records) | 6,000 invalid records: 0 accepted, 0.31 to 0.68 ms each, node up (redteam row 10) | CPU per record | nobody |
|
||||
| The aggregator naming itself as every prover | provers committed in the proof's public values and checked (P12 fixed) | 0 | nobody |
|
||||
| Record ordering race | the first valid record carried wins: a producer can front-run honest provers' records in its own block (the forgery line) | fixed by consensus verification (rank 1), then by aggregator sortition (O-7.3) | |
|
||||
|
||||
### 4.6 The execution layer
|
||||
|
||||
| attack | today's rule | bound | note |
|
||||
|---|---|---|---|
|
||||
| Snapshot poisoning over p2p | `p2p_snapshot_gate` refuses tip 0, below the restart, at or below the own tip, and anything while the executor runs unblocked; a snapshot whose state at the restart block differs from `exec_restart_state_root` is refused (release-0.3.14.md) | a wrong state ABOVE the restart block is not detectable by the node: headers commit to no execution root (spec 02 2.6: blocks carry transactions only), certificates sign (chain id, index, block hash) only (C2) | the poisoned node's native statement then disagrees with every carried record, it pays nothing and sees every honest record as invalid; the signal exists but nothing reads it as an alarm |
|
||||
| The pin | `exec_restart_number / hash / state_root / trust_daa` arrive by the signed manifest on the devnet (the reddit review's admin-key finding, 1.6 item 3); on mainnet no manifest exists, so the pin is genesis-only or absent | a release-key holder sets execution state on the devnet; on mainnet the same power would need the 95% signal | disclose (the review's key-powers table) |
|
||||
| Deep reorg never resets execution | a reorg reloads the newest persisted generation at or below the fork (ring 2,048) else blocks loudly and asks a peer; never a genesis replay on a pruned node (0.3.14) | under active finality a reorg is bounded by the lock (90 to 120 s), far inside 2,048; during a pause the 12-h depth is 43,200 blocks, 21x the ring, so a pause-time deep reorg blocks every pruned executor until a peer's snapshot arrives, which is the poisoning path above | the ring should reach the finality depth (proposal 12) |
|
||||
| Duplicate and nonce games, pgas bombs, malformed bodies | exec-attacks suite 96 of 97 checks, every executed block under B_p, an over-budget transaction refused at the mempool with the pgas metered (redteam row 28) | per block B_p of proving gas and B_e of execution gas; an aborted transaction pays | |
|
||||
|
||||
### 4.7 Peer to peer
|
||||
|
||||
| attack | what it achieves | defence | bound | price |
|
||||
|---|---|---|---|---|
|
||||
| Eclipse of one node | feed it a private chain: its difficulty eases to the attacker's hash within about 11 min (the 50x step-down takes 657 s), so 1% of the network's hash produces a plausible 1 block/s chain for the victim within 20 min; it sees no certificates and reports `finality_active` false | the exchange guidance (treat a pause as PoW with a 12-h depth); a node that holds locks will not follow a chain missing them (F1) | a victim that follows its node's finality flag loses nothing credited under a lock; one that credits at a PoW depth is Kaspa's or Monero's eclipse victim | a few IPs |
|
||||
| Eclipse or outage of the hands | tonight's pause was NOT this (lane 3, 3.1: locks 6824 to 6842 formed with the hub down, the zero-aggregator fallback carried 64 of 251 certificates); the attack stands in general: a fleet that peers only through two hosts is a star, and a star with its centre down is a partition into n islands, each under 2/3, finality paused until the heal; longer than merge depth (60 min) it is the 3.7 item 9 fork | aggregator fallback (any node aggregates after 15 DAA s); votes ride in blocks (Q2); neither crosses a dead hub | pause for the outage; proposal 4 (peer floor) removes the star | 0 hash: the DoS of two hosts |
|
||||
| Crash a pruned node from any peer (main, 19:57Z: the hub, a pruned 0.3.14 node, panicked on an `unwrap` over `KeyNotFound` at the devnet genesis when a re-joining peer synced below its retention; `consensus/src/processes/sync/mod.rs:87`, fix in 0.3.15) | any peer takes any pruned node down by asking for history it does not hold | none today; the rule: no `unwrap` or `expect` on a path a peer's request reaches, a `SyncManagerError` instead, and a fuzz of the sync request space (locator low/high, antipast low/high, missing-bodies high, pruning-point anticone) against a pruned node as the CI gate | zero hash; one request per node; repeated, a liveness attack on every pruned node (every mainnet node prunes) | 0 |
|
||||
| Eclipse of the seeds (3 Hetzner DNS seeds) | a fresh node bootstraps into attacker peers and, with no trusted certificate, follows their DAG (F5 cold start) | none implemented; F5 designed | the long-range attack's price (4.3) for the DAG, zero for the eclipse | USD 9k to 8.8M for the DAG |
|
||||
| Handshake refusal (params digest) | a peer with another digest is refused before any flow (X18); an attacker cannot make honest peers refuse each other | it is a defence; the only cost is the digest-less allowance still open on devnet and simnet | 0 | |
|
||||
| The p2p snapshot path | 4.6 | | | |
|
||||
| Memory under flood | 269 to 780 MB per minute of flood on 0.3.4 (M30), bounded since 0.3.5 to the record window plus pruning depth | | | |
|
||||
|
||||
Siblings of the 19:57Z crash class, read-only grep of the fork (main checkout `vendor/igneum-node`, 6 Oct) for `unwrap` and `expect` on paths a peer's request reaches. The sync manager's entry points are called from the request flows (`protocol/flows/src/v10/request_headers.rs`, `request_antipast.rs`, `request_block_locator.rs`, `request_ibd_chain_block_locator.rs`, `request_pruning_point_and_anticone.rs`, `request_block_bodies.rs`) through `consensus/src/consensus/mod.rs:1341` (`get_hashes_between`), `:1624` (`get_missing_block_body_hashes`), `:1647` (`create_block_locator_from_pruning_point`):
|
||||
|
||||
| file:line | what panics | reached by |
|
||||
|---|---|---|
|
||||
| `consensus/src/processes/sync/mod.rs:87, 88` | `ghostdag_store.get_blue_score(low/high).unwrap()`: the hub's crash when `low` is below retention | `antipast_hashes_between` from a peer's antipast or headers request |
|
||||
| `sync/mod.rs:94` | `ghostdag_store.get_data(current).unwrap()` on the forward chain walk | the same |
|
||||
| `sync/mod.rs:117` | `find_highest_common_chain_block(...).expect("because of the pruning rules such block has to exist")`: false once the peer's `low` is pruned | the same |
|
||||
| `sync/mod.rs:123, 125, 130, 135, 148` | pruning point, selected-chain tip and index lookups `.unwrap()` | `create_virtual_selected_chain_block_locator` from a peer's locator request |
|
||||
| `sync/mod.rs:162, 172, 182, 194, 196` | status lookups `.unwrap()` along a peer-named `high` | `get_missing_block_body_hashes` from a peer's IBD blocks request |
|
||||
| `sync/mod.rs:211, 221` | `get_blue_score(low)`, `get_compact_data(current)` `.unwrap()` | `create_block_locator_from_pruning_point` from a peer's IBD chain locator request |
|
||||
| `protocol/flows/src/v10/request_headers.rs:98` | `hashes.last().expect("caller ensured ...")` | the headers request flow, after `get_hashes_between` |
|
||||
| `protocol/flows/src/ibd/negotiate.rs:40, 47, 112, 166, 172` | `locator_hashes.last().unwrap()` on a locator the PEER sent | the IBD negotiation (the syncee side, a hostile syncer) |
|
||||
| `protocol/flows/src/ibd/flow.rs:307, 361, 436, 440, 700, 717` | `async_get_header(...).unwrap()`, `async_validate_pruning_points(...).unwrap()`, `pruning_points.last()/first().unwrap()` on peer-sent pruning points | IBD against a hostile syncer |
|
||||
|
||||
The rest of the hits in those files are test code (`request_headers.rs:199 to 222`, `request_pruning_point_and_anticone.rs:197 to 280`, `trusted_data.rs:81 to 149`, `proof.rs:144 to 230`) or channel sends. Bound of the class: zero hash per crash; every pruned node on the network can be taken down by one request each, repeatedly, which during a pause or the first month is a liveness attack on the chain itself and at any time on the hands. Proposal (lane 1, 4 hours): convert the sync manager's unwraps to `SyncManagerError` variants, make the negotiation and IBD paths return `ProtocolError` on a missing block, and add `tools/ci` fuzz `sync-request-fuzz` that drives the six request flows with random and below-retention hashes against a pruned fast-time node and fails on any exit; gate: 10,000 requests, node alive, 0 panics.
|
||||
|
||||
### 4.8 The harness run (real DAG, 0.3.14 binary, rule v3, fast time)
|
||||
|
||||
`tools/finality-attacks/run.mjs s6 --fast-time`, SCALE 0.4, rule v3 forced on (`finality_v3_activation_daa` 0), the 0.3.14 Mac binary (`vendor/igneum-node/target-0314/release/igneumd`, built 6 Oct 17:56) and its `igneum-miner` (`vmine`), ports 29800 to 29812, suffix 980, `/tmp/igneum-horizon-fin`, run lock held 19:53 to 20:03Z. The 0.3.14 node refuses the master copy of `infra/fast-time/override-60x.json` (unknown fields `program_class_v4_activation_daa` and `program_class_v4_signal_window_daa`, which only the `ca3-v4-node` binary knows), so the harness ran from a scratch copy of `tools/finality-attacks` and `infra/fast-time` with those two fields removed; nothing tracked was edited. Six vmine voters at 6 blocks/s in all (3 per side), window 120 DAA, warm 168 s, split 60 s, heal 60 s.
|
||||
|
||||
| scenario | measured | reading |
|
||||
|---|---|---|
|
||||
| A, 3/3 split | window DAA ~1,019 at the cut, 6 voters, max locked 34 on both sides; new locks during the 60-s split 12, the first on each side at 30 s; locks resumed after the heal; conflicting certificates 9 / 11 after the heal | the recorded F21 shape at this scale: a side at 3 blocks/s advances its own DAA 3 a second, so the frozen table (one window of 120 DAA after the last common lock) expires 30 to 40 s into the split and the sliding table then locks each side alone, exactly as the redteam's row 18 (36 and 49 s) and the simulator's M3; the harness criterion (`zero new locks`) asserts more than the rule promises past one window |
|
||||
| B, 4/2 split | window DAA ~900, max locked 30; the 4 side locked 30 to 38 (first at 33 s), the 2 side stayed at 30 for the split; conflicting certificates 1 / 12 after the heal | the 4 side holds 4/6 = two thirds of the frozen table and locks as Q3 allows (inclusive); the 2 side locks nothing while the table stands; the post-heal conflicts are the 2 side's late solo locks after its own table expired (redteam row 18 saw the same 4 late locks), the F21 residual |
|
||||
|
||||
What it adds to the sim: the real DAG at fast time reproduces the frozen-table bound within its own DAA clock and the v2 control's shape (`split50-v2.md`: 3 solo locks at 126 s, 4 conflicts); nothing contradicts the simulator. What it does not test: a withholding attacker (vmine has no withhold flag) and any run longer than the window.
|
||||
|
||||
## 5. Model: the formulas and what holds
|
||||
|
||||
### 5.1 The ordering race against the lock
|
||||
|
||||
Inputs: lambda blocks/s (measured 1 on the devnet), k = 18 (cited, Kaspa's table), d (measured 0.34 to 2.3 s), checkpoint interval I = 30 blue score and depth d_cp = 60 (designed), lock latency after determination median 2.5 s, p99 4.6 s (simulated, `results_v2.md` A), 0.8 to 1.08 s on the test network (measured). A deposit at time 0 lies under the checkpoint block mined at most 30 s later, which locks at most 30 + 60 + 3 = 93 s later. An attacker forking before the deposit must win the race before that lock (after it, F1 excludes its chain). From 3.1: wins need H >= 1/2 for any T, or T < k H / ((1 - 2H) lambda) below it; at T = 90 s that is H >= 0.45 with Poisson noise (70 to 85 percent at 45 to 51 percent, 5 to 10 percent at 34 percent). Hence: a majority reorders at most the lock latency; a 34 percent miner at most about 60 s; a 20 percent miner at most the last k blocks. The honest guidance (credit on the lock) makes every case a zero.
|
||||
|
||||
### 5.2 Weight
|
||||
|
||||
share_renter(t) = (t/30) A (verified, B, sweep R); share_buyer(t) = A (1 - t/30) + r t/30 (verified, K, sweep K); two certificates need 4/3 of total in signatures, so the equivocator bound is 1/3 in every view (3.11.2, H at 2/3, M5 under v3); a partition side's own table reaches 2/3 on day 30 (2/3 - s)/(1 - s) under v2 (L4) and never before day 30 under v3 (M2, M3); a departed share x pauses 30 (1 - 1/(3x)) days under v2 and 30 days under v3 (L2, M4, sweep C). The weight ceiling of a withholder is 3.1's w(H) with the 60-s rows.
|
||||
|
||||
### 5.3 Rent
|
||||
|
||||
cost = A/(1 - A) x N x hours x 11.7 USD (measured price); earned = 0.8 x A x 31.688 x 3600 x hours IGN (spec 02 2.5); N_eq = 0.8 x 31.688 x 3600 x P / 11.7 GH/s at IGN price P (assumption inputs). At N_eq the veto nets about 0.48 x 480 h of subsidy and locking alone 0.33 x 720 h.
|
||||
|
||||
### 5.4 What holds, what breaks
|
||||
|
||||
| claim | holds | evidence |
|
||||
|---|---|---|
|
||||
| No hash share under 2/3 of weight reverses a certified checkpoint | yes | 3.11.2, H and M5 (0 conflicts under 1/3 in every seed), the DAG sim (the race ends at the lock) |
|
||||
| 51% of hash never reaches 2/3 of weight while honest miners stay | yes, with margin: 56% ceiling under red-flooding (approx.), 51% honest | B, sweep R, 3.1 |
|
||||
| A majority cannot forge a proof the nodes re-execute | yes for state; NO for payment: the proof itself is not checked and the pool is capturable | spec 07 7.7 item 4, 7.8 item 8 |
|
||||
| A rule changes only at 95% signalling | yes for the signal path; the floor is a fixed height that can cross with stale nodes | P2, signalling.py |
|
||||
| A majority loses more than it earns | for the veto and the lock-alone, yes (net 48% and 33% of the period's subsidy at equilibrium); for the pause-time 12-h double spend and the fake records, NO | cost_model.py |
|
||||
| The pause is safe | safe for history, costly for liveness, and buyable at zero hash by taking the hub down | tonight; sweep S and C |
|
||||
|
||||
## 6. Ranked proposals: the defences we do not have
|
||||
|
||||
| rank | proposal | evidence | model | hours | consequence per tier | gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Verify the aggregated segment proof in consensus (a record whose proof does not verify against the pinned aggregator key is invalid; the per-shard v0 record stays payout-only until then and is capped at the exclusive window) | 4.5: a producer captures H to most of the 20% pool with fake records; P21 "stated, not fixed" | capture = pool x (H inside the window + most outside); verification cost per record = SP1 light verifier (P3: 1.3 to 2.1 s setup, then per-proof ms, to measure) x 2 records per block | 8 to 12 | home miner (8/12/16 GB): its honest shard records are paid, not front-run; rig and pool: proving income real; prover: the market is honest; holder: 20% of emission is not a miner's bonus; rollup customer: a paid proof is a verified proof; node: +verify CPU per block | fast-time: a correct-statement fake record is refused, an honest one paid, p95 block validation under 50 ms with 2 records |
|
||||
| 2 | Weight-gated deep fork choice: among candidate tips, a tip whose fork point is older than D (say 10 min of past-median time) is a candidate only if the blocks on it since the fork were produced by keys holding at least 1/3 of the weight table at the fork point (a function of the block's past, deterministic) | 4.3 "during a pause": a renter with fresh keys double-spends at the 12-h depth for USD 146 at 1 GH/s; the DAG sim's 90-s race | rented hash has zero weight for 10 days (W2), so its deep chain is never a candidate; honest partition sides over 1/3 keep today's behaviour; a side under 1/3 cannot reorg the other past D, which is the desired outcome | 10 to 16 (virtual processor candidate filter + weight-at-fork from the finality tables) | home miner and rig: nothing changes; pool: nothing; holder and exchange: a pause-time or first-month deep reorg needs 1/3 of weight, 20 days in public, not 12 h of rent; node: one table lookup per deep candidate; rollup customer: PoW-depth credits become weight-backed | fast-time: a fresh-key renter at 3x the hash forking 2 min back is refused for ever; a 40%-weight honest side forking 2 min back is adopted |
|
||||
| 3 | Vote-or-burn: a block whose producer key has participation under 0.5 over the presence window in the block's own past burns 20% of its producer share | 4.3 "the pause as a liveness attack": zero marginal cost | the attacker's pause then costs 0.2 x A x 0.8 x 114,077 IGN/h: 7,757 IGN/h at 34% (USD 155/h at 0.02); partition-safe because the test is the block's own past (a side's keys vote their own checkpoints); honest outages (2.2%) leave participation above 0.9 | 6 to 8 (coinbase rule + the finality manager's participation count at the block) | home miner under dust: not a voter, counts 1, unaffected; a miner whose node never revealed a vote key: loses 20% until it does (an incentive); pool: votes or pays; holder: silent weight stops being free | fast-time: a 40% silent set's coinbases shrink 20%, honest ones do not, both sides of a 3/3 split unaffected |
|
||||
| 4 | Peer floor and mesh for the fleet and the node: the fleet lib dials at least 3 other boxes beside the hands; the node logs an alarm and the app shows it when outbound peers fall under 3 or when no vote has been received for 2 checkpoints | run A's star (321 tips, 77% red); the hands as the fleet's only peers (lane 3 refuted this as tonight's cause; the shape stands) | a star with its hub down is n islands; with 3 extra peers per box the graph stays connected under any single failure | 2 to 4 (fleet lib) + 3 (node alarm) | every tier: finality stays up when a hand dies; home miner: sees "no peers" instead of a silent pause | Devnet 2: kill the hub for 10 min, locks continue; the alarm fires on the known-bad case and not on the known-good |
|
||||
| 5 | Vote over the execution root too: the vote signs (chain id, index, block hash, post_root of the checkpoint's segment); a certificate then pins the state, snapshots are checked against the last certificate, and the poisoned-snapshot node cannot join the quorum | 4.6 snapshot poisoning: a wrong state above the pin is undetectable | every voter is a full node that executes natively (spec 07); the cost is exec lag added to lock latency (the executor runs seconds behind the tip) | 8 to 12 | holder and exchange: a certified checkpoint carries its state; rollup customer and light client: one object says ordered and executed; node: a divergent executor is visible at once; miner: a vote waits for its executor (lock latency + exec lag) | fast-time: three nodes agree; a fourth with a tampered snapshot votes a different root and is outvoted, alarm raised; lock latency rises by the measured exec lag only |
|
||||
| 6 | Signalling over 7 consecutive daily windows, floor no nearer than 7 days past the publish, the stale-box list empty before the floor | 4.4 | the renter's bill x7; a week of visible share | 3 | pool and rig: a week more before a class change; home miner: a week to update | fast-time gate: 6 of 7 days at 95% does not flip; 7 does |
|
||||
| 7 | Detector-driven alarms: the share-pattern detector (counter-asic-3-status, Detector row) and the finality flag feed one node-side `security_alert` (correlated group over 1/3 of a day's blue blocks; pause; conflict) that wallets and the explorer show as "confirm at 12 h" | 4.3, the exchange guidance exists only as text | an alarm when any single party crosses the veto line in public, which the rule says takes 10 to 20 days | 4 to 6 | holder and exchange: a number to act on; home miner: the app shows the chain's state | fast-time: a 34% silent set raises the alarm within 5 min; the honest run never does |
|
||||
| 8 | The signed departure (LEAVE: lane 3's rank 1, `finality-and-weight.md` section 6; a `leave` item carried in blocks, the key out of every denominator one hour after inclusion, sent by the app and the fleet library on a clean stop) and F5's trusted certificate implemented | tonight's departure (lane 3, 3.1): 42.7% left in three minutes and the frozen table held finality for a window; a view cannot tell a departure from a partition, so no automatic rule re-enables locks without reopening L4/M3 | stripping lowers total; an attacker stripping stolen honest keys is K's bound (needs keys worth 1 - a/(2/3)); lane 3's sim T: first lock 1 h after a 34 to 50% departure, 0 conflicts in every partition row | 6 (lane 3) + 4 | pool and rig: an orderly stop keeps finality up for everyone; holder: no 30-day pause after a planned fleet move; node: the operator's certificate for the disorderly case | fast-time: 45% of weight stops with exits, locks continue; without, the pause |
|
||||
| 9 | Client-shipped checkpoint: each release carries the latest certified checkpoint (index, hash) and voter-table digest; a cold node refuses a DAG missing it (assumevalid's shape) | 4.3 long-range, 4.7 seeds | the long-range attack must then out-work the public DAG since the release, not since the window | 3 | every tier: a fresh install cannot be bootstrapped onto a private DAG; trust is the release key already trusted for the binary | a cold node offered only a private heavier DAG refuses it |
|
||||
| 10 | Exec generations spaced geometrically to the finality depth (1, 2, 4 ... 43,200 blocks: about 16 generations) so a pause-time deep reorg never needs a peer's snapshot | 4.6 | 16 x state size on disk (about 1.8 GB today, measured 114.8 MB per snapshot) | 3 | node operator: disk; every tier: no blocked executor after a deep reorg | fast-time: a 5,000-block reorg re-executes from a generation, no snapshot request |
|
||||
| 11 | Checkpoint anchoring to proof records (the certified checkpoint's hash as a public value of the next aggregated proof; the verifier checks the certificate natively) | asked by the brief | buys light clients and bridges one object (certified and proven); changes nothing a full node does, since the proof chain already commits to block hashes and a reorg already needs new proofs; in-circuit BLS is 40+ hours and not worth it | 6 (public-value commit) | rollup customer and light client: one verification; others: nothing | a light client verifies a proof carrying a certificate hash and the certificate |
|
||||
| 12 | Prover attestations as a second finality leg (a lock also needs proof records from a quorum over the checkpoint) | asked by the brief | NOT recommended: provers are the miners (same vote keys), so no new party; proving covers 2.4% of blocks today and the pool is "not active" (reddit review 1.4), so every lock would wait on proofs and finality would pause constantly; what it would add (execution validity) rank 5 gives without the liveness cost | 8 if ever | every tier: lock latency becomes proof latency (20 to 60 s target, minutes today) | only once coverage is 100% and rank 1 is in |
|
||||
| 13 | Time-locked (vesting) weight | asked by the brief | NOT recommended: a bought key transfers vested weight, so K's bound is unchanged; honest new cohorts wait N days longer than G's 20 | 3 if ever | new home miners: later vote; attacker: unchanged | none |
|
||||
| 14 | Any rule that keeps finality on after a large honest set leaves abruptly without a signed exit | asked by the brief | NOT possible safely: departure and partition are the same observation in one view; re-enabling locks under the frozen table reopens the double lock of L4 and M3 at the same day; the honest options are rank 8 (exit) and a shorter frozen expiry, which trades the partition bound one for one | 0 | | |
|
||||
|
||||
One paragraph each on the two that matter most.
|
||||
|
||||
Rank 1, proof verification in consensus. Today a record is paid on a signature and a statement match; the statement is computable by every node, so a producer writes the right statement, random proof bytes and its own payout address into its own coinbase and is paid the shard or the aggregator share. The exclusive window limits it to the slots it is assigned (by weight, so H of them) for 10 DAA s; after that the first record carried wins, and the producer's block is first. The only thing that stops it is a verified proof as a condition of payment. SP1's light verifier exists (`igneum-prove-host --mode verify-segment`); the cost to measure is the per-proof verification time on the validation path, and if it is over a few tens of milliseconds the aggregated record (2 per block) is the one to verify in consensus while the per-shard record stays payout-only inside the window. Consequence per tier: an 8 GB home miner that proves on the patched prover is paid for what it proves; a pool's proving income is real; a holder's 20 percent of emission goes to proofs.
|
||||
|
||||
Rank 2, weight-gated deep fork choice. Finality's whole argument is that weight cannot be rented; fork choice today ignores weight, so during a pause or the first month a renter's heavier chain reorganises up to 12 hours. The rule: a candidate tip whose fork point is more than D of past-median time behind the node's selected tip is a candidate only if the keys that produced its chain blocks since the fork hold at least a third of the weight table at the fork block (the same `voters_at` the finality manager computes). It is deterministic (a function of the DAG), it leaves every reorg under D to GHOSTDAG as now, it leaves honest partition sides over a third exactly as now, and it makes the pause-time double spend cost the veto (20 days in public) instead of 12 hours of rent. What it costs: a side of a partition under a third of weight that is heavier by work cannot reorganise the other side past D at the heal, which is the outcome the certificate would have produced anyway; and a cold node with no table yet follows F5. Gate: the fast-time run in the table.
|
||||
|
||||
## 7. Open questions and what could not be run
|
||||
|
||||
| item | why |
|
||||
|---|---|
|
||||
| The star's effective relay delay | the model needs the hub's measured relay time at 12 blocks/s to 41 peers; main's run A rows give the outcome (77% red, reorg 55) and this lane gives the curve (3.3); the fleet measures the delay |
|
||||
| The finality sweep's R row at 90% | the renter's 9x hash makes 905 honest keys fall under dust in the model (B's dust effect), so the simulated share overshoots the formula by 1 to 2 points, as B recorded |
|
||||
| Proof verification time on the validation path | not measured; P3 gives setup only; rank 1's hours depend on it |
|
||||
| Weight-gated fork choice against the C4 certificate-driven reorg | a certificate over a deep block must still force the reorg (3.5); the gate must exempt certified tips; not modelled |
|
||||
| The DAG simulator has equal work per block, no difficulty, no bodies | a withholder also controls its blocks' timestamps and the attack-side difficulty; the 10-s rules bound the clocks, the DAA lane bounds the rest |
|
||||
| igneumd harness | one s6 run on the 0.3.14 Mac binary (4.8); the node-side numbers for the other scenarios are the recorded runs cited |
|
||||
| The live pause's end | lane 3's arithmetic (4.1): the frozen table of lock 6842 expires at DAA 216,402, about 20:40Z, unless departed keys holding 9.4 points of it return first; main reads the chain |
|
||||
|
||||
## 8. Summary paragraph
|
||||
|
||||
The ordering layer and the lock together bound a hash majority to the lock latency: the DAG simulator shows 45 to 51 percent winning the 90-second race 70 to 85 percent of the time and nothing beyond it, 34 percent winning only inside 60 s, 20 percent only the last k blocks; weight cannot be rented faster than 10 days per third, bought keys decay as the window slides, no equivocator under a third splits finality in any view, and the costs in rented hash are USD 6k to 5.8M for the veto and 17k to 17M to lock alone across 1 GH/s to 1 TH/s, of which the attacker earns back half as subsidy. Three things a hash majority does buy today: the proving pool, by writing fake records into its own blocks (the one line that earns more than it costs); a 12-hour proof-of-work double spend during a pause or the first month for USD 146 to 146k; and a pause needs no attacker at all, since a planned 43 percent departure caused tonight's and the frozen table holds it for a window (lane 3, 3.1). The three findings as numbered lines:
|
||||
|
||||
1. A 51 percent withholder reorganises at most the lock latency (90 to 120 s; 32 to 46 chain blocks at 1 block/s, 80 percent success), reaches a weight ceiling of about 56 percent by red-flooding (never two thirds), and costs the honest side a quarter of its subsidy while it lasts (`ghostdag_sim.py`).
|
||||
2. The veto costs 20 days of 51 percent in public (USD 6k at 1 GH/s, 5.8M at 1 TH/s, half earned back) and then holds a pause for free; a pause-time 12-hour double spend costs USD 146 to 146k (`cost_model.py`, `finality_horizon.py` S and C); weight-gated deep fork choice (rank 2) makes it cost the veto instead.
|
||||
3. Any block producer captures from H to most of the 20 percent proving pool today with correct-statement fake records (11,636 IGN an hour at 51 percent), because consensus does not verify the proof (spec 07 7.7 item 4); proof verification in consensus is rank 1.
|
||||
|
||||
## 9. Files and how to run them
|
||||
|
||||
| file | run |
|
||||
|---|---|
|
||||
| `sim/horizon/consensus-security/ghostdag_sim.py` | `with-lock.sh run nice -n 19 python3 sim/horizon/consensus-security/ghostdag_sim.py --seeds 20 --out ghostdag_results_1bps.md`; `--bps 10 --k 124 --delays 0.35,0.67,2 --holds 10,30,60,90 --warm 60 --post 15` for the 10 bps grid |
|
||||
| `sim/horizon/consensus-security/finality_horizon.py` | `with-lock.sh run nice -n 19 python3 sim/horizon/consensus-security/finality_horizon.py --out finality_horizon_results.md` (imports `sim/finality_v2.py`) |
|
||||
| `sim/horizon/consensus-security/signalling.py` | `python3 sim/horizon/consensus-security/signalling.py --out signalling_results.md` |
|
||||
| `sim/horizon/consensus-security/cost_model.py` | `python3 sim/horizon/consensus-security/cost_model.py --out cost_results.md` |
|
||||
| results | `ghostdag_results_1bps.md` and `.json`, `ghostdag_results_10bps.md` and `.json`, `finality_horizon_results.md`, `signalling_results.md`, `cost_results.md` in the same directory |
|
||||
356
docs/analysis/horizon/economy-and-utility.md
Normal file
356
docs/analysis/horizon/economy-and-utility.md
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
# Horizon lane 4: economy and utility. What IGN is for beyond gas, the 80/20 under stress, ten years without a treasury, the dev fee, miner signalling, and what the other chains got wrong
|
||||
|
||||
6 October 2026, evening UK. Lane 4 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon`). Every model is in `sim/horizon/economy-and-utility/` with a README line per script; every dollar figure names its inputs and its label. Nothing here is a price prediction, an offer to sell anything, or a change to any consensus parameter.
|
||||
|
||||
**Read:** `docs/spec/05-fees-and-economics.md` (whole, 5.10 and 5.11 included), `02-consensus.md` 2.5, `07-execution.md` (7.2 to 7.8); `docs/design/payment-routes.md`, `developer-adoption.md` (2a to 2c), `execution-layer.md` (4.3 to 6), `miner-dev-fee.md`; `docs/plans/funding.md` 1 to 5; `docs/analysis/security-budget.md`, `economy-2026-10-04.md`, `base-fee-floor.md`, `prover-floor.md`, `prover-tiers-real-cards.md`; `sim/economy/` (README, sim.py, security_budget.py, results.md, levers.md); `docs/commercial/prover-customer-brief.md`; `docs/fud-ledger.md` E1 to E8 (E9 to E18 are cross-referenced from the spec and the status updates, the file carries E1 to E8 as sections), P6, P8, P9, P10, P14, P22, D1 to D6, C9, C10, G8, L6, L7; `site/litepaper.html` Building, Economics, Governance; `docs/bench-log.md` lines 1226 (the dev fee measured), 1910 (proving v1), 2349 (aggregation cost), 2582 (rental cost of hash); `docs/plans/counter-asic-3-node.md` section 6 (the P2 rule) and `counter-asic-3-status.md` P2; `docs/analysis/horizon/frontier.md` section 0 (the ranking) and 3.1, 3.2, 3.5, 3.6, 3.11 to 3.13; lane 3's `sim/horizon/consensus-security/cost_results.md` and `signalling_results.md`; `vendor/rusty-kaspa` (main checkout) `consensus/core/src/config/params.rs` and `consensus/src/processes/coinbase.rs`. `block-rate-devnet2.md` was still a template (RUN_A, RUN_B empty) at 21:50 UK; nothing here depends on it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Method
|
||||
|
||||
| Question | What was run | Where |
|
||||
|---|---|---|
|
||||
| Task 1, utility beyond gas | Arithmetic: the measured eleven-card table turned into a cost per v1 shard and per billion cycles (alone, beside the miner, rented), against the published prices; five demand curves on a low/base/high grid at three IGN prices | `utility.py`, output `utility_out.md` |
|
||||
| Task 2, the 80/20 and the burn under stress | `sim/economy/sim.py` copied and changed in six named places (the measured cards, the renter farm, the 25-s window and 120-s timeout, a FIFO backlog with the 600-s record window, burn per day, the new scenarios), 8 scenarios x 3 seeds, 30 days, plus two sensitivity runs; under the main checkout's run lock at nice 19 on the M5 Max, about 18 s a run | `stress.py`, outputs `results/stress_main.md`, `stress_busy.md`, `stress_elecfarm.md` |
|
||||
| Task 3, ten years without a treasury | Arithmetic: `sim/economy/security_budget.py` extended with the lane's fee grid, the pool line, the sustained hash at the measured card economics and the rented 34% weight attack at USD 281 per GH/s-day | `security_budget_10y.py` |
|
||||
| Task 4, the dev fee | Arithmetic from `miner-dev-fee.md` and the bench-log measurement | `devfee.py` |
|
||||
| Task 5, signalling | Arithmetic on the three thresholds; lane 3's `signalling.py` covers the 95-percent rule and is cross-referenced, not re-run | `signal_game.py` |
|
||||
| Task 6, the other chains | Reading: `vendor/rusty-kaspa` for Kaspa; everything else named and marked approximate where no clone exists | this file, section 5.6 |
|
||||
|
||||
No node harness was started and no measurement was taken: the fleet, the PCs and the devnet were on the class v4 rehearsal and the Devnet 2 block-rate runs. Every hardware number is the fleet's from 6 October (`prover-tiers-real-cards.md`) or the bench-log's.
|
||||
|
||||
---
|
||||
|
||||
## 2. Evidence
|
||||
|
||||
### 2.1 Measured inputs
|
||||
|
||||
| Input | Value | Source |
|
||||
|---|---|---|
|
||||
| v1 shard | 4,717,439 cycles; the adopted `S_p` is 30,000 pgas = 30 M cycles, so the fixture is 16% of a full shard | `prover-tiers-real-cards.md`; spec 5.11 |
|
||||
| Shard alone, compressed, patched server 2^26 | 3060 14.4 s, 3080 7.1, 3090 14.9, 4060 Ti 16 GB 11.6, 4060 Ti 8 GB 9.6, 4060 18.4, 4070 12.1, 4090 6.3, 5070 4.8, 5090 6.3, A5000 8.3 | `prover-tiers-real-cards.md` table |
|
||||
| Shard beside the running miner (compressed) | 3060 37.5 s, 3080 25.6, 3090 19.9, 4060 Ti 16 GB 34.6, 4070 27.3, 4090 26.1, 5070 37.2, 5090 10.7, A5000 34.6; the 8 GB cards core-only 22.1 to 26.3 | same |
|
||||
| Hash and watts mining (rented boxes) | 3060 23.78 MH/s at 103.7 W ... 5090 98.48 at 258.2 (the table) | same; the 4060's 0.0 W reading replaced by its 115 W rating, approximate |
|
||||
| The miner's loss while its card proves | 5090: 124.72 to 119.74 MH/s, 4.0%, on empty shards; 8 GB cards 17.1 to 16.0 MH/s, about 6%, on v1 shards | bench-log, proving v1 step 1; `prover-tiers-real-cards.md` 8 GB row |
|
||||
| Watts mining and proving at once | 5090: 328.6 W max against 258 W mining (about 70 W more) | bench-log proving v1 step 1; the fleet row |
|
||||
| Rental price of hash | USD 0.0117 per MH/s-hour (1,748 MH/s for USD 20.44 an hour, 38 pods); USD 11.7 per GH/s-hour, USD 281 per GH/s-day; the market gave 0 of 20 pods asked at the TH/s scale | bench-log line 2582 |
|
||||
| Aggregation per block on a mining 5090 | 9.6 to 9.7 s (2.1 s with the card to itself) | bench-log line 2349 |
|
||||
| Dev fee on a test network | 9 fee blocks in 785 (1.15%; the template rule is exact at 1 in 100) | bench-log line 1226 |
|
||||
| Proof record sizes | 274 bytes per shard record, 586 per segment record, 1,272,897 bytes per compressed proof | spec 7.7, 7.8; bench-log proving v1 |
|
||||
|
||||
### 2.2 Published prices (all approximate or secondary; none cloned)
|
||||
|
||||
| Supplier | USD per billion cycles | Label |
|
||||
|---|---|---|
|
||||
| Boundless (RISC Zero), Base | about 0.21 median lock price, trailing day 4 Oct 2026 | `developer-adoption.md` 2b, secondary summary; approximate |
|
||||
| Succinct Prover Network | 0.046 base plus up to 0.46 per billion PGU in the quickstart's EXAMPLE request at USD 0.23 per PROVE | `developer-adoption.md` 2b; example parameters, not a market price |
|
||||
| RISC Zero Bonsai | never published a per-cycle list price; paid proving moved to Boundless in 2025 | not cloned, approximate |
|
||||
| Ethereum L1 block at the ethproofs cluster cost, Sep 2026 | sub-half-cent a block; at 0.2 to 1.3 B cycles a block (14 to 44 SP1 cycles per gas, measured on Igneum) about 0.004 to 0.025 | `frontier.md` 2.6 (secondary); `base-fee-floor.md` |
|
||||
| A Taiko-class rollup per batch | taiko-mono not cloned; Taiko Alethia proves batches through its own prover market with SGX and ZK tiers (SP1 and RISC0 accepted); the ZK proof's cost per batch is of the order of the ethproofs figure times the batch's cycles: cents to tens of cents | approximate |
|
||||
|
||||
### 2.3 What the earlier models said that this lane re-tests
|
||||
|
||||
| Claim | Source | What changed tonight |
|
||||
|---|---|---|
|
||||
| Hybrid loses the whole hash for the proof's duration plus a 5-s swap | `sim/economy/sim.py` TPROVE + 2 x swap | Measured: the miner loses 4 to 6% while the card proves; the lottery wins the card's arbitration and the proof is 3 to 4x slower instead |
|
||||
| A 3060 proves a shard in 20 s (target) | ledger P1 | Measured: 14.4 s alone, 37.5 s beside the miner, on the 4.7 M-cycle fixture; a full 30 M-cycle shard is unmeasured on it (linear scaling would say 92 s alone, approximate) |
|
||||
| 10-s window, 300-s claim timeout | spec 7.2 as designed | 25 s and 120 s decided 6 Oct 2026 (ledger P9) |
|
||||
| The farm pays electricity at USD 0.05 | `sim/economy/sim.py` | The farm is a renter at the measured USD 0.0117 per MH/s-hour |
|
||||
|
||||
---
|
||||
|
||||
## 3. Model
|
||||
|
||||
### 3.1 The supply side of proving
|
||||
|
||||
For a card with hash `h` (MH/s), network hash `N` (MH/s), shard time `t` (s), watts `w` and price `P` (USD per IGN):
|
||||
|
||||
```
|
||||
cost_alone = w t / 3.6e6 x 0.10 electricity
|
||||
+ (h / N) x 0.8 x 31.688 x t x P the subsidy the card forgoes while it proves
|
||||
cost_beside = 70 t / 3.6e6 x 0.10 + (h / N) x 0.8 x 31.688 x t x 0.04 x P (4% measured on the 5090, approximate elsewhere)
|
||||
cost_rented = h x 0.0117 / 3600 x t the renter's cost; no subsidy, no electricity
|
||||
per billion cycles: x 1e9 / 4,717,439
|
||||
```
|
||||
|
||||
### 3.2 Demand curves at the adopted floors (spec 5.11; design 6 for jobs)
|
||||
|
||||
```
|
||||
transfer = 21,000 x 100 gwei + 300 x 10,000 gwei = 0.0051 IGN, burned; tip 21,000 x 1 gwei, 80% miners+provers, 20% burned (no registered frame)
|
||||
batch post 100 KB = (21,000 + 16 x 100,000) x 100 gwei + 300 x 10,000 gwei = 0.1651 IGN, burned
|
||||
job of C cycles = C / 1000 x 10,000 gwei x 1.5 = 15 IGN per billion cycles; 90% provers, 10% burned once IGN-settled
|
||||
payments cap = B_p / 300 = 400 transfers a block = 34.6 M a day; EIP-1559 target half of that, 17.3 M a day
|
||||
records = 274 x shards + 586 / 8 bytes a block in the coinbase; 1,272,897 bytes per proof on p2p
|
||||
```
|
||||
|
||||
### 3.3 Sustainability
|
||||
|
||||
```
|
||||
sustained hash (GH/s) = miners' USD per day / (electricity + capital per GH/s-day)
|
||||
electricity = 258.2 W / 98.48 MH/s x 24 / 1000 x USD 0.10 = USD 6.29 per GH/s-day (measured card, the brief's price)
|
||||
capital = USD 2,000 / 98.48 MH/s / 1,095.75 days = USD 18.53 per GH/s-day (approximate)
|
||||
total USD 24.8 per GH/s-day; the rental price is USD 281, 11.3x
|
||||
34% weight attack = rent 1.04 N for 20 days (lane 3's rule, spec 3 headline) = 1.04 x N x 281 x 20; the attacker earns 51% of the producer subsidy meanwhile
|
||||
```
|
||||
|
||||
### 3.4 The stress simulator
|
||||
|
||||
`sim/economy/sim.py` with: eleven card classes (`HASH`, `PMINE`, `TPROVE` alone, `TBESIDE`, `CANHYB` from the measured table; `MIX` an approximate installed-base shape), hybrid capacity `cards x T / TBESIDE` and hybrid hash `1 - 0.04 x duty`, operator 0 a renter (`cost = cards x MH/s x 0.0117 x hours`), window 25 s, timeout 120 s, a FIFO of open shards with a 600-s expiry (expired credit stranded), burn per day = 10% of IGN-settled external jobs plus `blocks x content_shards x 0.51 IGN` (paid content 0.03 shards a block at launch traffic), one proving shard a block (measured on the devnet). The thresholds T1 to T5 are the 4 October definitions (hash under 50% of the pre-event mean for an hour; backlog over 600 s; a growing backlog; a day under 90% within 60 s; a 10-point proving-share swing).
|
||||
|
||||
---
|
||||
|
||||
## 4. Results, task by task
|
||||
|
||||
### 4.1 Task 1: what IGN is for beyond gas
|
||||
|
||||
#### (a) Proving as a sellable service
|
||||
|
||||
| Card | Alone, 1 GH/s | Alone, 100 GH/s | Alone, 1 TH/s | Beside its miner, 100 GH/s | Rented (no subsidy) | Electricity only |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 3060 12 GB | 36.81 | 0.377 | 0.046 | 0.054 | 0.236 | 0.0088 |
|
||||
| 4070 12 GB | 32.50 | 0.331 | 0.039 | 0.041 | 0.208 | 0.0065 |
|
||||
| 4060 Ti 16 GB | 21.92 | 0.224 | 0.027 | 0.040 | 0.140 | 0.0049 |
|
||||
| 4090 24 GB | 35.38 | 0.361 | 0.042 | 0.069 | 0.227 | 0.0068 |
|
||||
| 5090 32 GB | 66.69 | 0.676 | 0.076 | 0.050 | 0.427 | 0.0096 |
|
||||
| Boundless median (approximate) | 0.21 | 0.21 | 0.21 | 0.21 | 0.21 | |
|
||||
| ethproofs L1 cluster (approximate) | 0.004 to 0.025 | | | | | |
|
||||
|
||||
USD per billion cycles at USD 0.02 per IGN (`utility_out.md` 1.3; the IGN price moves only the opportunity term).
|
||||
|
||||
What it says. Electricity is under a cent per billion cycles on every card; "marginal cost close to power" (ledger C10's wording) is true of the electricity and false of the price, because the price a prover must charge is the subsidy it forgoes, and that scales as 1 / network hash. At today's devnet scale (1.16 GH/s) a prover that stops mining to prove must charge 100 to 300x Boundless's median. At 100 GH/s a card proving alone is at 1 to 3x Boundless; a hybrid card beside its miner is at 0.2 to 0.4x (USD 0.04 to 0.08), which is the only row where Igneum undercuts the market, and it rests on the 4% figure measured on one card. The renter's row, USD 0.13 to 0.43, is the floor below which no rented prover ever sells. The floor-priced job (15 IGN per billion cycles) is USD 0.075, 0.30 and 1.50 at the three prices: a third of Boundless at 0.005, 1.4x at 0.02, 7x at 0.10. The floor is denominated in IGN and the market in dollars, and the floor moves by a two-week 60% vote (spec 5.11): it cannot follow a price. Frontier 3.11 (rank 15) already shows the market is three to four orders under year-1 emission; this lane adds that at the adopted floor Igneum overprices the market at any IGN price above about USD 0.014.
|
||||
|
||||
| Tier | Consequence |
|
||||
|---|---|
|
||||
| Home 8 GB | proves alone only (compressed does not fit beside the miner); its price is the alone row: competitive only above about 300 GH/s of network hash |
|
||||
| Home 12 GB | mines and proves on headless Linux (37.5 s beside on the 3060, 27.3 s on the 4070); competitive beside its miner at 100 GH/s; a full 30 M-cycle shard beside the miner is unmeasured (about 3 min by linear scaling, approximate, outside the 120-s claim timeout) |
|
||||
| Home 16 GB | the cheapest beside-row (USD 0.040 per billion at 100 GH/s) |
|
||||
| Home 24 or 32 GB | the 5090 is the cheapest prover per billion beside its miner above 100 GH/s and the dearest alone (its subsidy is the largest) |
|
||||
| Rig | eight 4090s beside their miners: USD 0.07 per billion at 100 GH/s; 8 x 26.1 s per shard, so a rig delivers a 30 M-cycle shard in about 21 s with all eight on one shard (approximate; SP1 proves one shard per server) |
|
||||
| Pool user | nothing: the pool's provers carry the proofs |
|
||||
| Prover | its quote is a function of network hash it does not control; publish the price as `h/N x subsidy x t`, never as a number |
|
||||
| Holder | job demand buys IGN only after the proof bridge (phase two); at launch customers pay on their own chain, so (a) is zero IGN demand at launch |
|
||||
| Rollup customer | the customer brief should carry the band above and the condition (network hash) rather than any price |
|
||||
|
||||
#### (b) Rollup settlement, (c) bridges, (d) payments, (e) storage
|
||||
|
||||
Dollars per day to miners and provers (`utility_out.md` 3.3) and burn (3.4), base scenario, USD 0.02 per IGN:
|
||||
|
||||
| Period | External jobs to provers, USD (own chain) | Rollups settling here, IGN to provers | Bridges, IGN to provers | Payment tips, IGN | IGN flows in USD | Burn, IGN | Burn, % of daily emission |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| launch | 450 | 19,440 (1 rollup) | 3,038 (1 bridge) | 1.68 (100 k transfers) | 450 | 5,748 | 0.21% |
|
||||
| year 2 | 1,800 | 58,320 (3) | 9,112 (3) | 16.8 (1 M) | 1,349 | 23,316 | 0.85% |
|
||||
| year 5 | 9,000 | 194,400 (10) | 15,188 (5) | 168 (10 M) | 4,195 | 126,716 | 4.6% |
|
||||
| year 5 high | 90,000 | 972,000 (50) | 30,375 (10) | 290 (17.3 M, the target) | 20,058 | 711,481 | 26% |
|
||||
|
||||
Low scenarios are a tenth to a fifth of these; the full grid at the three prices is in `utility_out.md`. Reading each curve:
|
||||
|
||||
- **Rollups** are the only line that pays provers in IGN at scale: one rollup posting a batch a minute with a 1 B-cycle proof job pays 19,440 IGN a day at the floor, 3.6% of the daily pool. Ten of them in year 5 pay 194,400 IGN a day, 36% of the pool before the second halving and 142% of the pool after it. The condition is the floor price staying under the market's (above). The burn it causes: 10% of the job plus the batch's base fee, 2,398 IGN a day per rollup.
|
||||
- **Bridges** at 225 updates a day pay 3,038 IGN a day each; a tenth of a rollup. No bridge is official (spec 7.3), so the count is anyone's.
|
||||
- **Payments** cost USD 0.000026 to 0.00051 a transfer (the three prices). A transfer undercuts a 1 bps rail on any payment above USD 0.26 to 5.10 and a 10 bps rail above USD 0.03 to 0.51; a USD 100 payment pays 0.003 to 0.05 bps. The fee is flat in IGN, so payments give the coin burn and almost no income: 10 M transfers a day burn 51,042 IGN (1.9% of emission) and tip 168 IGN at the 1 gwei default. The proving dimension caps the chain at 34.6 M transfers a day and the fee leaves the floor above 17.3 M; above that the burn is set by willingness to pay and no model here knows it, so the year-5 high row is clamped at the target and says so.
|
||||
- **Storage.** Records are 347 bytes a block at one shard (1,025 at 3.5): 11 to 32 GB a year, USD 0.17 to 0.50 of disk per node per year (approximate HDD price), paid by whoever runs a node and by nobody else. Proof bytes (1.27 MB each) never enter a block; a node keeps the 600-block pool (about 3 GB at four shards a block) and a light client one proof. An archive of every proof would be 80 to 180 TB a year (USD 1,200 to 2,700 of HDD, approximate): a service someone sells, not a protocol cost. Frontier 3.16 (rank 9) is the research-dataset version of the same bytes.
|
||||
|
||||
The honest total. In the base scenario all five uses together put USD 450 a day to miners and provers at launch and USD 4,200 in year 5 at 0.02, against USD 54,800 of daily emission in year 1 and 13,700 in year 5. Fees are 1.6% of security spend in year 1 and 48% in year 5 (`security_budget_10y_out.md`, base at 0.02), and most of the year-5 share is the external USD line, which is in dollars and does not move with the coin. Burn is 0.2% to 4.6% of daily emission in base scenarios. The Economics section's "part of every payment on Igneum is burned" is true and small: with the ramp's 37 M never minted, burn under 1% of emission a day leaves the cap's approach unchanged to the second decimal for years.
|
||||
|
||||
### 4.2 Task 2: the 80/20 split and the burn under stress
|
||||
|
||||
`results/stress_main.md`: 8 scenarios x 3 seeds, 30 days, the eleven measured cards, the farm (20% of hash, 925 to 1,016 5090s) a renter at USD 0.0117 per MH/s-hour, window 25 s, timeout 120 s, one proving shard a block, USD 0.012 at t = 0. The model's network is about 700 GH/s of potential hash (15,000 cards), so every number below is at that scale; the renter's rent against the subsidy is the N_eq of lane 3 (`cost_results.md` section 3): 94 GH/s at USD 0.012.
|
||||
|
||||
| Metric | a: baseline | p10: price x10 day 7 | pd10: price /10 day 7 | c: no external | x100: external x100 | cartel: top 10% of weight never proves | refuse: nobody proves days 10 to 20 | halving: 15.844 IGN a block |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Hash min / pre-event (seed min) | 0.99 | 0.99 | 0.55 (0.49) | 0.99 | 0.91 | 0.99 | 0.99 | 0.98 |
|
||||
| Hash day 30 / pre-event | 1.00 | 1.25 | 0.74 | 1.00 | 0.97 | 1.01 | 0.99 | 1.00 |
|
||||
| T1 hours under 50% | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
| Cards off, day 30 | 9% | 0% | 31% | 9% | 8% | 0% | 9% | 9% |
|
||||
| Cards proving / hybrid, day 30 | 6% / 58% | 6% / 66% | 14% / 44% | 6% / 48% | 10% / 55% | 6% / 57% | 6% / 76% | 4% / 58% |
|
||||
| Backlog max, shards; T2 age max, s | 0; 0 | 0; 0 | 0; 0 | 0; 0 | 0; 0 | 0; 0 | 766; 565 (capped by the 600-s expiry) | 0; 0 |
|
||||
| Blocks within 60 s, mean / T4 worst day | 1.00 / 1.00 | 1.00 / 1.00 | 1.00 / 1.00 | 1.00 / 1.00 | 1.00 / 1.00 | 1.00 / 1.00 | 0.67 / 0.00 | 1.00 / 1.00 |
|
||||
| T5 proving-share swing, points | 1.6 | 0.1 | 2.0 | 0.6 | 2.9 | 2.2 | 0.0 | 4.4 |
|
||||
| Burn, IGN a day (of which external) | 19,928 (18,606) | 6,758 (5,437) | 146,737 (145,415) | 1,322 (0) | 1,576,947 (1,575,623) | 16,954 (15,632) | 13,366 (12,044) | 17,247 (15,926) |
|
||||
| Burn, USD a day at the run's mean price | 214 | 578 | 506 | 15 | 15,535 | 218 | 148 | 224 |
|
||||
| Share of the 20% pool paid / stranded | 1.00 / 0 | 1.00 / 0 | 1.00 / 0 | 1.00 / 0 | 1.00 / 0 | 1.00 / 0 | 0.67 / 0.33 (182,387 IGN a day averaged; 547,570 a day during the refusal) | 1.00 / 0 |
|
||||
| Renter farm cards on at day 30; margin over rent | 0; -77% | 984; +114% | 0; -77% | 0; -76% | 54 (0 to 162); -6% | 984 (forced on); -79% | 0; -77% | 0; -88% |
|
||||
| Flags tripped (of 3 seeds) | none | none | none (T1 min 0.49 in one seed, under an hour) | none | none | none | T4 3/3 | none |
|
||||
|
||||
Per card class, USD per card-day (every mode, off included) and the share of shards proven, baseline: 3060 1.30 (0%), 3080 3.09 (5%), 3090 2.69 (11%), 4060 Ti 16 GB 1.63 (4%), 4060 Ti 8 GB 2.03 (16%), 4060 0.85 (1%), 4070 1.96 (12%), 4090 3.86 (11%), 5070 3.36 (12%), 5090 3.95 (24%), A5000 3.30 (4%). The full tables are in `results/stress_main.md`.
|
||||
|
||||
What holds and what breaks:
|
||||
|
||||
1. **The 80/20 holds under every price and demand shock; the price is what moves hash.** T1 never trips. The /10 price shock is the closest: hash troughs at 55% of pre-event (49% in one seed for under an hour), 31% of cards go off (the 3060 and 4060 classes at median electricity and above), and the chain is at the T1 line with no backlog and every block proven inside 60 s. A x10 shock brings the renter farm on (margin +114%) and hash ends 25% up: rented hash arrives exactly when the subsidy per GH/s-day clears USD 281, which is lane 3's N_eq. The first halving at a flat price changes nothing at this price level (the same 9% off), because the cards that remain are above break-even at 0.013; a halving is a /2 price shock and the /10 row says where /2 would land between the two.
|
||||
2. **External demand x100 is the burn story and the renter story.** USD 200,000 a day of jobs at 10% burn is 1.58 M IGN a day burned, 58% of daily emission, at the run's price of about USD 0.01; and it brings 0 to 162 rented 5090s on at a -6% margin. Hash troughs at 91%: cards leave the lottery for jobs (the 4 October finding, scenario b), and the renter's cards arriving for jobs do not hash. The burn in that row is a transfer from customers to holders of 15,500 dollars a day; it is the only row where burn is material, and it needs IGN settlement, which is phase two.
|
||||
3. **A cartel of the top 10% of weight that never proves costs the chain nothing.** In this population the top 10% of weight is one operator, the 20% farm, so the row is a 20% cartel: with 8 draws by weight the chance that every assignee is the cartel's is 0.2^8, under three in a million, so almost every shard still finds an assignee and the rest go open after 25 s; every block is proven inside 60 s, the backlog is zero, and the cartel loses USD 6.77 per card-day (forced on, as a renter, to hold its weight). Sortition with 8 draws is why: the 4 October result (scenario e, 30%) stands with the measured cards.
|
||||
4. **A refusal by every prover is the one scenario that breaks a threshold, and what breaks is the pool, not the chain.** For ten days no block is proven (T4 0 on every refusal day), execution and finality do not wait (spec 5.3, ledger P9), and the backlog never passes 600 s because the record window expires the shards: 547,570 IGN a day of pool credit is stranded in the escrow, 5.5 M IGN over the ten days, and no rule returns it. When provers come back the queue is at most 600 s deep and clears in minutes; 76% of cards end in hybrid. The refusal's whole cost is the refusers' own income plus a silent supply reduction nobody voted for (proposal 2).
|
||||
5. **The window excludes the slow hybrids.** At 25 s a 3060 beside its miner (37.5 s on the small fixture), a 4060 Ti 16 GB (34.6), a 5070 (37.2) and an A5000 (34.6) cannot land an assignment and win only open races or prove alone; the 3060 class proves 0% of shards in every scenario, the 4060 1%. The 4060 Ti 8 GB proves 16% by proving alone at 9.6 s. The 4 October proposal (window = the fleet's 90th-percentile shard time plus a swap) would set it near 38 s on this fixture; on a full 30 M-cycle shard the number is unmeasured (proposal 8).
|
||||
6. **Burn at launch traffic is USD 15 to 220 a day**, 0.05% to 0.7% of emission; the base fee part is 1,322 IGN a day at 0.03 content shards a block. Everything above that is the external 10%, which does not exist until jobs settle in IGN.
|
||||
|
||||
Sensitivities (`results/stress_busy.md`, `stress_elecfarm.md`, 2 seeds each). At 3 proving shards a block (the 4 October busy value, 3x the load) nothing changes in a, cartel or x100: backlog 0, every block inside 60 s, hash min 0.95 to 1.00; the refusal strands the same 33% of the pool with a queue of 2,265 shards at its deepest, and the renter farm comes on in x100 at 213 cards (181 to 244). With the farm on electricity at USD 0.05 instead of rent (the 4 October assumption) the baseline has 0% of cards off (the farm's 968 cards stay on) and the /10 shock takes 25% of cards off with hash troughing at 63% of pre-event and ending at 77%: the rent is what decides whether the 20% farm is on at all, and with it 20 points of hash at every price; the `renter farm margin` column of that file is undefined at zero rent and should be read as blank.
|
||||
|
||||
| Tier | Consequence of the stress runs |
|
||||
|---|---|
|
||||
| Home 8 GB | proves alone and wins open races (16% of shards on the 4060 Ti 8 GB); the first class off in a /10 shock on expensive power |
|
||||
| Home 12 GB | the 3060 proves 0% at the 25-s window; the 4070 12% (27.3 s beside, loses the window, wins open races alone); first off in a price fall |
|
||||
| Home 16 GB | 4% of shards; stays on in every row but pd10 |
|
||||
| Home 24 or 32 GB | 11 to 24% of shards; hybrid is the dominant mode (58% of all cards); the last class off |
|
||||
| Rig | as the 24 GB card per card; a rented rig is off below N_eq and on above it (p10 row) |
|
||||
| Pool user | the pool's provers' share; unchanged by any row |
|
||||
| Prover | income is 20% of emission in every row but refuse, where the refusers strand it; the x100 row is the one where jobs pay more than the pool |
|
||||
| Holder | burn is 0.05 to 0.7% of emission a day at launch; 58% in the x100 row, phase two only |
|
||||
| Rollup customer | every job delivered in every row but refuse (67%) |
|
||||
| Node operator | the backlog never passes the 600-s record window because the window expires it |
|
||||
|
||||
### 4.3 Task 3: no-treasury sustainability over ten years
|
||||
|
||||
`security_budget_10y_out.md`. The brief's formula gives a sustained hash proportional to the miners' dollars and an attack cost proportional to that hash, so the ratio is a constant: a 20-day 34% weight attack rents 1.04 N at USD 281 per GH/s-day against an honest fleet that costs USD 24.8 per GH/s-day, 11.8x the honest fleet's 20-day cost, minus the 51% of subsidy the attacker earns back. The subsidy never falls under the attack cost in ratio terms; the halvings shrink both until the absolute number is small. The honest statement is the absolute net cost by year:
|
||||
|
||||
| Year | Net cost of the 20-day veto, USD, at 0.005 | at 0.02 | at 0.10 | Sustained hash at 0.02, GH/s | Fees as % of total security spend, base at 0.02 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 2,375,000 | 9,501,000 | 47,506,000 | 1,699 | 1.6% |
|
||||
| 3 | 1,233,000 | 4,933,000 | 24,666,000 | 882 | 18.6% |
|
||||
| 5 | 617,000 | 2,467,000 | 12,334,000 | 441 | 48.3% |
|
||||
| 7 | 308,000 | 1,234,000 | 6,168,000 | 221 | 65.1% |
|
||||
| 9 | 154,000 | 617,000 | 3,085,000 | 110 | 78.9% |
|
||||
|
||||
The veto's net cost drops under USD 1 M in year 5 at 0.005, year 9 at 0.02, and not within ten years at 0.10; under USD 100 k only after year 10 at 0.005. The rental market's supply, not its price, is the other bound (0 of 20 pods at the TH/s scale on 6 October), and it is not modelled. What the fees change: in the base scenario the proving-pool and job lines reach provers, not miners, and the brief's security line is miners' hash; of the 48% fee share in year 5, under 1% reaches miners (tips at 1 gwei). The chain's security budget after year 5 is the subsidy to miners, and nothing else in the design pays for hash. Frontier 3.1 (rank 7) redirects part of the subsidy to the pool during rental spikes and would lower the miners' line further; this lane's number for it is in 5.1.
|
||||
|
||||
The audits. `funding.md` prices the first cryptanalysis at USD 80,000 to 160,000 and nothing prices the second, which a class or era change in year 3 would need. What the entity's own lines earn (every price an input):
|
||||
|
||||
| Year | Price | Dev fee, 50% of hash on Ember | Entity's provers at 5% of the pool | Second cryptanalysis (USD 160 k) as % of the dev fee |
|
||||
|---|---|---|---|---|
|
||||
| 3 | 0.005 | 10,000 | 25,000 | 1,600% |
|
||||
| 3 | 0.02 | 40,000 | 100,000 | 400% |
|
||||
| 3 | 0.10 | 200,000 | 500,000 | 80% |
|
||||
|
||||
The dev-fee row here uses 1% of the producer share (80% of emission), because the fee template moves only the `IGNA` payout and the pool is paid per record; `funding.md` section 4 took 1% of all rewards and overstates the ceiling by a quarter (48,000, 193,000 and 963,000 should read 38,520, 154,080 and 770,400). The honest options for the second audit, ranked:
|
||||
|
||||
| Rank | Option | What it pays in year 3 at 0.02 | Why this rank |
|
||||
|---|---|---|---|
|
||||
| 1 | The entity's own provers (5% of the pool and a share of jobs) | USD 100,000 a year at 5% of the pool, more with jobs | Open-market income the design already names (spec 5.5); scales with the chain, no rule, no switch; the cost is running cards |
|
||||
| 2 | The Ember dev fee | USD 40,000 a year at 50% of hash on Ember | Exists and is measured; falls with every halving and with every miner who flips the switch; alone it funds a review every four years at 0.02 |
|
||||
| 3 | A user-paid review market: customers (rollups) co-fund the audit that protects their settlement, as a condition of their integration | unknown; a Taiko-class customer's whole annual proving spend is of the order of USD 100 k (frontier 3.11) | Honest and voluntary; the customer has the motive; it depends on having a customer |
|
||||
| 4 | Founders' mined coins (the litepaper's own answer for grants) | depends on hash share | Visible addresses; finite; the ledger's E2 and E8 live here |
|
||||
| 5 | A burn-funded bounty or review escrow | the base-fee burn at launch traffic is 51 to 20,578 IGN a day: USD 1 to 412 at 0.02 | Frontier 3.6 (rank 11, "watch") and its Monero attack: a burn redirect is a payee by rule, which is the switch spec 5.5 removed. The protocol cannot have it because it has no treasury, and that is the contradiction stated plainly: the no-treasury rule means the SECOND audit is paid by whoever earns in the open or it is not paid, and `funding.md` should say so in a row of its own |
|
||||
|
||||
### 4.4 Task 4: the dev fee
|
||||
|
||||
What it is (`miner-dev-fee.md`): one block template in 100, chosen by an exact counter (templates 99, 199, ...), is requested with the project's payout address in the coinbase extra data; the vote key and the UTXO address stay the user's, so a fee block still votes for the user and only the execution-layer payout moves. Default on; `--dev-fee 0`, the app's Settings switch or HiveOS `DEV_FEE=0` turns it off; the start line prints the state; `igneum-miner payouts` tags the dev address on the chain. Measured: 9 fee blocks in 785 on a test network, the miners' counters and both nodes agreeing (bench-log line 1226).
|
||||
|
||||
What it pays (`devfee_out.md`): 1% of the producer share of emission times the share of hash on Ember with the fee on.
|
||||
|
||||
| Year | Price | 20% keep it on | 50% | 100% | Home 4070 at 100 GH/s network, a month | Rig 8x 4090, a month |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 0.005 | 7,704 | 19,260 | 38,520 | | |
|
||||
| 1 | 0.02 | 30,816 | 77,040 | 154,080 | USD 3.33 (6.6 blocks) | USD 55.74 (110 blocks) |
|
||||
| 1 | 0.10 | 154,080 | 385,200 | 770,400 | | |
|
||||
| 5 | 0.02 | 8,000 | 20,000 | 40,000 | | |
|
||||
|
||||
What share would turn it off. Precedent (approximate, from memory): T-Rex 1%, lolMiner 0.7 to 1.5%, PhoenixMiner 0.65%, TeamRedMiner 0.75 to 2.5% and NBMiner 1 to 2% were not switchable, and together they held the large majority of Ethereum's GPU hash over the fee-free ethminer because they were faster; NiceHash is a marketplace that takes about 2% of the buyer's payment, not a dev fee; nobody measured an opt-out share because none offered one. Igneum's switch is one flag and the miner is open source, so the rational solo miner with any time at all turns it off; the pool operator decides for its members; the one-click app user keeps the default. A working estimate for planning: 20 to 50% of hash keeps it on, which is the devfee table's first two columns and USD 7,700 to 77,000 a year in year 1 at 0.005 to 0.02. This is a planning input, not a measurement, and the first month of the public testnet measures it from the chain (`payouts`).
|
||||
|
||||
Is "optional" honest? Ledger E18's charge is "a protocol fee with better PR". Three facts answer it. It is not in the protocol: the chain pays whatever `IGNA` address the template names, and a block with the dev address is indistinguishable in consensus from a block paying any other address. It is switchable in one flag and the chain shows who paid (`payouts`). It is default-on, and defaults are what most users run, so "optional" describes the mechanism and "default-on, switchable" describes the behaviour. The public line should be the second: "1 block in 100 pays the project unless you turn it off". Two things to add to E18: the ceiling correction above, and the fact that the fee buys the project a visible address holding 1% of mined coins, which is the E5 critic's point restated as a holder consequence.
|
||||
|
||||
| Tier | Consequence |
|
||||
|---|---|
|
||||
| Home 8 to 32 GB on Ember | 1% of blocks unless switched off; USD 2.34 to 13.13 a month at 0.02 and 100 GH/s network |
|
||||
| Rig | the same per card; a rig operator on HiveOS sets `DEV_FEE=0` once |
|
||||
| Pool user | the pool's choice: a pool on its own template software pays 0%; a pool on Ember pays 1% of its templates and passes it on or not |
|
||||
| Prover | untouched: the pool share is paid per record, never through a template |
|
||||
| Holder | one address accumulates up to 1% of producer emission; the project's incentive to keep Ember the fastest client is the fee |
|
||||
| Rollup customer | nothing |
|
||||
|
||||
### 4.5 Task 5: miner-signalled parameters
|
||||
|
||||
What genesis leaves to miners (spec 5.5, 5.9, 5.11): the base-fee floors `f_e` and `f_p`, the proving budget `B_p` (and with it `S_p`), set by a proposal at 60% of blue blocks over 1,209,600 DAA s (two weeks), the BIP 9 model. Upgrades (new code) need 90% (spec 5.7, window open, O-5.3). The P2 rule for a PoW class change needs 95% of blue blocks over a one-day window ending at each epoch's seed block, monotone, with a floor height as the backstop (`counter-asic-3-node.md` section 6: `CLASS_SIGNAL_THRESHOLD_BPS` 9,500, window 86,400 DAA, the fast-time gate green on three cases and its failed case). The documents disagree about the number: spec 5.7, CLAUDE.md's design paragraph and the litepaper's Governance section say 90% for upgrades; the P2 design and the Horizon preamble say 95% for class changes; the litepaper's Mining section says "a 90% miner signal turns one on". One sentence should carry all three (60 parameter, 90 upgrade, 95 class with a floor) or the three should become two.
|
||||
|
||||
The game (`signal_game_out.md`; lane 3's `signalling_results.md` for the 95% rule):
|
||||
|
||||
| Rule | Who can block | A 30% pool | Renter's cost to force at 100 GH/s | What ends a block |
|
||||
|---|---|---|---|---|
|
||||
| 60% over 14 days | over 40% of blue blocks | cannot block alone; needs 11 more points | USD 590,000 (1.5 N for 14 days) | the proposal fails; re-register |
|
||||
| 90% over 14 days | over 10% | blocks it | USD 3.5 M (9 N) | the proposal fails; re-register |
|
||||
| 95% over 1 day, floor | over 5% | blocks it | USD 534,000 (19 N for a day) | the floor height |
|
||||
|
||||
A 6% holdout costs USD 18 a day at 1 GH/s and USD 1,800 at 100 GH/s on top of the subsidy it earns like anyone, so near zero (lane 3 section 2); it buys delay to the floor and nothing else. A 30% pool holds a permanent veto over upgrades at 90% and over class changes until the floor at 95%; the devnet's top three vote keys held 34.5% of blocks on 4 October (litepaper, Governance). Signal then defect is bounded by what is signalled: a PoW class defector loses its own blocks (its PoW fails, `check_header_version` then the PoW check); a consensus-rule defector forks itself and whoever trusts it; an execution-parameter defector produces VALID blocks with a different state (blocks carry no state claim, design 1.1), which is a silent state fork for that node unless the parameter is in the consensus digest that the handshake refuses (G12, X18): `Params.fees` is in the digest (spec 5.11), so today it is isolated rather than split, and any future miner-signalled execution parameter must enter the digest the same day or the defector is a quiet fork.
|
||||
|
||||
What Bitcoin and Kaspa did. BIP 9: version bits, a 95% threshold of 2,016-block retarget periods, states DEFINED, STARTED, LOCKED_IN, ACTIVE, FAILED, a timeout; BIP 8 added a lock-in-on-timeout flag so a flag day ends a holdout (bips repository, bip-0009.mediawiki and bip-0008.mediawiki; not cloned, approximate). Kaspa's Crescendo (1 to 10 BPS) was a fixed DAA score, not a signal: `crescendo_activation: ForkActivation::new(110_165_000)` for mainnet and `88_657_000` for testnet, with `ForkActivation::is_active(daa)` as `current_daa_score >= self.0` (`vendor/rusty-kaspa/consensus/core/src/config/params.rs` lines 28 to 60, 648, 704, main checkout), and the coinbase keeps the activation score for ever to compute the subsidy month across it (`consensus/src/processes/coinbase.rs` lines 40 to 43, 238 to 253); `docs/crescendo-guide.md` tells miners to upgrade before the activation. The P2 rule is BIP 8 in shape: a signal path plus a flag day. Igneum's 6 October incident (DAA 198,000 crossed by a half-updated fleet) is the flag-day hazard, and P2's floor keeps it.
|
||||
|
||||
What SHOULD be miner-signalled and is not, with the risk of each:
|
||||
|
||||
| Parameter | Today | Should be | Risk if signalled | Risk if not |
|
||||
|---|---|---|---|---|
|
||||
| The block rate step (1 to 4 to 10 BPS) | a planned fork with its own test campaign, "as Kaspa's Crescendo" (spec 2.1) | a 90% upgrade signal with a floor, like P2: it is a consensus change crossed by a whole fleet | a 10% pool vetoes the step; a renter forces it a day early for USD 5.3 M at 1 TH/s | a fixed height on a half-updated fleet: the 229-block reorg of 6 October at mainnet scale |
|
||||
| The dataset growth step | automatic, genesis schedule (spec 1, 2 GiB doubling at years 4, 12, 28) | NOT signalled, by design: it is an anti-ASIC escalator and a chip-holding cartel would vote growth down. Allow a 60% signal to ACCELERATE only (monotone), never to delay | a 40% holdout blocks acceleration: no worse than today | none: the schedule runs |
|
||||
| The 80/20 lottery/proving split | fixed (spec 2.5) | a 60% parameter inside a hard band [10%, 30%] | 80% of the voters are the lottery; without the band they vote the pool to 0 and the provers go; with the band the worst case is 10% | the simulator says 20% is not load-bearing at launch traffic and 30% helps at 100 shards a block (economy-2026-10-04 5.3); fixed means a 90% upgrade to move it |
|
||||
| The base-fee floors and `B_p` | 60% over 14 days (spec 5.11) | a bounded per-block dial, Ethereum's gas-limit mechanism (frontier 3.5, rank 8): the dollar market moves faster than two weeks (4.1) | a 51% majority walks the dial to the bound in days; the bound and a cost curve are the defence | the job price is pinned in IGN while the market is in dollars; at 0.10 the floor is 7x Boundless and a two-week vote cannot follow it |
|
||||
| The job premium 1.5 and the external claim timeout 120 s (O-5.6) | design 6 constants | the same bounded dial | as above | a constant calibrated once on the phase 4 devnet |
|
||||
| The exclusive window 25 s | a consensus constant (P9) | a function of the fleet's measured shard-time distribution, published per era (economy-2026-10-04 proposal 1; frontier I3) | none: it reads a measurement | a 12 GB fleet whose shard time drifts past the window loses every assignment to the open race (the 4 October finding at 10 s) |
|
||||
|
||||
### 4.6 Task 6: what Kaspa, Monero, Ethereum and the zk rollups did and got wrong
|
||||
|
||||
| Area | Chain | What it did | Where | What went wrong, or what it costs | Igneum's rule | Avoids or repeats |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Emission | Kaspa | A pre-deflationary phase at 500 KAS a block (`pre_deflationary_phase_base_subsidy: 50000000000`, `deflationary_phase_daa_score: 15778800 - 259200`), then the chromatic schedule: 426 monthly steps, each month's subsidy the previous times 2^(-1/12), from 440 KAS a block (`SUBSIDY_BY_MONTH_TABLE[0] = 44000000000`), halving every twelve months smoothly | `vendor/rusty-kaspa/consensus/core/src/config/params.rs` 631 to 638, 687 to 694; `consensus/src/processes/coinbase.rs` 22 to 25, 222 to 253, 280 | Steep and smooth: no halving-day cliff, but the subsidy fell 50% a year and the chain leaned on price appreciation it could not promise; the table is divided by BPS at Crescendo so the per-second rate is unchanged | 1 B a year halving every two years, in DAA seconds; a 30-day ramp; no tail (spec 2.5, 5.10) | Avoids the yearly rate (slower), repeats the cliff (a step, not a glide); E6 concedes it |
|
||||
| Emission | Monero | A tail emission of 0.6 XMR a block for ever after the main curve | monero repository `src/cryptonote_basic/cryptonote_basic_impl.cpp`, `get_block_reward` (not cloned, approximate) | Security paid for ever at about 0.9% a year inflation falling toward zero; the cost is a soft supply cap critics name | No tail; a review trigger that puts a tail to a 90% vote if proving revenue is under a fifth of the subsidy after year 5 (spec 5.10.3) | Repeats Bitcoin's bet, keeps Monero's door ajar by vote |
|
||||
| Emission and burn | Ethereum | EIP-1559: the base fee burned, the tip to the proposer; issuance by stake since the Merge, about 0.5 to 1% a year gross, net near zero when burn is high | ethereum/EIPs `EIPS/eip-1559.md`; ethereum/execution-specs `src/ethereum/london/fork.py` (`calculate_base_fee_per_gas`); not cloned, approximate | The burn removes the proposer's incentive to stuff blocks, at the cost that usage pays security nothing; proposers' income moved to tips and MEV | Both base fees burned, tip 80/20 to miners-provers and apps; the same trade-off, stated (security-budget.md section 5) | Repeats on purpose (E3 is the reason); the EIP-1559 step is copied (`next_base_fee`, denominator 8) |
|
||||
| Fee market | Ethereum | A base fee that cannot fall below 7 wei in practice and has no floor; the gas limit voted per block by proposers within 1/1,024 | execution-specs `fork.py`; geth `core/block_validator.go` VerifyGaslimit; approximate | A near-zero base fee when idle makes spam cheap; the gas-limit vote is the one continuous miner dial that worked for a decade | A floor per dimension (spec 5.11) calibrated for spam; `B_p` and the floors by a two-week 60% vote | Avoids the idle-spam gap; does not take the per-block dial (frontier 3.5 asks for it) |
|
||||
| Proving market | Aleo | Proof-of-succinct-work: provers compete on proofs for coinbase rewards; the fastest prover (GPUs, then FPGAs and ASICs) took the reward share | AleoNet/snarkOS and snarkVM (not cloned, approximate; CLAUDE.md "the Aleo lesson", ledger C9) | The proving reward centralised to the fastest hardware; small provers earned nothing | The lottery and the proving are separate; shards by sortition on 30-day weight, 8 assignees, 25 s, then open (spec 7.2) | Avoids the race for assigned shards; the open race after the window is where fast cards win beyond their weight (economy-2026-10-04 3.1 item 5) |
|
||||
| Proving market | Boundless (RISC Zero) | A reverse auction per request; provers post ZKC collateral; PoVW pays ZKC per cycle proven | docs.boundless.network/zkc/mining/overview and provers/performance-optimization (read, not cloned) | A token gate on supply and a stake that scales with work; the median price USD 0.21 per billion (approximate) | No bond for shards; a coin bond only on external jobs (O-5.6); frontier 3.2 (rank 2) replaces even that with work-stake | Avoids the token gate for internal proving; repeats a bond for jobs |
|
||||
| Proving market | Succinct | A real-time auction settled in PROVE; provers stake PROVE to bid | docs.succinct.xyz/docs/provers (read, not cloned; ledger C10) | The same gate; example prices, no public market price | As above; prices in dollars settled in the token (spec 5.4) | Avoids the gate; repeats "settled in our token" once IGN settlement starts |
|
||||
| Governance | Monero | Scheduled hard forks (six-monthly, now 9 to 12 monthly), decided by the core team and the community off-chain | getmonero.org and the monero repository's release history (approximate) | Works because the community trusts a small team; the schedule itself is a central clock | No scheduled human releases; automatic escalators at genesis; 90% (or 95%) miner signalling for anything else (spec 5.7) | Avoids the clock; the price is that pools hold the vote (G8) |
|
||||
| Governance | Kaspa | KIPs discussed off-chain, activated at fixed DAA scores; Crescendo at 110,165,000 after a testnet campaign | `params.rs` 648; `docs/crescendo-guide.md` | A flag day; a node not upgraded forks off; it worked because the community upgraded in time | P2: a signal plus a floor height; Devnet 2 as the staging chain for every cut (CLAUDE.md 6 Oct rules) | Avoids the bare flag day, keeps it as the floor |
|
||||
| Governance | Ethereum | All Core Devs calls decide; clients ship; activation by timestamp; no on-chain vote | ethereum/pm repository (approximate) | Works by rough consensus among client teams; a single client bug is a chain-wide event (the 2016 Shanghai attacks, the 2020 Geth split, approximate) | One client today; a second independent client is the first priority after launch (litepaper, Governance) | Repeats the single-client risk until the second client exists |
|
||||
| Rollups | Taiko and the zk rollups | Pay their own prover networks per batch; based sequencing; multi-proof tiers | taiko-mono (approximate) | Proving cost is a line item that falls 3 to 30x a year (frontier 2.6); settlement and proving are bought from two suppliers | Settlement and proving from the same miners in one flow (litepaper, Building) | New; the price condition is 4.1 (a) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Ranked proposals
|
||||
|
||||
| Rank | Proposal | Evidence | Model | Hours | Consequence per tier | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Decouple the job price from `f_p`: a job's reserve is the measured proving electricity per pgas (USD 4.4e-9 at 0.15 per kWh, base-fee-floor.md 3) converted at a published settlement rate, and the requester bids above it; the 1.5 premium becomes a bid, not a floor | At the adopted floor a billion-cycle job is 15 IGN = USD 0.075 / 0.30 / 1.50 at the three prices against Boundless's 0.21 (4.1 a); a two-week 60% vote cannot follow a dollar market | `utility.py` section 2 | 16: the reserve rule in `Prover.request` (6), the rate oracle as the review-trigger's published reading (spec 5.10.3 already defines it) (4), spec 5.4 and design 6 text (6) | Prover: sells at the market, not at a vote; Rollup customer: a quote it can compare; Holder: job demand for IGN survives a price rise; Miner: nothing; Pool user: nothing | A simulated job book at the three prices clears within 20% of Boundless's median at every price |
|
||||
| 2 | Define the stranded pool: an unproven shard's credit rolls forward into the next proven segment's pool instead of sitting in the escrow for ever | The spec is silent on credit nobody claims; a 10-day refusal strands 5.5 M IGN (4.2); the devnet already burns the coinbase 20% output (litepaper, Economics) | `stress.py` refuse scenario, `stranded_share` | 8: the roll-forward in `split_pool_credit` (4), spec 5.3 and 7.8 item 7 text (2), a unit test with a 10-segment gap (2) | Prover: a refusal costs the refusers and pays the returners; Holder: no silent burn; Miner: nothing | On the fast-time harness, 100 unproven segments then 10 proven: the escrow returns to zero within the 10 |
|
||||
| 3 | Publish the prover's price as a formula, never a number: `price per billion = (h / N) x 0.8 x 31.688 x t x P x 212 / cycles`, with N the live network hash | The same card is 100 to 300x Boundless at 1 GH/s and 0.2 to 0.4x at 100 GH/s (4.1 a); the customer brief says "priced in dollars" with no condition | `utility.py` 1.3 | 3: a paragraph in the customer brief and the litepaper's Proving section, with the table | Rollup customer: no promise it cannot hold the project to; Prover: knows when to sell; everyone else: nothing | The brief and the litepaper carry the condition before any customer conversation |
|
||||
| 4 | Make the 80/20 split a 60% parameter inside a hard band [10%, 30%], and record the three signalling numbers (60 parameter, 90 upgrade, 95 class with floor) in one sentence in spec 5.7, CLAUDE.md and the litepaper | The split is not load-bearing at launch traffic and 30% buys backlog relief at 100 shards a block (economy-2026-10-04 5.3); the documents carry two upgrade thresholds (4.5) | `stress.py` `--set pool=` | 10: the band in `Params` (4), the proposal kind (3), text (3) | Prover: a floor of 10% of emission by rule; Miner: a vote on its own share, bounded; Holder: nothing | The fast-time harness: a 60% vote moves the pool to 30%; a 100% vote cannot pass 30% or go under 10% |
|
||||
| 5 | Every miner-signalled execution parameter enters the consensus digest the same release, with a CI check that fails a `Params` field marked signalled and absent from the digest | A signal-then-defect on an execution parameter is a silent state fork unless the handshake refuses the defector; `Params.fees` is in the digest, nothing guarantees the next one is (4.5) | `signal_game.py` section 3 | 6: the check in `tools/ci` (4), a test (2) | Node operator: a defector is isolated, never quietly wrong; everyone else: nothing | The check fails on a planted field and passes on the live set |
|
||||
| 6 | The block-rate steps become P2-shaped activations (signal plus floor), not fixed heights | Crescendo was a fixed DAA score (`params.rs` 648); the 6 October incident was a fixed height; spec 2.1 still says "a planned fork" (4.5) | lane 3 `cost_results.md` forced-flip row | 4: spec 2.1 text and a line in the Devnet 2 gate | Miner and rig: no flag day crossed while updating; Pool user: nothing | Spec text; the first step's rehearsal on Devnet 2 passes the same gate as the class v4 cut |
|
||||
| 7 | Correct `funding.md` section 4's dev-fee ceiling (1% of the producer share, not of all rewards) and add a row that names who pays the SECOND cryptanalysis | 48,000 / 193,000 / 963,000 overstate by a quarter (4.4); no row prices a second review (4.3) | `devfee.py`, `security_budget_10y.py` section 3 | 1 | Holder and critic: a number that matches the mechanism | The file's git history |
|
||||
| 8 | Measure the two numbers every price here rests on: the miner's hash loss while each card proves (4% is one card), and a full 30 M-cycle shard beside the miner on the 12 GB and 16 GB tiers | The hybrid row is the only one that undercuts the market and it rests on one measurement (4.1 a); the 4.7 M fixture is 16% of `S_p` (2.1) | `utility.py` `hybrid_hash_loss` | 6 on the fleet: eleven boxes, two fixtures, `tools/fleet/lib` | Home 12 and 16 GB: whether they are provers at all beside their miner; Rig: the same per card | Eleven rows with both numbers in `prover-tiers-real-cards.md` |
|
||||
|
||||
**1. Decouple the job price from `f_p`.** `f_p`'s floor exists to price spam above the electricity it imposes (base-fee-floor.md section 3: 230x the electricity at USD 0.10 per IGN). Design 6 then prices every external job at `maxPgas x f_p x 1.5`, so the same floor that is 230x electricity for spam is the job market's minimum: 15 IGN per billion cycles, which is a third of Boundless at USD 0.005 and 7x at 0.10. A rollup compares in dollars every week; a 60% vote takes two weeks and a quorum. Lane 7 (frontier 3.5, rank 8) proposes the continuous dial for the floors themselves and it would help; this proposal is narrower and independent of it: the job reserve is the electricity, published as a rate the review trigger of spec 5.10.3 already needs ("converted at the window's settlement rate and published with the reading"), and the price above the reserve is the requester's bid against the sortition's assignees. Cost 16 hours. Gate: a simulated job book clearing within 20% of Boundless at all three prices. Per tier: the prover sells at a market price; the rollup customer gets a comparable quote; the holder keeps job demand for IGN through a price rise (at 0.10 and the floor, every rollup leaves); miners, pools and home cards see nothing.
|
||||
|
||||
**2. Define the stranded pool.** Spec 5.3 pays "the first valid proof included in a block"; 7.7 item 3 refuses a record older than 600 chain blocks; 7.8 item 7 says an unproven segment's aggregator share "stays in the escrow". Nothing says what happens to the shard credit nobody claimed. In the refusal scenario (4.2) the whole 20% is stranded for ten days: 5.5 M IGN that reach nobody and that nobody decided to burn. A roll-forward (the next proven segment's pool is larger by what was stranded) makes a refusal a transfer from refusers to returners, which is the incentive the design wants, and makes the pool's total over any month equal to 20% of emission as the litepaper's table promises. 8 hours. Gate on the fast-time harness.
|
||||
|
||||
**3. The prover's price as a formula.** The whole of 4.1 (a) is one line: price per billion cycles = the subsidy the card forgoes per shard, which is `h/N`. At the devnet's 1.16 GH/s every quote is 100x the market; at 100 GH/s hybrids undercut it. The customer brief's "priced in dollars per proof" and the litepaper's "proofs at the cost of power" need the condition beside them, or the first customer conversation ends with the number. 3 hours of text.
|
||||
|
||||
**4. The 80/20 as a bounded parameter, and one sentence for the thresholds.** The 4 October lever study found the pool share not load-bearing at launch traffic and useful at 30% under heavy traffic; tonight's runs (4.2) agree. A band of 10 to 30% lets miners trade lottery for proving capacity when the traffic says so, and the band stops the lottery's 80% from voting the provers out. The same change should carry the three signalling numbers in one place; today a reader finds 90 in spec 5.7 and CLAUDE.md, 95 in P2 and the preamble, 60 in 5.5, and "a 90% miner signal turns one on" in the litepaper's Mining section about a class change the P2 rule sets at 95.
|
||||
|
||||
**5. Signalled execution parameters enter the digest, by CI.** Blocks carry transactions only. A node that signalled a fee change and runs the old rule accepts every block and computes a different state; its proof records fail everyone else's statement and everyone else's fail its own, which is loud for provers and silent for a wallet. `Params.fees` is in the digest and the handshake refuses a different digest, so today the defector is cut off. The next signalled parameter has no such guarantee until a check fails without it. 6 hours.
|
||||
|
||||
**6. Block-rate steps as signal-plus-floor.** Spec 2.1 names the steps "a planned fork with its own test campaign, as Kaspa's Crescendo". Crescendo was a fixed DAA score (`params.rs` line 648) and Igneum's own fixed height cost it a 229-block reorg on 6 October. P2 exists; the steps should use it. 4 hours of text and a gate line.
|
||||
|
||||
**7. The funding corrections.** One number and one row. 1 hour.
|
||||
|
||||
**8. Measure the two numbers.** The hybrid row is the only competitive one and it rests on the 5090's 4% and a fixture a sixth of a full shard. Six hours on the fleet, through `tools/fleet/lib`, eleven boxes.
|
||||
|
||||
Cross-references to lane 7 by name and rank: 3.1 (rank 7, the rental tax) would move 25 to 75% of a spiking block's subsidy to the pool; against 4.3's constant 11.8x ratio it doubles the renter's break-even and does not change the year the absolute cost gets small. 3.2 (rank 2, work-stake) removes the coin bond this lane's job model carries; the numbers here do not depend on the bond's form. 3.5 (rank 8, continuous dials) is the general form of proposals 1 and 4 here. 3.6 (rank 11, burn bounties) is option 5 of 4.3 and is rejected on the same ground. 3.11 (rank 15) and 3.12 to 3.14 are the market-size and verifiable-compute ceilings this lane's demand grid sits under. I7 (equivocation bounty in sortition slots) is the one treasury-less incentive in lane 7 that this lane's stranded-pool rule could fund without coins: stranded credit to the evidence carrier is a variant worth one line in the ledger, not a proposal here.
|
||||
|
||||
---
|
||||
|
||||
## 6. Open questions and what I could not run
|
||||
|
||||
- **The full-shard beside-the-miner times** on every tier (proposal 8). Linear scaling from the 4.7 M fixture says 92 s alone on a 3060 and 240 s beside the miner; if that holds, no 12 GB card meets the 120-s claim timeout beside its miner and the 25-s window is for 24 GB cards and up. The fleet was on the class v4 rehearsal tonight.
|
||||
- **The hash loss while proving on Ampere and Ada**: the 5090's 4% is Blackwell with 32 GB; the 8 GB cards showed 6%; the 12 to 24 GB tiers are unmeasured and the hybrid row of 4.1 (a) moves with them.
|
||||
- **Price elasticity of job demand**: every demand count is an assumption. The customer brief's "low millions a year" is the only market figure and it is approximate.
|
||||
- **The rental market's supply curve**: 0 of 20 pods at the TH/s scale on 6 October; the attack costs assume the hash can be had at the measured price, which the bench entry says it cannot above about 2 GH/s.
|
||||
- **The Devnet 2 block-rate runs** (RUN_A, RUN_B) were empty at writing; a 10 BPS chain changes shards per segment, records per block and the per-block fee step, and 4.1 (e) should be re-read when they land.
|
||||
- **The economy simulator's price process** is exogenous; burns do not move it (4.2's burn is a number, not a feedback).
|
||||
- **BIP 8 and BIP 9 texts, the Ethereum specs, Monero's reward code, Aleo, Boundless and Succinct** are cited by repository and path from memory or from the project's earlier readings and are marked approximate throughout; no clone exists in `vendor/`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary for the coordinator
|
||||
|
||||
Lane 4 turned the eleven measured cards into a price per proof, built five demand curves with dollars and burn, re-ran the economy simulator with the measured table and the measured rental price under eight stresses, extended the security budget ten years with those fees, priced the dev fee and the signalling game, and tabulated what the other chains did. Three findings:
|
||||
|
||||
1. **The proving price is `h/N`, not "the cost of power".** Electricity is under a cent per billion cycles on every card; the price a prover must charge is the subsidy it forgoes, which is 100 to 300x Boundless's USD 0.21 at today's 1.16 GH/s and 0.2 to 0.4x at 100 GH/s for a card proving beside its miner (`utility.py` 1.3). And the adopted floor prices a job at 15 IGN per billion cycles, USD 0.075 / 0.30 / 1.50 at the three prices: above USD 0.014 per IGN the chain overprices the market by rule, and a two-week vote cannot follow a dollar market (proposal 1).
|
||||
2. **Fees are not a security budget for a decade.** All five uses together put USD 450 a day to miners and provers at launch and USD 4,200 in year 5 in the base scenario at 0.02 (`utility.py` 3.3) against USD 54,800 and 13,700 of daily emission; of the 48% fee share in year 5 under 1% reaches miners. The 20-day 34% weight attack costs 11.8x the honest fleet's 20 days at every price and year (`security_budget_10y.py`); its absolute net cost drops under USD 1 M in year 5 at 0.005 and year 9 at 0.02. The second audit has no payer by rule: the entity's own provers (USD 100 k a year at 5% of the pool, year 3, 0.02) are the only line that scales.
|
||||
3. **The 80/20 survives every stress but one, and that one strands the pool.** With the eleven measured cards, the 25-s window and a renter farm at the measured rent, T1 to T5 hold under price x10 and /10, external zero and x100, a 20% proving cartel and the first halving (`stress.py`, 8 scenarios x 3 seeds; hash troughs at 55% of pre-event under the /10 shock with 31% of cards off, the one row at the T1 line). A ten-day refusal by every prover breaks T4 only, and what it costs is 547,570 IGN a day of pool credit stranded in the escrow with no rule to return it (5.5 M IGN over the ten days): a silent supply cut nobody voted for (proposal 2). The renter farm is off in every row but the x10 price shock (margin +114%) and partly on under x100 external demand (-6%), which is lane 3's N_eq in an agent model.
|
||||
|
||||
Rules for main: the customer brief and the litepaper's "proofs at the cost of power" need the `h/N` condition before any customer conversation (proposal 3); `funding.md` section 4's dev-fee ceiling is a quarter too high (the fee moves the producer payout only); the three signalling thresholds are stated inconsistently across spec 5.7, CLAUDE.md, the P2 design and the litepaper's Mining section; and the spec is silent on pool credit nobody claims (proposal 2).
|
||||
389
docs/analysis/horizon/finality-and-weight.md
Normal file
389
docs/analysis/horizon/finality-and-weight.md
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
# Horizon lane 3: finality and weight
|
||||
|
||||
Date: 6 October 2026, evening UK (written 19:30Z to 21:00Z, while the live devnet's finality was paused). Lane: finality-and-weight (the measured behaviour of the weight rule and the designs that extend it; lane 1 holds the attack catalogue and the 51 percent paper, cross-referenced by name). Worktree: `/Users/joshm/Projects/igneum-wt-horizon` (branch `horizon`). Models: `sim/horizon/finality-and-weight/` (README there says how to run every number).
|
||||
|
||||
What was read: `docs/spec/03-finality.md` (whole, 3.11 included), `04-seeds-and-vdf.md`, `10-light-client.md`, `06-open-items.md` (O-3.1 to O-3.19), the fud-close worktree's `docs/spec/03-finality.md` 3.4.2 (the proposed vote and bitmap bounds, decided 6 Oct 2026 per `docs/plans/ledger-decisions.md` line 57); `docs/fud-ledger.md` F1 to F25 (F9, F14, F16, F18, F19, F20, F21 via its status lines, F22, F23, F24), P3, P4, P22, X20; `sim/README.md`, `sim/results_v2.md` (A to M), `sim/finality_v2.py` (this worktree's and fud-close `1544c63` with the block reading and scenario O); `docs/benchmarks/finality-v3-2026-10-04/` (fold-v2, fold-v3, split50-v2, split50-v3, split70-v3), `docs/benchmarks/round4-consensus-2026-10-04/results-final2.md`; `tools/finality-attacks/README.md`; `docs/plans/finality-v3-rollout-devnet.md`, `finality-v3-devnet-publish.md`; `docs/bench-log.md` entries of 4 to 6 October mentioning finality (floor 2/3, first live lock, rule v3, the C4 fix, round-4 items, the finality route, the rental cost of hash at line 2582); the gpu-fleet worktree's `docs/bench-log.md`, `docs/plans/`, `tools/fleet/` (grep for finality, lock, pause, voters, weight: the fleet has written no lock-delay or voter-count row yet; `docs/analysis/block-rate-devnet2.md` is still the template with RUN_A and RUN_B empty at 19:45Z), `docs/analysis/prover-tiers-real-cards.md`; the observer database (read-only SELECTs over `live_checkpoints`, `live_certificates`, `live_blocks`, `live_events`, `live_state`), node 1's log `/tmp/igneum-devnet/node1.out` on the Mac; `vendor/igneum-node` `Cargo.toml` and `consensus/core/src/finality.rs` for the BLS crate; the SP1 6.8.1 crates in the cargo registry (no SP1 clone exists under `vendor/`).
|
||||
|
||||
## 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 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.
|
||||
|
||||
## 2. Method
|
||||
|
||||
Measured: the observer's Neon database (tables written by `tools/observer/observer.mjs`: `live_checkpoints` per index with state, signed and total weight, votes seen and voter count; `live_certificates` with the voter table and bitmap per certificate; `live_blocks` with `vote_key_hash` per block; `live_events`), read with SELECTs only through a scratchpad script (`fetch` to the Neon SQL endpoint, refusing any statement that is not SELECT; the queries are quoted inline). Node 1's log on the Mac for determination-to-lock delays (the `determined` and `LOCKED` lines per index; the log ends at 18:46:32Z when node 1 stopped). The departed keys were matched from certificate voter tables (48-byte public keys) to block producers (`vote_key_hash`) with BLAKE2b-256 keyed by `IgneumVoteKeyHash` (the fork's domain, `consensus/core/src/finality.rs`), 93 of 93 keys matched.
|
||||
|
||||
Simulated: `sim/horizon/finality-and-weight/finality_horizon.py`, a copy of `sim/finality_v2.py` (fud-close `1544c63`) with four candidate rules and three scenarios (T, P, Q), run on igneum-build-1 (`/srv/builds/horizon-finality-and-weight/sim/`, Python 3.12, numpy 1.26.4, `nice -n 19`, one process per candidate, seeds 7, 11 and 13, about 25 minutes wall while the box carried other agents' builds at load 20 to 60); the smoke run on the Mac under `tools/lock/with-lock.sh run`. The model is the one `sim/results_v2.md` describes (1,000 Pareto keys, three regions, 2-s inter-region delay, 97 and 99.5 percent uptime, no DAG) at mainnet scale (30-day window); the devnet's window is 7,200 DAA s, so a mainnet day is four devnet minutes. Arithmetic scripts: `weight_capture.py`, `lightclient_cost.py`.
|
||||
|
||||
Not run: the fast-time node harness (`tools/finality-attacks`) for the leave message (no such message exists in the node); any BLS timing on this machine (the figures are approximate from the crate's published benchmarks, anchored to the one measured pure-JavaScript verifier); the fleet's block-rate run (its file was still a template at 19:45Z).
|
||||
|
||||
## 3. Evidence
|
||||
|
||||
### 3.1 Tonight's pause, from the observer rows and node 1's log
|
||||
|
||||
Times UTC. DAA scores advance about 1 per second on the live devnet. The observer's node is the view throughout; "votes" are the votes that node had seen for the index.
|
||||
|
||||
| When | What | Source |
|
||||
|---|---|---|
|
||||
| 17:20 to 17:55Z | Four keys mine their last blocks on the live chain (92376b1a 105 blocks of the later frozen table, d1ee753c 22, 25dbfb0b 155, c4eb3431 131: 413 blocks, 5.8 percent) | `live_blocks`, max(received_at) per key before 18:43Z |
|
||||
| 18:27 to 18:30Z | Thirteen fleet keys mine their last blocks (d5996917 248, 39d2dafc 246, 52d9d8c8 236, 3ca93140 227, 8debbb2d 219, 92dabf9d 216, f155fac3 215, 6cd0935e 212, 0cf69e5c 208, 11047080 200, b42641ab 144, cf860e4d 121, 2aaa021b 91: 2,583 blocks, 36.5 percent of the frozen table): the class v4 rehearsal job stopping their miners | `live_blocks`; CLAUDE.md 6 Oct rules |
|
||||
| 18:30:02Z | Node 1's last own lock, 6823 (DAA 208,631), 78 signers, 68.7 percent of total; node 1 and the observer go down with the desktop app until 18:42:08Z (`Observer reconnected to the node`) | node1.out; `live_events` |
|
||||
| 18:30 to about 18:39Z | Locks 6824 to 6842 form without the hub (DAA 208,660 to 209,202): 83 signers, 78.5 to 79.7 percent of total; aggregators 3ca93140, 8debbb2d, 69570532 and the zero fallback | `live_checkpoints` (ingested at 18:42:09Z), `live_certificates` bitmaps |
|
||||
| about 18:39:40Z | The last lock, 6842 at DAA 209,202, 79 of 91 signers. Its frozen table (the voter table at 6850 the observer stored with it): 93 keys, 7,083 blocks; the 20 departed keys hold 3,026 (42.7 percent), the stayers 4,057 (57.3 percent) | `live_certificates` 6842 voters, matched to `live_blocks` |
|
||||
| about 18:40Z | 6843 at DAA 209,233 determined and never locked: 75 votes, 3,757 of 7,080 = 53.1 percent of total. The departed boxes' nodes have left the live chain (their blocks had already stopped), the signing weight is under two thirds: the rule pauses | `live_checkpoints` |
|
||||
| 18:42:08Z | The observer reconnects and the pause becomes visible on the hub; node 1 ingests 6828's certificate by gossip (`70.3% of the table frozen at lock 6827`) | `live_events`, node1.out |
|
||||
| about 18:50Z | Twenty indices without a lock (6862, DAA 209,800): `finality_active` false, reason `paused` (spec 3.9) | `live_checkpoints` |
|
||||
| 18:58 to 19:03Z | Votes seen fall to 49 (48.5 percent): five more keys quiet for five minutes (the hands' own restarts, approximate) | `live_checkpoints` 6876 to 6885 |
|
||||
| 19:14:53Z | 6912 at DAA 211,301: 79 votes, 5,355 of 7,152 = 74.9 percent of the SLIDING table, over two thirds; no lock. The stayers hold 57.3 percent of the table frozen at 6842 (Q5), under two thirds: "held by the frozen table" | `live_checkpoints`; spec Q5 |
|
||||
| 19:29Z (write-up) | Still paused: 6938 proposed at 79.2 percent with 78 votes. Expected first lock when the frozen table expires at DAA 216,402 (209,202 + 7,200), about 20:40Z, or when departed keys holding 9.4 points of the frozen table return | `live_checkpoints`; arithmetic |
|
||||
|
||||
Under rule v2 (sliding table only) the stayers' share rises as the departed blocks age out: from 53.1 percent at 6843 to two thirds after 7,200 x (1 - 1/(3 x 0.469)) = 2,082 DAA (spec 3.3.1's churn formula at the devnet window), which is checkpoint 6912 at 19:14:53Z: a 35-minute pause. Under v3 it is one window: 2 hours here, 30 days on mainnet (spec 3.7 item 2; `sim/results_v2.md` M4). The coordinator's working hypothesis of 19:3xZ (topology: the hub was down and the fleet's votes could not reach the VRF-picked aggregators) is refuted by three rows above: certificates formed while the hub was down (6824 to 6842), the zero-aggregator fallback of Q4 is in routine use (64 of the 251 certificates stored between 16:30 and 19:00Z name aggregator `00000000`, the next most frequent key 34), and the pause began at the checkpoint where the signing weight fell to 53.1 percent, which is the rule's threshold and not a routing failure. The observer's node itself held 74.9 percent of the sliding weight in votes from 19:14Z and did not certify, which only Q5 explains.
|
||||
|
||||
Per tier: a home miner, rig or pool user on the live devnet saw `finality_active` false for 2 hours and lost nothing (blocks, execution and payouts continued; the exchange guidance of 3.9 applies); a prover's records were still paid; the fleet operator learned that a standing box never leaves the live chain for an experiment (CLAUDE.md, the standing-fleet rule). On mainnet the same event, 43 percent of weight leaving in three minutes, is a 30-day pause under v3 and a 7.7-day one under v2.
|
||||
|
||||
### 3.2 Lock delay against voter count (measured)
|
||||
|
||||
Determination-to-lock on node 1 (the `determined` and `LOCKED` lines per index, 6 October 2026, per UTC hour; voter counts from the observer's `live_checkpoints` for the same hour).
|
||||
|
||||
| Voters above dust | UTC hours | Locks | Delay p50 | p90 | p99 | Max | Source |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 4 to 9 | 01 to 06 | 119 to 120 per hour | 0.86 to 0.97 s | 1.17 to 1.31 s | 1.53 to 1.80 s | 1.59 to 2.29 s | node1.out |
|
||||
| 17 to 24 | 07 to 11 | 118 to 120 | 0.82 to 0.97 s | 1.09 to 1.29 s | 1.53 to 1.83 s | 2.18 s (the 111-s p90 of 08Z is a restart) | node1.out |
|
||||
| 24 to 30 | 12 to 14 | 104 to 122 | 0.94 to 1.32 s | 1.36 s (quiet hours) | 48 s (restarts) | | node1.out |
|
||||
| 43 | 16 | 54 (hour cut by a restart) | 0.92 s | 1.17 s | | | node1.out |
|
||||
| 63 | 17 | 124 | 1.13 s | 1.44 s | 1.47 s | 8.8 s | node1.out |
|
||||
| 92 to 93 | 18 | 125 (to 18:30Z) | 1.26 s | 1.50 s | 1.82 s | 1.82 s | node1.out |
|
||||
| 6 (fast time, 300-ms proxied links) | 4 Oct | 11 to 12 per node | 1.008 s | | | | `docs/benchmarks/finality-v3-2026-10-04/fold-v3.md` |
|
||||
| 12 (cloud devnet, 5 locations) | 4 Oct | 212 indices | 1.24 s to the first certificate (p99 1.71 s), the last vote 1.45 s (p90 2.36 s) | | | | ledger F22, `infra/cloud-devnet/results/2026-10-04/f22-vote-timing.md` |
|
||||
| 1,000 (simulator, 2-s inter-region delay) | | 172,883 | 2.5 s | | 4.6 s | 6.1 s | `sim/results_v2.md` A |
|
||||
|
||||
The devnet's delay is the miner's 1-s template poll plus one gossip round (the 250-ms gossip pump per hop, ledger F22): 0.9 s at a handful of voters, 1.26 s at 93. A straight line through the devnet rows is 0.9 s + 4 ms per voter (approximate; 93 points on one topology), which puts 1,000 voters near 5 s and 8,192 near 34 s, past the 30-s interval. Section 4.3 says why that line does not hold and what does.
|
||||
|
||||
### 3.3 Sizes and crates (from the fork)
|
||||
|
||||
| Item | Value | Source |
|
||||
|---|---|---|
|
||||
| BLS crate | `blst = "0.3.17"` (workspace), min-pubkey variant: 48-byte G1 keys, 96-byte G2 signatures | `vendor/igneum-node/Cargo.toml` line 238, `consensus/core/Cargo.toml` line 20, `consensus/core/src/finality.rs` line 17 (`blst::min_pk`) |
|
||||
| Aggregation and verification | `AggregateSignature::aggregate` over the votes' signatures (line 230); `fast_aggregate_verify` over the summed public keys and one vote message (line 239 to 253) | `consensus/core/src/finality.rs` |
|
||||
| Vote item | 281 B (tag 1, index 8, checkpoint 32, key 48, signature 96, sortition proof 96) | spec 3.4.2 item 1 (fud-close), the fork's `Vote::LEN` |
|
||||
| Certificate | 273 B plus ceil(V/8) B of bitmap: 285 B at 93 voters, 398 at 1,000, 1,297 at 8,192, 8,465 at 65,536 | same |
|
||||
| Bitmap wire bound | 1 MiB today (`Certificate::read`, `bitmap_len > 1 << 20`, line 429); 8,192 B proposed and decided (65,536 voters, 8x the S2 switch) | finality.rs; spec 3.4.2 item 3; ledger-decisions line 57 |
|
||||
| Per-block vote bound | 48 on the devnet; 384 on mainnet proposed and decided (107,904 B, 21.6 percent of the compute mass; a checkpoint's 8,192 votes drain in 21.3 blocks) | spec 3.10 Q1/Q2 row; 3.4.2 item 2 |
|
||||
| Votes per checkpoint, single-vote carriage | 93 voters: 26,133 B; 1,000: 281,000 B; 8,192: 2,301,952 B (4.6x one block's mass); 65,536: 18.4 MB | 281 x V |
|
||||
| Votes per checkpoint, aggregated carriage (Q2 allows one BLS signature plus a bitmap per (index, hash)) | about 0.4 KB at 93 voters, 1.2 KB at 8,192, 8.6 KB at 65,536 | spec 3.4.2 item 2 |
|
||||
|
||||
### 3.4 What the simulator already measured (cited, `sim/results_v2.md`)
|
||||
|
||||
| Fact | Value | Section |
|
||||
|---|---|---|
|
||||
| Churn under v2: first lock after a set holding x stops mining and signing | 35 percent: 1.7 days (analytic 1.4); 50 percent: 10.1 to 10.3 days (analytic 10.0) | L2, D |
|
||||
| Churn under v3 | 30.00 days at 35 and 50 percent (the frozen table's expiry) | M4 |
|
||||
| Silent set that keeps mining | 34 percent and above: no lock for as long as it is silent; first lock 0 min after it returns | J, L1 |
|
||||
| Equivocator across a 50/50 split | 33 percent: 0 conflicts; 34 percent: 2 to 54 conflicts from minute 2 to 77 (the one-third bound) | H, M5 |
|
||||
| Long honest partition, view-local weight, v2 against v3 | 50/50: both sides lock alone from day 10.1 to 10.3 under v2, never in 12 days under v3, both at day 30.00 of a 31-day split | L4, M2, M3 |
|
||||
| Acquired keys worth 40 percent, attacker at 30 percent of hash | veto from day 1 to day 19 or 20, 30 percent on day 30; silent, 63,307 to 68,716 of 86,400 checkpoints stalled | K |
|
||||
|
||||
## 4. Model
|
||||
|
||||
### 4.1 The pause arithmetic (spec 3.3.1 and 3.7, restated with tonight's inputs)
|
||||
|
||||
Let x be the share of the window weight that stops mining and signing at once, W the window (2,592,000 DAA s on mainnet, 7,200 on the devnet).
|
||||
|
||||
| Rule | First lock after the departure | Tonight (x = 0.469 on the observer's node at 6843, W = 7,200) | Mainnet, same x |
|
||||
|---|---|---|---|
|
||||
| v2, sliding table | W x (1 - 1/(3x)) (never for x at or under 1/3) | 2,082 DAA, 35 min: measured as the moment the sliding share crossed two thirds (6912) | 7.7 days |
|
||||
| v3, frozen table (live) | W after the last lock, whatever x over 1/3 | 7,200 DAA, 2 h (expected 20:40Z) | 30 days |
|
||||
| (iv) leave, delay D | D after the signed leave (0 if sent D before the stop) | 1 h, or 0 with notice | 1 h |
|
||||
| (i) decay, grace T, rate r per hour | at most T + (1/r) x (1 - (1 - x)/(2x)) hours: the departed weight decays until the stayers hold two thirds of what is left | T 1 h, r 0.5: 1 h 17 min (x 0.469); T 6 h, r 1/24: 20 h | the same hours |
|
||||
| (iii) hysteresis, H hours, low floor f | H hours, then only if the stayers hold f x 2/3 of total | f = 0.85: 56.7 percent needed, the stayers held 53.1 then 57.3 percent: after H plus the ageing to 56.7 percent, 1 to 1.5 h | about 1 day |
|
||||
| (ii) two-tier | provisional at once (2/3 of the active denominator); final as v3 | provisional 0 min, final 2 h | provisional minutes, final 30 days |
|
||||
|
||||
### 4.2 Why the view-dependent candidates fail (and the sim's confirmation)
|
||||
|
||||
Spec 3.11.2's bound comes from counting: two certificates at one index need 2/3 of the denominator each, 4/3 in all, so a third signed both and that third is equivocating. The denominator has to be the same number on both sides of a partition for that sum to mean anything. A rule that removes weight on what a view has not seen (a vote missing for T hours, blocks missing) gives each side a different denominator: side A removes side B's keys, side B removes A's, and both sides' own share rises toward 1 at the same rate. For a 50/50 split under decay(T, r) each side's own share reaches 2/3 when the other side's factor is 0.5, at T + 1/(2r) hours (2 hours at T 1 h, r 0.5; 18 hours at T 6 h, r 1/24), and every checkpoint after that is a conflicting lock, the hazard of `sim/results_v2.md` E in a new coat. The frozen table does not save it when the decay is applied to the frozen table too (which is the only way decay helps tonight). The hysteresis floor is view-dependent in the same way (each side measures its own connected share), so after H hours both sides run the 0.85 rule and the 13.3 percent equivocator bound of 3 October returns (the sim's 33 percent row under `hyst` shows one side locking at minute 59). A rule that removes weight on what a view has seen, a signed leave carried in the DAG or equivocation evidence, is seen by both sides at the heal and by at most one side during the split; during the split the side that saw the leave removes L from its denominator and needs 2/3 (1 - L) of signers while the other side still needs 2/3 of the full table; both locking needs s_A + s_B at least 2/3 (2 - L), more than the 1 - L available without an equivocator, so the one-third bound survives (with an equivocator a, the bound is a at least (1 - L)/3 of the remaining weight, the same statement over the reduced table).
|
||||
|
||||
Why leaving bought keys buys nothing (Q2 in the sim and arithmetic): an attacker holding w of the window who buys L and makes it leave holds w / (1 - L) of what remains; to lock alone it needs w at least 2/3 (1 - L), so w + L at least 2/3 + L/3, never under two thirds of the window, and signing with the bought keys (w + L at least 2/3) is the cheaper use of the same purchase. In the model the attacker's share after leaving its bought 40 percent was 4.8 percent on day 3, the position of its own hash alone.
|
||||
|
||||
### 4.3 Lock delay as a function of voters and message delay
|
||||
|
||||
delay = template poll (1 s on the devnet; the node's own determination on mainnet, 0) + hop_1 (block to voter) + hop_2 (vote to aggregator) + processing + certificate gossip (one hop). The simulator's two hops at Delta give median 0.7 s at 0.5 s, 2.5 s at 2 s, 6.2 s at 5 s (A); the devnet's 0.9 s is the poll plus a 250-ms pump. The per-voter term is the aggregator's verification of each vote as it arrives: one BLS verify is a pairing, about 1.6 ms (approximate, blst 0.3.17 published figures; the fork verifies each vote on ingest, `verify_vote_signature`, finality.rs line 192), which is 150 ms per checkpoint at 93 voters, 1.6 s at 1,000, 13 s at 8,192 and 105 s at 65,536 on one core: past 1,000 voters the single-vote path is a CPU bound before it is a bandwidth bound, and at 8,192 it is more than a quarter of every core's time on every node (every node verifies every vote it relays). The fix is already in the spec's text (Q2 aggregated carriage) and in the crate (`AggregateSignature::aggregate` then one `fast_aggregate_verify`): aggregate first, verify once per (index, hash), which costs V G1 additions (about 1 us each) plus one pairing: 1.7 ms at 93, 2.6 ms at 1,000, 10 ms at 8,192, 67 ms at 65,536. Its price is the batch-poisoning vector (one invalid vote fails the batch and forces bisection); S2's sub-user sortition at 8,192 bounds the signer count at about 4,000 expected either way.
|
||||
|
||||
| Voters | Vote bytes per checkpoint (single) | Verify per checkpoint, single votes (approximate) | Aggregate-first (approximate) | Lock delay, model (Delta 2 s) | Bitmap |
|
||||
|---|---|---|---|---|---|
|
||||
| 12 | 3.4 KB | 19 ms | 1.6 ms | 2.5 s (sim A, 1,000 keys) ; 1.24 s measured | 2 B |
|
||||
| 40 | 11 KB | 64 ms | 1.6 ms | about 2.5 s | 5 B |
|
||||
| 100 | 28 KB | 160 ms | 1.7 ms | about 2.6 s; 1.26 s measured at 93 on the devnet | 13 B |
|
||||
| 1,000 | 281 KB | 1.6 s | 2.6 ms | about 4 s single, 2.5 s aggregated | 125 B |
|
||||
| 8,192 | 2.3 MB (4.6x block mass) | 13 s per node per checkpoint: breaks the 30-s cadence on a shared core | 10 ms | 2.5 s aggregated; S2 switches to about 4,000 sub-users here | 1,024 B |
|
||||
| 65,536 | 18.4 MB | 105 s: impossible single | 67 ms (3.9 s of key decompression once) | 2.5 s aggregated | 8,192 B, the proposed wire bound |
|
||||
|
||||
Where the aggregator path breaks down: not at the 8 VRF-picked aggregators (anyone MAY aggregate, Q4's fallback at 15 DAA is in routine use tonight: 26 percent of certificates) but at per-vote verification above about 1,000 voters and at the per-block vote carriage above 8,192 (spec 3.4.2 item 2's 384-per-block bound drains a checkpoint in 21 blocks; participation accounting lags, locks do not, because certificates form from gossiped votes). The message-delay term scales the two hops and nothing else; at 5 s inter-region delay the slowest region already loses participation to the 15-s grace (A).
|
||||
|
||||
### 4.4 Weight capture cost (task 3; `weight_capture.py`)
|
||||
|
||||
share(t) = (t/30) x A/(N + A) for A rented against N for t days (spec 3.1, sim B within 0.04 points). To hold W at day t: A = N q/(1 - q), q = 30W/t (needs t over 30W). Cost = A x t x 24 x USD 11.7 per GH/s-hour (measured 6 Oct 2026, bench-log "Rental cost of hash": 1,748 MH/s for USD 20.44/h on RunPod community pods; the 8x 4090 rig USD 5.92/h for 459 MH/s). Cost falls with t, so the cheapest attack takes the full window: A = N W/(1 - W), cost = 8,424 x N x W/(1 - W) USD per GH/s of network.
|
||||
|
||||
| Target | Hash to rent | Day noticed (the chart step) | N = 1 GH/s | N = 10 GH/s | N = 100 GH/s | N = 1 TH/s |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 34 percent in 30 days (veto) | 0.52 x N | day 1: +52 percent | USD 4,300 | USD 43 k | USD 434 k | USD 4.3 M |
|
||||
| 34 percent in 21 days | 0.94 x N | day 1: +94 percent | USD 6 k | USD 56 k | USD 557 k | USD 5.6 M |
|
||||
| 51 percent in 30 days | 1.04 x N | +104 percent | USD 8.8 k | USD 88 k | USD 877 k | USD 8.8 M |
|
||||
| 67 percent in 30 days (locks alone) | 2.03 x N | +203 percent | USD 17 k | USD 171 k | USD 1.71 M | USD 17.1 M |
|
||||
| 67 percent in 25 days | 4.10 x N | +410 percent | USD 29 k | USD 288 k | USD 2.9 M | USD 28.8 M |
|
||||
| 67 percent in 22 days | 10.6 x N | +1,058 percent | USD 65 k | USD 654 k | USD 6.5 M | USD 65 M |
|
||||
|
||||
What the market supplies: RunPod gave 0 of 20 pods asked at 18:59Z to 19:15Z (bench-log); 38 pods were 1.75 GH/s. So at tonight's 1.16 GH/s devnet every row of the first column is a dinner; at 100 GH/s the 52 GH/s for a veto did not exist on the one market asked (approximate). The alarm that sees the step is lane 1's detector.
|
||||
|
||||
Buying old keys (F19): a key is a 32-byte scalar named in headers by `vote_key_hash`; it can be handed over, W5 succession moves its history once (not implemented, O-3.11), and the seller can keep a copy. Its worth is its blocks: keys worth b of the window are the position of having rented b/(1 - b) x N for 30 days (USD 2,100 per GH/s of network at b 20 percent, 4,300 at 34, 5,600 at 40), and that position decays as b (1 - t/30) + r t/30 (sim K, within 0.6 points). What makes weight unbuyable: nothing in the protocol. What makes bought weight a bad buy: it ages out in 30 days whatever the buyer does, a seller's copy can equivocate it away (3.6), and a pool's key is its payout identity, so the price is the pool. What the header does not do: it does not tell anyone the key changed hands until its blocks stop matching its old profile (the detector's job). A rule that would make it harder, decaying a key whose block profile breaks, is view-dependent in a partition (section 4.2) and is not recommended.
|
||||
|
||||
Per tier: a home miner's or rig's key (one per machine under 3.4.2 item 4) is worth its 30 days of blocks and nothing a buyer would pay for; a pool's key is the only one worth buying and the only one whose sale is visible; a holder's finality rests on the 2/3 of weight no one can rent cheaply past a few GH/s; a rollup customer's bridge inherits the same bound.
|
||||
|
||||
### 4.5 Long-range and checkpoint sync for light clients (task 4; `lightclient_cost.py`)
|
||||
|
||||
What a node joining after 60 days trusts (spec 10.1 and 10.3): the trusted checkpoint shipped in its release, refreshed from N of M seed nodes (M 5, N 3, O-10.5), and from there every certificate it fetches is verified against the voter set, which in checkpoint mode it takes from nodes (N of M agreement) and in full-header mode recomputes from 30 days of headers (W2). The cold-sync node of X20 selects the heaviest DAG then follows certificates found in it (F5), so its first 30 days of history are proof of work in the sense of 3.9. The weak-subjectivity window Igneum has in fact is one weight window: a certificate older than 30 days can be checked only against a voter table the client cannot recompute from less than 30 days of headers, and under v3 the frozen table expires 30 days after a lock, so a node offline longer than 30 days cannot tell a certified chain from a chain certified by keys that have since left the window; the same class of assumption as Ethereum's weak-subjectivity period for a sync-committee checkpoint (not cloned here; approximate), with the window the parameter.
|
||||
|
||||
| Mode, per year of chain | 93 voters | 1,000 | 8,192 | 65,536 | Source |
|
||||
|---|---|---|---|---|---|
|
||||
| Every certificate (1,051,200) plus its header, bytes | 720 MB | 839 MB | 1.78 GB | 9.3 GB | 285 to 8,465 B per certificate plus 400 B header |
|
||||
| Verify time, one laptop core (approximate: V G1 adds plus hash-to-G2 plus two pairings, 1.8 to 67 ms each) | 32 min | 48 min | 2.9 h | 20 h | `lightclient_cost.py` |
|
||||
| On a phone core (3x, approximate) | 1.6 h | 2.4 h | 8.7 h | 59 h | |
|
||||
| One certificate per presence window (4,380 a year, spec 10.3 item 3) | 3.0 MB, 8 s | 3.5 MB, 12 s | 7.4 MB, 44 s | 39 MB, 4.9 min | the voter set at each stop is not paid for |
|
||||
| Full-header mode, headers alone | 12.6 GB a year at 400 B per header | | | | |
|
||||
|
||||
The measured anchor: the pure-JavaScript verifier of a 16-signer certificate took 58 to 68 ms warm on the M5 Max (bench-log, sweep round 6, P3), about 30x the native estimate here; a phone has not been measured (O-10.3).
|
||||
|
||||
The ZK light client (phase two, O-10.8), designed: one recursive proof per checkpoint whose step statement is "certificate i verifies under voter table T_i; T_i follows from T_(i-1) by the interval's 30 blue headers and the window's ageing; the signers hold at least two thirds of T_i and of the frozen table; C_i's selected chain passes through C_(i-1)", with the previous step's proof verified inside (SP1's deferred-proof path, `VERIFY_SP1_PROOF` in `sp1-core-executor-6.8.1/src/syscall_code.rs`; the recursion crates are `sp1-recursion-{circuit,compiler,executor,machine,gnark-ffi}-6.8.1` in the cargo registry, no SP1 clone under `vendor/`). What the circuit costs, approximate: key aggregation V x BLS12381_ADD (about 500 cycles each: 46,500 cycles at 93 voters, 0.5 M at 1,000, 4.1 M at 8,192); hash-to-G2 about 0.3 M (SHA-256 is precompiled, the Fp2 arithmetic is BLS12381_FP2_*); the two pairings 10 to 30 M cycles, the dominant term, because 6.8.1 has Fp and Fp2 precompiles for BLS12-381 (ADD, DOUBLE, FP_ADD/SUB/MUL, FP2_ADD/SUB/MUL) and no pairing precompile; 30 BLAKE2b header hashes and the table transition 1 to 2 M (no BLAKE2b precompile). Against the measured shard curve (`docs/analysis/prover-tiers-real-cards.md`: 4.7 M cycles compressed in 4.8 to 14.4 s alone, 10.7 to 37.5 s beside the miner) a 15 to 35 M cycle step is 15 to 100 s alone and 40 to 260 s beside a miner, plus the recursion step measured at 2.2 to 2.5 s idle and 7.9 to 9.7 s beside the miner on the 5090 (bench-log, agg-cost and `chain-pc2-pv1c`). One checkpoint every 30 s therefore needs 1 to 4 proving-only cards (or 2 to 9 mining ones) at it continuously. A Groth16 wrap for the phone is the unbuilt R4 (P3).
|
||||
|
||||
What it buys each tier: a phone wallet verifies one wrapped proof per open (about 400 B, milliseconds once the wrapper exists) instead of a certificate chain and a trusted voter set, and the "voter set: from nodes" status disappears (spec 10.4); a bridge verifies one proof per checkpoint it settles on and never a BLS certificate on-chain (an on-chain BLS12-381 aggregate verify at 1,000 voters is about 1,000 G1 additions and one pairing, which on Ethereum is the point-evaluation and pairing precompile budget, approximate); a rollup customer gets a finality statement its own verifier can check without Igneum's voter list; a node operator pays nothing (full nodes keep the native rule); a prover tier gains a steady job (one proof per 30 s) at the cycle counts above; a home miner with one 12 GB card beside its miner (27 to 37 s per 4.7 M-cycle shard) cannot keep up with a 30-s cadence alone and joins as one of several; a 24 or 32 GB card alone does it in the interval.
|
||||
|
||||
### 4.6 Prover attestations as a second finality leg (task 5)
|
||||
|
||||
Design: a checkpoint locks when (a) its certificate carries two thirds of weight (Q3, Q5) AND (b) proof records covering every chain block in (C_(i-1), C_i] from at least k distinct prover keys are in the past of some block the certificate's signers could see. Measured inputs: the proof lag on the live devnet, block to carried record, p50 44 s, p90 52 s, p99 62 s, max 65 s (bench-log, proving v1 coverage windows, 5 Oct); coverage 2.4 to 4.7 percent of blocks with one prover (the same rows); the chain-mode cost 17 s per empty block on a mining 5090, about 5 s proving-only; a 12 GB card beside its miner 27 to 37 s per v1 shard (prover-tiers); a mandatory rule needs about 6 proving-only 5090s or 18 mining ones for an empty-block chain at 1 block/s (bench-log table), 45 proving-only at B_p.
|
||||
|
||||
| Measure | Weight alone (today) | Weight AND k-prover attestations | Label |
|
||||
|---|---|---|---|
|
||||
| Lock delay after the checkpoint block | 1.26 s at 93 voters (3.2) | at least the slowest block's proof lag inside the interval: p99 62 s today, so about 60 to 70 s; the transaction-to-lock figure of C1 rises from 90 to 120 s to about 150 to 190 s | measured lag, derived sum |
|
||||
| Checkpoints that could lock on tonight's devnet | all with two thirds signing | 2.4 to 4.7 percent (one prover): finality paused 95 percent of the time until proving is mandatory and the fleet is 6 to 18 cards | measured coverage |
|
||||
| What it stops that weight does not | nothing for a full node: it re-executes and vetoes a statement that is not the native one (spec 7.2 item 5, the native veto) | a two-thirds weight holder cannot lock a checkpoint whose execution has no valid proof, which protects the LIGHT client, who trusts certificates and cannot execute (10.1); the design already gives the light client that by requiring the segment proof beside the certificate (10.4 item 4), so the leg moves the requirement from the client into the lock | design |
|
||||
| Withholding to pause | a silent third pauses (L1) | a prover set that withholds proofs pauses finality for as long as no one else proves; the shard sortition names 8 provers by weight with a 10-s exclusive window and then anyone MAY prove (spec 7.2), so the price of a pause is out-proving every honest card for the whole pause, which in a thin market (tonight: one prover at times) is one card's outage | design, measured market |
|
||||
| Per tier | unchanged | a 12 GB card beside its miner proves one 4.7 M-cycle shard in 27 to 37 s, so k = 2 provers per block means 37k mining 12 GB cards (or 10k proving-only 4070s at 12 s) kept busy for an empty chain, approximate; a pool user nothing; a holder a longer wait; a rollup customer the same proof it already needs | prover-tiers, derived |
|
||||
|
||||
Verdict: not as a lock condition now. The leg converts "locked" into "locked and proven" at the cost of a minute of lock delay and a pause whenever proving coverage drops, which tonight is almost always. The design's four-state interface (included, executed, proven, locked; O-7.2) already gives the exchange and the wallet the conjunction as a reading. Gate before it could become a rule: 99 percent of chain blocks proven within 60 s for 7 days on the public testnet with at least 3 distinct provers per block, measured by `tools/proving-v1/coverage.mjs`.
|
||||
|
||||
## 5. Results of the candidate runs (`finality_horizon.py`, seeds 7, 11, 13)
|
||||
|
||||
### 5.1 T. Tonight's departure: first lock after x of weight stops mining and signing at once (31 days, seeds 7, 11, 13)
|
||||
|
||||
| departed weight | rule | first final lock after the departure | provisional tier | stalled checkpoints | stalled after the first lock | conflicting final locks | conflicting provisional locks |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 34% | v2 | 0.77 to 0.87 d (devnet 3 to 3 min) | | 4057 to 4727 | 1531 to 2489 | 0 | |
|
||||
| 34% | v3 | 30.00 d (devnet 120 min) | | 86388 to 86472 | 0 to 31 | 0 | |
|
||||
| 34% | leave | 0.04 to 0.04 d (devnet 0 to 0 min) | | 119 to 521 | 0 to 398 | 0 | |
|
||||
| 34% | decay1 | 0.05 to 0.05 d (devnet 0 to 0 min) | | 133 to 534 | 0 to 398 | 0 | |
|
||||
| 34% | decay6 | 0.30 to 0.30 d (devnet 1 to 1 min) | | 897 to 1298 | 24 to 442 | 0 | |
|
||||
| 34% | hyst | 0.04 to 0.04 d (devnet 0 to 0 min) | | 881 to 1445 | 761 to 1325 | 0 | |
|
||||
| 34% | twotier | 30.00 d (devnet 120 min) | provisional 0.000 to 0.004 d (devnet 0.0 to 0.0 min) | 86388 to 86472 | 0 to 31 | 0 | 0 |
|
||||
| 45% | v2 | 7.85 to 8.03 d (devnet 31 to 32 min) | | 23947 to 25153 | 1369 to 2065 | 0 | |
|
||||
| 45% | v3 | 30.00 d (devnet 120 min) | | 86382 to 86420 | 0 to 22 | 0 | |
|
||||
| 45% | leave | 0.04 d (devnet 0 min) | | 119 to 204 | 0 to 83 | 0 | |
|
||||
| 45% | decay1 | 0.07 to 0.08 d (devnet 0 to 0 min) | | 224 to 296 | 9 to 83 | 0 | |
|
||||
| 45% | decay6 | 0.64 to 0.67 d (devnet 3 to 3 min) | | 1946 to 1985 | 0 to 129 | 0 | |
|
||||
| 45% | hyst | 30.00 d (devnet 120 min) | | 86382 to 86420 | 0 to 22 | 0 | |
|
||||
| 45% | twotier | 30.00 d (devnet 120 min) | provisional 0.031 to 0.034 d (devnet 0.1 to 0.1 min) | 86382 to 86420 | 0 to 22 | 0 | 0 |
|
||||
| 50% | v2 | 10.09 to 10.34 d (devnet 40 to 41 min) | | 30107 to 31264 | 1065 to 1456 | 0 | |
|
||||
| 50% | v3 | 30.00 d (devnet 120 min) | | 86387 to 86514 | 0 to 10 | 0 | |
|
||||
| 50% | leave | 0.04 d (devnet 0 min) | | 118 to 490 | 0 to 371 | 0 | |
|
||||
| 50% | decay1 | 0.08 to 0.09 d (devnet 0 to 0 min) | | 244 to 609 | 0 to 371 | 0 | |
|
||||
| 50% | decay6 | 0.76 to 0.77 d (devnet 3 to 3 min) | | 2231 to 2543 | 0 to 371 | 0 | |
|
||||
| 50% | hyst | 30.00 d (devnet 120 min) | | 86387 to 86514 | 0 to 10 | 0 | |
|
||||
| 50% | twotier | 30.00 d (devnet 120 min) | provisional 0.042 d (devnet 0.2 min) | 86387 to 86514 | 0 to 10 | 0 | 0 |
|
||||
|
||||
### 5.2 P1. Partitions of 360 minutes (each side retargets and counts only its own blocks)
|
||||
|
||||
| rule | case | partition min | conflicting final locks | conflicting provisional locks | first conflict, min | first lock per side, min | every pre-heal lock kept | first lock after heal, min | stalls in 2 h after heal |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| v2 | 50/50 honest, 0% attacker | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| v2 | 50/50 + 20% equivocator (sides 60/60) | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| v2 | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 0 | | never | never / 279 (never in 2 of 3) | yes | 0 to 0 | 0 |
|
||||
| v2 | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 142 to 291 | | 14 to 77 | 11 to 48 / 0 to 76 | yes | 0 to 0 | 0 |
|
||||
| v2 | 40/40/20 honest | 360 | 0 | | never | never / never / never | yes | 0 to 0 | 0 |
|
||||
| v2 | 60/40 honest | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| v2 | 70/30 honest | 360 | 0 | | never | 0 to 4 / never | yes | 0 | 0 |
|
||||
| v3 | 50/50 honest, 0% attacker | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| v3 | 50/50 + 20% equivocator (sides 60/60) | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| v3 | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| v3 | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 139 to 283 | | 14 to 78 | 12 to 50 / 0 to 77 | yes | 0 to 0 | 0 |
|
||||
| v3 | 40/40/20 honest | 360 | 0 | | never | never / never / never | yes | 0 to 0 | 0 |
|
||||
| v3 | 60/40 honest | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| v3 | 70/30 honest | 360 | 0 | | never | 0 to 4 / never | yes | 0 | 0 |
|
||||
| leave | 50/50 honest, 0% attacker | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| leave | 50/50 + 20% equivocator (sides 60/60) | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| leave | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| leave | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 139 to 283 | | 14 to 78 | 12 to 50 / 0 to 77 | yes | 0 to 0 | 0 |
|
||||
| leave | 40/40/20 honest | 360 | 0 | | never | never / never / never | yes | 0 to 0 | 0 |
|
||||
| leave | 60/40 honest | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| leave | 70/30 honest | 360 | 0 | | never | 0 to 4 / never | yes | 0 | 0 |
|
||||
| decay1 | 50/50 honest, 0% attacker | 360 | 467 to 473 | | 121 to 126 | 120 to 122 / 121 to 123 | yes | 0 | 0 |
|
||||
| decay1 | 50/50 + 20% equivocator (sides 60/60) | 360 | 528 to 535 | | 93 to 94 | 91 / 92 to 94 | yes | 0 | 0 |
|
||||
| decay1 | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 587 to 595 | | 64 to 65 | 62 / 63 to 65 | yes | 0 to 0 | 0 |
|
||||
| decay1 | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 575 to 612 | | 14 to 62 | 12 to 50 / 0 to 62 | yes | 0 to 0 | 0 |
|
||||
| decay1 | 40/40/20 honest | 360 | 815 to 822 | | 142 to 144 | 140 to 142 / 140 to 140 / 166 to 166 | yes | 0 to 0 | 0 |
|
||||
| decay1 | 60/40 honest | 360 | 435 to 436 | | 142 to 142 | 92 to 96 / 141 to 142 | yes | 0 to 0 | 0 |
|
||||
| decay1 | 70/30 honest | 360 | 403 to 414 | | 154 to 156 | 0 to 4 / 154 to 156 | yes | 0 | 0 |
|
||||
| decay6 | 50/50 honest, 0% attacker | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| decay6 | 50/50 + 20% equivocator (sides 60/60) | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| decay6 | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| decay6 | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 139 to 283 | | 14 to 78 | 12 to 50 / 0 to 77 | yes | 0 to 0 | 0 |
|
||||
| decay6 | 40/40/20 honest | 360 | 0 | | never | never / never / never | yes | 0 to 0 | 0 |
|
||||
| decay6 | 60/40 honest | 360 | 0 | | never | never / never | yes | 0 to 0 | 0 |
|
||||
| decay6 | 70/30 honest | 360 | 0 | | never | 0 to 4 / never | yes | 0 | 0 |
|
||||
| hyst | 50/50 honest, 0% attacker | 360 | 0 | | never | never / never | yes | 0 | 0 |
|
||||
| hyst | 50/50 + 20% equivocator (sides 60/60) | 360 | 565 to 597 | | 60 to 61 | 58 to 61 / 58 to 61 | yes | 0 | 0 |
|
||||
| hyst | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 596 to 603 | | 60 to 62 | 59 to 60 / 60 to 62 | yes | 0 to 0 | 0 |
|
||||
| hyst | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 160 to 342 | | 14 to 60 | 12 to 50 / 0 to 60 | yes | 0 to 0 | 0 |
|
||||
| hyst | 40/40/20 honest | 360 | 0 | | never | never / never / never | yes | 0 to 0 | 0 |
|
||||
| hyst | 60/40 honest | 360 | 0 | | never | 58 to 61 / never | yes | 0 to 0 | 0 |
|
||||
| hyst | 70/30 honest | 360 | 0 | | never | 0 to 4 / never | yes | 0 | 0 |
|
||||
| twotier | 50/50 honest, 0% attacker | 360 | 0 | 589 to 603 | never | never / never | yes | 0 | 0 |
|
||||
| twotier | 50/50 + 20% equivocator (sides 60/60) | 360 | 0 | 653 to 660 | never | never / never | yes | 0 | 0 |
|
||||
| twotier | 50/50 + 33% equivocator (sides 66.5/66.5) | 360 | 0 | 705 to 715 | never | never / never | yes | 0 to 0 | 0 |
|
||||
| twotier | 50/50 + 34% equivocator (sides 67/67, the bound) | 360 | 139 to 283 | 713 to 720 | 14 to 78 | 12 to 50 / 0 to 77 | yes | 0 to 0 | 0 |
|
||||
| twotier | 40/40/20 honest | 360 | 0 | 1056 to 1062 | never | never / never / never | yes | 0 to 0 | 0 |
|
||||
| twotier | 60/40 honest | 360 | 0 | 516 to 564 | never | never / never | yes | 0 to 0 | 0 |
|
||||
| twotier | 70/30 honest | 360 | 0 | 523 to 532 | never | 0 to 4 / never | yes | 0 | 0 |
|
||||
|
||||
|
||||
P2. The poisoned eclipse (a 34% attacker plus a 20% pool; the eclipsed side holds 54% of total)
|
||||
|
||||
| rule | eclipse h | conflicting final locks | conflicting provisional locks | first conflict, min | locks on the eclipsed side | honest-side stalls during | stalls after heal |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| v2 | 1 | 0 | | never | 0 | 0 | 0 |
|
||||
| v2 | 2 | 0 | | never | 0 | 0 | 0 |
|
||||
| v2 | 4 | 0 | | never | 0 | 0 | 0 |
|
||||
| v3 | 1 | 0 | | never | 0 | 0 | 0 |
|
||||
| v3 | 2 | 0 | | never | 0 | 0 | 0 |
|
||||
| v3 | 4 | 0 | | never | 0 | 0 | 0 |
|
||||
| leave | 1 | 0 | | never | 0 | 0 | 0 |
|
||||
| leave | 2 | 0 | | never | 0 | 0 | 0 |
|
||||
| leave | 4 | 0 | | never | 0 | 0 | 0 |
|
||||
| decay1 | 1 | 0 | | never | 0 | 0 | 0 |
|
||||
| decay1 | 2 | 17 to 24 | | 109 to 111 | 22 to 24 | 0 | 0 |
|
||||
| decay1 | 4 | 257 to 264 | | 109 to 111 | 263 to 265 | 0 | 0 |
|
||||
| decay6 | 1 | 0 | | never | 0 | 0 | 0 |
|
||||
| decay6 | 2 | 0 | | never | 0 | 0 | 0 |
|
||||
| decay6 | 4 | 0 | | never | 0 | 0 | 0 |
|
||||
| hyst | 1 | 0 | | never | 0 | 0 | 0 |
|
||||
| hyst | 2 | 0 | | never | 0 | 0 | 0 |
|
||||
| hyst | 4 | 0 | | never | 0 | 0 | 0 |
|
||||
| twotier | 1 | 0 | 19 to 24 | never | 0 | 0 | 0 |
|
||||
| twotier | 2 | 0 | 137 to 145 | never | 0 | 0 | 0 |
|
||||
| twotier | 4 | 0 | 377 to 385 | never | 0 | 0 | 0 |
|
||||
|
||||
|
||||
P3. Long honest partitions with view-local weight, 12 days
|
||||
|
||||
| rule | honest split | days | first lock per side, day | conflicting final locks | conflicting provisional locks | first conflict | every pre-heal lock kept | first lock after heal, min |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| v2 | 50/50 | 12 | 10.15 to 10.18 / 10.12 to 10.34 | 2890 to 3550 | | 10.22 to 10.35 d | yes | 0 |
|
||||
| v2 | 60/40 | 12 | 5.15 to 5.27 / never | 0 | | never | yes | 0 to 0 |
|
||||
| v3 | 50/50 | 12 | never / never | 0 | | never | yes | 0 |
|
||||
| v3 | 60/40 | 12 | never / never | 0 | | never | yes | 0 to 0 |
|
||||
| leave | 50/50 | 12 | never / never | 0 | | never | yes | 0 |
|
||||
| leave | 60/40 | 12 | never / never | 0 | | never | yes | 0 to 0 |
|
||||
| decay1 | 50/50 | 12 | 0.08 to 0.08 / 0.08 to 0.09 | 34146 to 34212 | | 0.08 to 0.09 d | yes | 0 |
|
||||
| decay1 | 60/40 | 12 | 0.06 to 0.07 / 0.10 to 0.10 | 33931 to 34254 | | 0.10 to 0.10 d | yes | 0 to 0 |
|
||||
| decay6 | 50/50 | 12 | 0.77 to 0.77 / 0.75 to 0.78 | 32120 to 32246 | | 0.77 to 0.78 d | yes | 0 |
|
||||
| decay6 | 60/40 | 12 | 0.52 / 0.92 to 0.93 | 31545 to 31873 | | 0.93 to 0.93 d | yes | 0 to 0 |
|
||||
| hyst | 50/50 | 12 | never / never | 0 | | never | yes | 0 |
|
||||
| hyst | 60/40 | 12 | 0.04 to 0.04 / never | 0 | | never | yes | 0 to 0 |
|
||||
| twotier | 50/50 | 12 | never / never | 0 | 34267 to 34418 | never | yes | 0 |
|
||||
| twotier | 60/40 | 12 | never / never | 0 | 34094 to 34381 | never | yes | 0 to 0 |
|
||||
|
||||
### 5.3 Q1. Silent weight that keeps mining for 6 hours, then resumes
|
||||
|
||||
| rule | silent weight | silent hours | first lock after the stop, min | stalled while silent | longest gap, min | first lock after resume, min | conflicting locks |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| v2 | 30% | 6 | 0 | 0 to 40 | 1 to 8 | 0 to 0 | 0 |
|
||||
| v2 | 34% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| v2 | 45% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| v3 | 30% | 6 | 0 | 0 to 40 | 1 to 8 | 0 to 0 | 0 |
|
||||
| v3 | 34% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| v3 | 45% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| leave | 30% | 6 | 0 | 0 to 40 | 1 to 8 | 0 to 0 | 0 |
|
||||
| leave | 34% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| leave | 45% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| decay1 | 30% | 6 | 0 | 0 | 1 | 0 to 0 | 0 |
|
||||
| decay1 | 34% | 6 | 66 to 68 | 134 to 135 | 66 to 68 | 0 to 0 | 0 |
|
||||
| decay1 | 45% | 6 | 108 to 112 | 217 to 229 | 108 to 112 | 0 to 0 | 0 |
|
||||
| decay6 | 30% | 6 | 0 | 0 to 40 | 1 to 8 | 0 to 0 | 0 |
|
||||
| decay6 | 34% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| decay6 | 45% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| hyst | 30% | 6 | 0 | 0 to 40 | 1 to 8 | 0 to 0 | 0 |
|
||||
| hyst | 34% | 6 | 59 to 61 | 120 | 59 to 61 | 0 to 0 | 0 |
|
||||
| hyst | 45% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| twotier | 30% | 6 | 0 | 0 to 40 | 1 to 8 | 0 to 0 | 0 |
|
||||
| twotier | 34% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
| twotier | 45% | 6 | never | 714 to 722 | 360 | 0 to 0 | 0 |
|
||||
|
||||
|
||||
Q2. Acquired keys that sign, stay silent or leave, under v3 + leave 1 h (30 days)
|
||||
|
||||
| bought weight | bought keys | attacker's peak share of the denominator | share at the end | holds the veto (1/3) | stalled checkpoints | conflicting locks |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 40% | sign | 39.8 to 39.8% | 30.0 to 30.0% | day 1 to 19 | 0 | 0 |
|
||||
| 40% | silent | 39.8 to 39.8% | 30.0 to 30.0% | day 1 to 19 | 86402 to 86491 | 0 |
|
||||
| 40% | leave | 30.0 to 30.0% | 30.0 to 30.0% | never | 119 to 144 | 0 |
|
||||
| 49% | sign | 48.5 to 48.6% | 30.0 to 30.0% | day 1 to 23 to 24 | 0 | 0 |
|
||||
| 49% | silent | 48.5 to 48.6% | 30.0 to 30.0% | day 1 to 23 to 24 | 86371 to 86408 | 0 |
|
||||
| 49% | leave | 30.0 to 30.0% | 30.0 to 30.0% | never | 143 to 188 | 0 |
|
||||
|
||||
Under v3 the silent bought 40 percent stalls every checkpoint of the 30 days (86,402 to 86,491), where `sim/results_v2.md` K at v2 measured 63,307 to 68,716: the frozen table keeps the bought weight in the denominator after the sliding table has aged it out, so a silent buyer pauses finality for a window, not until day 19 to 20. The leaving buyer holds 30.0 percent at most (its own hash), never the veto.
|
||||
|
||||
### 5.4 Reading
|
||||
|
||||
What holds. Every candidate keeps 0 conflicting final locks in every honest partition under two thirds (50/50, 40/40/20, 60/40, 70/30 for 360 minutes) except the fast decay, and every candidate conflicts at the 34 percent equivocator (139 to 612 locks in 360 minutes, the one-third bound of 3.11.2, unchanged). The leave rule's rows equal v3's in every partition, eclipse and equivocator case (no key leaves in those scenarios, which is the point: a partition does not sign leaves) and it is the only candidate that both ends tonight's pause in under an hour (0.04 days at 34, 45 and 50 percent: the one-hour delay) and keeps 0 conflicts in the 12-day splits.
|
||||
|
||||
What breaks. The fast decay (T 1 h, r 0.5/h) conflicts in EVERY 360-minute partition, including 50/50 honest with no attacker (467 to 473 conflicting locks, both sides locking alone at minute 120 to 123, exactly T + 1/(2r) = 2 h) and the poisoned eclipse at 2 and 4 hours (17 to 264 conflicts, the eclipsed side locking the attacker's fork after 109 to 111 minutes); it also fails the 12-day splits in 2 hours (34,146 to 34,254 conflicts). The slow decay (T 6 h, r 1/24 per h) passes every 360-minute row because the decay has not started, then both sides of the 50/50 split lock alone at day 0.75 to 0.78 and the 60/40 at 0.52 and 0.93 (31,545 to 32,246 conflicts in 12 days): the hazard moved to the day scale, not removed. The hysteresis floor keeps the honest splits clean (the 60 side of 60/40 locks alone at minute 58 to 61, 0 conflicts) and reopens the equivocator bound: 20 percent across a 50/50 split gives 565 to 597 conflicting locks from minute 60 (the 13.3 percent bound of 3 October is back after H hours), and it does nothing for tonight's 45 percent departure (the stayers' 55 percent is under its 56.7 percent floor: 30.00 days, the same as v3). The two-tier's provisional tier conflicts in every partition and eclipse (516 to 1,062 provisional locks per 360 minutes, 19 to 385 in the eclipses, 34,000 in 12 days) while its final tier equals v3; it is a report of "the connected majority agrees", never a lock.
|
||||
|
||||
The pass line (0 conflicting final locks in every scenario AND tonight's pause under an hour) is met by one candidate: the departure announcement. v2 would have met the hour on the devnet (35 minutes measured, 31 to 32 simulated at 45 percent) and not on mainnet (7.85 to 8.03 days); v3 meets neither (30.00 days, 120 devnet minutes, the frozen table's expiry, as the live chain is showing at the time of writing: 89.2 percent of the sliding table signing at 19:50Z and no lock).
|
||||
|
||||
### 5.5 The aggregation path tonight (the coordinator's question of 19:3xZ)
|
||||
|
||||
Measured: the 8 VRF-picked aggregators are drawn by weight (spec S1, fin-fixes); the fallback (any node, 15 DAA after the determination, aggregator `00000000`) produced 64 of the 251 certificates stored between 16:30 and 19:00Z; locks 6824 to 6842 formed while node 1 and the observer were down; the observer's node received 75 then 79 of 93 voters' votes through its 3 peers during the pause. The star the fleet forms is around the seed (`docs/bench-log.md`, the finality route: "on a Vast box the seed is the only peer"), not the Mac; if the seed fell, a one-peer box would lose blocks as well as votes, and the right fix is a peer floor (at least 4 outbound peers from the address book before a node reports synced), which is lane 5's bandwidth and p2p lane. Model of P(certificate | hub down) under the rule as written: with the fallback, a certificate forms whenever any node connected to two thirds of the signing weight exists, so the probability is 1 for any topology in which votes reach any node; without the fallback and with a star through the hub it is 0 for 8 aggregators or 800. The rule is already the right one; the harness case to add is `tools/finality-attacks` s4's shape (the eclipse) with the hub cut instead of a pool: N boxes peered to the seed and the hub, the hub killed for 300 s, pass line a lock within 2 checkpoints of the cut while two thirds of weight stays connected through the seed, and 0 conflicting certificates at the hub's return. Vote bytes per checkpoint are in 3.3 (26 KB at 93 voters, 2.3 MB at 8,192 single, about 1.2 KB aggregated).
|
||||
|
||||
## 6. Ranked proposals
|
||||
|
||||
| Rank | Proposal | Evidence | Model | Hours | Consequence per tier | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | The departure announcement (candidate iv): a `leave` item (key, DAA score, signature) carried in blocks; D = 1 h after inclusion the key is in no denominator (sliding and frozen) and its votes are invalid; the fleet and app send it on a clean stop | tonight's pause (3.1): 42.7 percent left in three minutes and the frozen table held finality for a window; sim T: first lock 1 h after the departure at 34, 45 and 50 percent, 0 conflicts in every P row, Q2: leaving bought keys gains the attacker nothing | section 4.2 arithmetic; `finality_horizon.py` `leave` | 6 (spec text 3.1 W7 and 3.3; node: the item, its carriage, `voters_at` and `frozen_table` exclusion, unit test; app and fleet library: send on stop; fast-time harness case) | home miner, rig: the app sends the leave on Stop, so a clean exit never holds the network; a crash still ages out over 30 days (v3) unless the operator sends the leave on return, which the app offers; pool: one leave per server on maintenance; holder: fewer and shorter pauses; rollup customer: the same; node operator: one more item type | harness: 45 percent of weight stops with leaves, first lock within D + 1 checkpoint, 0 conflicts in the 50/50 and 60/40 splits and the 34 percent eclipse; sim T and P rows reproduced on the node |
|
||||
| 2 | Operational rule, no protocol change: a standing box never leaves the live chain for an experiment, and any orchestrated departure over 10 percent of weight is staged in slices under 10 percent an hour | the rehearsal took 36.5 percent at once; sim L2 and M4: a gradual departure costs nothing (every lock re-freezes the table) | spec 3.7 item 2 | 1 (the fleet library refuses to swap a standing box's chain; a `--slice` on the rehearsal script) | fleet operator: the swap takes longer; everyone else: no pause | the next rehearsal: `finality_active` stays true throughout |
|
||||
| 3 | Aggregate-first vote verification and aggregated in-block carriage as the mainnet default (spec 3.4.2 item 2, decided) | 4.3: per-vote verification is 1.6 s per checkpoint per node at 1,000 voters and 13 s at 8,192 (approximate); the crate already has `aggregate` and `fast_aggregate_verify` | 4.3 table | 8 (node: batch the votes for one (index, hash) and verify once, bisect on failure; the in-block aggregate item; measure on the fast-time harness at 1,000 synthetic keys) | home miner, rig, pool: a node that stays under one core at 1,000 voters; node operator: the same; holder: lock delay flat at 2 to 3 s to 8,192 voters | fast-time harness with 1,000 and 8,000 synthetic voters: lock delay p50 under 3 s, CPU under 25 percent of one core, 0 conflicts |
|
||||
| 4 | Report the two-tier state (candidate ii) as `finality_provisional` beside `finality_active`, never as a lock | sim P: 516 to 1,062 provisional conflicts per 360-minute partition, 19 to 385 per eclipse, about 34,000 in 12 days, final 0; sim T: provisional 0 to 0.2 devnet minutes after tonight's departure | 4.1 | 3 (node RPC field, explorer and wallet copy; spec 3.9 row) | exchanges: a third row in the guidance table ("provisional: proof of work plus a majority of the connected weight; credit nothing on it"); a holder sees why the pause is a pause | the explorer shows the field through a forced pause on the devnet; the guidance text reviewed by an operator (O-3.13) |
|
||||
| 5 | The ZK light client's circuit as a phase-two design doc with a cycle measurement | 4.5: 15 to 35 M cycles per step, approximate; the pairing is the term to measure | `lightclient_cost.py` | 10 (an SP1 guest that verifies one certificate at 93 and 1,000 voters with the Fp2 precompiles; cycle count on the 5090 and a 12 GB card) | phone, bridge, rollup customer: the per-year columns of 4.5 become one proof; prover tiers: a steady 30-s job | measured cycles within 2x of the estimate; proof per checkpoint under 30 s on a proving-only 5090 |
|
||||
| 6 | Hub-cut harness case for the aggregation path | 5.5: the fallback carried 26 percent of tonight's certificates; the seed, not the Mac, is the fleet's star | 5.5 | 3 | fleet operator: a proven answer to tonight's question | the case passes as written in 5.5 |
|
||||
| 7 | Do NOT adopt the decaying denominator (i) or the hysteresis floor (iii) | sim P1 and P3 (5.2): decay1 467 to 473 conflicting locks in a 360-minute 50/50 honest split and 34,146 to 34,254 in 12 days; decay6 31,545 to 32,246 in 12 days; hyst 565 to 597 at a 20 percent equivocator from minute 60 | 4.2 | 0 | a holder keeps the one-third bound in every view | none: a negative result |
|
||||
| 8 | Do NOT make prover attestations a lock condition before the coverage gate | 4.6: coverage 2.4 to 4.7 percent tonight, lag p99 62 s | 4.6 table | 0 now; 12 after the gate | holder: no new pause source; rollup customer: nothing lost, the four-state reading exists | 99 percent of blocks proven within 60 s for 7 days, 3 provers per block |
|
||||
|
||||
**1. The departure announcement.** Tonight's cost was a window-long pause caused by keys that left on purpose, under a job that knew it was taking them. The frozen table (F21) exists so that a side of a partition cannot fill its own table, and it does that; its price is that it cannot tell a departure from a partition. A signed leave is the one thing a departing key can give that a partitioned key cannot: it is seen, not inferred. Section 4.2 shows the one-third bound survives it in both halves of a split, the sim shows 0 conflicting locks in every partition, eclipse and equivocator row under it and a first lock one hour after a 34, 45 or 50 percent departure where v3 waits 30 days, and Q2 shows an attacker who buys keys to leave them is worse off than one who signs with them. The hour is a parameter: it must exceed the certificate relay plus one presence of the leave in blocks (minutes), and shorter is better for the operator; one hour matches the merge-depth bound and gives a key that leaves by mistake time to see it. What it does not cover: a crash, a power cut, a region going dark, which still age out as today; the app's Stop button and the fleet library send the leave, and a node that restarts after an unplanned outage can send it on return to shorten the pause from that point.
|
||||
|
||||
**2. Staged departures.** Every lock re-freezes the table, so weight that leaves while locks continue ages out of the frozen table as it ages out of the sliding one (M4's "gradual departure costs nothing"). Ten percent an hour keeps the stayers over two thirds at every step for any total departure under a third per three hours. This is a fleet-library rule, one afternoon, and it would have kept tonight's finality on without any protocol change; proposal 1 covers the case where staging is not possible.
|
||||
|
||||
**3. Aggregate-first verification.** The delay data of 3.2 is flat to 93 voters because the devnet's cost is the poll and the pump, not the pairings; the arithmetic of 4.3 says the pairings take over near 1,000 voters and break the 30-s cadence near 8,192. The crate the fork already uses has both halves of the fix; what is missing is the batching in `ingest` and the in-block aggregate item that 3.4.2 item 2 proposes. The gate is a synthetic-voter harness, which `tools/finality-attacks` can host (its `lib/net.mjs` starts nodes; a vmine with 1,000 keys is a flag away).
|
||||
|
||||
**4. The two-tier report.** The provisional tier is the active-denominator rule the project rejected on 3 October, and the sim says again why: it conflicts in every partition. As a reported state it is useful to a holder who wants to know whether the pause is a silent third (provisional true: the connected majority still agrees) or a split (provisional conflicting on the two sides), and useless to an exchange, which must credit nothing on it. Three hours, mostly copy.
|
||||
|
||||
**5 and 6** are measurements with a design attached, priced above. **7 and 8** are the negatives this lane is confident about: a denominator that shrinks on silence is the 3 October rule under another name, and a lock that waits for proofs is a lock that pauses whenever the proving market is thin, which it is.
|
||||
|
||||
## 7. Open questions and what could not be run
|
||||
|
||||
- The fleet wrote no lock-delay or voter-count row by 19:45Z (`block-rate-devnet2.md` is a template); the 93-voter figures here are node 1's own log. When RUN_A and RUN_B land, the 10 blocks/s row should be checked against 4.3's claim that the hop term, not the voter term, sets the delay.
|
||||
- The leave item does not exist in the node; the harness case of proposal 1 is designed, not run. Its interaction with W5 succession (O-3.11) and with a key that leaves and keeps mining (its blocks earn nothing, as the spec says for a succeeded key) needs the spec text.
|
||||
- BLS costs are approximate (crate benchmarks from memory, anchored by one measured JavaScript run). A `cargo bench` of `fast_aggregate_verify` at 93, 1,000 and 8,192 keys on the Mac is one hour and belongs with proposal 3.
|
||||
- The ZK light client's pairing cost in SP1 6.8.1 is the number the whole of 4.5 turns on; it is approximate until the guest of proposal 5 is counted.
|
||||
- The pause's end was not observed at the time of writing (expected about 20:40Z at the frozen table's expiry, or earlier if the departed boxes return); the observer rows will show which.
|
||||
- The box ran the sims at nice 19 under other agents' load; the tables are counts and days, not timings, so the load does not touch them.
|
||||
|
||||
## 8. Summary for the coordinator
|
||||
|
||||
Tonight's finality pause (first unlocked checkpoint 6843 at about 18:40Z, still paused at 19:30Z) is the two-thirds rule doing what it says, then the frozen table doing what F21 asked: 20 keys holding 42.7 percent of the frozen table left the live chain, the stayers held 53.1 percent at the first unlocked checkpoint and 74.9 percent of the sliding table from 19:14:53Z, and only Q5 explains why 74.9 percent did not lock; certificates formed through the hub outage and the aggregator fallback carried a quarter of them, so the topology hypothesis is refuted. The three findings: (1) under v2 the pause would have ended at 19:14:53Z, 35 minutes in, and on mainnet the same event is 7.7 days (v2) or 30 days (v3); (2) of the four candidate rules only the departure announcement keeps the one-third bound (0 conflicts in every partition row, first lock one hour after the departure), while decay and hysteresis reopen the double lock and the two-tier is a report; (3) renting a veto costs USD 8,424 x N x 0.52 for 30 days (USD 4,300 per GH/s of network), locking alone 2.03 x N for 30 days, and bought keys cost the same and decay in 30 days.
|
||||
675
docs/analysis/horizon/frontier.md
Normal file
675
docs/analysis/horizon/frontier.md
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
# 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 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.
|
||||
|
||||
**Read for grounding:** `docs/spec/00-overview.md`, `05-fees-and-economics.md`, `07-execution.md`, `09-pool-protocol.md`, `10-light-client.md`; `site/litepaper.html` (whole page, including "What Igneum does not claim"); `docs/fud-ledger.md` sections 3 (P1 to P10 present in the file; P11 to P23 are cross-referenced from the spec and round-3 entries), 4 (E1 to E8), 6 (C1 to C12), 9 (D1 to D6); `docs/commercial/prover-customer-brief.md`; `docs/design/payment-routes.md`, `developer-adoption.md`, `execution-layer.md`; `docs/analysis/chip-model-v3.md` sections 5 to 6; `docs/plans/counter-asic-3-status.md` section 4; `docs/analysis/prover-tiers-real-cards.md` (eleven rented cards, 6 October); `docs/analysis/economy-2026-10-04.md`; `docs/analysis/security-budget.md`; `docs/bench-log.md` line 2582 (rental cost of hash, measured 6 October); `vendor/rusty-kaspa/consensus/core/src/config/params.rs` (main checkout). `block-rate-devnet2.md` was still a template at 22:00 UK (RUN_A and RUN_B empty); nothing here depends on it.
|
||||
|
||||
**Model:** `sim/horizon/frontier/frontier_model.py` (pure Python, no numpy, about 50 ms; `python3 sim/horizon/frontier/frontier_model.py > sim/horizon/frontier/out.md`). Every table below marked "model" is printed by it; its `INPUTS` block labels each input measured, cited, designed or approximate with the source. Not run under the lock: it is arithmetic, not a measurement.
|
||||
|
||||
---
|
||||
|
||||
## 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 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 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 3.3 The weight table carried inside the recursive segment proof: a consensus proof at mergeset cost, so a browser verifies finality from one proof and asks no node for the voter set | 5 | 60 | do now (design and guest prototype) | Phase two's hardest item becomes incremental: each segment proof updates W2 by its own blue blocks; the cost is one BLS aggregate verify per 30 s inside the zkVM, which SP1 has precompiles for (approximate) |
|
||||
| 2 | 3.2 Work-stake: vote weight as the external-job bond | 5 | 24 | prototype | A bond nobody can buy: 30 days of blocks. The at-risk pool income is thousands of IGN against a 0.0015 IGN coin bond (model section 3); the design stays "no stake" because weight is work, not coins |
|
||||
| 3 | 3.7 Reproducible-build attestations in a registry contract; Ember refuses a release under N of M attestations | 4 | 12 | do now | Bitcoin's guix.sigs with the chain as the sigs repo; closes the devnet's "release key acts as operator" sentence (litepaper, Governance) |
|
||||
| 4 | 3.4 A WebAssembly verifier of the wrapped block proof in the tab | 4 | 16 | do now | Three working precedents (a16z Helios WASM, ProjectZKM ziren-wasm-verifier, xycloo wasm-groth16-verifier); the certificate half already runs at 58 to 155 ms (bench-log) |
|
||||
| 5 | 3.8 Ember as node, wallet and light client for everyone: node count equals miner count | 4 | 20 | do now | 1,000 testnet miners become 1,000 verifying nodes; Monero shows about 5,000 peers in 72 h (monero.fail), Ethereum 8,136 execution nodes (ethernodes); Sybil counts are irrelevant here because nothing counts nodes |
|
||||
| 6 | 3.15 A 30-s randomness beacon from the checkpoint VDF | 4 | 24 | prototype | The pipeline exists (10-min VDF, 4.47 ms verify); a drand-class beacon with no league and 120x drand's latency; honest limit is the class-group ASIC (Chia timelords) |
|
||||
| 7 | 3.1 The reward rule that prices rented hash out (pay per block falls when hash arrives faster than the 30-day weight) | 4 | 40 | prototype | Doubles the renter's break-even price (model section 2) and routes the cut to the proving pool, not to incumbents; the cost is a 30-day income ramp for honest newcomers, which the vote already imposes |
|
||||
| 8 | 3.5 Continuous miner-voted parameters, bounded per block like Ethereum's gas limit, for `B_p`, the floors and the window | 3 | 30 | prototype | Replaces two-week 60 percent proposals with a drift anyone can read on the chain; Kaspa's Crescendo was a fixed DAA score (`params.rs:648`), Bitcoin's BIP9 a 95 percent tally; neither moves a number continuously |
|
||||
| 9 | 3.16 The hourly program swap as a research dataset and the fleet library as a product | 3 | 10 | do now | 8,760 random kernels a year, compiled on three vendors with per-variant timings; compiler and GPU-architecture researchers have no such corpus; income small, standing large |
|
||||
| 10 | 3.13 Igneum as the settlement layer for GPU rental | 3 | 40 | prototype (escrow plus sampled verification) | The chain's fee is 3 to 5 orders under a 7 to 15 percent platform take (model section 5), but it can settle only what it can verify; the verifiable subsets are named |
|
||||
| 11 | 3.6 Treasury-less audit funding: burn-redirect bounties by 60 percent signal, review escrow on upgrade proposals | 3 | 20 | watch | The money exists only when the chain is used (USD 1,600 to 16,000 per 30 days at launch traffic, model section 4), and it is the switch spec 5.5 removed, with a veto |
|
||||
| 12 | 3.14 Proofs sold to AI labs for verifiable inference | 2 | 40 | watch | zkLLM: 803 s of proving per forward pass on LLaMA-2-13B (arXiv 2609.27367 citing 2404.16109); the competitor is a USD 0 TEE attestation on H100-class cards the fleet does not own |
|
||||
| 13 | 3.9 Hardware wallets that verify proofs | 2 | 12 | never on the secure element; do the companion verify | A bn254 pairing on a Cortex-M-class secure element is seconds to minutes (approximate); Ledger and Trezor do their heavy work in the companion app, and Igneum Wallet already verifies certificates with the node's code |
|
||||
| 14 | 3.12 The fleet as a public compute market (rendering, inference) priced in IGN | 2 | 60 | never for unverifiable work; do for the verifiable subsets | An escrow without a verifier is a trust-me payment with lower fees; Render uses result quorums, Akash reputation, io.net attestations (secondary), none of which a chain can check |
|
||||
| 15 | 3.11 Miners paid for proving others' chains as the main income, the lottery a tiebreaker | 1 | 0 | never by 2030; watch | All of Ethereum L1's proving at the Sep 2026 tracker cost is USD 36 a day; Igneum's year-1 emission is USD 13,700 a day at 0.005 (model section 7); demand must grow 1,000x against a cost curve falling 3x to 30x a year |
|
||||
| 16 | 3.10 Proof-of-useful-work: the lottery hash partly a proof | 1 | 0 | never | Every coupling of leader election to proving re-opens Aleo (fastest prover wins); Ball et al. 2017 and Ofelimos 2022 show the sampleability conditions, and zkVM proving meets none of them |
|
||||
|
||||
Predictions (section 2) are not ranked; they are inputs. The three ideas a Monero or Kaspa core developer would not have thought of are 3.2, 3.3 and 4.3 (the hourly program as a hardware census), argued in section 4.
|
||||
|
||||
---
|
||||
|
||||
## 1. Method
|
||||
|
||||
Three kinds of work:
|
||||
|
||||
1. **Trend lines with arithmetic.** Each 2030 prediction has a cited anchor (a product page, a tracker, a standards body, a secondary analysis labelled as such) and a formula in the model script. Where the trend is from memory (consumer VRAM generations) the row says approximate.
|
||||
2. **Idea arithmetic.** For the five ideas whose value depends on numbers (the rental tax, work-stake, the burn bounty, rental settlement, the proving-income ceiling) the script prints the table and the document reads it. The attack costs use the measured rental entry (`docs/bench-log.md` line 2582: 1,748 MH/s for USD 20.44 an hour, USD 0.0117 per MH/s-hour, about USD 11.69 per GH/s-hour) and are given per GH/s and evaluated at 1, 10, 100 GH/s and 1 TH/s, as the brief asks.
|
||||
3. **Prior art.** WebSearch on 6 October 2026 for every idea; the paper or repository that tried it is cited, or the idea is marked "new" when none was found. Claims about other chains cite the repository file when a clone exists in the main checkout (`vendor/rusty-kaspa`) and are marked "not cloned, approximate" otherwise.
|
||||
|
||||
No simulator in `sim/` was re-run and no node harness was started: every idea here is a design question whose first gate is a measurement named in its section, and tonight the fleet, PC 1, PC 2 and the devnet were in use for the class v4 rehearsal and the Devnet 2 block-rate runs. What I could not run is listed in section 6.
|
||||
|
||||
---
|
||||
|
||||
## 2. Predictions to 2030
|
||||
|
||||
Each row: the trend, its anchor, the arithmetic, and what it does to the chip model and the proving tiers. Tables marked model are `frontier_model.py` section 1.
|
||||
|
||||
### 2.1 GPU memory per card
|
||||
|
||||
| Year | Flagship consumer card | GB | Label |
|
||||
|---|---|---|---|
|
||||
| 2016 | GTX 1080 | 8 | approximate (from memory) |
|
||||
| 2018 | RTX 2080 Ti | 11 | approximate |
|
||||
| 2020 | RTX 3090 | 24 | approximate |
|
||||
| 2022 | RTX 4090 | 24 | approximate |
|
||||
| 2025 | RTX 5090 | 32 | cited (`chip-model-v3.md` 5.1: 16 x 2 GB GDDR7 on 512 bits) |
|
||||
|
||||
Compound growth 1.167 a year (4x in 9 years). Extrapolated: 51 GB in 2028, 69 GB in 2030 (model). The module arithmetic is sharper than the curve: a 512-bit board is 16 devices; Micron has ended 2 GB GDDR7 (TrendForce, 24 September 2026, via `chip-model-v3.md` 5.1) and 3 GB devices are USD 60 to 70, so the next flagship is 48 GB (16 x 3 GB) and 64 GB is the 2030 shape. The RTX 60 series on Rubin (GR20x) is reported for 2028 after two slips (kopite7kimi via videocardz.com and thepcenthusiast.com; rumour, not a product). Datacentre: HBM4 at 36 GB per 12-high stack, 288 GB per GPU on Rubin NVL72 (Wikipedia HBM page, Micron March 2026 production).
|
||||
|
||||
What it does to the chip model: nothing for the stored-dataset chip (f = 1), whose memory is already 24 to 32 GB against a dataset of 2 GiB growing to 4 GiB at year 4 (`chip-model-v3.md` 5.7: dataset size is "not a lever against this chip"). What it does for miners: the dataset schedule (2 GiB, doubling at years 4, 12, 28; litepaper) stays under every card from 8 GB for twelve years, and the proving side, not the mining side, is what wants VRAM (section 2.6).
|
||||
|
||||
### 2.2 Memory dollars per GB
|
||||
|
||||
| Point | USD per GB | Source |
|
||||
|---|---|---|
|
||||
| 2023, GDDR6 | 3.38 | Tom's Hardware, "GDDR6 VRAM prices plummet", USD 27 per 8 GB |
|
||||
| 2025, GDDR6 | 2.50 | TechSpot, "AI is eating all the DRAM" (2026) |
|
||||
| 2026, GDDR6 | 3.30 | TechSpot, same |
|
||||
| Sep 2026, GDDR7 2 GB device | 10.00 | TrendForce via `chip-model-v3.md` 5.1 |
|
||||
| Sep 2026, GDDR7 3 GB device | 21.67 | TrendForce (USD 60 to 70 per device) |
|
||||
| Oct 2026, HBM3E 36 GB stack | about 8.3 | siliconanalysts.com/data/hbm-pricing (factory gate; contract about 2x), approximate |
|
||||
| Oct 2026, HBM4 36 GB stack | about 15.3 | siliconanalysts.com (USD 550 per stack); Samsung quoting USD 4.50 to 4.90 per Gb for HBM4 against 1.50 for HBM3E (BigGo Finance), approximate |
|
||||
|
||||
GB per dollar fell in 2026 for the first time in a decade and DRAM supply is forecast tight through 2027 with new fabs in 2028 (SoftwareSeni "HBM4 delays and GDDR7 shortages"). Memory is reported at 70 to 80 percent of the bill of materials of high-VRAM consumer cards by late 2025 (BuySellRam, secondary).
|
||||
|
||||
What it does to the chip model: the f = 1 chip and the GPU buy the same devices, so the ratio of their memory bills is fixed; what moves is the share of each bill that is memory. The chip's bill is about 70 percent memory (USD 320 of USD 470, `chip-model-v3.md` 5.4) and the 5090's about 16 percent at MSRP (USD 320 of USD 1,999) or 9 percent at the 2026 street price of USD 3,695 (localaimaster.com). A doubling of device prices raises the chip's cost 1.7x and the card's 1.1x to 1.2x: **the stored-dataset chip gets dearer relative to the GPU through 2027**, and the dollars-per-MH/s row (USD 2.8 against 14.7 at MSRP, 5.4 against 27 at street prices) narrows a little and no more. The per-joule row does not move at all, and per joule is where the chip wins (section 2.3).
|
||||
|
||||
### 2.3 Random-read bandwidth: GDDR7, HBM3E, HBM4
|
||||
|
||||
The lottery is latency-bound: one hash advances one dependent 4-byte read per memory latency, so the number that matters is random reads per second per watt, not GB/s (`chip-model-v3.md` 5.3 and 5.5). That ceiling is set by bank count and activate windows (tRC, tFAW), not by pin speed, so 48 Gbps GDDR7 is 28 Gbps GDDR7 here, and HBM3E is HBM3.
|
||||
|
||||
| Memory system | Reads/s ceiling | Why | Label |
|
||||
|---|---|---|---|
|
||||
| GDDR7, 16 devices, 512-bit (the 5090 board) | 21.3 G | 4 activates per 12 ns per channel x 64 channels | approximate (`chip-model-v3.md` 5.3) |
|
||||
| RTX 5090 measured | 17.5 G | 82 percent of the ceiling | measured (bench-log Counter ASIC 2.0) |
|
||||
| HBM3 or HBM3E, one stack | 10.7 G | 16 channels | approximate |
|
||||
| **HBM4, one stack** | **21.4 G** | JEDEC JESD270-4 raises channels per stack from 16 to 32, each with two pseudo-channels (allaboutcircuits.com, EDN), which doubles activate parallelism if tFAW per channel holds | approximate, derived (model 1.3) |
|
||||
| A 48 GB GDDR7 board | 21.3 G | capacity does not add channels | approximate |
|
||||
|
||||
The f = 1 chip in 2028 on HBM4 (model 1.4; every figure arithmetic, approximate):
|
||||
|
||||
| Chip | MH/s per chip | W bare / with a 150 W shadow core at k = 1 | uJ per hash bare / shadow | Gain per joule vs the 5090 bare (2.40 uJ) | Gain under the class v4 shadow (card 2.95 uJ at N = 100,000) |
|
||||
|---|---|---|---|---|---|
|
||||
| GDDR7 f = 1 (today's row) | 166 | 78 / 228 | 0.47 / 1.37 | 5.1x | 2.2x |
|
||||
| HBM3 one stack | 84 | 27 / 177 | 0.32 / 2.12 | 7.5x | 1.4x |
|
||||
| HBM4 one stack (2028) | 167 | 36 / 186 | 0.22 / 1.11 | 11.0x | 2.6x |
|
||||
|
||||
**Prediction:** HBM4 doubles the stored-dataset chip's rate per stack at about the same watts, so its bare per-joule edge rises from about 7x to about 11x, and under the class v4 shadow from about 2.3x to about 2.7x. The lever that answers it is N, the program work in the latency shadow: at N = 200,000 the HBM4 chip reads 1.74x at k = 1, at N = 330,000 (the 5090's full ALU budget) 1.33x (model 1.4). The verifier cost is N x 32 ops per warp: about 1 ms at N = 100,000 on one M5 Max core (measured class, counter-asic-3-status item 8), about 3 ms at 330,000, inside the 10 ms gate; the 2019-class core is unmeasured (O-1.14).
|
||||
|
||||
**Consequence and proposal (for the coordinator, not a change tonight):** write the schedule for N into the era draw at genesis, the way the dataset size already is: a candidate is a doubling of N per era until the verifier gate binds (about 1,000,000 ops, 10x of headroom on the M5 Max). The memory generation it answers arrives every two to three years and the chain must not need a human release to answer it. Per tier: no hash-rate cost while cards stay latency-bound (the M5 Max binds at about 290,000, the 9070 XT at about 650,000, approximate), watts up toward TGP (a 5090 from 326 toward 575 W, which the Ember power cap already manages), a verifier cost nodes and pools pay in milliseconds.
|
||||
|
||||
### 2.4 Price per card
|
||||
|
||||
| Card | Launch MSRP | 2026 street | Source |
|
||||
|---|---|---|---|
|
||||
| RTX 5090 | USD 1,999 | USD 3,695 to over 5,000 | `chip-model-v3.md` 5.1; localaimaster.com; tech-insider.org ("RTX 5090 tops USD 5,000"), secondary |
|
||||
| RTX 4090 | USD 1,599 (approximate) | rental USD 0.28 to 0.60 an hour (gpus.io median) | rental cited, MSRP from memory |
|
||||
|
||||
Prediction: consumer card prices track memory prices through 2027 and ease in 2028 when fab capacity lands. For Igneum the price per card matters twice: the honest fleet's capital cost (not in the security budget, which is power only, `security-budget.md` section 6) and the renter's hourly price, which fell to USD 0.21 to 0.44 per 5090-hour on Vast.ai (getdeploying.com, 6 October 2026) even as purchase prices rose, because rented supply is sunk capital. **The rental market, not the purchase market, prices the 51 percent attack**, and the measured entry is USD 11.69 per GH/s-hour at RunPod list prices with the market unable to supply 20 more pods when asked (bench-log line 2582). At a TH/s: USD 11,700 an hour, and no supply.
|
||||
|
||||
### 2.5 The chip-fab cost curve
|
||||
|
||||
| Node | Mask set | Source | Igneum reading |
|
||||
|---|---|---|---|
|
||||
| 28 nm | USD 1 to 3 M | TubeTime (3 M); VBsemi (over 1 M) | The f = 1 memory-controller chip: no mixer on the die, a USD 5 to 30 M project (`asic-resistance-history.md` 2.5) |
|
||||
| 7 nm | USD 10 to 15 M | VBsemi; Hacker News thread | The f = 0 recompute chip with 256 MiB on die: USD 50 to 75 M |
|
||||
| 5 nm | USD 6.5 M (2026 data) to 30 M (2023 estimate) | siliconanalysts; HN | The shadow core (30 mm^2 at N5 for N = 100,000) drags the f = 1 chip toward this node, or to a reticle-class 28 nm die |
|
||||
| 3 nm | USD 15 to 22 M (Q4 2025), up to 40 M (older) | siliconanalysts; semianalysis | Not relevant to a chip whose cost is memory |
|
||||
|
||||
Prediction: mask cost at a fixed node falls (5 nm quoted at 30 M in 2023, 6.5 M in 2026) while the leading node rises. So the shadow lever's fab-bill teeth weaken about 4x over three years; what holds in 2030 is the rate and joule arithmetic of 2.3, not the bill. **The stored-dataset chip gets cheaper to design and dearer to populate** through 2027, and the net is roughly flat against the GPU on dollars; per joule it gains with each memory generation unless N grows with it.
|
||||
|
||||
### 2.6 zkVM proving speed per dollar
|
||||
|
||||
| Point | USD per Ethereum L1 block proof | Hardware | Source |
|
||||
|---|---|---|---|
|
||||
| Jan 2025 | 1.69 | about 160 RTX 4090s for 90 percent real-time, USD 300 to 400 K cluster | HackMD "Ethproofs 2025 review" (willcorcoran); Succinct SP1 Hypercube blog (May 2025), secondary |
|
||||
| Dec 2025 | under 0.04 | 16 x RTX 5090 (SP1 Hypercube: 99.7 percent of blocks under 12 s; cluster under USD 100 K); Pico Prism 16 GPUs (Brevis blog, Feb 2026) | same, secondary |
|
||||
| Apr 2026 | | Cysic Venus 7.4 s on 24 GPUs | bex.co, secondary |
|
||||
| Aug 2026 | | ZisK p99 9.62 s on 4 x RTX 5090 | GitHub comparative analysis (Ricosworks1), secondary |
|
||||
| Sep 2026 | about 0.005 | "sub-half-cent" fields on ethproofs | same, secondary |
|
||||
|
||||
The 20-month ratio is 338x, about 33x a year (model 1.6). That cannot continue: it is software catching up with hardware. The table below uses 1.5x, 3x and 10x a year from the shard times measured on eleven rented cards on 6 October (`prover-tiers-real-cards.md`, the v1 shard, 4.7 M cycles).
|
||||
|
||||
| Card | Beside the miner today, s | Alone today, s | 2028 at 1.5x a year (beside / alone) | 2028 at 3x | 2028 at 10x | Under 10 s beside the miner in 2028? |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RTX 3060 12 GB | 37.5 | 14.4 | 16.7 / 6.4 | 4.2 / 1.6 | 0.4 / 0.1 | at 3x or more |
|
||||
| RTX 4060 8 GB (core-only beside) | 22.1 | 18.4 | 9.8 / 8.2 | 2.5 / 2.0 | 0.2 / 0.2 | even at 1.5x |
|
||||
| RTX 4070 12 GB | 27.3 | 12.1 | 12.1 / 5.4 | 3.0 / 1.3 | 0.3 / 0.1 | at 3x or more |
|
||||
| RTX 4060 Ti 16 GB | 34.6 | 11.6 | 15.4 / 5.2 | 3.8 / 1.3 | 0.3 / 0.1 | at 3x or more |
|
||||
| RTX 3080 10 GB | 25.6 | 7.1 | 11.4 / 3.2 | 2.8 / 0.8 | 0.3 / 0.1 | at 3x or more |
|
||||
| RTX 3090 24 GB | 19.9 | 14.9 | 8.8 / 6.6 | 2.2 / 1.7 | 0.2 / 0.1 | even at 1.5x |
|
||||
| RTX 4090 24 GB | 26.1 | 6.3 | 11.6 / 2.8 | 2.9 / 0.7 | 0.3 / 0.1 | at 3x or more |
|
||||
| RTX 5070 12 GB | 37.2 | 4.8 | 16.5 / 2.1 | 4.1 / 0.5 | 0.4 / 0.0 | at 3x or more |
|
||||
| RTX 5090 32 GB | 10.7 | 6.3 | 4.8 / 2.8 | 1.2 / 0.7 | 0.1 / 0.1 | even at 1.5x |
|
||||
|
||||
**Prediction:** at the floor rate (1.5x a year, which is the GPU hardware cadence alone) only the 24 GB and 32 GB cards mine and prove inside 10 s in 2028; at 3x a year (half the historical software rate) every card from the 3060 up does, and an 8 GB card alone proves in 2 s. **The block-proof target ("under 10 s as provers improve", CLAUDE.md) should be written as a function of the measured fleet median shard time, re-read each era, not as a date.** Per tier: a 12 GB desktop card is the swing tier; under 1.5x it proves alone in under 7 s but not beside its miner, so the hand-off profile (`prover-tiers-real-cards.md`) is the thing to ship, not a bigger card.
|
||||
|
||||
What the cost curve does to the economics: the dollars per proof on the open market fall as fast as the volume rises, which is the arithmetic behind the "never by 2030" of 3.11.
|
||||
|
||||
---
|
||||
|
||||
## 3. The ideas, one section each
|
||||
|
||||
Each section: the idea in a paragraph; why nobody shipped it (cited, or "new"); what Igneum already has; the model; hours; the gate; the per-tier consequence; the Monero attack; the Kaspa attack; the verdict.
|
||||
|
||||
### 3.1 The reward rule that prices rented hash out
|
||||
|
||||
**The idea.** The pulse attack M14 (a renter arrives, mines for an hour, leaves) is recorded in the ledger and the finality rule already denies rented hash a vote. The reward side is untouched: a renter is paid per block like anyone. The rule: the block subsidy paid to a producer is multiplied by `m = clamp(W30 / H_now, 0.25, 1)`, where `W30` is the 30-day work-weighted hash the finality window already computes (blue blocks per DAA second over the W2 window) and `H_now` the DAA-window estimate. The remainder `(1 - m)` of the subsidy goes to that block's proving-pool escrow, not to incumbents and not to a burn. Fees are untouched. Hash that arrives faster than the 30-day weight can follow is paid less per block until the weight catches up.
|
||||
|
||||
**Why nobody shipped it.** Bitcoin Cash's EDA and every emergency rule since adjusted difficulty, never pay (approximate; the consensus engineer's list). Kaspa's DAA retargets per block over a sampled window (`vendor/rusty-kaspa/consensus/src/processes/difficulty.rs`, main checkout) and pays per block. Monero's RandomX changes what hash is, not what it is paid. Ethash coins and Ergo pay per block. No chain ties the subsidy to the ratio of fresh to sustained hash, because no chain had a sustained-hash number in consensus; Igneum has it in W2. Prior art searched: none found. New.
|
||||
|
||||
**What Igneum already has.** W2 (blue blocks per key over 30 days) and the DAA estimate are both in the node; the proving-pool escrow exists in execution state (spec 7.7 item 6); the emission split is a state transition by rule (design 1.1).
|
||||
|
||||
**The model** (frontier_model.py section 2, the measured rental entry USD 11.69 per GH/s-hour):
|
||||
|
||||
| Network hash | Attacker adds | H_now / W30 | m | Attacker IGN per hour, no rule | With rule | Rent USD per hour | Break-even IGN price, no rule | With rule |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 GH/s | 1 GH/s | 2.0 | 0.50 | 57,038 | 28,519 | 12 | 0.00020 | 0.00041 |
|
||||
| 10 GH/s | 10 GH/s | 2.0 | 0.50 | 57,038 | 28,519 | 117 | 0.00205 | 0.00410 |
|
||||
| 100 GH/s | 100 GH/s | 2.0 | 0.50 | 57,038 | 28,519 | 1,169 | 0.02049 | 0.04099 |
|
||||
| 1 TH/s | 1 TH/s | 2.0 | 0.50 | 57,038 | 28,519 | 11,690 | 0.20495 | 0.40990 |
|
||||
| 10 GH/s | 50 GH/s | 6.0 | 0.25 | 95,064 | 23,766 | 584 | 0.00615 | 0.02459 |
|
||||
|
||||
A renter who doubles the network needs twice the coin price to break even; one who sextuples it needs four times. The diverted subsidy (57,000 to 85,000 IGN an hour in these rows) reaches the provers of the same blocks, who are the sustained population by sortition weight (spec 7.2). The honest-growth cost: a listing that doubles honest hash overnight cuts every miner's subsidy per block in half on top of the halving difficulty already imposes, until W30 catches up (the 30-day ramp of ledger C7, 0.9x by day 28 to 31). The floor 0.25 bounds the worst case at 4x.
|
||||
|
||||
**Hours.** 40: the rule in the coinbase state transition (8), W30 as a consensus value from the window the finality module keeps (8), the economy simulator scenario f with and without the rule (8), the fast-time harness timestamp test (8), spec text and tests (8).
|
||||
|
||||
**The gate.** (a) `sim/economy/sim.py` scenario f (a pool with the network's own hash arriving on day 10): incumbents' income under the rule above the no-rule row for all 30 days, newcomers' below. (b) The fast-time harness with headers back-dated inside Kaspa's 132-s tolerance: `m` moves under 2 percent. (c) Scenario d (a 20 percent operator vanishes): `m` stays at 1 (hash fell, so H_now < W30 and nobody is cut).
|
||||
|
||||
**Per tier.**
|
||||
|
||||
| Tier | Consequence |
|
||||
|---|---|
|
||||
| Home miner, 8 to 32 GB | In a doubling month, 25 percent of the pre-event subsidy instead of 50; unchanged in a steady month; a newcomer earns half-rate for its first month, as it votes nothing for its first month already |
|
||||
| Rig | The same per block; a rig that joins during a listing spike earns half for a month |
|
||||
| Pool user | The same through PPLNS; the pool's dashboard should show `m` |
|
||||
| Prover | Gains: the diverted share lands in the pool escrow and is paid by sortition weight |
|
||||
| Holder | Emission schedule unchanged in total; a larger share of it reaches sustained keys during spikes |
|
||||
| Rollup customer | Nothing |
|
||||
| Node operator | One more consensus value (W30) and one multiplier in the coinbase rule |
|
||||
|
||||
**The Monero core developer's attack.** "You have built an incumbents' cartel. Every rule that pays old miners more than new miners entrenches whoever was there first; RandomX exists so that a newcomer with a laptop earns exactly what a veteran earns per hash. Your 'to the pool, not incumbents' is cosmetic: sortition is by weight, and weight is the incumbents. And your W30 is your own finality window, so a 30-day-old farm that goes dark and returns is 'sustained' while a thousand honest newcomers after a listing are taxed for a month. Qubic reached 23 to 34 percent of our hash for weeks in August 2025 (arXiv 2512.01437) by renting and by paying miners in its own token; your rule would have taxed the honest miners who moved to P2Pool to fight it, because they were new keys." Answer: the tax is per block not per key, so moving pools under the same key costs nothing (spec 9.6), and a returning farm's W30 is its own blocks, which it did not make while dark. The entrenchment point stands and is the cost the gate measures.
|
||||
|
||||
**The Kaspa core developer's attack.** "H_now is your DAA estimate and the DAA is manipulable by timestamps inside the tolerance; a producer can lower H_now for its own block by back-dating within 132 s and raise its own m. Second, W30 is a function of the DAG past and differs between two honest tips every second; a reward that depends on it makes two honest nodes disagree about the coinbase amount of the same block unless W30 is read at a fixed ancestor (the checkpoint), and then it lags. Third, you have made emission depend on a window of 2.6 million blocks: your pruning point must now keep that window's per-second counts, which Kaspa prunes." Answer: read both numbers at the block's selected parent's last certified checkpoint (deterministic, in every node's past), accept the 30-s lag, and the timestamp test is gate (b). The pruning point already keeps the W2 window for finality (spec 3), so no new retention.
|
||||
|
||||
**Verdict: prototype.** Doubles the renter's break-even and costs honest newcomers a month of half subsidy, which is the same month the vote already costs them; gate (a) decides whether miners will wear it.
|
||||
|
||||
### 3.2 Work-stake: vote weight as the external-job bond
|
||||
|
||||
**The idea.** The external job market needs a bond because a customer waits (spec 5.4, O-5.6: `maxPgas x f_p x 1.5` in IGN, slashed on a late or bad proof). Replace the coin bond with the key's 30-day vote weight: a key that claims an external job and delivers late or wrong loses a share `s` of its weight for 30 days, the way equivocation strips 100 percent (spec 3.6). Weight is blue blocks. It cannot be bought, borrowed or bridged; it can only be mined, in public, over 30 days. The job market then needs no IGN escrow from the prover, which removes the capital barrier that keeps home cards out of Boundless (ZKC collateral) and Succinct (PROVE staking, docs.succinct.xyz/docs/provers) while keeping a bond larger than either.
|
||||
|
||||
**Why nobody shipped it.** Every proving network bonds in its own token (Boundless: stake scales with aggregate proving work per epoch, docs.boundless.network/zkc/mining/overview; Succinct: stake required to bid, more stake more concurrent auctions). No proof-of-work chain had a non-transferable, slowly earned weight per key until Igneum's finality rule. Decred's tickets are bought with coins; Ethereum's slashing is coins. New.
|
||||
|
||||
**What Igneum already has.** W2 per key, the 30-day strip for equivocation, the sortition that already draws assignees by weight (spec 7.2), the job record format (design 6).
|
||||
|
||||
**The model** (frontier_model.py section 3):
|
||||
|
||||
| Key's hash share | Blocks per 30 days at 1 bps | 30-day pool income at risk, IGN | Lost at s = 25 percent | Lost at s = 100 percent | The coin bond for one 1 B-cycle job at the floor |
|
||||
|---|---|---|---|---|---|
|
||||
| 0.01 percent | 259 | 1,643 | 411 | 1,643 | 0.0015 IGN |
|
||||
| 0.1 percent | 2,592 | 16,427 | 4,107 | 16,427 | 0.0015 IGN |
|
||||
| 1 percent | 25,920 | 164,271 | 41,068 | 164,271 | 0.0015 IGN |
|
||||
| 10 percent | 259,200 | 1,642,706 | 410,676 | 1,642,706 | 0.0015 IGN |
|
||||
|
||||
The at-risk amount is five to nine orders of magnitude above the designed coin bond, before counting the lost vote. A late proof must be defined in DAA time against the claim (the P9 decision's 120-s claim timeout is the starting value), with one strike of grace per 30 days so a partition does not strip an honest key on its first miss.
|
||||
|
||||
**Hours.** 24: the strip rule in the finality module keyed by a job-fault record (8), the fault record in the job contract (design 6) with the evidence a node checks (8), tests and a devnet injection script (8).
|
||||
|
||||
**The gate.** Phase 4 devnet: 1,000 jobs with a 10 percent injected late or wrong rate: every injected fault stripped; zero honest keys stripped across a 60-s partition; a customer's job never waits more than the claim timeout plus one open window.
|
||||
|
||||
**Per tier.**
|
||||
|
||||
| Tier | Consequence |
|
||||
|---|---|
|
||||
| 8 GB solo miner below dust (under 100 blocks in 30 days) | No weight, so no external jobs under this rule; shards (no bond) unchanged; the pool protocol gives it a route (the pool's key, the pool's weight) |
|
||||
| 12 to 32 GB home miner above dust | Takes external jobs with no IGN locked; one bad job costs a quarter of a month's sortition income and a quarter of its vote for 30 days |
|
||||
| Rig | The same, at the rig's weight; the rig operator's whole weight backs each job, so a rig claims only jobs it can finish |
|
||||
| Pool user | The pool's weight is the bond; the member's share of pool income carries the pool's record |
|
||||
| Prover | A reputation nobody can buy, visible on chain per key |
|
||||
| Holder | No IGN is locked in bonds, so no bond capital sits idle |
|
||||
| Rollup customer | A bond measured in 30 days of public mining instead of a token balance; the customer brief's "your chain's own bond and slashing apply" becomes "Igneum's weight is at stake" once jobs settle on Igneum |
|
||||
| Node operator | One more strip condition in a module that already strips |
|
||||
|
||||
**The Monero core developer's attack.** "CLAUDE.md says no stake anywhere in consensus. You have just made the vote weight a stake: it is at risk for an execution-layer fault. Whatever you call it, a prover now rationally hedges by splitting its mining across two keys, one that votes and never proves, one that proves and holds dust weight, which your F17 says buys nothing for the vote but buys everything here: the proving key has nothing to lose. So the bond is only real for operators too small to split, which is backwards." Answer: the sortition draws assignees in proportion to weight (spec 7.2 step 2), so a dust key is drawn with probability near zero and the proving key must carry weight to be assigned at all; the hedge costs the prover its assignments. The attack is right that this is a stake of work; the design's "no stake" means no coin balance in consensus, and that still holds. The spec wording needs the distinction.
|
||||
|
||||
**The Kaspa core developer's attack.** "'Late' is not a fact on a DAG. A proof included in a block at DAA score D is late relative to the claim at D minus T only along a chain; a reorg moves D. You will strip a key on one chain and not on another, and the strip is a consensus input to finality. Also, the fault evidence rides in blocks, so a producer who dislikes a prover can withhold its proof for T seconds and then carry the fault record. That is a griefing vector you did not have when nothing waited on a prover (ledger P9)." Answer: the evidence rule must be relative to the carrying block's own chain (as proof records are, spec 7.2 item 5), the deadline must be long relative to merge depth, and the withholding vector is real: a proof gossips to every producer, so withholding needs a majority of producers for T seconds, and a strip is only applied if no block in the carrier's past carried the proof. The griefing cost is one window of a majority, the same bound the finality rule already lives with.
|
||||
|
||||
**Verdict: prototype.** The bond nobody can buy; the two attacks name the wording (work-stake is not coin-stake) and the rule (evidence relative to the carrier's chain) that the prototype must carry.
|
||||
|
||||
### 3.3 The weight table carried inside the recursive segment proof: finality attested by provers, a consensus proof at mergeset cost
|
||||
|
||||
**The idea.** Phase two's consensus proof is scoped as "a zkVM program over the 30-day header window and the vote certificates" (design 7; O-10.8), which is 2.6 million headers per proof and the reason it is phase two. But the segment proof already recurses: segment N verifies segment N minus 1 (spec 7.8 item 1, measured on the 5090). Carry the W2 weight table as a public commitment inside that recursion. Each segment's guest takes the previous segment's committed weight table, adds the blue blocks of its own mergeset per vote key hash (the segment is exactly the mergeset of its chain block, spec 7 terms), ages out the blocks that left the 30-day window, and commits the new table. Every 30 s, when a certificate exists for a checkpoint inside the segment, the guest verifies the BLS aggregate against the table it holds and emits "checkpoint i certified under rule v2 with x percent of total weight". The segment proof then attests both execution and finality, and a light client verifying one wrapped proof learns the certified checkpoint and the state root with no voter set fetched from any node. The provers are the attesters of finality, by construction, with no new role.
|
||||
|
||||
**Why nobody shipped it.** Ethereum's sync-committee light clients (Helios, a16zcrypto.com "Building Helios") trust a committee and fetch it; Succinct's eth-proof-of-consensus (github.com/succinctlabs/eth-proof-of-consensus) proves sync-committee signatures in a SNARK but over a fixed committee, not a weight table that moves with every block. No proof-of-work chain has a weight table to carry. Mina carries a recursive proof of the whole chain but its consensus is stake (approximate, not cloned). The incremental-weight-in-recursion form: new.
|
||||
|
||||
**What Igneum already has.** The aggregator guest with `chain_len` and `prev` (spec 7.8 item 1), the 340-byte `BlockOutput`, the canonical voter list and bitmap (spec 3.10 C3), SP1 with BLS12-381 precompiles (approximate: SP1's precompile set includes bls12-381 field operations; the pairing cost inside the guest is unmeasured), O-10.2 which already asks for a header commitment to the weight table.
|
||||
|
||||
**The model.** Cost per segment: the segment adds at most 180 blocks per chain block at 1 BPS (mergeset limit, spec 7.1) times `N = 8` chain blocks, so about 1,440 table updates (a hash-map add and an age-out) per segment, which is negligible beside the execution; plus one BLS aggregate verification per certificate, at most one per 30 s. The BLS verify is the cost: G1 aggregation of up to V keys and one pairing. In SP1 with the bls12-381 precompiles a pairing is of the order of tens of millions of cycles (approximate, from memory of the precompile benchmarks; unmeasured here), so at 1 pgas = 1,000 cycles it is tens of thousands of pgas, about one shard's budget (`S_p` 30,000 pgas) per 30 s. That is a real cost: about one extra shard per 30 blocks, 3 percent of proving capacity at launch traffic. The table commitment is 32 bytes in the public values; the voter list for a 10,000-key table is 10,000 x 60 bytes = 600 KB of witness per segment, which gossips with the shard witnesses (design 5.1: witnesses are not consensus data).
|
||||
|
||||
**Hours.** 60: the guest's table update and commitment (16), the BLS verify inside the guest and its cycle count on the 5090 (16, needs PC 2 or a fleet box), the light-client path that reads the certified index from the public values (8), the spec text for 7.8 and 10 (8), a fast-time run where a partition's two certificates are both presented to the guest and it accepts one (12).
|
||||
|
||||
**The gate.** (a) Cycle count of one certificate verification inside the guest under 50 M cycles on the pinned SP1 (so under two shards). (b) The browser card (spec 10.8) shows "voter set: verified" with no node asked for the set. (c) The fast-time C4 scenario (a certificate over a chain the node is not on, `docs/fud-ledger.md` C4 sweep): the proof refuses a certificate whose signers' weight at that block is under 2/3 of the table it carries.
|
||||
|
||||
**Per tier.**
|
||||
|
||||
| Tier | Consequence |
|
||||
|---|---|
|
||||
| Home miner, any card | Nothing changes in mining; a 12 GB prover's shard gets the certificate verification about once in 30 shards |
|
||||
| Rig, prover | About 3 percent more proving work at launch traffic, paid from the same pool; the aggregator's record grows by 32 bytes |
|
||||
| Pool user | Nothing |
|
||||
| Holder, wallet user | A phone or tab verifies "locked" from one proof and trusts no node for the voter set; the spec 10.1 row "voter list from nodes" is deleted |
|
||||
| Rollup customer | The bridge on Ethereum verifies one wrapped proof and needs no relayer or committee: this is the proof bridge of spec 7.3, delivered earlier |
|
||||
| Node operator | The witness gossip carries the voter list per segment (600 KB at 10,000 keys) |
|
||||
|
||||
**The Monero core developer's attack.** "You have moved finality's safety from a BLS signature every node checks to a SNARK every node trusts. A soundness bug in SP1 (ledger P7) now forges not only a state root, which full nodes veto by native execution, but a certificate, which full nodes cannot veto because they verify the real BLS certificate separately and will disagree with the proof. Which do you believe? And your 2/3 test inside the proof is against a table the proof itself computed; a bug in the table update is a bug in finality, and it ships in a guest program, not in node code anyone reads." Answer: full nodes keep verifying the BLS certificate natively and the native-execution veto extends to the public values (a record whose certified index or table commitment differs from the node's own is invalid, spec 7.2 item 5 as written), so a forged certificate is, as for state, a light-client problem and never a chain split. The table update is a second implementation of W2 and must be differential-tested against the node's (gate c).
|
||||
|
||||
**The Kaspa core developer's attack.** "The weight table is defined over the block's DAG past (W2 counts blue blocks in the past of the chain block); your segment is the mergeset of chain block C in GHOSTDAG order, so the incremental update is only correct if every block in the window is in exactly one segment's mergeset, which holds for blue and red blocks of the selected chain's mergesets, but a reorg of the selected chain re-cuts the segments and the table must be re-derived from the fork point. Your proof chain breaks at every reorg deeper than one segment, and at 10 BPS with k = 124 a reorg of 8 chain blocks is ordinary." Answer: correct, and the recursion already restarts at an unproven segment (spec 7.8 item 7, the unproven rule); a reorg deeper than a segment invalidates the records of the abandoned chain as it does today. The table commitment must therefore be part of the statement per chain block, re-proven on the new chain, which is what re-proving the segment already does. The cost at 10 BPS is the open question for the gate.
|
||||
|
||||
**Verdict: do now (design and guest prototype).** It turns phase two's hardest item into an incremental one on code that exists, and it is the only road to "your browser verifies Igneum" with no node in the trust row.
|
||||
|
||||
### 3.4 A WebAssembly verifier of the wrapped block proof in the browser
|
||||
|
||||
**The idea.** The homepage card verifies a BLS certificate in JavaScript today (58 to 68 ms warm, 139 to 155 ms cold, bench-log round 6, P3). Ship the other half: the Groth16 or Plonk wrapper of the segment proof verified in WebAssembly in the tab, with the measured millisecond count shown.
|
||||
|
||||
**Why nobody shipped it on a proof-of-work chain.** Because no proof-of-work chain proves its blocks. The working precedents are rollup-side: ProjectZKM's `ziren-wasm-verifier` (github.com/ProjectZKM/ziren-wasm-verifier: "Verify STARK, Groth16 and Plonk proofs in browser", one Rust codebase to native and WASM), xycloo's `wasm-groth16-verifier` (github.com/Xycloo/wasm-groth16-verifier, with a live demo), a16z's Helios shipped as `@a16z/helios` on npm with WASM bindings (github.com/a16z/helios issue 76 and the npm package).
|
||||
|
||||
**What Igneum already has.** `site/verify/core.js` (BLAKE2b header hashes, canonical voter list, BLS aggregate over `@noble/curves`), the SP1 light verifier as a 58 MB native binary (bench-log, "the program id split"), the `wrap` step in the `ProofSystem` trait (design 5.6) unbuilt.
|
||||
|
||||
**The model.** A Groth16 proof over bn254 is three group elements, about 128 bytes compressed (spec 10.5, approximate); verification is one multi-pairing. In WASM a bn254 pairing is of the order of 10 to 50 ms on a laptop core (approximate, from the ziren and xycloo demos' order of magnitude; unmeasured here). Bytes per day in phase two on-demand mode: 800 bytes per open (spec 10.5).
|
||||
|
||||
**Hours.** 16: wrap the pinned aggregator proof to Groth16 with SP1's wrapper on a 24 GB fleet card (8, the P3 phase 2 benchmark brought forward), compile the verifier to WASM and wire it to the card (8). The wrapper's own cost on consumer hardware is the open measurement R4.
|
||||
|
||||
**The gate.** The card shows the wrapped proof verified in the tab, with bytes and milliseconds, on a phone-sized viewport, against the live devnet; the number lands in the bench-log with the browser and the device.
|
||||
|
||||
**Per tier.** A home miner's Ember node serves the proof to the tab; a holder with no node verifies state in the tab (the state root still rests on a certificate the client is given until 3.3 lands); a rollup customer sees the verifier it will run on its own chain; node operators serve one more 128-byte object.
|
||||
|
||||
**The Monero core developer's attack.** "A verifier in a tab served by your domain verifies whatever your domain says the verifying key is. Your 'no middleman' is your web server. Monero's answer to this class is: run a node." Answer: correct, which is why spec 10.8 already removed "no node, no trust, no middleman" and why the phone app with a pinned seed list is the client that meets 10.6; the tab is a demonstration with its trust row stated on the card.
|
||||
|
||||
**The Kaspa core developer's attack.** "Fine, it verifies a proof. Of which chain? The proof commits to a chain block hash; the tab needs to know that block is on the selected chain at or below a certified checkpoint, which it asks a node for (spec 10.4 item 4). You verified the arithmetic and trusted the topology." Answer: correct until 3.3 folds the certified index into the same proof.
|
||||
|
||||
**Verdict: do now.** Cheap, precedented, and the phase 2 wrapper measurement has to happen anyway.
|
||||
|
||||
### 3.5 Continuous miner-voted parameters, bounded per block, in place of two-week proposals
|
||||
|
||||
**The idea.** Spec 5.8 sets a parameter the genesis rules leave to miners by a registered proposal passing 60 percent of blue blocks over two weeks. For the handful of parameters that are dials rather than switches (`B_p`, `S_p`, the base-fee floors, the exclusive window, the external claim timeout) use Ethereum's gas-limit mechanism instead: each block carries the producer's vote for each dial, the value in force at a block is the median of the window's votes, and the median may move at most 1/1,024 of its value per block, within a hard range fixed at genesis. No proposal, no bit, no two-week window, no human. Switches (a new instruction family, a proof-system version) keep the 90 percent signal.
|
||||
|
||||
**Why nobody shipped it this way.** Ethereum moves its gas limit by producer vote, bounded to 1/1,024 of the parent's limit per block (geth `core/block_validator.go`, VerifyGaslimit; approximate, not cloned). Bitcoin's BIP9 is a 95 percent tally over 2,016 blocks with LOCKED_IN and one more retarget before activation (bips.dev/9); BIP 135 generalised the thresholds. Kaspa's Crescendo was a fixed DAA score: `crescendo_activation: ForkActivation::new(110_165_000)` for mainnet and `88_657_000` for testnet (`vendor/rusty-kaspa/consensus/core/src/config/params.rs` lines 648 and 704; the struct at line 28), with nodes connecting only to protocol version 7 peers from 24 hours before (docs/crescendo-guide.md at v1.0.0). Monero's upgrades are scheduled hard forks, formerly every six months, now every 9 to 12 months (getmonero.org). Igneum's own 6 October incident was a fixed-height activation crossing a half-updated fleet (CLAUDE.md, Devnet 2 rules). Nobody applied Ethereum's dial to a proof-of-work chain's economic parameters. The combination is new; the mechanism is Ethereum's.
|
||||
|
||||
**What Igneum already has.** The header's version bits (O-5.3 candidate), the 60 percent rule, the fee parameters as `Params.fees` per network (spec 5.11), the DAA window every node computes.
|
||||
|
||||
**The model.** At 1/1,024 per block and 1 BPS a dial can move 2.3x in a day (1.001^86,400) if every producer votes the same way, 1.07x if 51 percent do and 49 percent vote the other way (the median moves only when a majority agrees, and then one step per block). A hostile 51 percent can therefore walk a dial to the genesis bound in days; the bound is the defence, as it is on Ethereum (the gas limit has a hard floor and no cap besides the vote).
|
||||
|
||||
**Hours.** 30: the vote field and median rule (10), the clamp and bounds in `Params` (6), tests including the 51 percent walk (8), spec 5.8 text (6).
|
||||
|
||||
**The gate.** On the fast-time harness, 100 producers at 60/40 split: the dial moves toward the 60 side at the predicted rate and stops at the bound; with a 50/50 split it does not move; a producer that votes outside the range is invalid.
|
||||
|
||||
**Per tier.** Miners set the dials with their blocks (Ember shows the vote and defaults to "hold"); a pool votes for its members in mode A and B templates and the member sees it (spec 9.4); provers watch `B_p` and `S_p` move with the fleet's measured shard time instead of waiting for a human; holders and rollup customers see fee floors that track usage; node operators gain one field per header.
|
||||
|
||||
**The Monero core developer's attack.** "You have handed the fee floor to whoever has 51 percent of blocks, with no social veto. On Monero the dynamic block size has a penalty curve exactly so that a majority cannot cheaply walk it; your 1/1,024 is a speed limit, not a cost. A pool with 51 percent lowers `f_p` to its floor, bloats blocks with wash gas it no longer pays for, and the provers eat the backlog." Answer: the base fee is burned in full, so wash gas is never free (ledger E3), and the backlog rule halves `B_p` regardless of the vote (design 4.3); the range bound caps the walk. The point stands that a dial needs a cost curve, not only a speed limit: the prototype should add Monero's shape (a vote away from the median costs the producer a fraction of its subsidy).
|
||||
|
||||
**The Kaspa core developer's attack.** "A per-block vote on a DAG: which blocks vote? Blue blocks of the selected chain's mergesets, in order, and the median over a window is a function of the block's past, fine. But a parameter in force 'at a block' must be the same for every node validating that block: use the value at the block's selected parent's checkpoint, or two honest nodes meter the same transaction at two prices. You have the same determinism bug the proof-record rule had before P11." Answer: correct; the value in force is read at the last certified checkpoint in the block's past, as 3.1's W30 is.
|
||||
|
||||
**Verdict: prototype.** The chain's economic dials follow the fleet without a human; the two attacks give it the two rules (a cost curve, a checkpoint-anchored read) it needs.
|
||||
|
||||
### 3.6 Treasury-less audit funding: bounties from the burn, a review escrow on upgrades
|
||||
|
||||
**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).
|
||||
|
||||
**What Igneum already has.** The base-fee burn in both dimensions, the 60 percent signalling, the ledger's break-submission rule (spec 0.5), the proposal registration transaction (spec 5.8).
|
||||
|
||||
**The model** (frontier_model.py section 4):
|
||||
|
||||
| Chain traffic (fraction of full blocks) | Base fee burned per day, IGN | 30-day redirect, IGN | USD at 0.02 | USD at 0.10 |
|
||||
|---|---|---|---|---|
|
||||
| 0.01 | 2,592 | 77,760 | 1,555 | 7,776 |
|
||||
| 0.10 | 25,920 | 777,600 | 15,552 | 77,760 |
|
||||
| 0.50 | 129,600 | 3,888,000 | 77,760 | 388,800 |
|
||||
| 1.00 | 259,200 | 7,776,000 | 155,520 | 777,600 |
|
||||
|
||||
At launch traffic a 30-day redirect is under one audit contest; at half-full blocks it is a serious bounty. The money exists only once the chain is used.
|
||||
|
||||
**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 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.
|
||||
|
||||
**The Monero core developer's attack.** "This is a dev fund with extra steps. You removed a 5 percent fund because 'a switch that routes money to an address somebody controls is the first thing a critic points at'; you have now written a switch that routes money to an address somebody controls, gated by the same miners who gate everything else, and you have made the miners the judge of which cryptographer gets paid. Our CCS works because it is off-chain and voluntary and no consensus rule touches it. Keep the burn a burn." The attack is right. Answer: there is no counter besides the honest one: the alternative is the one the litepaper already states (client fee, founders' mined coins, grants off-chain), and the ledger should carry this entry as considered and rejected on the same ground as E4.
|
||||
|
||||
**The Kaspa core developer's attack.** "Also a soft target: a miner cartel with 60 percent invents a break, 'accepts' it, and un-burns 30 days of fees to itself. Your spec 0.5 reproducibility rule is a human judgement; consensus cannot check it." Correct.
|
||||
|
||||
**Verdict: watch.** Design it, do not ship it; record it in the ledger beside E4 as the honest answer to "where do audits come from with no fund": from the company's dev fee and from the people who care, in the open.
|
||||
|
||||
### 3.7 Reproducible-build attestations in a registry contract; Ember refuses a release under N of M attestations
|
||||
|
||||
**The idea.** Bitcoin Core builds with Guix and independent builders publish signed attestations of the output hashes to the `guix.sigs` repository (`bitcoin/bitcoin` PR 21462 added `guix-attest` and `guix-verify`; bitcoinops.org reproducible builds). Put the attestation registry on Igneum: a contract where a builder set posts `(release tag, artefact hash, signature)`; the Ember updater refuses to install a release whose artefact hash has fewer than N attestations from M builders listed in the release key's policy, and shows the attesters. Windows exes are already reproducible here (`-Wl,--no-insert-timestamp`, CLAUDE.md), so the hash is well defined.
|
||||
|
||||
**Why nobody shipped it on chain.** Bitcoin keeps its sigs in a Git repository because Bitcoin has no contract state; Ethereum clients attest off-chain. Igneum has an EVM and a signed updater that already checks a manifest hash. The on-chain registry read by the updater: new in placement, not in idea.
|
||||
|
||||
**What Igneum already has.** Reproducible builds on the box (`tools/build-remote.sh`, `cross-remote.sh`), the signed manifest and updater in Ember (litepaper, Ember table), the commit-string check (`tools/ci/commit-string-check.sh`), the release key in genesis (spec 8).
|
||||
|
||||
**The model.** Trust goes from one key (the release key, which on the devnet "acts as the operator", litepaper Governance) to N of M builders, with M growing as outside builders arrive. The failure the rule catches: a release signed by a stolen key whose hash no independent builder reproduced. Cost per release: one transaction per builder.
|
||||
|
||||
**Hours.** 12: the registry contract (4), the updater's N-of-M check and the display (6), the CI step that posts the box's attestation (2).
|
||||
|
||||
**The gate.** A release whose binary is altered after signing is refused by Ember on three machines; a correct release with N attestations installs; the registry shows both.
|
||||
|
||||
**Per tier.** Every miner's updater refuses an unattested binary and names who attested; a pool operator and a node operator get a chain-readable answer to "is this the binary everyone runs"; holders and rollup customers see that the "release key as operator" sentence has a closing mechanism.
|
||||
|
||||
**The Monero core developer's attack.** "Who are the M builders at launch? The founder, under three names. Reproducible builds are only as good as the independence of the builders, and a pseudonymous one-founder project has one builder. Gitian and Guix were worth something because dozens of people with names attested. You are moving a sigs repo on chain; you are not adding a builder." Answer: correct, and the registry is what lets a second builder exist with a public record; the gate should be the first attestation from a machine the project does not own.
|
||||
|
||||
**The Kaspa core developer's attack.** "A node that reads a contract to decide whether to update is a node whose update path depends on the chain being live and unforked; during the 6 October two-sided chain you would have had two registries." Answer: the updater installs nothing while finality is paused, which is a rule worth adding anyway.
|
||||
|
||||
**Verdict: do now.** Twelve hours, and it closes a sentence the litepaper has to carry today.
|
||||
|
||||
### 3.8 Ember as node, wallet and light client for everyone
|
||||
|
||||
**The idea.** Ember already runs a node, mines, proves and keeps a key; Igneum Wallet reads Ember's node when present. Make the one app the client for everyone: a holder who does not mine runs Ember in "verify" mode (the light client of spec 10 inside the same binary, the node card to pin a seed), a miner runs it in full mode. Every miner is a node; every holder is at least a light client; nobody runs a browser wallet against someone else's RPC by default. The node count becomes the miner count plus the holders who chose full mode.
|
||||
|
||||
**Why nobody shipped it.** Bitcoin Core is a node and a wallet but not a miner; miners run separate software (approximate). Monero's GUI runs a node and a wallet and can mine on the CPU (approximate; the litepaper's reason for no CPU lane is botnets). Kaspa's miners run kaspad plus a separate miner. Igneum's Ember already supervises node, miner, prover and key (litepaper, Ember table), so the step is small. Not new; the combination with the light client and the vote key in one binary is Igneum's.
|
||||
|
||||
**What it does to node counts and Sybil counts.** Bitcoin: 24,682 reachable nodes (bitnodes.io, 5 October 2026); Ethereum: 8,136 execution clients on ethernodes.org, 11,781 on Etherscan the same day; Monero: about 5,000 peers seen in 72 hours by one tracker (monero.fail), all secondary. Igneum's gate 4 is 1,000 independent miners; with Ember as the node those are 1,000 full nodes on day one of the public testnet, each with a vote key. Sybil counts are irrelevant on Igneum by design: nothing in consensus counts nodes or keys (spec 3.1 W6, ledger F17), so a Sybil inflates a node map and nothing else. The one place a count matters, "no verifier, no vote" (spec 9.7 item 2), is served better: every member has a verifier because the signer is the verifier.
|
||||
|
||||
**Hours.** 20: the light-client engine inside Ember's process with a mode switch (12), the wallet reading it (4), the node card pinning (4).
|
||||
|
||||
**The gate.** A machine with no GPU runs Ember in verify mode, shows "locked" from a certificate it verified, and sends a transaction; a miner's Ember shows one key in the header of its blocks and the same key signing votes.
|
||||
|
||||
**Per tier.** A home miner runs one program; a holder with a laptop runs a verifier instead of trusting an RPC; a pool user's member process is Ember (spec 9.1); a rig runs one signer and many workers (spec 9.6); the node operator is now everyone.
|
||||
|
||||
**The Monero core developer's attack.** "A node that is also a hot wallet with a vote key and a miner is one process with every secret in it; one bug in the dashboard's local HTTP server (you serve it behind a per-launch token) and the key, the vote and the coins go together. Monero separates the daemon from the wallet for this reason." Answer: the separation stays at the process level (signer, workers, node, wallet are separate processes under one supervisor), and the vote key and the payout key are different keys; the attack names the test (the dashboard token's threat model) that gate 4 must include.
|
||||
|
||||
**The Kaspa core developer's attack.** "Node count is a vanity metric; what matters is who produces blocks and who the honest majority peers with. A thousand Ember nodes behind home NAT accept no inbound connections, so your reachable count is your seed list plus the rigs, and an eclipse of the seed list eclipses the fleet." Answer: correct; spec 10.6's pinned seed list with identity keys and the local peer set is the defence, and the eclipse test O-3.7 with Ember nodes is the gate.
|
||||
|
||||
**Verdict: do now.** Twenty hours; the testnet gate then measures nodes, not only miners.
|
||||
|
||||
### 3.9 Hardware wallets that verify proofs
|
||||
|
||||
**The idea.** A Ledger or Trezor that verifies the wrapped block proof (or the certificate) before signing, so "final" is checked on the device, not in the companion app.
|
||||
|
||||
**Why nobody shipped it.** Ledger's secure element is EAL6+ certified and Trezor's Safe 3 and 5 use an Infineon OPTIGA Trust M (trezor.io; ledger.com), both small, slow, memory-poor chips. The cryptographic work hardware wallets do is signing; the heavy lifting (sync, proofs, history) is in the companion. A bn254 pairing on a Cortex-M-class secure element is seconds to minutes (approximate, from memory; the Bulletproofs-on-Trezor paper, eprint 2020/281, shows what Micropython on a Trezor costs for range proofs, and it is slow). Igneum Wallet already verifies the finality certificate with the node's own code (litepaper, Wallet table), which is the companion doing it.
|
||||
|
||||
**The model.** Certificate verify: G1 aggregation of up to 10,000 keys plus one BLS12-381 pairing; on a laptop in JavaScript 58 to 155 ms (bench-log). A secure element is 100x to 1,000x slower on scalar arithmetic than a laptop core (approximate), so 6 to 150 s per certificate, every 30 s. Not shippable.
|
||||
|
||||
**Hours.** 12 for a companion-side integration (the wallet already does it); the on-device path is not worth hours.
|
||||
|
||||
**Per tier.** Holders get "final means final" in the companion today; nobody gets it on the secure element.
|
||||
|
||||
**The Monero core developer's attack.** "Monero's Ledger app exists and does nothing but sign; it took years and a custom protocol (eprint 2020/281). You will not get a proof verifier onto a secure element and you should not pretend to." Correct.
|
||||
|
||||
**The Kaspa core developer's attack.** "A device that verifies a proof still needs to know which chain tip the proof is of; it will take that from the companion, which is the thing you did not trust." Correct.
|
||||
|
||||
**Verdict: never on the secure element; do the companion verify** (already done in Igneum Wallet 0.1.1; the Ledger and Trezor apps when they exist should display the companion's verified state and sign).
|
||||
|
||||
### 3.10 Proof-of-useful-work: the lottery hash partly a proof
|
||||
|
||||
**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 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.
|
||||
|
||||
**What Igneum already has instead.** The separation (litepaper: "the lottery and the proving are kept separate on purpose"), the 20 percent pool paid by sortition by weight, PoVW-like cycle metering through pgas.
|
||||
|
||||
**Hours.** 0.
|
||||
|
||||
**Per tier.** Nothing changes; the 12 GB card still earns from proving through the pool.
|
||||
|
||||
**The Monero core developer's attack.** "RandomX's whole point is that the work has no second use, because any second use is a subsidy to whoever does the second thing best, and that is a specialist. The moment your hash is 'partly a proof', the best prover is the best miner, and the best prover is a datacentre. You know this; it is in your own CLAUDE.md."
|
||||
|
||||
**The Kaspa core developer's attack.** "Leader election on a DAG must be a memoryless Poisson process so that GHOSTDAG's k and the orphan analysis hold; a target that depends on the miner's past hour of proving is not memoryless and your blue-set bounds no longer apply."
|
||||
|
||||
**Verdict: never.** Both attacks are correct and the second is fatal to the DAG analysis. The honest version of "useful work" is the one Igneum has: the same card, two jobs, two payments, no coupling. Lane 8 may take one adjacent idea by name: **the shadow-useful puzzle**, in which the program work placed in the latency shadow (class v4, 100,000 ops per hash that cost the card nothing) is itself a small verifiable sub-computation drawn from chain state (a hash-based commitment to a sampled Merkle path of the segment's state witness), so the shadow ops have a second use that does not change who wins. It changes nothing about leader election because the shadow is free; whether a useful shadow program is as chip-hostile as a random one is lane 8's question.
|
||||
|
||||
### 3.11 Miners paid for proving others' chains as the main income, the lottery as the tiebreaker
|
||||
|
||||
**The idea as asked.** Invert the design: proving is the income, the lottery only orders.
|
||||
|
||||
**The arithmetic** (frontier_model.py section 7):
|
||||
|
||||
| Income line | USD per day | Basis |
|
||||
|---|---|---|
|
||||
| Proving every Ethereum L1 block at the Sep 2026 tracker cost | 36 | USD 0.005 x 7,200 blocks; a buyer pays above cost, call it 10x: 360 |
|
||||
| The same at the Dec 2025 cost | 288 | under USD 0.04 per block |
|
||||
| All rollup proving spend (customer brief) | 8,200 to 27,400 | "low millions a year", approximate |
|
||||
| Boundless, trailing day in the explorer, 4 Oct 2026 | 2 | 8.4 T cycles at USD 0.21 per billion, `developer-adoption.md` 2b, approximate |
|
||||
| Igneum year-1 emission at USD 0.005 per IGN | 13,700 | 31.688 IGN per block x 86,400 |
|
||||
| At USD 0.02 | 54,800 | |
|
||||
| At USD 0.10 | 273,800 | |
|
||||
|
||||
The whole public proving market is three to four orders of magnitude under year-1 emission at any price input. The cost curve (section 2.6) falls 3x to 30x a year, so dollars per proof fall as fast as volume rises; for proving to be the main income by 2030, paid demand must grow about 1,000x in dollars. The design's own claim is the defensible one: a second income that keeps cards on after the subsidy fades (spec 5.10.2).
|
||||
|
||||
**Why nobody shipped it.** Succinct and Boundless are exactly this (proving as the income) and have a token for the lottery's role; their provers are datacentre operators (ledger C10). Nobody has made it a GPU home-miner's main income because the market is this size.
|
||||
|
||||
**Hours.** 0.
|
||||
|
||||
**Per tier.** The 12 GB home card earns pool emission today and job income later; the number that matters to it is the pool share, not the market.
|
||||
|
||||
**The Monero core developer's attack.** "Your 'paid, useful, verifiable work' line implies the work pays. It does not and will not; say so in the litepaper's income table." Answer: the litepaper already says "small market today", "upside, not a promise" (ledger P6); the arithmetic above should join it.
|
||||
|
||||
**The Kaspa core developer's attack.** "If proving were the income, the lottery would be a cost centre miners minimise, hash would fall to the floor, and your 51 percent cost would be the cost of a few 5090s. Keep the lottery paid." Correct.
|
||||
|
||||
**Verdict: never by 2030 as the main income; watch the market yearly.**
|
||||
|
||||
### 3.12 The GPU fleet as a public compute market beyond proofs, priced in IGN
|
||||
|
||||
**The idea.** Rendering, inference, transcoding, simulation sold by Igneum miners for IGN, through the same client that switches between hashing and proving.
|
||||
|
||||
**The honest problem.** General compute is unverifiable: a renter cannot tell a rendered frame from a cheaper one, an inference from a smaller model's, without redoing the work. The existing markets answer with trust substitutes: Render uses result quorums for graphics, Akash provider auctions plus reputation, io.net proof-of-work-style attestations (all secondary, io.net's own comparison page and a 2026 DePIN survey). None of those is checkable by a chain.
|
||||
|
||||
**The verifiable subsets, named.**
|
||||
|
||||
| Work | How it is verified | Status for Igneum |
|
||||
|---|---|---|
|
||||
| ZK proving jobs | The proof | The precompile (design 6), Designed |
|
||||
| Deterministic recompute with sampling | Commit to every intermediate, a verifier re-runs a random fraction (Statistical Proof of Execution, arXiv 2503.18899; sampled layerwise proofs for inference, arXiv 2609.27367) | Feasible as an app on the precompile: the sampled chunk is the job; the rest is a commitment |
|
||||
| Rendering with result quorum | Two or three miners render the same frame; the chain pays on agreement (Render's approach, approximate) | An app; the chain pays per agreement, cannot judge quality |
|
||||
| TEE-attested inference | NVIDIA confidential computing attestation on H100 and H200 (phala.com GPU TEE) | The fleet's cards have no TEE; not Igneum's |
|
||||
| Bitwise-reproducible training | Verde-style proofs of learning on a rollup (secondary, io.net comparison page) | Research |
|
||||
|
||||
**Hours.** 60 for a sampled-recompute job type on top of the precompile; 0 for the general market.
|
||||
|
||||
**Per tier.** A 24 GB card could sell sampled-recompute work; an 8 GB card cannot hold most inference models; the rollup customer is unaffected; a holder sees IGN demand only for the verifiable subset.
|
||||
|
||||
**The Monero core developer's attack.** "You would be Golem, Render and Akash with a worse token story and a settlement layer nobody asked for. The honest answer to 'GPU owners should be paid for useful work' is a market with reputation, and reputation is not a consensus rule." Correct for the general case.
|
||||
|
||||
**The Kaspa core developer's attack.** "Every second a card spends on a render is a second off the lottery; the design's own economy model shows hash falling 14 percent when external pay rises 10x (scenario b). A compute market large enough to matter would empty the lottery." Correct, and it is the reason 3.11 is never.
|
||||
|
||||
**Verdict: never for unverifiable work; do the verifiable subsets as apps on the precompile** (the sampled-recompute job type is the one worth 60 hours).
|
||||
|
||||
### 3.13 Igneum as the settlement layer for GPU rental itself
|
||||
|
||||
**The idea.** Vast.ai and RunPod match renters and hosts and take a platform cut; the escrow, the metering and the payout could run on Igneum, where every miner is already a host with a funded wallet and a card that is on.
|
||||
|
||||
**The fee arithmetic** (frontier_model.py section 5):
|
||||
|
||||
| Card | Vast.ai on-demand USD/h | Platform take modelled | Host loses USD per card-year | Igneum settlement per rental (2 transfers at the floor) | At USD 0.02 / 0.10 per IGN |
|
||||
|---|---|---|---|---|---|
|
||||
| RTX 5090 | 0.44 (getdeploying.com, 6 Oct 2026) | Vast about 15 percent (secondary) | 579 | 0.0102 IGN | 0.0002 / 0.0010 |
|
||||
| RTX 5090 | 0.44 | RunPod about 7 percent (secondary: hosts keep 93) | 270 | 0.0102 IGN | |
|
||||
| RTX 4090 | 0.31 | Vast about 15 percent | 408 | 0.0102 IGN | |
|
||||
| RTX 4090 | 0.31 | RunPod about 7 percent | 190 | 0.0102 IGN | |
|
||||
|
||||
Caveat on the takes: secondary comparisons put Vast at about 15 percent and RunPod at about 7 percent; Vast's own June 2024 product update says the host fee was removed and replaced by a surcharge it does not publish, so the 15 percent is a market estimate, not a fee page. The chain's fee is three to five orders of magnitude under either. The platform's take pays for matching, images, dispute, trust and the verification of delivered work, and the last is what the chain cannot do (3.12).
|
||||
|
||||
**What Igneum already has.** Funded miner wallets, the pool protocol's TLS transport and member identity (spec 9), the job escrow shape (design 6), the sampled-recompute path above.
|
||||
|
||||
**Hours.** 40: a rental escrow contract with hourly streaming and a sampled attestation of liveness (the host signs a challenge per minute with the vote key; proves possession of the card by running one lottery warp on it, which the CPU verifier checks in 0.44 ms) (24), a client-side matching list (16). It settles payment and liveness; it does not verify the renter's workload.
|
||||
|
||||
**The gate.** Ten rentals between fleet boxes with one host that goes dark: the escrow pays to the minute of the last valid challenge; the renter's refund is exact; the chain fee per rental under 0.02 IGN.
|
||||
|
||||
**Per tier.** A home miner rents out idle hours with no platform cut and a 30-day public record as a host; a rig lists eight cards; a pool user is unaffected; the prover role and the host role compete for the same seconds; a holder sees IGN demand per rental; a rollup customer is unaffected.
|
||||
|
||||
**The Monero core developer's attack.** "Escrow is 1 percent of a marketplace. The 15 percent is the other 99: the people who answer when a pod dies. You will have a cheaper escrow and no renters, and every renter you do get will be running the thing Vast bans. Also: a card that is rented is a card that is not mining, so you are paying people to leave your lottery." The last point is 3.12's and stands.
|
||||
|
||||
**The Kaspa core developer's attack.** "Streaming payments per minute at 1 BPS are 1,440 transactions a day per rental, each burning a base fee; at a thousand rentals that is your whole block budget. Use a channel, settle twice." Correct, and the model's two transfers assume exactly that.
|
||||
|
||||
**Verdict: prototype** the escrow with the liveness challenge, because it reuses the vote key and the CPU verifier in a way no other chain can, and because miners are hosts already; do not call it a marketplace.
|
||||
|
||||
### 3.14 Proofs sold to AI labs for verifiable inference
|
||||
|
||||
**The state of the art, cited.** zkLLM (arXiv 2404.16109, CCS 2024) proves a 13 B-parameter LLM's inference in under 15 minutes with proofs under 200 kB, verified in 1 to 3 s; the 2026 sampled-layerwise paper (arXiv 2609.27367) measures 803 s of proving per forward pass on LLaMA-2-13B and extrapolates about 18 days per 2,000-token generation under full ZK. EZKL's median proof time on small workloads is about 8.2 s and a 100 M-parameter model is about 10,000 s per proof at today's throughput (proofoftech.org, secondary). Modulus Labs' Remainder prover was benchmarked at USD 0.085 per proof to verify on Base; the team joined Tools for Humanity in late 2024 and no longer sells (proofoftech.org). The competitor is a TEE: NVIDIA confidential computing on H100 and H200 with remote attestation, sold today at near-zero overhead (phala.com; arXiv 2607.19353 benchmarks), and sampling schemes (SPEX, arXiv 2503.18899) that are statistical, not cryptographic.
|
||||
|
||||
**Cost per token, approximate.** 803 s of one GPU per forward pass on a 13 B model at a USD 0.44 5090-hour is about USD 0.10 per token proven. An unverified 13 B token is of the order of USD 0.0000002 (secondary inference pricing pages, 2026). The gap is five to six orders of magnitude.
|
||||
|
||||
**What Igneum could sell by 2030.** Not inference proofs for frontier models. Proofs that a committed small model (under 100 M parameters) produced an output from a committed input, batched; proofs of aggregation over many small inferences; proofs of a sampled layer (the hybrid in arXiv 2609.27367) as a job type. `developer-adoption.md` 2b already draws the line at "verifiable compute, not verifiable AI".
|
||||
|
||||
**Hours.** 40 for a sampled-layer job type once the precompile exists; 0 today.
|
||||
|
||||
**Per tier.** A 24 GB card could prove a small model's inference as a job; nothing for smaller cards; a rollup customer is unaffected.
|
||||
|
||||
**The Monero core developer's attack.** "A lab that wants verifiable inference buys an H100 with a TEE and gets an attestation for free. Your 100,000x-slower proof is for people who do not trust NVIDIA's attestation key, and those people are not buying GPU time from strangers." Fair for 2026 to 2028.
|
||||
|
||||
**The Kaspa core developer's attack.** "Nothing here touches consensus; it is an app on the precompile. Stop listing apps as protocol ideas." Fair.
|
||||
|
||||
**Verdict: watch** the cost curve yearly; the crossing where ZK beats a TEE on cost per token is not in sight by 2030 on the cited numbers.
|
||||
|
||||
### 3.15 The Igneum program pipeline as a verifiable randomness beacon
|
||||
|
||||
**The idea.** The chain already derives an unbiasable seed once an hour: a certified checkpoint, through a 10-minute class-group VDF (spec 04; 516-byte proof, 4.47 ms verify). Run the same VDF on every certified checkpoint hash at a 30-s delay and publish the output: a public randomness beacon at 30-s cadence with no league, no threshold key and no trusted set.
|
||||
|
||||
**What drand is, cited.** The League of Entropy runs drand: threshold BLS over `H(round)` in unchained mode, a 2/3 threshold of a fixed set of organisations (the threshold must exceed 50 percent), quicknet at 3-s rounds since October 2023, timelock encryption built on it (docs.drand.love quicknet post and cryptography page). Its trust assumption is that under a third of a named set collude.
|
||||
|
||||
**The model** (frontier_model.py section 6):
|
||||
|
||||
| Beacon | Period | Latency | Unbiasability | Trust |
|
||||
|---|---|---|---|---|
|
||||
| drand quicknet | 3 s | about 3 s | threshold BLS, under 1/3 of about 20 organisations collude | a league |
|
||||
| Igneum epoch seed today | 3,600 s | 600 s | certified checkpoint plus a VDF the last producer cannot evaluate in time | nobody |
|
||||
| Proposed per-checkpoint beacon | 30 s | 30 to 60 s | the checkpoint is locked by 2/3 of 30-day weight before the VDF starts; a last-block grind costs a block's subsidy per try and buys a bit only if the attacker evaluates the VDF faster than the chain | nobody; the honest limit is the class-group ASIC (Chia's timelords are software or ASIC, docs.chia.net) |
|
||||
|
||||
Chia's hardware timelords are the precedent for "the fastest squarer learns the value first" (Boneh, Bonneau, Bünz, Fisch, eprint 2018/601 for the VDF; Chia's class-group VDF competition repository for the implementation lineage). That is a front-running edge measured in seconds, not a bias.
|
||||
|
||||
**What Igneum already has.** The VDF prototype (`proto-vdf/`), `seed_source` in headers, PREVRANDAO already defined from the epoch VDF (spec 7.1), the certificate every 30 s.
|
||||
|
||||
**Hours.** 24: a 30-s VDF parameter set and the proof relay per checkpoint (12), an RPC and a `wss` feed (6), a contract exposing the latest value and a verify function (6).
|
||||
|
||||
**The gate.** 2,880 values a day on the devnet for a week; every value verified by an independent client in under 5 ms; no value published before its checkpoint locked; a deliberate withholding of the last block before a checkpoint measured for its effect on the output (none, because the checkpoint is what is locked).
|
||||
|
||||
**Per tier.** A node operator evaluates one 30-s VDF per checkpoint (one core); a miner does nothing new; an app developer gets a 30-s beacon and timelock encryption; a holder sees a product that drand's users (lotteries, raffles on Sui, approximate) might pay gas for; a rollup customer could read it through the proof bridge.
|
||||
|
||||
**The Monero core developer's attack.** "Your beacon is only as unbiasable as your finality, and your finality pauses whenever under 2/3 of weight is connected (spec 03). A beacon that stops when the chain is partitioned is not a beacon; drand ran through every outage its members had because it needs a threshold, not a supermajority of all." Answer: correct; the beacon publishes nothing during a pause and must say so, which is still a stronger statement than a league's liveness.
|
||||
|
||||
**The Kaspa core developer's attack.** "A 30-s VDF on a 1-BPS chain is fine; at 10 BPS your checkpoints are still 30 s of DAA time, fine; but the VDF input must be the checkpoint hash as every node agrees it, and your C4 finding showed two honest nodes can hold two certified checkpoints at one index for a window. Two beacons." Answer: the beacon for index i is published only when a single certificate for i is in the past of the next certified checkpoint, which is the F24 re-determination path; one window of delay in the worst case.
|
||||
|
||||
**Verdict: prototype.** Twenty-four hours on code that exists, and a product no proof-of-work chain offers.
|
||||
|
||||
### 3.16 The hourly program swap as a research dataset; the fleet library as a product
|
||||
|
||||
**The idea.** Igneum generates 8,760 random GPU kernels a year, compiles each on Metal, CUDA and OpenCL, races up to 17 variants per card (lever 1, measured +17 to +21 percent on the M5 Max), and logs per-card per-variant timings to the fleet log (lever 2). That corpus does not exist anywhere: a continuous stream of random, bit-exact-across-vendors integer kernels with measured performance on every consumer GPU, under a fixed memory footprint. Publish it (the generator is public with the spec; the timings are the product) and the fleet library (the per-card best-variant table) as a dataset.
|
||||
|
||||
**Who would pay, what for.** Compiler teams (LLVM's NVPTX and AMDGPU backends, Apple's Metal compiler) for a regression corpus with ground truth across vendors; GPU microarchitecture researchers for a latency-bound random-read benchmark across generations (the dependent-read ceilings of `chip-model-v3.md` 5.3 are exactly what such a corpus measures); the project's own cryptanalysts (the weak-program census, `weak-program-census-2026-10-03.md`) for the distribution of program properties. Money: small (research datasets are grants and goodwill, not revenue); standing: large, and it is the public benchmark the litepaper promises for January 2027 made continuous.
|
||||
|
||||
**Why nobody shipped it.** RandomX programs are per hash, interpreted, and never logged; ProgPoW's period changes were never published as a corpus (approximate). New as a dataset.
|
||||
|
||||
**Hours.** 10: a daily export of the fleet log and the generator seed list to a public bucket with a schema (6), a README with the citation form (4).
|
||||
|
||||
**The gate.** One outside group cites it.
|
||||
|
||||
**Per tier.** Every miner's timings are in it (anonymised to card model); a 9070 XT owner sees why their card is 7x worse per joule than a 5090 on dependent reads (`chip-model-v3.md` 5.8); nothing else changes.
|
||||
|
||||
**The Monero core developer's attack.** "A public corpus of your programs with timings is the chip designer's training set." Answer: the generator is public already (github.com/igneum-network/spec) and a chip must run next hour's program, not last year's; what the corpus gives a chip designer is the distribution, which the spec gives too.
|
||||
|
||||
**The Kaspa core developer's attack.** "Not a consensus matter." Correct.
|
||||
|
||||
**Verdict: do now.** Ten hours and it makes the benchmark promise continuous.
|
||||
|
||||
---
|
||||
|
||||
## 4. What would make a Monero or Kaspa core developer say "I had not thought of that"
|
||||
|
||||
Three, with the exact reasoning each would use to attack it. The first two are 3.2 and 3.3 restated as the thing that is new; the third is new in this file.
|
||||
|
||||
### 4.1 Work as the only stake, and it is slashable
|
||||
|
||||
Monero's and Kaspa's shared premise: in proof of work nothing is at stake except the block you are mining, so misbehaviour by a miner outside block production (a bad job, a withheld proof) cannot be punished, only priced. Igneum's finality weight is a quantity that is at stake, is earned by work alone over 30 days, cannot be transferred, and is already stripped for equivocation. Extending the strip to execution-layer faults (3.2) gives proof of work a slashable bond with no coin and no stake class.
|
||||
|
||||
**The Monero developer's attack, verbatim form.** "Then it is stake. You have a class of participants with something to lose that others do not, and a rule that takes it from them for a judgement call. Every argument you make against proof of stake (capture, cartels, nothing-at-stake inverted into everything-at-stake) applies to a stake made of blocks. Worse, your stake depreciates on its own in 30 days, so the rational prover front-loads bad behaviour in the last days of its weight." Answer: the weight is not transferable and not purchasable, which removes capture by capital; the last-days attack is bounded by the 30-day re-earn, and the sortition is proportional to current weight, so a depreciating key is drawn less. The concession: the spec must stop saying "no stake" and say "no coin stake; the only thing at stake is 30 days of public work".
|
||||
|
||||
**The Kaspa developer's attack.** "Any slashing condition needs an objective, deterministic fault; on a DAG 'late' needs a clock, and your clock is DAA score along the carrier's chain, which is deterministic. Fine. But you now have a second use for the weight table that the finality module computes, and the two uses must read the same table at the same block or two honest nodes strip differently. Your proof-record rule needed P11 for this; write the same sentence now." Accepted.
|
||||
|
||||
### 4.2 Finality carried forward inside the execution proof
|
||||
|
||||
The Kaspa premise: finality on a DAG is a fork-choice property computed by every node from the DAG it holds; it cannot be a proof. The Monero premise: a light client trusts whatever gave it the checkpoint. Igneum's segment proof already recurses from genesis; carrying the weight table in it (3.3) makes "certified under rule v2" a public output of the same proof that attests the state root, with the update costing one mergeset per segment, not a 30-day window per proof.
|
||||
|
||||
**The Kaspa developer's attack.** "The proof attests a chain; finality is about the DAG. Your W2 counts blue blocks in the chain block's past, and 'blue' is GHOSTDAG's judgement, which the proof does not recompute (it would have to run GHOSTDAG over k = 18 or 124 anticone sets inside a zkVM). So the proof takes blueness as a witness from the node, and a node that lies about which blocks are blue gives the proof a wrong table. You have proven the arithmetic and trusted the colouring." This is the sharp one. Answer: the colouring is committed by the header (the mergeset and blue set are determined by the parents, which the header commits to), so the witness is checkable against headers the proof also carries; but checking it means running GHOSTDAG's blue-set rule for each merged block inside the guest, which is bounded (anticone size at most k) and unmeasured. The gate for 3.3 must add: cycle count of the GHOSTDAG colouring check per mergeset inside the guest, and if it is too heavy, the colouring stays a witness and the light client's trust row says "blue set from nodes" until it is not.
|
||||
|
||||
**The Monero developer's attack.** "You have made finality depend on your proof system's soundness in the light client. Say so on the card." Already in spec 10.1 for the proof system row; the row must now name finality too.
|
||||
|
||||
### 4.3 The hourly program as an 8,760-question hardware census
|
||||
|
||||
**The idea.** Every hour the chain hands every card a new random program and every card races 17 compiled variants of it and reports which won and how fast (lever 1, measured; lever 2, shipped). A chip built for the lottery cannot look like a GPU on 8,760 different programs a year: its best variant, its timing distribution across programs, its sensitivity to instruction mix are a fingerprint. Make the fingerprint part of the share protocol: a pool records, per member and per epoch, the variant that won and the share-rate ratio between consecutive programs; the chain's observer publishes the distribution per card model from the fleet library; a key whose ratio pattern sits outside every known card's envelope for N epochs is flagged publicly (the share-pattern detector of Counter ASIC item 4, which found Monero's chips by nonce patterns, now with a per-program timing axis a chip must fake 24 times a day).
|
||||
|
||||
**Why it is new.** RandomX programs are per hash and no pool sees their timing; Monero's chip detection used nonce distributions (MoneroCrusher, approximate); ProgPoW audits priced the chip but had no running census. Igneum's hourly swap with per-card racing produces the census as a by-product. New.
|
||||
|
||||
**The Monero developer's attack.** "Timing is self-reported. A chip reports whatever a 4090 would report; it has the 4090's published envelope from your own dataset (3.16). And MoneroCrusher found us the chips not by timing but by nonce patterns, which a chip emulates trivially once it knows you look. Detection that depends on the attacker's cooperation is theatre." Answer: the share rate per epoch is not self-reported; it is the pool's count of verified shares, and a chip that throttles itself to a 4090's per-program envelope on every program forfeits its edge on the programs where it is strong, which is a cost measured in hash. The detector cannot prove a chip; it can price the chip's camouflage. That is the honest claim.
|
||||
|
||||
**The Kaspa developer's attack.** "We welcomed chips, so nothing here is for us. But as engineering: your per-epoch ratio depends on the pool's vardiff and on network luck; the envelope for one card model will be wide, and a 2x chip sits inside it. Your detector finds a 10x chip and misses the 2x one your model says is the threat." Fair: the detector's resolution is the gate (one epoch's share-rate variance per member at one share per 10 s is about 5 percent over an hour; a 2x step is 40 standard deviations, a 1.2x step 4; so it resolves 1.2x in a day and 1.05x in a month, approximate).
|
||||
|
||||
**Hours.** 16: the per-epoch ratio in the pool protocol's `stats` (spec 9.5) and the observer's envelope per card model (12), the public page (4).
|
||||
|
||||
**The gate.** The observer flags a deliberately throttled fleet box (a 5090 capped to a 4070's rate) within 24 epochs, and flags no honest card over a week.
|
||||
|
||||
**Per tier.** Every miner's card model gets an envelope; a home miner on an unusual card (Apple, Intel) must be in the library or will be flagged; pools carry one more statistic; nothing in consensus.
|
||||
|
||||
**Verdict: watch,** then do once the pool protocol exists: it is the cheapest instrument the chain has for the question the chip model cannot answer from a spreadsheet.
|
||||
|
||||
---
|
||||
|
||||
## 5. The incremental list
|
||||
|
||||
Smaller than the sections above; each with hours and a gate.
|
||||
|
||||
| # | Item | Hours | Gate | Why now |
|
||||
|---|---|---|---|---|
|
||||
| I1 | Expose per-key 30-day weight and blue-block count in `IgneumInfo` so hashrate forwards and hardware-finance contracts settle from chain state (`developer-adoption.md` 2c) | 6 | A forward contract settles on the devnet against `getFinalityWeights` with no oracle | The data is already maintained for finality |
|
||||
| I2 | Write the N schedule (latency-shadow program length) into the era draw at genesis, a doubling per era until the verifier gate binds (section 2.3) | 8 (spec text and the draw) | Verifier under 10 ms on a 2019-class core at the year-6 N | HBM4 arrives in 2027 to 2028 and the chain must answer it without a release |
|
||||
| I3 | Define the block-proof target as a function of the fleet's measured median shard time, published per era, not as "under 10 s" (section 2.6) | 4 | The site reads it from the bench table | Honesty about the 12 GB tier |
|
||||
| I4 | A second zkVM implementation of the `ProofSystem` trait (RISC Zero or OpenVM) running on one fleet box as a shadow verifier, so a soundness bug in one system is detected by disagreement before it reaches a light client | 24 | 1,000 segments agree across both; one injected bad proof disagrees | Ledger P7, D6: the veto protects full nodes, nothing protects light clients today |
|
||||
| I5 | The Ember updater installs nothing while finality is paused (3.7's Kaspa attack) | 2 | A paused devnet, a published release, no install | Free |
|
||||
| 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 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 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 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Open questions and what I could not run
|
||||
|
||||
- **The BLS verification cycle count inside the SP1 guest** (3.3's gate a) needs a 24 GB card; PC 2 and the fleet were on the class v4 rehearsal and the Devnet 2 block-rate runs tonight. Without it, the 3 percent proving-capacity cost is an estimate.
|
||||
- **The GHOSTDAG colouring check inside the guest** (4.2's Kaspa attack) is unmeasured and may be the real cost of 3.3; it is added to that gate.
|
||||
- **The wrapper** (3.4) does not exist in the repository (ledger P3, R4); the 16 hours include building it on a fleet card.
|
||||
- **HBM4 energy per random read** (section 2.3) is an unsourced estimate (1.0 nJ); JEDEC timing is behind the paywall; the 2x channel count is cited, the tFAW-per-channel assumption is mine.
|
||||
- **The rental-tax rule's determinism** (3.1) depends on reading W30 and H_now at a checkpoint in the block's past; the lag's effect on the renter's first 30 s is unmodelled.
|
||||
- **Vast.ai's actual take** is unpublished; the 15 percent is secondary.
|
||||
- **No Coinbase paper on useful work was found**; if one exists its title is needed to cite it.
|
||||
- **`block-rate-devnet2.md`** was a template at writing time; the 10 BPS question matters for 3.3 (segments re-cut at reorgs) and 3.5 (votes per block), and should be re-read when RUN_A lands.
|
||||
- **Lane 8** holds the shadow-useful puzzle (3.10's handover) and anything about new puzzle shapes; nothing here designs a puzzle.
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary for the coordinator
|
||||
|
||||
Lane 7 read the spec, the litepaper, the ledger sections asked, the design files, the chip model and the fleet's eleven-card table, searched prior art for sixteen ideas, and wrote one arithmetic model (`sim/horizon/frontier/frontier_model.py`) behind every number. Three findings:
|
||||
|
||||
1. **HBM4 raises the stored-dataset chip's per-joule edge from about 7x to about 11x bare and from about 2.3x to about 2.7x under the class v4 latency shadow at N = 100,000 (model 1.4, every chip figure arithmetic), because JEDEC doubled channels per stack (16 to 32); N = 200,000 brings it to 1.7x and N = 330,000 to 1.3x at k = 1. The N schedule belongs in the era draw at genesis (I2), with 10x of verifier headroom.**
|
||||
2. **Vote weight is a slashable, non-purchasable bond (3.2): a key with 0.1 percent of hash has 16,427 IGN of 30-day pool income and its vote at risk against a designed coin bond of 0.0015 IGN per job (model section 3). The design's "no stake" must become "no coin stake".**
|
||||
3. **The consensus proof can be incremental (3.3): carry the W2 table inside the recursive segment proof and update it by one mergeset per segment, with one BLS verify per 30 s (about one shard's budget, approximate, unmeasured). It is the only road to a browser that trusts no node for the voter set, and its real cost is the GHOSTDAG colouring check inside the guest (4.2), which is the first measurement to run.**
|
||||
|
||||
Two honest nevers with arithmetic: proving others' chains cannot be the main income by 2030 (all of Ethereum L1's proving is USD 36 a day at the Sep 2026 tracker cost against USD 13,700 a day of year-1 emission at USD 0.005; 3.11), and the lottery hash cannot be partly a proof without re-opening Aleo and breaking the DAG's memoryless election (3.10). One rule for main: the litepaper's "proving: a second income" line should carry the 3.11 arithmetic (I14), and the spec should carry the "no coin stake" sentence (I13) before any work-stake prototype starts.
|
||||
244
docs/analysis/horizon/network.md
Normal file
244
docs/analysis/horizon/network.md
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
# Horizon lane 5: network. Block rate, propagation, node cost, bandwidth, pruning and the snapshot path
|
||||
|
||||
6 October 2026, evening UK. Lane 5 of the Horizon programme. Worktree `/Users/joshm/Projects/igneum-wt-horizon`, branch `horizon`. Scripts in `sim/horizon/network/` (README there). Nothing live was touched: the Devnet 2 seed log on igneum-build-1 and `~/Desktop/fleet/bps/A.jsonl` were read, never written.
|
||||
|
||||
What was read: `docs/spec/02-consensus.md` (2.1 parameters, 2.3 the difficulty rule, 2.4 header, 2.5 emission), `08-client-security.md`, `10-light-client.md`; the fud-close worktree's `docs/spec/03-finality.md` C1 and 3.4.2 (vote item 281 bytes, the bitmap and per-block bounds); `docs/fud-ledger.md` M20, M21 (block sizes, k re-derived, the 490 KB body run), X20, M30; `docs/bench-log.md` entries "4 October 2026, cloud devnet" (inter-region RTT and propagation), "ledger M30" (RSS, the 256 MiB cache, the s8 steady slope), "6 October 2026, 12:25 to 13:20Z, the finality route" (the seed as the only peer, the route overflow), "Rental cost of hash, 6 October 2026"; `docs/plans/hands-on-build-1.md` (node 1 and the observer: data dirs 856 and 822 MB, the 127.6 MB exec snapshot), `seed-nodes.md`, `cloud-devnet.md`, `release-0.3.14.md` (the snapshot path and the restart pin), `release-0.3.13.md` 4a; the fleet worktree's `docs/analysis/block-rate-devnet2.md` (read at 19:5xZ and again at 20:1xZ: RUN_A, RUN_B, TIER_TABLE and RECOMMENDATION are still placeholders), `tools/fleet/bps-collect.py`, `box-dn2.sh` (one `--addpeer=<seed>`: the star), `devnet2-override.json`, `docs/bench-log.md` of that branch; the live record of run A: `~/Desktop/fleet/bps/A.jsonl` (43 rows, 19:18 to 19:51Z), `collect-A.log`, `runA-start2.log`, `runB.sh` (run B starts at 20:25Z) and the seed's log `/home/build/dn2seed-A.log` on igneum-build-1 (20,529 lines, 19:17 to 19:59Z, read over ssh); `vendor/rusty-kaspa` `consensus/core/src/config/bps.rs` (the k table, `calculate_ghostdag_k`, parents, mergeset, pruning depth), `constants.rs` (delay bound 5 s, delta 0.01, DAA window), `params.rs` (mainnet `BlockrateParams::new::<10>()`, Crescendo activation 110,165,000), `consensus/src/processes/difficulty.rs` and `window.rs` (the window holds every mergeset block, blue and red), `docs/crescendo-guide.md` (10-bps node requirements); the fork `vendor/igneum-node` (read-only) at `release-0.3.14-node`: `consensus/core/src/igneum.rs` `difficulty` (CAP_BLOCKS 20, the sanitised clock, the clamps), `consensus/src/processes/difficulty.rs` `igneum_difficulty_bits` (blue-work steps on the selected chain), `consensus/src/processes/finality.rs` (MAX_VOTES_PER_BLOCK 48, KEEP_CHECKPOINTS 2,000), `igneum/exec/src/proving.rs` `p2p_snapshot_gate` and `on_exec_snapshot`, `igneum/exec/src/snapshot.rs`; the fork branch `devnet2-bps` 1279a1d6 (`IGNEUMD_DEVNET_BPS`); lane 3's `finality-and-weight.md` sections 5.5 and 8 (the aggregation path tonight), lane 4's `sim/horizon/consensus-security/ghostdag_results_{1,10}bps.md`.
|
||||
|
||||
## 1. The question and the answer in one paragraph
|
||||
|
||||
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
|
||||
|
||||
| Step | What | Where | Machine, lock |
|
||||
|---|---|---|---|
|
||||
| Record | run A's per-minute rows (blocks, blue score, tips, peers, exec tip, CPU, RSS, bytes), the seed log's `PoW accepted` per minute, `Processed` lines (parents, mergeset per 10 s), `Finality: checkpoint N determined` (blue score against DAA), reorg lines, route drops | `~/Desktop/fleet/bps/A.jsonl`; `/home/build/dn2seed-A.log` on igneum-build-1 | read only |
|
||||
| Propagation | event simulation: Poisson production, star or mesh, lognormal links, inv/request/block hops, a hub (or every node) as a single server with a per-block cost, GHOSTDAG colouring with the first k-cluster condition, parent and mergeset caps, reorg depth at miner 0 | `sim/horizon/network/propagation.py`, `results.md`, `results-2.md` | Mac, `with-lock.sh run nice -n 19`, seed 7 |
|
||||
| Controller | the fork's estimator against a wide DAG in closed loop with the 3% / 10% clamps, against the whole-DAG estimator and a red-corrected blue estimator | `controller.py`, `controller-10bps.md`, `controller-1bps.md` | Mac, seed-free (deterministic) |
|
||||
| Arithmetic | k and parameters per rate, votes and bounds, bytes per node per day, CPU budget, RSS and disk, pruned and archival growth, subsidy and payout intervals, finality timing, light-client bytes | `cost.py`, `cost-tables.md` | Mac |
|
||||
|
||||
Nothing was built. No node ran. The fleet's run B (1 bps control, starts 20:25Z) and the mesh variant A2 (not scripted in the fleet worktree at 20:1xZ: no `mesh` or `A2` in `tools/fleet/` or its plans) had not landed when this file closed; section 7 names what they owe.
|
||||
|
||||
## 3. Evidence
|
||||
|
||||
### 3.1 Run A, measured (igneum-devnet-2, 10 bps, 41 miners through the seed on igneum-build-1, genesis bits 505413632)
|
||||
|
||||
Phases from the seed log's checkpoint series (blue score against DAA score; red share = 1 - blue / DAA over the interval) and `Processed` lines; CPU per block from `A.jsonl` `cpu_rss` (ps lifetime %CPU times elapsed, differenced) over the accepted-block deltas.
|
||||
|
||||
| Window (Z) | Production at the seed, blocks/s | Blue rate, blocks/s | Red share over the window | Mergeset per block (Processed) | Parents | Tips at the seed | Hub CPU per accepted block | Hub RSS | What the log shows |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 19:23 to 19:29 | pods join (peers=1 each, blocks=0 synced=false on some), 26 blocks/s peak at 19:30 | | 33% (cp 1 to 29) | 1 to 35 | 1 to 16 | 6 to 30 (pods) | 61 ms | 0.45 to 1.9 GB | the 55-block reorg at 19:27:41Z "unwinding to height 1" (a late-joining pod's chain from genesis) |
|
||||
| 19:29 to 19:31 | 19 to 26 | 1.3 | 77% (cp 29 to 37) | 29 to 36 | 15 | | 115 ms | 1.9 to 2.5 GB | |
|
||||
| 19:31 to 19:37 | 13 to 19 | 0.7 | 94% (cp 37 to 45) | 53 to 197 | 12 to 15 | 295 | | 2.5 to 4.0 GB | `Accepted 97 / 100 / 31 blocks ... via relay` batches; headers ahead of blocks |
|
||||
| 19:37 to 19:41 | 8 to 14 | 0.5 | 95% (cp 45 to 49) | 150 to 190 | 12 | 218 to 295 | 345 ms (19:37 to 19:51 mean) | 4.0 to 4.5 GB | window filled at 19:37Z; first lock cp 47 at 19:40:08Z |
|
||||
| 19:41 to 19:47 | 3 to 7 | 1.0 | 50 to 88% (cp 49 to 61) | 80 to 125 | 13 to 15 | 230 to 350 | | 4.6 to 5.2 GB | `incoming route for IgneumFinality is full, message dropped ... the peer stays`: 109 to 2,547 drops per peer by 19:56Z |
|
||||
| 19:47 to 19:59 | 3.3 to 3.7 | 1.4 to 1.8 | 41 to 56% (cp 61 to 92) | 47 to 90 | 15 to 16 | 348 to 396 | | 5.4 GB at 19:51 | 16 of 47 checkpoints after the window filled LOCKED (cp 47 to 79); cp 61 determined 19:47:00, locked 19:47:18 |
|
||||
| Cumulative to 19:59:32Z | 6.1 (DAA 12,233 in 2,012 s) | 1.38 (blue 2,786) | 77.2% | | | | | | main's 12.4 blocks/s, 77 percent, 321 tips, max reorg 55 are the 19:3xZ to 19:45Z readings of the same record |
|
||||
|
||||
Other measured rows of the run: 421 selected-chain reorgs at the seed (132 of depth 1, 62 of 2, 42 of 3, 25 of 4, 19 of 5; 2 of 43, 1 of 44, 1 of 55); the seed's `rx_tx` counter (netns-wide, so an upper bound) 325 MB in and 2,133 MB out between 19:37:05 and 19:51:15Z (850 s, 4,284 accepted blocks): 2.5 MB/s out, 61 KB/s per peer, about 12 KB per block per peer (a block with its finality section of up to 48 votes at 281 bytes is about 14 KB); exec tip 154 chain blocks at 19:51Z against 11,239 blocks (the executor follows the selected chain, which advanced about one chain block per 10 s at the widest). The run's difficulty values are not in the record: `bps-collect.py` strips `difficulty=` from the watch line before storing it and the node log carries no bits; main's statement that the rule "lowered difficulty" is therefore unverified here, and the production curve (26 to 3.5 blocks/s at a hash the pods' peers=41 say stayed connected) is the measured fact section 5.4 reads.
|
||||
|
||||
### 3.2 Measured propagation and block sizes (the inputs the model takes)
|
||||
|
||||
| Quantity | Value | Source | Label |
|
||||
|---|---|---|---|
|
||||
| Inter-region RTT | 35 ms (hel1-fsn1) to 289 ms (sin-ash); 0.4 to 0.7 inside a location | bench-log, cloud devnet 4 Oct | measured |
|
||||
| Block propagation to 80% of 12 nodes over about 3 hops, 723-byte bodies | p50 343, p90 497, p99 666, max 2,313 ms | same | measured |
|
||||
| One relay hop on 100-ms proxied links (inv, request, block) | p50 318 to 329 ms; own-node processing 6 to 16 ms | fud-ledger M21 run, 5 Oct | measured |
|
||||
| Two hops with 490 KB bodies | p50 641, p99 812, max 857 ms (59-byte bodies: 626 / 1,093 / 2,099) | same | measured |
|
||||
| Live devnet block, 1 coinbase, 22 keys' votes partly | p50 723 B, p90 1,022, max 6,908; coinbase payload p50 395, max 6,580 | fud-ledger M21 sweep | measured |
|
||||
| Vote item, certificate, proof record | 281 B; 273 B + V/8; 274 B | spec 03 3.4.2 (fud-close), fork | cited |
|
||||
| k from the measured delays at 1 bps | p99 0.67 s gives k 5, max 2.3 s gives k 10; k 18 is the 5-s bound | fud-ledger M21, `calculate_ghostdag_k` | cited |
|
||||
| Kaspa 10-bps node requirements | 8 cores, 16 GB RAM, 256 GB SSD, 40 Mbit/s minimum; 12 to 16 cores, 32 GB preferred | `vendor/rusty-kaspa/docs/crescendo-guide.md` | cited |
|
||||
|
||||
### 3.3 Measured node figures
|
||||
|
||||
| Quantity | Value | Source | Label |
|
||||
|---|---|---|---|
|
||||
| PoW cache | 256 MiB per day key, KEEP_DAYS 3, 768 MiB worst, 512 MiB around midnight UTC | bench-log M30 entry | measured |
|
||||
| RSS slope, narrow DAG, 60x fast time, 1 block/s | 30.2 MB per 1,000 blocks (s8 steady, 1,500 blocks) | same | measured |
|
||||
| The live app node on the devnet profile | 1,081 MB at 27 min, 2,258 MB at 4 h 14 min (about 80 KB per block, derived) | same | measured, derived |
|
||||
| Run A's seed | 153 MB before the chain, 5.42 GB at 12,000 blocks (about 440 KB per block; 41 peers, mergeset up to 197) | A.jsonl | measured |
|
||||
| Exec snapshot | 127,564,588 B at tip 141,700 chain blocks (0.9 KB per chain block); `ExecState.records` 1 to 2 KB per chain block | hands-on-build-1.md; M30 note | measured; approximate |
|
||||
| Node 1 and observer data dirs after 3 days | 856 MB and 822 MB | hands-on-build-1.md | measured |
|
||||
| Lottery verify per header | class v3 2.79 ms on a loaded M5 Max core; class v4 4.90 steady, 5.06 cold, 8.23 ms on a half core (box proxy); gate 10 ms | consequences C29; lane 2 section 5.5; spec 01 | measured |
|
||||
| Hub CPU per accepted block at 10 bps | 61 ms (mergeset about 8), 115 ms (about 30), 345 ms (150 to 200) | A.jsonl, section 3.1 | measured |
|
||||
|
||||
### 3.4 What the fleet still owes (read `block-rate-devnet2.md` at 20:1xZ)
|
||||
|
||||
RUN_A, RUN_B, TIER_TABLE and RECOMMENDATION are placeholders. Run B (1 bps, same boxes, fresh genesis, 30 min) starts at 20:25Z by `runB.sh` and its rows land about 21:00Z; the mesh variant A2 is not scripted (`box-dn2.sh` takes one `--addpeer`). The collector's `red` field is 0 in every row (its grep pattern matches no log line), so the fleet's red share must come from blue score against DAA as section 3.1 does; its `--report` payout arithmetic uses 28 MH/s for a 4070 where the brief uses 25. Section 5.2 states this lane's prediction for A2 before its rows land.
|
||||
|
||||
## 4. Model
|
||||
|
||||
Inputs are labelled measured (M), cited (C), simulated (S) or approximate (A).
|
||||
|
||||
1. **k and the parameters per rate** (C, `bps.rs`): k = min k with P(Poisson(2 D lambda) > k) < 0.01 at D = 5 s: 18, 124, 362 at 1, 10, 32 bps; 1,074 at 100 bps (the table stops at 32; `calculate_ghostdag_k` in f64 underflows at x = 1,000, the lane's `cost.py` does it in log space). Parents = clamp(k/2, 10, 16); mergeset limit = clamp(2k, 180, 512); merge depth 3,600 bps; finality 43,200 bps; pruning max(108,000 bps, the Prunality lower bound); maturity 100 bps.
|
||||
2. **Red share from topology and delay** (S, `propagation.py`): blocks arrive as Poisson(bps) split over miners; a block reaches a peer after one hop = 3 lognormal link latencies (median `--link`, sigma 0.29 from the cloud devnet's p50/p90) plus 10 ms processing; a star hub (or every node, `--node-s0`) is a single server with service s0 + s1 x mergeset ms and a FIFO queue; each block's colour is GHOSTDAG's first k-cluster condition over its own past (deterministic given parents); red share = reds in the mergesets of the final selected chain over merged blocks. The abstract rule behind it: reds appear when blocks in flight 2 d lambda exceed k, where d is the effective delay including queueing.
|
||||
3. **Hub queue** (A, M/M/1 reading): utilisation rho = lambda x s; the knee is rho = 1: s = 100 ms at 10 bps, 1 s at 1 bps, 31 ms at 32 bps, 10 ms at 100 bps. Above the knee the wait grows without bound and d becomes the wait, not the link.
|
||||
4. **The controller against a wide DAG** (C for the rule, `controller.py` for the loop): the fork walks the selected chain; a step carries work = blue_work(b) - blue_work(selected parent) (the mergeset blues only) and solvetime = min(clock step, 20 T) (`igneum.rs` CAP_BLOCKS, `difficulty.rs` `igneum_difficulty_bits`). With chain-step spacing sigma = max(1/lambda, d), mergeset m = lambda sigma and blues = min(m, k + 1): estimate / true hash = (blues / m) x (sigma / min(sigma, cap)). Two biases: when sigma > cap the step is capped and the rule over-reads by sigma / cap and hardens to a DAG rate of target x cap / sigma; when the DAG is wide (m > k + 1) and sigma <= cap it under-reads by (k + 1) / m and eases. Kaspa's window (`window.rs` `push_mergeset`: every mergeset block above the blue-score floor, blue and red; `difficulty.rs` `calculate_difficulty_bits`: average target x measured span / expected span) reads the whole-DAG rate over the real span: estimate / true = 1 in both regimes. A blue estimator divided by (1 - observed red share) is the same quantity.
|
||||
5. **Bytes per node per day** (C+S+A, `cost.py` section 3): blocks per day x (header 286 + 32 x parents + body 300 + 274 / 8) + relay overhead (degree x 40 B inv + 40 B request per block, Kaspa's blockrelay flow, A) + votes V x checkpoints per day x 281 + one certificate (273 + V / 8) per block. Checkpoints per day = 2,880 x bps if C1 stays "every 30 blue blocks", 2,880 if it is re-denominated in DAA seconds.
|
||||
6. **CPU budget** (A): the header pipeline validates in order, so per-block cost x bps must stay under about 0.8 core: 800 ms at 1 bps, 80 at 10, 25 at 32, 8 at 100. Cost = lottery verify (M) + BLS per carried vote (A 1.5 ms, unmeasured on this stack, O-10.3) + GHOSTDAG and reachability (O(k x mergeset) store reads; M at the hub only as a total) + exec and record checks.
|
||||
7. **RSS and disk** (M slopes, A steady state): caches 768 MiB worst plus the DAG store's growth per block (30, 80 or 440 KB measured in three settings) over the pruning window; disk per day = blocks x bytes + votes + exec records; archival = the same per year without pruning.
|
||||
8. **Payout and subsidy** (C): subsidy per block = 31.688 IGN / bps at full ramp, 80% to the producer; interval = 1 / (bps x miner hash / network hash).
|
||||
9. **Finality timing** (C, spec 03 C1): checkpoint every 30 blue blocks, determination d = 60 blue (placeholder, 20 on the devnet), lock about 3 s after determination: cadence 30 / bps s, lock (60 / bps + 3) s if C1 and d stay in blue blocks.
|
||||
|
||||
## 5. Results
|
||||
|
||||
### 5.1 Propagation alone: red share and reorg depth per rate and delay (mesh of degree 8, 42 nodes, ideal nodes; `results.md` section D)
|
||||
|
||||
| bps | k | hop delay median ms | red share | mergeset mean / max | tips mean | reorg max / p99 at miner 0 | delay to 90% of nodes, s |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | 18 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 1.1 / 2 to 2.3 / 7 | 1.1 to 2.4 | 1 / 1 to 2 / 2 | 0.09 to 1.82 |
|
||||
| 10 | 124 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 1.7 / 6 to 9.2 / 22 | 1.7 to 8.1 | 2 / 1 to 4 / 3 | 0.09 to 1.82 |
|
||||
| 32 | 362 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 2.9 / 9 to 18.8 / 42 | 2.9 to 14.4 | 3 / 2 to 6 / 5 | 0.09 to 1.82 |
|
||||
| 100 | 1,074 | 50 / 100 / 300 / 1,000 | 0.0% in every cell | 6.0 / 17 to 31.1 / 104 | 5.6 to 25.3 | 4 / 3 to 8 / 8 | 0.09 to 1.82 |
|
||||
|
||||
Reading. Kaspa's k is derived for a 5-s delay bound; at the measured delays (under 1 s per hop, under 2 s to 90 percent of nodes) blocks in flight stay far under k at every rate and no block turns red. The natural reorg depth is the DAG width: 2 to 4 chain blocks at 10 bps, 8 at 100 bps with 1-s hops. Lane 4's simulator (uniform one-way delay to every node, 8 miners) reads p99 11 at 10 bps with d = 0.35 s and 99 with d = 2 s (`ghostdag_results_10bps.md` section 1); the two agree in shape (depth grows with bps x d) and differ in the delay model, so the fleet's measured reorg distribution at 10 bps (section 3.1: 421 reorgs, p50 1, 4 over 40) is the arbiter: outside the start artefact and the saturated phase it sits at 1 to 8, inside this lane's model. Kaspa's published 10-bps red rates are not in the clone (`docs/crescendo-guide.md` carries requirements only); from the k derivation the design red rate is under 1 percent of blocks (delta 0.01 on anticones, approximate), which both simulators reproduce.
|
||||
|
||||
### 5.2 Run A predicted from topology, then from the hub's cost (`results.md` A to C, `results-2.md` E, F, I)
|
||||
|
||||
| Model input | Red share | Hub wait | Hub utilisation | Mergeset mean / max | Reorg max | What it says |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Star, 41 miners, link 40 ms, ideal hub, run A's measured production schedule, join spread 360 s | 0.0% | 0 | 0 | 3.5 / 14 | 4 | topology and latency alone predict no reds at 10 bps |
|
||||
| Star, hub cost 6 + 2 x mergeset ms (a linear GHOSTDAG cost), same schedule | 0.0% | 0.00 s | 0.14 | 3.7 / 15 | 3 | a hub under 20 ms per block keeps up |
|
||||
| Star, hub cost 61 ms per block (M, mergeset 8), same schedule, 1,200 s | 46.8% | 26 s | 0.65 mean (1.6 during the 26 blocks/s burst) | 8.8 / 194 | 43 | the genesis burst alone saturates a 61-ms hub for 5 minutes |
|
||||
| Star, hub cost 115 ms (M, mergeset 30) | 88.1% | 321 s | 1.23 | 20.7 / 199 | 2 | the measured mid-run regime |
|
||||
| Star, hub cost 345 ms (M, wide DAG) | 82.6% | 1,769 s | 3.66 | 9.8 / 45 | 3 | relay in batches, most blocks unmerged at the end |
|
||||
| Measured run A, cumulative | 77.2% | relay batches of 100 blocks | 180% of one core sustained (section 3.1) | 8 to 197 | 55 (start artefact) | |
|
||||
| Star at a constant 10 bps, hub cost 20 / 40 / 60 / 80 ms | 0.0% | 0.00 / 0.01 / 0.05 / 0.18 s | 0.20 / 0.39 / 0.61 / 0.81 | 3.5 to 4.9 | 2 to 4 | under the knee |
|
||||
| the same, 100 / 150 ms | 15.2% / 75.2% | 8.5 / 157 s | 1.01 / 1.51 | 20 / 116 and 27 / 66 | 8 / 3 | the knee is 100 ms per block at 10 bps |
|
||||
| Star at 10 bps, ideal hub, link 100 / 200 / 300 ms | 0.0% | 0 | 0 | 5.8 / 15, 10 / 20, 12.4 / 23 | 3 to 4 | pure latency up to a 2.2-s delay to 90% of nodes gives no reds |
|
||||
| Star with the 26 blocks/s burst for 5 min then 10 bps, hub 6 + 2 m | 0.0% | 0.01 s | 0.33 | 5.4 / 17 | 3 | the burst alone is harmless to a cheap hub |
|
||||
|
||||
So the model predicts run A's red share from the hub's measured per-block CPU and not from its topology: 47 to 88 percent against 77 percent measured, with the waits that the log's relay batches show. The prediction for the mesh variant A2 (`results-2.md` G): a mesh of degree 8 whose nodes each pay 40 or 80 ms per block runs 10 bps at 0 percent red (node wait 0.01 and 0.12 s); at 115 ms per block every node is its own hub and the mesh goes to 52 percent red with 26-s waits and reorgs of 16. The pods run the same binary as the seed on smaller CPUs, so A2 with tonight's genesis bits (a 26 blocks/s burst) is predicted red again; A2 with genesis bits set for 10 bps at 2 GH/s and every node started synced is predicted under 5 percent red if the per-block cost on a pod is under 80 ms, and 50 percent or more if it is 115 ms. The control at 1 bps (`results-2.md` H): 115 or 345 ms per block gives 0 percent red and waits of 0.01 to 0.07 s, which is the live devnet's experience.
|
||||
|
||||
### 5.3 What the hub's 61 to 345 ms per block is made of (the breakdown is not measured; the candidates and their bounds)
|
||||
|
||||
| Component | Per block at 10 bps | Label | Note |
|
||||
|---|---|---|---|
|
||||
| Lottery verify, class v3 (the Devnet 2 override activates v3 at epoch 1) | 2.8 ms | M | one warp per header |
|
||||
| BLS verification of carried votes, up to 48 per block | up to 72 ms at 1.5 ms per verify | A | run A's blocks carried up to 48 of 42 voters' votes; the route drops say the finality path was the hot one |
|
||||
| GHOSTDAG and reachability at mergeset m, k 124 | O(k x m) store reads: 1,000 at m 8, 25,000 at m 200 | C (protocol.rs shape) | the 61 to 345 ms rise tracks m |
|
||||
| Relay to 41 peers (serialise 14 KB x 41, inv handling) | 2.5 MB/s out measured | M | a mesh node of degree 8 does one fifth of it |
|
||||
| Exec of chain blocks, record checks | small: one chain block per 10 s at the widest | M | |
|
||||
|
||||
The gate that settles it is a profile (proposal 5). Whatever the split, the serial budget rule of section 4.6 is the design constraint for every block-rate step: at 10 bps the whole per-block path must stay under 80 ms on the slowest node the network wants to keep, at the mergeset limit 248, not at the narrow-DAG average.
|
||||
|
||||
### 5.4 The controller: what the record shows and what the model says (`controller-10bps.md`, `controller-1bps.md`)
|
||||
|
||||
| Regime (10 bps, k 124, cap 2 s) | Fork's estimator (blue work over the capped chain step) | Whole-DAG estimator (Kaspa's window shape) | Blue estimator corrected by (1 - r) |
|
||||
|---|---|---|---|
|
||||
| d = 0.3 / 1 / 2 s | DAG rate 10.00, red 0%, estimate 1.00x | 10.00, 1.00x | 10.00, 1.00x |
|
||||
| d = 3 s | settles at 6.67 blocks/s, estimate 1.50x, difficulty 1.50x the correct value | 10.00 | 10.00 |
|
||||
| d = 5 s | 4.00 blocks/s, 2.50x | 10.00 | 10.00 |
|
||||
| d = 10 s | 2.00 blocks/s, 5.00x | 10.00 (red 0%, mergeset 100) | 10.00 |
|
||||
| d = 30 s (a saturated hub) | 5.21 blocks/s, red 19.5%, estimate 12.1x, difficulty 1.93x | 10.00, red 58%, mergeset 300 | 10.00 |
|
||||
| 1 bps, k 18, cap 20 s: d = 20 / 30 / 60 s | runs away upward: 179 / 43 / 10 blocks/s, red 97 to 99%, estimate 0.01 to 0.09x (the under-read, main's direction) | 1.00 / 1.00 / 1.17 blocks/s | 1.00 / 1.00 / 1.17 |
|
||||
|
||||
Reading against the record. Run A's production fell from 26 blocks/s to 3.3 to 3.6 while blue stayed 1.4: the DAG hardened. The fork's rule at a chain-step spacing of 5 to 6 s (the hub's queue made chain blocks 10 to 60 s apart at the widest, 3 to 6 s late in the run) settles at target x 2 s / spacing = 3.3 to 4 blocks/s, which is the record's late plateau. The ease direction main reported is the other bias (blue work only) and the model shows it where chain steps stay under the cap while the DAG is wider than k + 1: at 1 bps with d of 20 s or more, or at 10 bps when production exceeds k / (2 d). Either way the rule is reading the wrong quantity: a controller that counts every mergeset block's work over the real span (what Kaspa's `calculate_difficulty_bits` does over its sampled window of blue and red mergeset blocks) holds 10.00 in every cell. A second inconsistency at 10 bps: CAP_BLOCKS 20 is 2 s while FUTURE_TOLERANCE_MS and BACK_TOLERANCE_MS stay 10 s, so a forged stamp (10 s) no longer fits inside half a cap; spec 2.3 derived the 10 s as half the cap at 1 bps. The cap, the tolerances and the 60 T lag bound should be denominated in DAA seconds (20 s, 10 s, 60 s) at every rate, and the step of a chain block that merges m blocks should be allowed m x 20 T before clamping.
|
||||
|
||||
Cost of the bug tonight per tier: a home miner's blocks were 77 percent red (a red inside the DAA window pays its 80% to the merging miner, spec 2.5, so the solo miner lost the subsidy of 3 blocks in 4); a rig the same; a pool user nothing (Devnet 2 is a staging chain); the node operator saw 5.4 GB RSS and 180% CPU on a 96-thread box; a prover saw the exec tip at 154 chain blocks against 11,239 blocks; a holder nothing (no value on Devnet 2).
|
||||
|
||||
### 5.5 Node tiers per block rate (`cost-tables.md` 4 and 5)
|
||||
|
||||
| Rate | Per-block CPU budget (0.8 core) | Class v4 verify share of it (box proxy 4.9 ms; a 2019 laptop core about 2.5x, lane 2's rule: 12 ms) | Votes per block (V/30 if C1 stays in blue blocks) and their BLS cost at V = 1,000 | RSS: caches + DAG store over the 30-h window at 30 / 80 KB per block | Disk per day (blocks, votes at V = 1,000, exec records) | Verdict: laptop 2019-class 8 GB SATA | Raspberry-class 8 GB USB SSD |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 bps | 800 ms | 0.6% (laptop 1.5%) | 33 votes, 50 ms | 0.77 + 3.3 to 8.6 GB | 0.92 GB | runs if the DAG store plateaus under about 4 GB (owed: the 30-h measurement); marginal at 80 KB per block | the same question; CPU fine (class v4 verify under 15 ms, GHOSTDAG at mergeset 1 to 2) |
|
||||
| 10 bps | 80 ms | 6% (laptop 15%) | 33 votes, 50 ms: 62% of the budget on its own | 0.77 + 33 to 86 GB | 9.0 GB | no: the hub's measured 61 ms at a narrow DAG already uses 76% of the budget on a server core; RSS over 8 GB within hours unless the per-block footprint falls 10x | no |
|
||||
| 32 bps | 25 ms | 20% (laptop 48%) | 33 votes, 50 ms: over budget | 0.77 + 104 to 276 GB | 29 GB | no | no |
|
||||
| 100 bps | 8 ms | 61% (laptop 150%) | over budget | 0.77 + 326 to 864 GB | 91 GB | no: the verifier gate alone forbids it | no |
|
||||
|
||||
Pruning changes the disk, not the RSS and CPU: the pruned node keeps the 108,000 DAA-s window (136 MB of headers and bodies at 1 bps, 1.55 GB at 10 bps, `cost-tables.md` 6) plus the UTXO set, the exec state (128 MB measured plus 1.5 KB per chain block) and the finality store's 2,000 indices (KEEP_CHECKPOINTS). What must change for 10 bps is the per-block footprint in RAM (30 to 440 KB measured; Kaspa's 16 GB minimum at 10 bps says their footprint is near 10 KB per block, derived from 16 GB over 1,080,000 blocks, approximate) and the per-block CPU (under 50 ms at mergeset 248 on a laptop core).
|
||||
|
||||
### 5.6 Bandwidth per node per day (`cost-tables.md` 2 and 3)
|
||||
|
||||
| Rate | Blocks (header + body + records) | Relay overhead (degree 8) | Votes at V = 12 / 100 / 1,000 / 8,192, C1 in blue blocks | Certificates at V = 12 / 8,192 | Total at V = 100 | Total at V = 8,192 | Mean Mbit/s at 8,192 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | 57 MB | 31 MB | 10 MB / 81 MB / 809 MB / 6.6 GB | 24 MB / 112 MB | 194 MB | 6.8 GB | 0.63 |
|
||||
| 10 | 649 MB | 311 MB | 97 MB / 809 MB / 8.1 GB / 66 GB | 237 MB / 1.1 GB | 2.0 GB | 68 GB | 6.3 |
|
||||
| 32 | 2.4 GB | 1.0 GB | 311 MB / 2.6 GB / 26 GB / 212 GB | 759 MB / 3.6 GB | 6.8 GB | 219 GB | 20 |
|
||||
| 100 | 9.3 GB | 3.1 GB | 971 MB / 8.1 GB / 81 GB / 663 GB | 2.4 GB / 11 GB | 23 GB | 687 GB | 64 |
|
||||
|
||||
With C1 in DAA seconds the vote column is 81 MB / 809 MB / 6.6 GB per day at every rate; with lane 3's aggregated certificate (1.2 KB per checkpoint) in place of carried votes it is 3.5 MB per day. What carries it: a home connection (10 Mbit/s up, 50 down, approximate) carries 1 bps at any voter count and 10 bps at under 1,000 voters or with C1 in seconds; a Raspberry-class box on ethernet the same, bounded by its CPU not its link; a mobile node is a light client: 3.4 MB per day in checkpoint mode at 1 bps and 1,000 voters, 34 MB at 10 bps if C1 stays in blue blocks (`cost-tables.md` 10). A star hub pays its peer count times the block bytes in upload: run A's seed sent 2.5 MB/s (20 Mbit/s) to 41 peers; the three testnet seeds (cx23, 20 TB per month included, `seed-nodes.md`) would spend 6.5 TB per month each at that rate, inside the allowance and outside good sense; the peer floor of proposal 4 spreads it.
|
||||
|
||||
### 5.7 Votes and the 3.4.2 arithmetic per rate (`cost-tables.md` 2)
|
||||
|
||||
| Rate | Checkpoints per day (C1 in blue blocks) | Votes per block at V = 8,192 (V / 30) | Drain capacity per checkpoint at the cap 48 / 384 | Vote bytes per day at 8,192, single votes | Archival votes per year at 1,000 / 8,192 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 2,880 | 273 | 1,440 / 11,520 | 6.6 GB | 296 GB / 2.4 TB |
|
||||
| 10 | 28,800 | 273 | 1,440 / 11,520 | 66 GB | 3.0 TB / 24 TB |
|
||||
| 32 | 92,160 | 273 | 1,440 / 11,520 | 212 GB | 9.5 TB / 77 TB |
|
||||
| 100 | 288,000 | 273 | 1,440 / 11,520 | 663 GB | 30 TB / 242 TB |
|
||||
|
||||
Because C1 counts blue blocks, the votes per block and the drain capacity per checkpoint are the same at every rate (the 3.4.2 arithmetic holds: 384 per block drains 8,192 in 21.3 blocks), while the bytes per day and the checkpoint cadence scale with bps: 3-s checkpoints at 10 bps, 0.3 s at 100. Tonight at 42 voters and 10 bps the seed's inbound IgneumFinality route (4,096 deep since the fin-route fix) overflowed at 109 to 2,547 drops per peer, and 16 of 47 determinable checkpoints locked; at 1 bps the same voters cost one tenth. Re-denominating C1 and d in DAA seconds (300 blue blocks and 600 at 10 bps) keeps the finality cost, the lock delay (63 s) and the light-client bytes at their 1-bps values across every rate step; it is a one-line spec change and a parameter in the fork.
|
||||
|
||||
### 5.8 Pruning, archival and the snapshot path
|
||||
|
||||
What a pruned node keeps (spec 02 2.1, `cost-tables.md` 6): headers and bodies back to the pruning point (108,000 DAA s, never past the latest certified checkpoint, F3), with their GHOSTDAG and reachability data (the 2x index factor is approximate); the pruning proof (levels of headers, `pruning_proof`); the UTXO set; the finality store's last 2,000 indices; the exec state (`exec-snapshot.bin` 128 MB measured at chain block 141,700, growing 0.9 KB per chain block, plus `ExecState.records` 1.5 KB per chain block until a window bounds it, M30 note); the proof-record window (`RECORD_WINDOW_CHAIN_BLOCKS`). An archival node keeps every block and its finality section: 40 GB per year of blocks at 1 bps (453 GB at 10 bps) plus votes as table 5.7, so the archival cost is the votes, not the blocks, until certificates replace carried votes (1.3 GB per year at 1 bps).
|
||||
|
||||
The p2p snapshot path as shipped in 0.3.14 (`proving.rs` `on_exec_snapshot`, `p2p_snapshot_gate`, read on `exec-sync-0313`): a peer's `ExecSnapshot` (version, chain id, genesis, tip number and hash, state root, records, the account and storage dump, fees, epoch, paid shards) is accepted only when its tip is a chain block known to this node's consensus, its tip is not 0, not below this node's exec restart, above this node's own executed tip, and only while the executor is blocked or has no state; and, when the restart pin is configured, the snapshot's record at the restart block must carry the pinned root (`exec_restart_state_root`, the fourteenth override field). The file is then written as the node's own resume point and served onward. Three things it does not check: the snapshot's state root at its tip against anything the chain commits to; the records between the restart block and the tip; agreement among peers. The poisoning attack: a peer of a blocked node (every node is blocked after a reorg deeper than its ring or after a restart below its retention root, the 6 October incident class) serves a snapshot whose tip is a real chain block above the victim's tip, whose record at the restart block is correct, and whose state at the tip is wrong. The victim loads it, executes forward from a false state, persists it, serves it to its own peers, and from then on refuses every honest proof record (`check_record`: the statement over its own records disagrees) while its own prover's records are refused by the network. Bound: it is a liveness attack on the victim and its downstream peers, not a consensus or a proof break (full nodes execute natively and a proof over the false state is a proof of the wrong statement; a light client verifies proofs against the chain's records, not against a node's state), and it needs the attacker among the victim's peers at the moment it is blocked, which the peer floor makes a 1-in-(peer count) race unless the attacker runs most of the victim's peers (an eclipse). The hardening: accept a snapshot only when its state root at the newest proven segment at or below its tip equals the `post_root` of the proof record the chain carries for that segment (the chain already carries 274-byte records in coinbase payloads, so this is a lookup, not a protocol change), and take the (tip, root) pair from N of M peers (the light client's 3 of 5, spec 10.6) before loading; a snapshot that fails either is refused and the peer is dropped for the session. Then poisoning needs a false proof record in the chain, which needs the proving key and a block that carries it, and the bound is the proof system's. Sizes per tier: the snapshot is the state (128 MB today, growing with accounts), so a home node's recovery is a 128 MB download and a sha256; an archival node's history is the table above; a light client never holds one.
|
||||
|
||||
### 5.9 Finality through this lane's eyes (cross-reference to lane 3)
|
||||
|
||||
Lane 3 (`finality-and-weight.md` 1 and 5.5) refutes the topology hypothesis for tonight's pause on the live devnet: certificates 6824 to 6842 formed while node 1 and the observer were down, the zero-aggregator fallback carried a quarter of the certificates, and the pause began at the checkpoint where signing weight fell to 53.1 percent of the frozen table, which is the two-thirds rule; the fleet's star is around the seed (the finality route entry: "on a Vast box the seed is the only peer"), not the Mac. This lane adds the measured shape of that star under load: 41 pods each with peers=1, every block and every vote through one process at 180 percent of one core, 2.5 MB/s of upload, the IgneumFinality route dropping thousands of messages per peer, 16 of 47 checkpoints locked. Lane 3's hub-cut harness case (its proposal 6) and this lane's peer floor (proposal 4) are one piece of work: the floor is what makes the harness case pass (a node with 4 outbound peers keeps blocks and votes when any one peer dies), and the fleet library is where both land.
|
||||
|
||||
### 5.10 Block rate: payout intervals, subsidy, lock delay and the solo-miner answer (`cost-tables.md` 7 to 9)
|
||||
|
||||
| Rate | Subsidy per block (full ramp), producer's 80% | 4070 (25 MH/s) at 1.16 GH/s / 100 GH/s / 1 TH/s / 10 TH/s | 5090 (128 MH/s) at the same | 8x 4090 rig (459 MH/s) at the same | Lock after a checkpoint if d stays 60 blue |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 31.69 IGN, 25.35 | 46 s / 1.1 h / 11.1 h / 4.6 d | 9.1 s / 13 min / 2.2 h / 21.7 h | 2.5 s / 3.6 min / 36 min / 6.1 h | 63 s |
|
||||
| 10 | 3.17, 2.54 | 4.6 s / 6.7 min / 1.1 h / 11.1 h | 0.9 s / 1.3 min / 13 min / 2.2 h | 0.3 s / 22 s / 3.6 min / 36 min | 9 s |
|
||||
| 32 | 0.99, 0.79 | 1.4 s / 2.1 min / 21 min / 3.5 h | 0.3 s / 24 s / 4.1 min / 41 min | 0.1 s / 6.8 s / 1.1 min / 11 min | 4.9 s |
|
||||
| 100 | 0.32, 0.25 | 0.5 s / 40 s / 6.7 min / 1.1 h | 0.1 s / 7.8 s / 1.3 min / 13 min | 0.0 s / 2.2 s / 22 s / 3.6 min | 3.6 s |
|
||||
|
||||
The solo-miner question: a 4070 at 25 MH/s sees 21.6 blocks a day at 1 bps on a 100 GH/s network (548 IGN a day at full ramp) and 2.16 a day at 1 TH/s (55 IGN); one block a day needs 0.046 bps at 100 GH/s, 0.46 bps at 1 TH/s and 4.6 bps at 10 TH/s. Kaspa's 10 bps buys the solo miner a daily block up to about 20 TH/s; Igneum at 1 bps gives it up to about 2 TH/s, which is 1,700x tonight's hash and USD 562,000 a day of rented hash at the bench entry's USD 281 per GH/s-day. What the rate costs the node, from 5.5 and 5.6: 10x the bytes (2 GB a day at 100 voters), a per-block CPU budget of 80 ms that the measured 61 to 345 ms does not meet, an RSS that leaves 8 GB within hours at the measured footprints, 3-s checkpoints and 66 GB a day of votes at 8,192 voters unless C1 moves to seconds, and the controller's cap inconsistency. Per tier: a home miner on one card gains variance relief it does not yet need (a 4070 at 1 bps sees a block an hour at 100 GH/s); a rig gains nothing (36 min per block at 1 TH/s already); a pool user nothing (pools remove variance); a prover gains nothing and pays the exec follower's 10x chain blocks; a holder gains faster locks (9 s) only if d is left in blue blocks, which costs the finality bytes of 5.7; a rollup customer the same lock question; the node operator pays all of it.
|
||||
|
||||
## 6. Ranked proposals
|
||||
|
||||
| Rank | Proposal | Evidence | Model | Hours | Consequence per tier | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Block rate: 1 bps for the public testnet and for mainnet launch; 10 bps stays a planned step (spec 2.1) behind three gates: per-block node CPU under 50 ms at mergeset 248 on a 2019 laptop core, C1 / d / the clock cap in DAA seconds, vote aggregation; no 32 or 100 bps step is proposed | 5.2 (reds are node throughput), 5.5 (budget 80 ms against 61 to 345 measured), 5.7 (66 GB a day of votes), 5.10 (1 bps already gives a 4070 a block an hour at 100 GH/s) | `propagation.py` F and G, `cost.py` 4 and 7 | 1 (the spec line) plus the gates below | home miner: no node change, a block an hour at 100 GH/s; rig and pool: unchanged; prover: the exec follower stays at 1 chain block per second; holder and rollup: 63-s locks; node operator: today's node | the three gates pass on Devnet 2 at 10 bps with zero red over the knee, before any rate step |
|
||||
| 2 | Controller: every lane counts every mergeset block's work (blue and red) over the real span; the clock cap, tolerances and lag bound in DAA seconds (20, 10, 60 s) at every rate; a chain step that merges m blocks may span m x 20 T before clamping | 3.1 (26 to 3.5 blocks/s with blue at 1.4), 5.4 (the fork over-reads by spacing / cap and under-reads by (k + 1) / m; Kaspa's window reads the whole DAG) | `controller.py`: 10.00 in every cell for the whole-DAG estimator | 8 (rule and unit tests in `difficulty.rs`, `igneum.rs`) + 3 (the harness case: fast-time 3 nodes, a 3-s proxy delay at 10 bps; pass = DAG rate within 10% of target and difficulty within 10% of the hash-implied value after 10 min, the same at 1 bps with a 30-s delay) | home miner: the subsidy stops tracking the controller's error (tonight 77% of blocks unpaid to their miner); node operator: no runaway DAG from a slow hub; holder: emission on schedule (spec 2.5's "when the controller lets the rate run" clause stops firing) | the harness case passes; `sim/difficulty/sim.py --dag-delay` replay of run A's production curve settles at 10 bps |
|
||||
| 3 | Finality interval and determination in DAA seconds (C1: checkpoint every 30 DAA s; d in DAA s), one carriage per vote (a block carries a vote only if no block in its past does, which is the rule as written; the relay dedups by (key, index)), and lane 3's aggregated certificate as the archival object | 5.7 (votes and route load scale with bps under the blue-block rule), 3.1 (2,547 route drops per peer, 16 of 47 locks) | `cost.py` 2 and 10 | 4 (spec 03 C1, 3.10 table) + 8 (fork: `finality.rs` interval in DAA s, relay dedup) | home miner and rig: finality bytes stay at 81 MB a day at 100 voters whatever the rate; node operator: the route stays inside 4,096; light client: 3.4 MB a day at every rate; holder: 63-s locks at every rate | a Devnet 2 run at 10 bps with 42 voters shows 0 route drops and every determinable checkpoint locked |
|
||||
| 4 | Peer floor and topology: a node reports synced and mines only with at least 4 outbound peers (8 on seeds); the fleet library gives every box the seed plus 2 random boxes; the seed list carries 3 seeds; the hub-cut harness case of lane 3's proposal 6 with the floor as its pass condition | 3.1 (peers=1 on every pod, 2.5 MB/s out of one process), lane 3 section 5.5 ("on a Vast box the seed is the only peer") | `propagation.py` B (mesh of degree 4 and 8 against the star: the same red share at an ideal hub, one fifth of the upload per node, no single queue) | 4 (fleet library and the synced rule in `tools/fleet/lib/`) + 3 (the harness case) | node operator and seed: upload spread 41 ways to 8 ways; home miner: a dead seed no longer stops blocks and votes; finality: votes reach aggregators by two paths | the hub-cut case passes: a lock within 2 checkpoints of the cut, 0 conflicting certificates at the hub's return, every box at 4 or more peers throughout |
|
||||
| 5 | Measure the per-block node cost and its split (lottery verify, BLS per vote, GHOSTDAG and reachability, relay, exec) with `perf` on a Devnet 2 box and on a 2019-class laptop at 1 and 10 bps, at a narrow DAG and at mergeset 248; set the budget rule (cost x bps under 0.8 core) as a CI check on the fast-time network | 3.1 and 5.3 (61 to 345 ms measured as a total only) | section 4.6 | 3 (profile) + 4 (the CI check in `tools/ci`) | every node tier: a number per machine class instead of the hub's; the 8 GB laptop and the Raspberry-class verdicts of 5.5 become measurements | the profile lands in the bench log with per-component ms; the check fails a build whose per-block cost exceeds the budget at the network's rate |
|
||||
| 6 | Snapshot path hardening: a p2p snapshot is accepted only when its root at the newest proven segment at or below its tip equals the chain's proof record for that segment, and (tip, root) agree on 3 of 5 peers; a failing peer is dropped for the session; the gate's unit test gains the two cases | 5.8 (the gate checks tips and the restart pin, never the state at the tip; the 6 October seed loaded a tip-0 snapshot from a fleet box before the gate) | section 5.8 | 6 (fork `proving.rs`, `snapshot.rs`; the record lookup exists in `check_record`) | node operator: recovery from a peer cannot be poisoned below an eclipse; prover: no false-state records from a poisoned node; home miner: the app's node recovers by itself after a deep reorg | `tools/exec-sync/reorg.mjs` gains a poisoned-snapshot case: refused, peer dropped, honest snapshot loaded after it |
|
||||
| 7 | Devnet 2 genesis bits set for the fleet's hash at the run's rate (no 26 blocks/s burst), and the fleet rule: a box mines only after synced with the peer floor (run A's pods mined from genesis for up to 6 minutes before they saw the chain) | 3.1 (the 55-block reorg "unwinding to height 1", the 33% red first phase), 5.2 (the burst alone saturates a 61-ms hub for 5 minutes) | `propagation.py` C and E | 2 (`box-dn2.sh`, `start-seed.sh`, `devnet2-override.json` per rate) | fleet operator: run A2 and run B measure the rate, not the start; every other tier: none | the next Devnet 2 run shows no reorg over 5 in its first 10 minutes and a first-minute production within 2x of target |
|
||||
| 8 | RSS steady state: run a node through a full 30-h pruning window at 1 bps (fast time) and at 10 bps on Devnet 2, read RSS every 10 minutes, bound the DAG store (reachability and GHOSTDAG per block) and `ExecState.records`; decide the 8 GB tier from the plateau | 3.3 (slopes 30, 80, 440 KB per block over short runs; none is a steady state), 5.5 (8 GB is marginal at 1 bps at 80 KB per block) | `cost.py` 5 | 3 (the run) + 8 (the bounds, if the plateau is over 4 GB) | home miner on an 8 GB laptop: a yes or no at 1 bps; Raspberry-class: the same; node operator: a RAM line on the requirements page | RSS plateau under 4 GB at 1 bps over 30 h; the requirements page carries the measured line |
|
||||
| 9 | Node and client tiers published with sizes: full pruned node (1 bps: 0.9 GB a day, 136 MB window plus state, 4 GB RAM target), archival node (40 GB a year of blocks plus 1.3 GB of certificates once aggregated; 296 GB a year of votes until then at 1,000 voters), light client checkpoint mode (3.4 MB a day), phase two (2.3 MB a day) | 5.6, 5.8, `cost-tables.md` 6 and 10 | `cost.py` | 2 (the requirements page and spec 10.5's table at 1 bps, with the rate rows) | every tier knows its cost before the testnet | the page's numbers match `cost-tables.md` and the first testnet week's measured bytes within 30% |
|
||||
| 10 | Bandwidth and route bounds per peer: at most 2 x V / 30 votes per peer per block interval accepted (a second belt behind the dedup), block relay to at most 16 peers per node, the IgneumFinality route sized from V and the rate | 3.1 (2,547 drops per peer), 5.6 | `cost.py` 3 | 4 (fork p2p flows) | seed and node operator: a bound on what one peer can make a node do; home miner: none | the s7 flood scenario gains a vote flood: 0 disconnects, bounded CPU |
|
||||
|
||||
**1. Block rate.** Everything measured tonight says 1 bps is the rate the current node can run and 10 bps is not: the hub spent 61 ms per block at a narrow DAG against a budget of 100, and the model turns that cost into tonight's red share (5.2). Nothing in propagation forbids 10 bps (5.1: zero reds at every measured delay with k 124), so the step stays on the plan (spec 2.1) as Kaspa took it, after a test campaign, with three gates: the per-block cost (proposal 5), the time denomination of finality and the controller (proposals 2 and 3), and vote aggregation (lane 3). The variance argument does not need the step yet: a 4070 sees a block an hour at 100 GH/s and two a day at 1 TH/s at 1 bps (5.10); a pool user never sees variance. Consequences: no node tier changes for the testnet; the testnet gives the measured bytes and RSS that fill proposals 8 and 9.
|
||||
|
||||
**2. Controller.** The rule walks the selected chain and sums blue-work increments over capped clock steps (`igneum_difficulty_bits`, section 4.4). In a wide DAG both parts mislead it: the cap turns a 5-s chain step into 2 s (over-read, harden, the record's fall to 3.5 blocks/s) and the blue-only work hides the reds (under-read, ease, the direction main reported). Kaspa's window pushes every mergeset block, red included, and divides by the real span (`window.rs`, `difficulty.rs`), which the model holds at target in every cell. The change is inside `igneum_difficulty_bits` and `igneum_target`: work = the mergeset's total work per chain step, the step allowed m x 20 T before the cap, the cap and tolerances in DAA seconds. The harness case is the proxy-delayed fast-time network of `sim/difficulty/attacks/README.md` at 10 bps with a 3-s hold, and `sim.py --dag-delay` replaying run A's production curve (the schedule file is in `sim/horizon/network/`).
|
||||
|
||||
**3. Finality interval in seconds.** C1 says 30 blue blocks, d says 60 blue blocks; at 10 bps that is a checkpoint every 3 s and ten times the votes, certificates, route load and light-client bytes per day (5.7), and tonight's seed showed the route overflowing at 42 voters. Written in DAA seconds the whole finality cost is rate-invariant and 3.4.2's arithmetic (384 votes per block drain 8,192 in 21 blocks) gets 10x more headroom at 10 bps. The one-carriage rule is already the spec's text ("not already in its past"); the relay dedup by (key, index) makes the wire match it.
|
||||
|
||||
**4. Peer floor.** Run A's pods had one peer each; the live fleet's boxes have the seed as their only peer; the hub paid 41x the upload and ran one queue for every block and vote. Four outbound peers before a node calls itself synced, the seed plus two random boxes in the fleet library, three seeds in the list: the mesh runs of 5.2 show the same zero red at an ideal hub with one fifth of the per-node upload and no single queue, and lane 3's hub-cut case becomes passable. This is the proposal that serves both lanes.
|
||||
|
||||
**5. The per-block profile.** The 61 to 345 ms is a total; the split decides which fix buys 10 bps: if BLS of carried votes dominates, aggregation and the dedup buy it; if GHOSTDAG at k 124 dominates, the mergeset limit and a cheaper reachability do; if relay dominates, the peer floor does. Three hours with `perf` on a Devnet 2 box, then a CI check that fails a build whose cost exceeds the budget at the network's rate, so the class is closed the way CLAUDE.md asks.
|
||||
|
||||
**6. Snapshot hardening.** The gate refuses tips, not states (5.8). The chain carries the proof records' roots, so a snapshot's root at its newest proven segment can be checked against them without a protocol change, and 3-of-5 peer agreement makes poisoning an eclipse. The attack is a liveness attack on the victim, never a consensus break, but a poisoned seed serving its state onward is the fleet-wide class the 6 October gate was written for.
|
||||
|
||||
**7 to 10.** Devnet 2's genesis bits and the start-synced rule make the next runs measure the rate instead of the start (the 55-block reorg and the first-phase reds were the start); the 30-h RSS run decides the 8 GB tier with a measurement instead of three slopes; the tier page and the per-peer bounds are two hours each and close open rows in spec 10.5 and the finality route entry.
|
||||
|
||||
## 7. Open questions and what could not be run
|
||||
|
||||
| Question | Why not tonight | What closes it |
|
||||
|---|---|---|
|
||||
| Run B (1 bps control) and A2 (mesh) rows | B starts at 20:25Z, rows about 21:00Z; A2 is not scripted | the fleet's RUN_B row; A2 built on `box-dn2.sh` with 3 `--addpeer` entries (seed plus two boxes) and genesis bits for 10 bps; this lane's prediction for A2 is in 5.2 |
|
||||
| The difficulty trajectory of run A | the collector strips `difficulty=`; the node log has no bits | `bps-collect.py` keeps the field; or `getBlockDagInfo` difficulty per minute on the next run |
|
||||
| The per-block CPU split | no profile; the hub's figure is a total that includes relay to 41 peers | proposal 5 |
|
||||
| RSS steady state at 1 and 10 bps | three slopes over 1,500 to 15,000 blocks, none over a pruning window | proposal 8 |
|
||||
| Kaspa's measured 10-bps red rate | not in the clone (`crescendo-guide.md` has requirements only) | the kaspanet KIP-14 text and the Crescendo testnet-10 reports, not cloned; approximate under 1 percent by the k derivation |
|
||||
| BLS verify per vote on this stack | O-10.3 open; 1.5 ms is approximate | `fast_aggregate_verify` timing with the forked `blst` build |
|
||||
| The pods' per-block cost (the A2 prediction's fork) | the pods' CPUs and their node logs were not read (fleet boxes are out of this lane's reach) | the fleet's collector reads `cpu_rss` on every box, not only the seed |
|
||||
| GHOSTDAG's second k-cluster condition | `propagation.py` applies the candidate's own anticone bound only; lane 4's `ghostdag_sim.py` carries both | re-run the grid with lane 4's colouring if a cell is contested (every cell here is 0 percent, so the undercount cannot change the reading) |
|
||||
| The 55-block reorg's cause | read from the log as a late-joining pod's chain from genesis ("unwinding to height 1") | the start-synced rule of proposal 7 removes the class; the next run's reorg distribution confirms |
|
||||
|
||||
## 8. Summary for the coordinator
|
||||
|
||||
Run A at 10 bps did not measure propagation; it measured a node that needs 61 to 345 ms per block through one hub whose budget at that rate is 100 ms. The propagation model with the measured links predicts under 0.1 percent red in the star and in the mesh; with the hub's measured per-block CPU it predicts 47 to 88 percent against the 77.2 percent measured, with the queueing waits the log's 100-block relay batches show, and it predicts that the mesh variant A2 goes red too unless every pod's per-block cost is under 80 ms and the genesis burst is removed. The controller then hardened (production 26 to 3.5 blocks/s with blue at 1.4) because its estimator reads blue work over a chain step capped at 2 s; the whole-DAG estimator Kaspa uses is unbiased in every regime the model covers. The block rate for the public testnet and mainnet launch is 1 bps; 10 bps stays a gated step, and the gates are a per-block cost under 50 ms on a laptop core at mergeset 248, the finality interval and the clock cap in DAA seconds, and vote aggregation. Lane 3's finding stands (the pause was the rule, the fleet's star is around the seed); the peer floor is the piece of work both lanes need.
|
||||
|
||||
1. Reds from node cost, not topology: red share 0.0 percent in every cell of the bps x delay grid (1 to 100 bps, 50 to 1,000 ms hops, k from Kaspa's table) and in the run A replay with an ideal hub; 47 / 88 / 83 percent with the hub at 61 / 115 / 345 ms per block (measured); the knee at 10 bps is 100 ms per block (`sim/horizon/network/propagation.py`, `results.md`, `results-2.md`).
|
||||
2. The controller's two biases: estimate / true hash = (blues / m) x (spacing / cap); at a 5-s chain-step spacing the fork settles the DAG at 4.0 blocks/s against 10 (record 3.3 to 3.6), at a 20-s spacing at 1 bps it runs away to 179 blocks/s; the whole-DAG estimator holds 10.00 and 1.00 in every cell (`controller.py`).
|
||||
3. Finality cost scales with the rate under C1 as written: 8,192 voters cost 6.6 GB a day per node at 1 bps and 66 GB at 10 bps (24 TB a year archival); in DAA seconds 6.6 GB at every rate, 3.5 MB a day with aggregated certificates; tonight's seed dropped up to 2,547 finality messages per peer and locked 16 of 47 checkpoints at 42 voters (`cost.py`, section 3.1).
|
||||
332
docs/analysis/horizon/new-pow.md
Normal file
332
docs/analysis/horizon/new-pow.md
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
# Horizon lane 8: a new proof of work (three candidate schemes, reviews, prototypes, verdicts)
|
||||
|
||||
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/<scheme>/`. 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 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)
|
||||
|
||||
| Time (UTC) | State |
|
||||
|---|---|
|
||||
| 19:05 | Lane started. Read: preamble, CLAUDE.md, the two personas, spec 01 (whole), 04, 07, chip-model-v3 (whole), asic-resistance-history (sections 0 to 3 and 4.3, 5), latency-shadow-2026-10-06 (whole), counter-asic-3-status sections 1 to 5, counter-asic-3-node section 6 (the P2 signalling rule), int8-matrix-family sections 1 to 3, scratch-soundness verdict, proving-methods (whole), fud-ledger M1, M7, M16, M22, M28, P2, F13, proto-cuda host.cu and the mx8-genesis pack (kernel.cu, memhard.h, program.h, vectors.h), proto-cuda/emu, family-probe.cu, the fleet's prover-tiers-real-cards.md, bench-log line 2582 (rental cost) |
|
||||
| 19:25 | Fleet agent asked for two boxes; answered at 19:29: two quiet RTX 4090s (RunPod, nvcc 12.8 at /usr/local/cuda/bin, directory /root/horizon-newpow, until 22:30Z). No quiet Ampere card exists tonight; a loaded 3090 is offered. Main's note: cost rows use bench-log 2582 (USD 0.0117 per MH/s-hour) |
|
||||
| 19:35 | File skeleton written. Two prototype sub-agents launched (budget two at once): `mma-shadow` on box 1 (<box-1-ip>), `state-dataset` on box 2 (<box-2-ip>) plus CPU rows on igneum-build-1. Designs being written in this file meanwhile |
|
||||
| 19:41 | Section 3 complete: the three designs, the one-table comparison, the migration path. Scheme A's verdict is already visible in its own numbers (A1 dead on 2.9 MB of openings per block, A2 dead on sampleability and a 32 to 40 ms proof verify; A0 is scheme C with the trace as state). Prototypes running: `mma-shadow` (box 1) and `state-dataset` (box 2 and igneum-build-1). mm8 two-output correction sent to the prototype |
|
||||
| 19:50 | Section 4 (reviews, the pick: B and C), section 7 (ranked next steps) and section 8 (open questions) written. Scheme C prototype complete and measured on box 2 and igneum-build-1: hash rate and watts unchanged (63.08 vs 63.09 MH/s, 207 W), build +1.4 ms, verifier +0.11 to 0.21 ms per unit, bit-exact 1,024 of 1,024 items and 128 of 128 lanes; rows in 5.2. Scheme B ladder running on box 1 (R = 0, 8, 32, 128 measured, 512 in progress) |
|
||||
| 20:20 | Scheme B ladder complete on box 1 (R = 0 to 512, rate flat at 63.08 MH/s, 201 to 216 W, every fingerprint PTX = reference, 1,024 of 1,024 lanes at every R, verifier delta 0.05 to 4.39 ms). Sections 5.1, 5.3, 6 and 9 written. Box 2 released 19:53Z; box 1 released on the mma agent's report. File complete |
|
||||
|
||||
## 1. What was read and the facts this lane stands on
|
||||
|
||||
Every figure below is from the named file; "approximate" marks a figure from memory.
|
||||
|
||||
| Fact | Value | Source |
|
||||
|---|---|---|
|
||||
| The shipped hash | 64 instructions x 8 iterations, 16 loads per program (128 dependent 4-byte reads per hash), 8 registers, 32-lane unit with xor shuffles, class v3 = mixer x8 item derivation over a 256 MiB ChaCha12 cache, 1 GiB dataset in the packs (2 GiB designed), era draws, VDF seeds | `docs/spec/01-lottery-hash.md` 1.4 to 1.13 |
|
||||
| Verifier today | 2.06 ms per unit on one M5 Max core (class v3, 4,096 item derivations), about 5.2 ms on a 2019-class core by the 2.5x rule; the 10 ms gate | `docs/plans/counter-asic-3-status.md` section 3, `latency-shadow-2026-10-06.md` section 4 |
|
||||
| RTX 5090 at the hash | 136.1 MH/s (readwidth), 132.2 (shadow control), 290 W in the app, 350 W in the bench; 2.34 to 2.65 microjoules per hash; 17.5 G dependent reads per second, 82 percent of the GDDR7 activate ceiling; 45.2 T int op/s; marginal ALU energy 10 to 13 pJ per counted op | `chip-model-v3.md` 5.1, `latency-shadow-2026-10-06.md` 5 |
|
||||
| The chip that matters | the f = 1 stored-dataset memory-controller chip: 5.1x per joule on GDDR7, 7.5x to 9.2x on HBM3 in the model; 2.1x to 4.8x by the Ethash precedent; the recompute chip (f = 0) 0.31x per chip, 1.86x per joule | `chip-model-v3.md` 5.4 to 5.6 |
|
||||
| The one lever against it | program work in the latency shadow: at N = 100,000 ops per hash the chip's edge over the 5090 falls from 5.6x to 2.1x at k = 1 (chip core energy per op equal to the GPU's 11 pJ), to 3.2x at k = 0.5, 4.1x at k = 0.3; class v4 candidate `mx8+sh256x27` | `latency-shadow-2026-10-06.md` 6 and 10 |
|
||||
| Step costs per family on the 5090 (ratio to the add-xor-rotate chain, 7,941 G lane-steps/s) | rotr 1.32, shflx 1.49, shfla 1.53, dot4 1.16, mm8 (`mma.m8n8k16.u8`, bit-exact) 2.43 | `counter-asic-3-status.md` section 3, item 6 |
|
||||
| mm8 on other vendors | AMD RDNA 4: WMMA iu8 builtin reaches gfx12, 1.68 to 1.83 per step, fragment layout UNVERIFIED (exactness not attempted); Apple: no integer simdgroup matrix in MSL, Metal 4 `matmul2d` uchar x uchar into int exists but not from the Swift toolchain used here, per-lane dot4 emulation 1.6x unsigned, 4.7x signed | `counter-asic-3-status.md` item 6 AMD column, `int8-matrix-family.md` 1 and 4 |
|
||||
| Proving today | SP1 6.8.1 Hypercube; the v1 shard (4.7 M cycles) proves in 4.8 to 18 s on 12 to 32 GB cards with the patched server, 7.4 to 8.0 GB alone; compressed proof 1.27 MB, verified in 32 to 40 ms; the aggregator 2.2 to 9.7 s per block | `prover-tiers-real-cards.md`, `proving-methods.md` 1 and 2.1 |
|
||||
| Proof payment | 80/20 lottery/proving split of the subsidy; shards by weighted sortition (8 assignees, 10 s window), aggregator share 1,000 bps, unproven deadline 600 DAA s; the native-execution veto: a record whose statement differs from the node's own execution pays nothing | `docs/spec/07-execution.md` 7.2, 7.7, 7.8 |
|
||||
| Class activation | P2: a class flips when 95 percent of blue blocks over a one-day window carry the object byte (header version high byte), with a fixed-height floor; one-sweep binary rollout; Devnet 2 gate first | `docs/plans/counter-asic-3-node.md` section 6, CLAUDE.md 6 Oct rules |
|
||||
| Rented hash | USD 0.0117 per MH/s-hour (1,748 MH/s for USD 20.44 per hour on RunPod community pods, 18:45Z); the live devnet 1.16 GH/s | `docs/bench-log.md` line 2582 |
|
||||
|
||||
## 2. Method
|
||||
|
||||
Designs first (section 3), each reviewed in two personas (section 4), two picked, two prototypes measured on real cards (section 5), verdicts (section 6). Prototype shape: the mx8-genesis pack's own kernel text (`proto-cuda/packs-ca2-mixer/mx8-genesis/kernel.cu`, `memhard.h`) modified by the smallest change each scheme needs, compiled with nvcc 12.8 on the two RunPod 4090 boxes, timed by CUDA events over 2^24-nonce batches with `nvidia-smi` at 1 Hz, and checked bit for bit against a C CPU reference that interprets a 32-lane unit register-major with lazy item derivation (the verifier's shape) on 1,024 random lanes. CPU rows on igneum-build-1 (EPYC 9454P, Zen 4, one core at up to 3.8 GHz, which is NOT a 2019 core; the 2.5x rule of the project stands in for that core, labelled). Chip rows are the chip-model-v3 method (section 5 of that file) applied to each scheme, approximate where that file is approximate.
|
||||
|
||||
## 3. The three candidate schemes
|
||||
|
||||
Shared notation: `K_d` the day key (spec 1.8.1), `S_e` the epoch seed words (spec 1.3), the unit = 32 aligned nonces (spec 1.9), `item(t)` the 16-word class v3 derivation of spec 1.8.5, `target64` as spec 1.10. Difficulty in every scheme is the unchanged 64-bit target comparison on the unchanged DAA (spec 2.3): none of the three changes what a block's work unit is worth, only what the unit of work consists of, so the difficulty controller sees the same statistics. Each scheme is a program class in the sense of spec 1.4.5 (a `generator` number and a class byte), so activation runs through P2 (section 3.5).
|
||||
|
||||
### 3.1 Scheme A: mining is proving ("proof of committed trace")
|
||||
|
||||
**The claim to test.** The lottery's work is a bounded piece of the chain's own proving, so the 80/20 split collapses into one payment and the hash rate is the proving capacity.
|
||||
|
||||
**The puzzle, in its most favourable form.** Every full node already runs the segment natively (spec 7, the native-execution veto). Add one step to that: every node also runs the zkVM executor (SP1's RISC-V executor, CPU, no proving) on the segment's shard inputs and keeps the shard TRACE: the cells of every table the shard touched, about 280 M cells for the adopted v1 shard, 1.1 GB (`proving-methods.md` 1.4). The lottery of epoch `e` then uses as its dataset the trace of the last segment whose last chain block has DAA score at most `3,600 e - 1,200` (the same 20-minute lead as the epoch seed, spec 4.3), serialised row-major and padded by zero to the dataset size, and the item derivation becomes `item_A(t) = class v3 derivation with s[i] ^= row(t)[i]` for the 16 words of trace row `t` (64 bytes of trace per item). Everything else is the shipped hash: 128 dependent 4-byte reads, the 32-lane unit, the fold, `target64`. Header commitment: nothing new; the trace is a function of the segment, the segment of the chain, the chain of the header's past, exactly as the epoch seed is (spec 4.3 item 4); the class byte 5 of P2 marks the object. Difficulty: unchanged. Verifier: the node derives up to 4,096 items per unit from the 256 MiB cache (as today) plus 4,096 reads of the trace rows it holds in RAM (1.1 GB): the measured cost of exactly this read pattern is scheme C's row in section 5 (the two schemes share the verifier shape). Under 10 ms on a 2019 core if scheme C's row is.
|
||||
|
||||
**Two stronger forms, and why each dies on a number.**
|
||||
|
||||
| Form | What the miner must hold or do | Verifier | Why it dies |
|
||||
|---|---|---|---|
|
||||
| A1, "proof of committed codeword": the dataset is the Reed-Solomon codeword of the trace (BaseFold's stacked encoding at blowup 4, 4.5 GB, `proving-methods.md` 1.1 and 1.4), whose Merkle root the shard record already publishes; the miner must have done the COMMIT stage of the proof (encode and hash) to mine | the LDE and the Merkle tree: the first stage of every STARK or BaseFold prover, the part a GPU spends a large share of its proving time on (approximate: 30 to 50 percent of the stage time, unmeasured here) | cannot derive a codeword word locally: one evaluation of a stacked column at one point is O(2^21) field operations (the stacking height, `proving-methods.md` 1.1), so 4,096 words per unit is about 8 G operations, about 1 s on a core, 100x over the gate. The block must therefore carry Merkle openings: 4,096 per unit (every lane's loads feed every other lane through `shfl`) x 22 levels x 32 bytes = 2.9 MB per block; with a 1-lane unit (no `shfl`) 128 x 22 x 32 = 90 KB per block, 7.8 GB per day of PoW witness at one block a second against about 200 B per header today (Kaspa header, approximate). Headers-first validation, the pruning proof and the light client (spec 10) all break on the bytes |
|
||||
| A2, "mine a proving step": each nonce selects a random piece of the pending proof work (a FRI fold of one column, a Poseidon2 Merkle layer, a zerocheck round) and the hash is of that piece's output | that piece | the verifier either recomputes the piece (then the verifier did the useful work, and the piece bought nothing: the definition of useless) or verifies a proof of it (an SP1 compressed proof verifies in 32 to 40 ms, `proving-methods.md` 2.1, four times the whole gate, and a per-piece proof does not exist). And the pieces run out: a segment's proof is about 5 s of one 5090 (`prover-tiers-real-cards.md`: 4.8 to 18 s per shard on 12 to 32 GB cards, 2.2 to 9.7 s of aggregation) against 8 G hashes per 8-s segment at 1 GH/s; the useful fraction of the lottery's work is bounded by (proving work per segment) / (network hashes per segment), about 8 percent at 1 GH/s and 0.08 percent at 100 GH/s (arithmetic on the cited figures), because proving work is set by gas and lottery work by the security budget. They are the same quantity only by coincidence at one network size |
|
||||
|
||||
**The sampleability objection, stated and answered.** Ball, Rosen, Sabin and Vasudevan, "Proofs of Useful Work" (ePrint 2017/203) build PoUW for problems with a random self-reduction (orthogonal vectors, 3SUM, all-pairs shortest paths): a useful instance is embedded into a random challenge so that the challenge is hard on average, solving it solves the instance, and verification is fast; the price is a polynomial blow-up and a problem class with that structure. Ofelimos (Fitzi, Kiayias, Panagiotakos, Russell, CRYPTO 2022) makes the useful work a doubly-parallel local search whose QUALITY improves with more work, so more hash rate yields better solutions. Primecoin (2013) mined Cunningham chains (useless, but sampleable: the nonce picks the chain's origin). Gridcoin pays BOINC credit through a trusted whitelist, not a puzzle (approximate, from memory for the last two). Proof generation has none of the three properties these constructions need: (1) a segment has one proof, not a distribution of instances, so there is nothing for the nonce to sample; (2) partial progress has no verifiable value under the gate without a proof, and a proof verify costs 32 to 40 ms; (3) the quantity of useful work is fixed by demand (gas), the quantity of lottery work by the security budget (hash rate), and a puzzle whose per-solution work is fixed by demand is not a difficulty-adjustable lottery. The answer this design gives is therefore no: the only form that survives the gate and the bytes is A0 above, in which the miner must HOLD the trace, which is "proof of stored state" with the trace as the state, and the useful work gained per hash is zero (every node computes the trace natively anyway; ledger F13). Mining does not become proving; it becomes proof that the miner executes.
|
||||
|
||||
**Chip edge per joule (chip-model-v3 method).** A0 changes the dataset's contents, not its size or read pattern; the f = 1 stored-dataset chip stores whatever the items are: 5.1x per joule on GDDR7, 7.5x to 9.2x on HBM3, unchanged from `chip-model-v3.md` 5.4. The f = 0 recompute chip must now hold the 1.1 GB trace somewhere (it cannot derive an item without row `t`), so it needs DRAM beside its SRAM and becomes an f = 1 chip; the 0.31x row disappears, which is a small gain since that chip was never the threat. A1 and A2 are not priced: they fail before a chip is drawn.
|
||||
|
||||
**Verifier cost.** A0: the class v3 derivation (2.06 ms per unit on an M5 Max core, measured) plus 4,096 random 64-byte reads from a 1.1 GB array; the read row is measured in section 5 (scheme C's CPU rows, the same pattern). A1: 2.9 MB of openings or a 1-lane unit (see table). A2: a proof verify (32 to 40 ms, measured) or nothing useful.
|
||||
|
||||
**Bit-exactness.** A0: the trace rows are bytes; the derivation is integer; nothing vendor-specific is added. The zkVM executor's trace must itself be deterministic across platforms (SP1's executor is Rust, no floating point in the trace path, approximate: not audited here); any disagreement about a trace cell is a consensus split, so A0 adds the SP1 executor to the consensus-critical code, which ledger P7 already names as a risk class for the proofs and which here would extend to the lottery.
|
||||
|
||||
**Known attacks.** Grinding: a block producer influences the trace through the transactions it includes, but the trace used is 20 minutes old and keyed by `K_d` through the mixer; a producer cannot predict a useful bias in 128 dependent reads of a keyed derivation (the MTP lesson, `asic-resistance-history.md` 2.3: Dinur and Nadler controlled addresses by controlling contents; here the contents enter only through a keyed, chained derivation whose addresses are the cache-line indices of the mixer state, not the trace). Outsourcing: pools serve the dataset (1 to 2 GiB per miner per day), so "the miner holds the trace" becomes "someone in the pool holds it". Precomputation: the dataset is computable 20 minutes early, as today. Light evaluation: as the f = 0 row above. Sampleability: the paragraph above. Empty segments: a day or an epoch with no transactions has a near-empty trace (the shard statement still applies rewards, spec 7.7 item 8), so the dataset is mostly padding and the hash degrades to today's class v3: graceful, not an attack.
|
||||
|
||||
**Game theory of the collapsed payment (if A1 or A2 had worked).** The 20 percent pool would go: provers are miners and the proof is the by-product of the lottery. Who is paid for what: the block reward pays for the block and the proof piece in it; a prover that mines earns exactly a miner that proves. Pools: the pool does the useful work and sells shares, as today, so proving centralises exactly as mining does (Aleo's proof of succinct work centralised on the same path: the coinbase puzzle was a synthetic circuit proven by whoever had the most GPUs; approximate, from memory, the cryptographer persona's own list). Proving demand at zero: the puzzle has no useful content and must fall back to a synthetic dataset, so the chain carries two puzzle modes and a mode switch, a consensus rule and a grinding surface. External jobs: a customer's proof could only be mined if its segment entered the puzzle, so the security budget would be spent on the customer's work at the marginal cost of inclusion, a subsidy from holders to customers unless priced by a burned fee. These are the reasons CLAUDE.md records the lottery and the proving as separate on purpose; nothing found tonight overturns them.
|
||||
|
||||
**Migration.** A0 is a class byte (P2): class v5 by the 95 percent signal with the floor height, one-sweep binary rollout, Devnet 2 gate first (CLAUDE.md 6 Oct). Every node must run the zkVM executor on every segment before the flip (CPU cost: the v1 shard is 4.7 M cycles; SP1's executor runs at tens of MHz on a CPU, approximate, so under a second per 8-s segment); a node without it cannot validate PoW after the flip, which is what the floor height is for.
|
||||
|
||||
**Verdict line (section 6 has the reasoning):** NEVER as "mining is proving" (A1, A2); A0 is scheme C with the trace as the state and is folded into C.
|
||||
|
||||
### 3.2 Scheme B: a GPU-structure-bound puzzle ("mx8+mm8xR", the tensor-shaped integer shadow)
|
||||
|
||||
**The claim to test.** Fill the latency shadow (the only lever that moves the f = 1 chip, `chip-model-v3.md` 5.7, `latency-shadow-2026-10-06.md`) with work shaped like a GPU's own tensor datapath rather than its scalar ALUs, so that a chip must carry GPU-class matrix units whose energy per operation NVIDIA's own silicon already sits near the floor of, and the chip's residual edge `k` (its energy per op divided by the GPU's) cannot fall far under 1. The brief's other candidates are rejected on definition: shared-memory bank timing and register-file width are timings and capacities, not values, and a consensus rule can only check values; a per-warp scratchpad was measured and found not sound as a chip layer (`docs/analysis/scratch-soundness.md`: the live state is bounded by the read-modify-write count, 64 to 320 bytes per lane, which a chip keeps in SRAM at under 5 percent of its mirror).
|
||||
|
||||
**The puzzle.** Class v3 (mixer x8, 16 loads, the 64 base instructions, the era draws) plus a block of `R` `mm8` steps executed at the end of every iteration, after instruction 63 and before the next iteration samples `sel`; `8 R` steps per hash, no load in the block, the base program untouched (the same seam the class v4 shadow uses, `latency-shadow-2026-10-06.md` section 2, so the acceptance rule of 1.4.6 keeps its verdict draw for draw). Step `k` has four draws from the program stream after the base and shadow draws: `a = below(8)`, `b = below(7) + (b >= a)`, `c = below(8)`, `c2 = below(7) + (c2 >= c)`. Its semantics over the unit:
|
||||
|
||||
```
|
||||
A: 8 x 16 uint8 lane l holds A[l >> 2][4 (l & 3) .. 4 (l & 3) + 3] = the 4 bytes of r[a] (byte 0 = lowest k)
|
||||
B: 16 x 8 uint8 lane l holds B[4 (l & 3) .. +3][l >> 2] = the 4 bytes of r[b]
|
||||
C = A x B C[i][j] = sum over k of A[i][k] B[k][j], exact in int32 (at most 1,040,400)
|
||||
r[c] = r[c] + C[l >> 2][2 (l & 3)] (mod 2^32)
|
||||
r[c2] = r[c2] + C[l >> 2][2 (l & 3) + 1] (mod 2^32)
|
||||
```
|
||||
|
||||
This is the fragment layout of PTX `mma.sync.aligned.m8n8k16.row.col.s32.u8.u8.s32` with the two `.s32` outputs `d0`, `d1` of each lane (PTX ISA, "Matrix Fragments for mma.m8n8k16", the integer layout; `docs/analysis/int8-matrix-family.md` 2.2 quotes it and `proto-cuda/family-probe.cu` carries the CPU reference that was bit-exact on the RTX 5090 on 5 October 2026, `counter-asic-3-status.md` item 6). The spec defines the matrices and the lane ownership, not the instruction: a vendor permutes its own fragment layout into this one (a shuffle) and the result is defined whatever the hardware. Why TWO destinations where the reserve entry R8 of 1.13.2 has one: with one element per lane only 32 of the 64 products of C are consumed, so a chip does 512 multiply-adds per step where the GPU does 1,024 and the GPU hands the chip a free 2x on the block; with both outputs consumed the whole tile is load-bearing. This is a correction to the reserve text as well (section 7). Unit of work: still the unit. Header, difficulty: unchanged; the class byte 5.
|
||||
|
||||
**Parameters and the verifier's law.** Per unit the verifier adds `8 R x 1,024` unsigned byte multiply-adds (the register-major interpreter already holds the 32 lanes' registers, so A and B are in hand). Plain scalar C: about 2 ops per multiply-add, 16,400 ops per step per unit; at the 18 G op/s the x8 verifier shows on the M5 Max core (`chip-model-v3.md` 5.7, measured) that is 0.9 microseconds per step per unit: `R = 128` adds about 0.9 ms, `R = 512` about 3.7 ms. With a byte-dot instruction (AVX-VNNI `vpdpbusd`, 64 multiply-adds per instruction; NEON `udot`, 16 per instruction) the same work is 4x to 16x fewer instructions (approximate). The headroom is the class v3 margin: 7.9 ms steady on the M5 Max core, 4.6 ms on a 2019-class core by the 2.5x rule (`latency-shadow-2026-10-06.md` section 4), so scalar `R` is bounded near 500 on the 2019 core and the measured rows of section 5 set it. On the GPU side the 5090's dependent-chain probe runs `mm8` at 2.43x the add-xor-rotate step, 7,941 / 2.43 G lane-steps per second = 102 G `mm8` per second per card, 104 T multiply-adds per second (the chain probe, not the tensor peak); at 4.25 M units per second (136 MH/s) the card could hide about 24,000 `mm8` per hash before the probe rate binds, `R` about 3,000, far above what the verifier allows. So the verifier binds first, at about `R = 500` scalar on a 2019 core (approximate until section 5) and perhaps 4x higher with VNNI: the honest card never leaves the latency bound on the candidate `R`.
|
||||
|
||||
**Chip edge per joule (chip-model-v3 method).** The f = 1 chip (memory, controller, static: 0.466 microjoules per hash on GDDR7, 0.321 on one HBM3 stack, `chip-model-v3.md` 5.4) plus a tensor array: energy per hash = memory + `8 R x 1,024 x e_mac x k_mma`, where `e_mac` is the honest card's marginal energy per multiply-add at the block (measured in section 5 as watts delta over MACs per second) and `k_mma` the chip's ratio to it. The difference from the ALU shadow (`k` down to about 0.3 for a fixed-datapath array at N5, `latency-shadow-2026-10-06.md` 6) is where the honest card's engine sits: NVIDIA's tensor cores are int8 multiply-add arrays at N4/N5-class density already, so a chip's array is the same circuit (approximate: int8 MAC datapath about 0.05 to 0.1 pJ at N5, the movement of fragments through the register file the larger term on both sides; from memory) and `k_mma` is near 1 with a floor near 0.5 for a chip that keeps the fragments in a local register file instead of the GPU's banked one. Rows at `k_mma` = 1, 0.5 and the measured `e_mac` are filled in section 5.3 from the 4090 measurement; the shape of the result is already clear: the block lowers the chip's edge by the same mechanism as the ALU shadow and the chip's best case is better bounded, at the price that the honest card's own watts rise by the block's energy (the tensor path is efficient, so the rise per unit of chip-forcing work should be smaller than the ALU shadow's 11 pJ per op; the measurement says).
|
||||
|
||||
**Bit-exactness across vendors.**
|
||||
|
||||
| Vendor | Path | State |
|
||||
|---|---|---|
|
||||
| NVIDIA sm_75 and later (Turing, Ampere, Ada, Blackwell) | one `mma.sync.m8n8k16.u8` per step, identity permutation, wrap on the `.s32` accumulate (the wrap edge vector of the R8 entry was bit-exact on the 5090, `int8-matrix-family.md` 3) | measured bit-exact on the 5090 (5 October) and on the 4090 tonight (section 5) |
|
||||
| NVIDIA sm_61 to sm_72 (Pascal GTX 10 series, Volta) | no `mma` with `.u8`; emulation: 8 `__shfl_sync` gathers plus 8 `dp4a` per lane per step (`dp4a.u32.u32`, sm_61+) | unmeasured; cost about 16 steps per `mm8` against 2.43 native, approximate; within the 8x emulation bound of 1.13.2 on a per-op basis only if the shuffles are cheap; a GTX 1080 owner is the first NVIDIA tier to pay |
|
||||
| AMD RDNA 3 and 4 (gfx11, gfx12) | `V_WMMA_I32_16X16X16_IU8` with the 8 x 16 and 16 x 8 tiles zero-padded to 16 x 16 and a fixed lane permutation (`ds_bpermute`) into the spec layout; the RDNA 4 builtin takes 2 ints per lane for A and B (`int8-matrix-family.md` 1) | the fragment layout is UNVERIFIED (status item 6: not in any source at hand; the CPU reference was not attempted rather than guessed). This is the gate for AMD: a PC 1 job on the 9070 XT with the spec reference. Until it passes, AMD runs the emulation path (`v_dot4_u32_u8` is native on gfx11 and gfx12, 1.06x per op measured) at about 12 to 16 steps per `mm8`, approximate |
|
||||
| AMD RDNA 2 and older, CDNA | no WMMA on RDNA 2; `v_dot4_i32_i8` exists (`dot1-insts`, signed only, approximate); CDNA 3 has `V_MFMA_I32_16X16X32_I8` with its own layout | emulation; unmeasured |
|
||||
| Apple (M-series, Metal) | no integer `simdgroup_matrix` in MSL; Metal 4 `mpp::tensor_ops::matmul2d` has `uchar x uchar -> int` (table 7.3, OS 26.4) through a tensor API not reachable from the Swift toolchain this project uses, and its wrap semantics are unverified (`int8-matrix-family.md` 1). The emulation: 4 shuffles plus 4 unsigned `dot4` emulations at 1.6x per op (measured 5 October), about 10 ALU steps per `mm8` | an Apple miner pays about 4x NVIDIA's per-step cost on the block (approximate); the M5 Max is latency-bound to about 130,000 counted ops per hash (measured), so `R = 128` (1,024 `mm8` per hash, about 10,000 steps emulated) fits inside its shadow and `R = 512` (about 41,000) still does by the ops count, with the hash-rate cost owed to a Metal measurement. Apple's path is the honest card's worst and the chip's argument does not depend on it |
|
||||
| Intel Arc | XMX through `cl_intel_subgroup_matrix_multiply_accumulate` (approximate, unverified); dp4a-class `dot` otherwise | unmeasured |
|
||||
|
||||
So the fleet splits by generation: Turing-and-later NVIDIA and RDNA 3-and-later AMD run the block natively; everything older and Apple emulate. The 5 percent rule (`counter-asic-2-public.md`) is checked per card with the block live, as 1.13.2 requires for an emulating vendor.
|
||||
|
||||
**Known attacks.** Grinding: none new; the block has no data-dependent control flow and reads no memory. Outsourcing, precomputation: as today (the draws are public per epoch; there is nothing to precompute because the inputs are the per-nonce registers). Light evaluation (the f = 0 chip): untouched; the block never reads the dataset. MTP-class content control: not applicable (no attacker-chosen memory). Sampleability: not applicable. New: (i) the half-tile shortcut, closed by the two-destination form above; (ii) a zero or low-entropy fragment (if `r[a]` is zero in every lane the step is free): the acceptance rule's register-saturation test (1.4.6 (c)) already rejects programs with stuck registers, and the base program's loads re-randomise every register every iteration; (iii) the trailing-step contraction: the last `mm8` of the last iteration writes two registers that feed only the fold; a chip could skip nothing because the fold reads all eight, but a step whose destinations are both never read again before the fold still costs the GPU a full tile; the draw rule should forbid `c, c2` outside the fold's register set, which is every register, so there is no such step. (iv) The licensable-IP objection (the history's reason for ranking `mm8` last in the reserve, `asic-resistance-history.md` 4.3 row 6): a chip maker licenses an int8 MMA block at any node. True, and it is the point of the design: the chip must then carry a GPU-class tensor array per 32 lanes in flight at the memory's activate ceiling, 1,172 lanes on GDDR7 (`chip-model-v3.md` 5.5), 37 tiles in flight, and its edge is `k_mma`, a ratio of two copies of the same circuit; the design does not claim the chip cannot be built, it claims the chip is a GPU.
|
||||
|
||||
**Game theory.** None of the payment changes; this is a hash change. A pool user sees nothing. A prover that mines pays the block's watts on the same card it proves on; the tensor path is also the proving path's (SP1's Poseidon2 and NTT kernels are integer, not tensor, so there is no contention beyond the power limit, approximate). When proving demand is zero nothing changes.
|
||||
|
||||
**Migration.** Class v5 by P2: the object byte 5, the 95 percent one-day window, the floor height; one-sweep binary rollout because the digest flips; Devnet 2 gate first. The kernel emitter (`igneum-pow/src/emit.rs`) gains the block in its three dialects, the CUDA one with inline PTX and the `IGNEUM_MM8_REF` fallback for sm_61 to sm_72, the OpenCL one with the AMD builtin behind a feature test and the emulation otherwise, the Metal one with the emulation; the one-click workers compile the text as they do today (NVRTC accepts inline PTX). Gates before a cut: the six gates of `counter-asic-2-rollout.md` section 7 plus the AMD layout verification and the Metal emulation's hash-rate cost on the M5 Max.
|
||||
|
||||
### 3.3 Scheme C: proof of stored state ("sd1", the dataset is the chain)
|
||||
|
||||
**The claim to test.** The dataset is the recent chain state, so every hash proves the miner holds the chain, and the lottery's reads double as a verifiable random sample of state for light clients.
|
||||
|
||||
**The puzzle.** Day `d`'s snapshot `SS(d)` is the execution state at the state root `R_d` of the certified checkpoint `C_day(d)`, the highest-index checkpoint whose block has DAA score at most `86,400 d - 1,200` (the epoch seed's lead, spec 4.3). Its leaves: the `(key, value)` pairs of the execution state trie in key order (storage slots, account records, 64-byte chunks of code), leaf `t` serialised as `leaf(t) = Blake2b-512(R_d || t_le32 || key_t || value_t)` (the chain's own hash, spec 0.6), 64 bytes each, so every leaf carries full entropy whatever its content and no two days share a leaf; for `t` beyond the state's leaf count `leaf(t) = 0`. When the state has more leaves than the dataset has items, the dataset holds the first `2^(D-4)` leaves in the order of `Blake2b-256(K_d || key)`: a keyed sample that cannot be chosen without the whole state. The item derivation is class v3's with one line added before the round loop: `s[i] ^= leaf(t)[i]` for `i` in 0..15. The hash kernel is byte for byte the shipped one; only the daily build changes. Header commitment: none new. `R_d` is the state root of a block in the header's own past at a fixed blue score, so "which snapshot was this block mined under" is a function of the header alone once the chain is known, as the program is (spec 1.12). The class byte 5 marks the object. Difficulty: unchanged. Unit of work: unchanged.
|
||||
|
||||
**The verifier.** A node holds the 256 MiB cache (as today) and `SS(d)` in RAM or mmap (2 GiB at the genesis dataset size, growing on the 1.13.3 schedule), derives up to 4,096 items per unit lazily as today and reads `leaf(t)` for each: 4,096 random 64-byte reads. The measured cost of those reads and of the derivation is section 5.2. Verifier memory: +2 GiB (+4 GiB at year 4). The daily snapshot build: one pass over the state trie, hashed per leaf (one Blake2b-512 per 64 bytes: about 2^25 hashes for 2 GiB, seconds on a core, section 5.2 measures the stand-in).
|
||||
|
||||
**What is new, and the prior art.** Permacoin (Miller, Juels, Shi, Parno, Katz, IEEE S&P 2014) made the puzzle a proof of retrievability over a large PUBLIC FILE chosen by a dealer, with Merkle openings in each block; the file was external to the chain and the openings were the bytes. Spacemesh (proof of space-time over a plotted file of random data) and Chia (Abusalah, Alwen, Cohen, Khilko, Pietrzak, Reyzin, "Beyond Hellman's time-memory trade-offs with applications to proofs of space", ASIACRYPT 2017; Chia's plots) prove storage of USELESS data. Verthash (Vertcoin, January 2021; `asic-resistance-history.md` row 8) is the nearest: a 1.2 GB file generated from the chain's own block headers, random reads, no chip after 69 months on a small prize; its data is headers (low entropy per byte, static once written) and it proves nothing about state. Ethash's DAG is from the epoch seed (random). What Igneum C adds: (1) the dataset is the EXECUTION STATE, keyed per day by `K_d` through the memory-hard derivation, so it is never easier than today's dataset (the leaf is one more 64-byte input to a 9,360-op chain) and it cannot be built from the day key alone: whoever builds it holds the state; (2) every block's 128 loads per lane name 128 keyed items whose leaves are a uniformly random sample of state (the addresses are the mixer-state cache-line indices through 128 dependent reads, unbiasable by the producer at a cost below a block), so a light client that asks any full node for the block's `(key, value)` leaves with their openings against `R_d` gets a free daily spot check of state availability; (3) the beacon: the lottery output is already public randomness, biasable by withholding at the cost of a block, as every PoW; C adds nothing there and the design says so. Honest limit: a pool can ship the 2 GiB dataset or the snapshot to its miners once a day (2 GiB per miner per day, 23 MB/s for a thousand miners), so "every miner holds the chain" is really "every mining OPERATION holds the state", which is still a change: today a pool miner needs nothing but the day key.
|
||||
|
||||
**Chip edge per joule.** Unchanged against the f = 1 chip (it stores items whatever they are): 5.1x on GDDR7, 7.5x to 9.2x on HBM3 in the model, 2.1x to 4.8x by the Ethash precedent. The f = 0 recompute chip must hold the leaves (2 GiB) in DRAM to derive anything, so it becomes an f = 1 chip and the 0.31x row disappears. C is therefore not an anti-chip scheme and does not claim to be; it composes with B (the shadow is in the kernel, the state is in the build).
|
||||
|
||||
**Bit-exactness.** The leaf is the output of the chain's own hash over bytes every node agrees on by consensus; the derivation is integer; nothing vendor-specific is added. The one new consensus-critical function is the canonical serialisation of state (key order, chunking of code), which every node must compute identically: a bug there splits the chain on a day boundary, the class of M20 and the DAA 198,000 incident. The Devnet 2 gate exists for exactly this.
|
||||
|
||||
**Known attacks.** State grinding: a producer can write state (pay gas) to influence leaves; the leaf is hashed with `R_d`, which depends on every leaf, and enters a keyed chained derivation whose read addresses are mixer state, so no bias on 128 dependent reads is reachable at a cost below a block (the MTP lesson, `asic-resistance-history.md` 2.3, is the reason for the hash and the key, not the plain bytes). Compressible state: an attacker fills state with zeros hoping a chip stores it compressed; the hashed leaves are full-entropy, and the padding region (`leaf = 0`) is today's dataset, which the chip already stores at 64 B per item. Precomputation: the snapshot is fixed when `C_day(d)` is certified and `K_d` is known, 20 minutes before the day (the same lead as the epoch seed), and the build is 13 to 77 ms on the GPUs measured (`counter-asic-3-status.md`) plus the leaf pass; a reorg across `C_day(d)` is a merge-depth-scale event, accepted as for the epoch seed (spec 4.3 item 4, O-4.3). Outsourcing: the pool ships the dataset (above). Light evaluation: the f = 0 row above. Long-range: an attacker building an alternative history must build its alternative state snapshots to mine on it, which it does anyway; no change. Finality pause: `C_day(d)` must be certified; if finality is paused for more than the lead the day's snapshot is not derivable and mining would stop, the coupling spec 4.3 argues against for the epoch seed. Rule, as there: take the selected-chain block at that blue score certified or not, deep enough that a reorg across it is a merge-depth event. Empty state at launch: every leaf is `Blake2b(R_d || t || empty)`, full entropy, the dataset as good as today's: graceful.
|
||||
|
||||
**Game theory.** No payment changes. A miner must run or rent a node (or trust a pool's dataset); the solo-mining floor rises by a full node's state (today's devnet: megabytes; a used chain: gigabytes). A pool user sees a 2 GiB daily download or nothing (the pool serves the dataset). A prover that mines already holds state. A holder gains a daily sample of state availability per block, for free. A rollup customer gains nothing directly.
|
||||
|
||||
**Migration.** Class v5 by P2 (object byte 5, 95 percent over a day, floor height, one-sweep rollout, Devnet 2 first). Every node needs the snapshot builder before the flip (a node without it cannot validate PoW after the flip: the floor height's job). Workers need nothing new: the kernel text is unchanged, the dataset arrives from the node's `prepare` line as today, built on the GPU from the cache plus a leaf array the node hands over (2 GiB per day over the local socket) or built on the node's CPU and uploaded. The one-click miner's "nothing to install but the driver" line holds; "nothing to download but the day key" does not.
|
||||
|
||||
### 3.4 The three schemes in one table
|
||||
|
||||
| Scheme | What it is | Chip edge per joule vs the 5090 (f = 1 chip, chip-model-v3 method) | Verifier ms per unit (model, then measured in section 5) | Vendor bit-exactness | Ships as class v5? |
|
||||
|---|---|---|---|---|---|
|
||||
| A, mining is proving | A1 committed codeword, A2 proving steps: dead on bytes and on sampleability; A0 trace-as-dataset survives and is C with the trace as state | A0 unchanged (5.1x GDDR7); A1, A2 not priced | A0: 2.06 + the leaf-read row; A1: 2.9 MB of openings; A2: 32 to 40 ms | A0 adds the zkVM executor to consensus | NEVER as mining = proving; A0 folds into C |
|
||||
| B, tensor-shaped shadow | class v3 plus `8 R` int8 8x8x16 tile steps per hash in the PTX fragment layout, two outputs per lane | memory + `8 R x 1,024 x e_mac x k_mma`; `k_mma` near 1 with a floor near 0.5 (approximate); rows from the measured `e_mac` in 5.3 | 2.06 + about 0.9 microseconds per step per unit scalar (R = 128: +0.9 ms; R = 512: +3.7 ms); VNNI 4x to 16x less | native on sm_75+ and RDNA 3+ (AMD layout unverified); emulated on Pascal, RDNA 2, Apple | before measurement: prototype further. After section 5: NEVER as class content (the block costs the honest card 0.056 to 0.70 pJ per multiply-add, so it forces no joules on a chip); kept as reserve R8 evidence |
|
||||
| C, stored state | the daily dataset derives from the execution state snapshot; kernel unchanged; a state sample per block | unchanged (5.1x GDDR7, 7.5x to 9.2x HBM3); the f = 0 chip disappears | 2.06 + 4,096 leaf reads (section 5.2) | nothing vendor-specific; the serialisation is the consensus risk | before measurement: prototype further. After section 5: SHIP as the class v5 candidate |
|
||||
|
||||
### 3.5 Migration through the class system (common to B and C)
|
||||
|
||||
The P2 rule as designed (`docs/plans/counter-asic-3-node.md` section 6): the header version's high byte carries the producer's object version; epoch `e` is the new class when the window of one day ending at its seed block has at least 9,500 bps of blue blocks at or above the byte, or when the floor height `N` is reached, or when epoch `e - 1` already was; the rule answers v5 only where it would answer v4. For B and C the object byte is 5 and the floor is set at the publish as DAA + 14,400 rounded up to the epoch boundary. The order: Devnet 2 crossing with `tools/fleet/devnet2-gate.sh` (zero rejected blocks across the flip, no reorg over depth 3, exec roots agreeing, a segment record paid, every node on the new version), then the live devnet in one binary sweep (the digest flips), then the flip by signal. A node that synced from a pruning proof takes the floor rule for epochs whose window reaches below its pruning point (the same class as the era witness, status item). For C the floor also bounds how long a non-upgraded node can keep validating PoW: none after the flip, so the sweep must be complete before the floor, which is the 10,800-DAA check already in the rule.
|
||||
|
||||
|
||||
## 4. Reviews: two personas, two short reviews per scheme
|
||||
|
||||
Written by this lane in the persona files' voices (`.claude/agents/cryptographer.md`, `.claude/agents/consensus-engineer.md`): every design claim with its attack, every claim about another chain with its file, and the smallest change the code allows. Each review names the break if there is one.
|
||||
|
||||
### 4.1 Scheme A, mining is proving
|
||||
|
||||
**Cryptographer.** The break is structural and has a name: a lottery needs a distribution of instances and proving has one instance per segment. A1 moves the work into the commit phase and pays for it in witness bytes (2.9 MB per block, or 90 KB with the unit cut to one lane, which also removes `shfl` and with it the only thing that makes the 32-lane unit a unit; `docs/spec/01-lottery-hash.md` 1.9). A2 either recomputes (useless by definition) or verifies a proof (32 to 40 ms measured, `proving-methods.md` 2.1; the gate is 10 ms). The useful fraction bound (proving work per segment over network hashes per segment) is the argument Ball, Rosen, Sabin and Vasudevan make in the negative direction: without a random self-reduction the embedded instance is a constant, and a constant is amortised to zero by the first miner who computes it. A0 is sound as far as it goes and is scheme C. Second attack on A0 that C does not have: the SP1 executor enters the consensus path for the lottery, so an executor bug that produces a different trace on one platform (an undefined-behaviour corner in a precompile patch, a `sha3` or `k256` version skew) splits the chain at the PoW, not at the proof; ledger P7 priced that class for proofs where the native veto bounds the damage to one payout; here nothing bounds it. Verdict: never for A1 and A2; A0 only as C with the trace, and then the state is the better choice of data because every node already agrees on it without a second executor.
|
||||
|
||||
**Consensus engineer.** The code says the same thing from the other side. Block validation in the fork validates the header's PoW before the body is fetched and before execution (`check_pow` on the header path, rusty-kaspa's `header_processor`; the fork's `igneum/exec` runs after the block is accepted into the DAG). A puzzle whose verification needs the segment's trace makes header validation wait on the executor of a segment 20 minutes old, which is fine for a synced node and fatal for IBD: a syncing node must execute every segment of history, in order, to validate the headers of history, so headers-first sync and the pruning proof (which validates headers without bodies) are gone; Kaspa's pruning-point sync (`consensus/src/pipeline/pruning_processor`, approximate location) assumes PoW is a function of the header and a small amount of context. A0 shares this break with C and C answers it in 4.3 by making the snapshot a function of a certified checkpoint's state root plus the state itself, which a pruned node fetches as a snapshot (the exec snapshot path the node already has, CLAUDE.md 6 Oct rule "the p2p snapshot path refuses a snapshot below the node's tip"). For A1 the 90 KB per header kills the header relay and the 600-block record window arithmetic alike. Verdict: never for A1 and A2; A0 is C with a worse data source.
|
||||
|
||||
### 4.2 Scheme B, the tensor-shaped shadow
|
||||
|
||||
**Cryptographer.** No break in the puzzle's soundness: the block is a straight-line integer map with no memory and no data-dependent control flow, its inputs are the per-nonce registers, and the two-output form makes the whole tile load-bearing. Two things to name. (i) The claim that `k_mma` cannot fall far under 1 rests on the honest card's tensor path being near the floor of int8 multiply-add energy; that is an engineering judgement, approximate, and the external chip review (status item 3, `funding.md`) is where it is tested, with the ALU shadow's `k` beside it. The design's advantage over the ALU shadow is bounded, not proven: it narrows the chip's best case from about 0.3 to about 0.5 (approximate) and does not remove the edge. (ii) The fragment layout is a specification of lane ownership; every emulating vendor must reproduce it exactly, and the AMD WMMA layout is unverified (status item 6). Until a 9070 XT run with the spec reference passes, B is bit-exact on one vendor's hardware and in every emulation, which is the state class v2 was in on 3 October (ledger M8) and not a state to cut from. No grinding, outsourcing or precomputation surface is added. A statistical point: the mm8 step's outputs are sums of 16 byte products, so each added value is at most 1,040,400 and its top 12 bits are zero; added into a register it changes the low 20 bits in a structured way. That is fine inside a chain of multiplies and rotates, and the stats run of `TESTS.md` section 3 should be run on the class before any vector is frozen. Verdict: prototype further; a class v5 candidate after the AMD gate and the stats run.
|
||||
|
||||
**Consensus engineer.** No consensus change beyond the class byte and the emitter: the block is kernel text (`igneum-pow/src/emit.rs` gains the step in the three dialects), the verifier (`verify.rs`) gains the step in the register-major loop, the acceptance rule is untouched by construction, and the node's seam is the v5 switch beside the v4 one (`counter-asic-3-node.md` section 1 lists every file the v4 switch touched; v5 is the same list with one more number). The break to name is operational: the fleet splits by hardware generation. Pascal, Volta, RDNA 2 and every Apple card emulate, and the one-click workers compile three paths where they compile one today; the OpenCL path on AMD needs a feature test at compile time (the WMMA builtin exists on gfx11 and gfx12 and not on gfx10, `int8-matrix-family.md` 1), which the pack text must carry as a preprocessor branch, and a wrong branch is a wrong hash, which the self-test catches before the worker serves (`packfile.h`, M28's fix). The 5 percent rule must be checked per emulating card with the block live, and the Apple row is the one most likely to fail it at high `R`. Verdict: prototype further; set `R` from the measured rows with the Apple emulation measured on the M5 Max before any cut; the AMD layout is a hard gate.
|
||||
|
||||
### 4.3 Scheme C, proof of stored state
|
||||
|
||||
**Cryptographer.** The construction is never weaker than today's: the leaf is one more input to the same chained derivation, keyed by the day key through the first mixer, and the f = 1 chip's row is unchanged, which the design says. The break to name is the one Permacoin and MTP both met: who controls the data controls the addresses, unless the data enters through a key the controller does not have. Here the controller of state content (anyone paying gas) does not control `K_d` (a VDF output fixed 20 minutes before the day, spec 4) and the leaf is `Blake2b(R_d || t || key || value)`, so the only lever is choosing content before `R_d` is known, which affects every leaf through `R_d` and none in a predictable way. I find no bias at a cost below a block. The second point: the "verifiable sample of state" is real but modest. A block commits to 128 leaves per lane through 128 dependent reads; a light client checking them needs openings against `R_d` from a full node (depth about 25 at 2^25 leaves, 128 x 25 x 32 B = 102 KB per block if fetched, arithmetic), which is a spot check of availability, not a proof of state correctness; the consensus proof of spec 10 and ledger P4 is still what a light client needs for correctness. The design says this. Third: the canonical serialisation is new consensus-critical code, and the day-boundary flip is the moment it bites (every node rebuilds at once). Verdict: prototype further; a class v5 candidate; the serialisation needs the same test discipline as the DAA switch (a Devnet 2 crossing over a day boundary with a non-trivial state).
|
||||
|
||||
**Consensus engineer.** The break I would have named, the IBD and pruning-proof problem of A0, C answers: the snapshot is a function of `R_d`, a state root at a certified checkpoint, and the node already carries an exec snapshot path (`exec_restart_*` fields, the p2p snapshot message, CLAUDE.md 6 Oct). A syncing node validates historical PoW only by holding every day's snapshot, which is 2 GiB per day of history, which is NOT acceptable for IBD. Fix, the smallest I can see: PoW of blocks below the pruning point is not re-validated (Kaspa's pruning proof validates the proof's headers' PoW; rusty-kaspa `consensus/src/processes/pruning_proof`, approximate), so historical snapshots are needed only for the headers inside the proof, which are a bounded set per level; and for them the proof carries the day's `R_d` and the node either holds that day's state (recent days) or trusts the certificate (the finality rule's lock already makes those headers irreversible). That is a spec item for `docs/spec/10-light-client.md` and the pruning section of spec 02, and it is the same shape as the era-seed witness (status item). Second: the verifier's RAM (+2 GiB, +4 GiB at year 4) and the daily build on a node without a GPU (a seed node, a Hetzner box: the leaf pass is CPU work, measured in 5.2) must stay inside the node's budget; the devnet hands on igneum-build-1 have 128 GB, a home node has 16. Third: a day boundary is now a consensus event that depends on a certified checkpoint 20 minutes before it; under a finality pause (6 October, 18:42Z) the day's snapshot falls back to the uncertified selected-chain block, as spec 4.3 argues for the epoch seed; the rule must be written once and tested on the fast-time harness across a pause. Verdict: prototype further; a class v5 candidate; three spec items (pruning-proof witness, the pause rule, the node RAM budget) before a cut.
|
||||
|
||||
### 4.4 The pick
|
||||
|
||||
B and C are the two to prototype: both are class objects on the shipped hash, both leave the dataset's memory bound untouched, and they compose (B is kernel text, C is the daily build). A is not prototyped: A1 and A2 fail on bytes and on sampleability before any kernel, and A0 is C with a worse data source. The order of merit at this point, before measurement: C first (no vendor risk, unchanged hash rate by construction, a real new property per block, the pool caveat stated), B second (a real lever against the f = 1 chip with a bounded `k`, a vendor split and an unverified AMD layout). Section 6 revisits the order on the measured rows: C holds, B retires into the reserve.
|
||||
|
||||
## 5. The prototypes and the measured rows
|
||||
|
||||
Both prototypes live under `proto-newpow/` with a README carrying the exact commands, the card, the driver and the RESULTS table; this section carries the rows and their consequences. Boxes: two RunPod RTX 4090 24 GB (driver 595.91, nvcc 12.8, `-arch=sm_89`), quiet, the card to itself; CPU rows on igneum-build-1 (EPYC 9454P, one pinned core, `nice -n 19`). Power by `nvidia-smi` at 1 Hz, the mean after the first 10 s of each timed run. Bit-exactness: the PTX path against the reference path on 2^24 lanes (fingerprint), and the GPU against the C CPU reference on 1,024 random lanes.
|
||||
|
||||
### 5.1 `mma-shadow` (scheme B), box 1
|
||||
|
||||
Prototype: `proto-newpow/mma-shadow/` (README with every command, `kernel_mm8.cu` with the PTX path and the `IGNEUM_MM8_REF` reference path, `bench.cu`, `verify_ref.c`, `gen_block.py`, `gen_ref_program.py`, `run.sh`, `out/` with every log and csv). Box 1: RTX 4090 24 GB (128 SMs), driver 570.172 (the box reports 570, not the 595 of box 2), nvcc 12.8.93, `-arch=sm_89`, 1 warp per block, 10 timed batches of 2^24 after a warm-up, power from `nvidia-smi` at 1 Hz over a 25-s sustained phase (mean after its first 10 s), idle 15.0 to 15.3 W. The two-output tile form of section 3.2 was built (the single-output form never was). Run 19:36 to 19:52 UTC.
|
||||
|
||||
| R (steps per iteration) | mm8 per hash (8 R) | MH/s (GPU time) | Ratio to R = 0 | Watts, mean | Block watts over R = 0 | SM MHz | Microjoules per hash | Marginal pJ per multiply-add | Fingerprint (2^24 at base 0) | PTX = reference path | CPU = GPU (1,024 lanes) | Verifier block delta, ms per unit (box core, scalar C) | Registers per thread |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 0 (control) | 0 | 63.08 | 1.000 | 201.2 | 0 | 2,670 | 3.19 | | 7c28cfb06c5c65a9 (the pack's; the 3 pack vectors PASS standalone and in batch) | yes | 1,024 of 1,024 | 0 (the R = 0 run read -0.10, noise) | 29 |
|
||||
| 8 | 64 | 63.08 | 1.000 | 204.1 | 2.9 | 2,670 | 3.24 | 0.70 | 06fc2593bfb94b4f | yes | 1,024 of 1,024 | +0.05 | 30 |
|
||||
| 32 | 256 | 63.08 | 1.000 | 207.7 | 6.5 | 2,670 | 3.29 | 0.39 | 26e83a65f519c865 | yes | 1,024 of 1,024 | +0.17 | 29 |
|
||||
| 128 | 1,024 | 63.08 | 1.000 | 212.7 | 11.5 | 2,670 | 3.37 | 0.17 | 42223c2113188335 | yes | 1,024 of 1,024 | +1.14 | 32 |
|
||||
| 512 | 4,096 | 63.08 | 1.000 | 215.9 | 14.7 | 2,670 | 3.42 | 0.056 | 02b7002d747f3711 | yes | 1,024 of 1,024 | +4.39 | 36 |
|
||||
|
||||
Sustained rates over the power window 63.07 to 63.03 MH/s at every R; wall and event rates agree to 0.01 MH/s; no spills, no stack, temperature 58 to 64 C, no throttling. The verifier totals in the box's logs (about 10 ms per unit) are a naive scalar interpreter with a lazy `mh_word` per load on a shared Zen 4 core at load 13 and are not comparable to the project's Rust verifier (2.06 ms per unit on an M5 Max core, interleaved); the block delta is the number: about 1.1 microseconds per `mm8` step per unit, 32 lanes x 32 byte products in plain loops. (At R = 512 the card issues about 258 G tiles per second, 2.6 x 10^14 multiply-adds per second, in the region of 40 percent of the 4090's dense int8 tensor peak, approximate from the published TOPS figure, so R = 512 is near the top of the free band, not its middle.)
|
||||
|
||||
What the rows say:
|
||||
|
||||
1. The tensor block is free in hash rate to R = 512 on the 4090: 4,096 tile instructions per hash leave the rate at 63.08 MH/s to the third decimal. The kernel is latency-bound on its 128 dependent loads and the tensor work fills stalls that were already there, as the ALU shadow did on the 5090 to 150,800 ops (`latency-shadow-2026-10-06.md` 5).
|
||||
2. The block costs the honest card almost nothing in energy: 2.9 to 14.7 W, 0.70 pJ per multiply-add at R = 8 falling to 0.056 pJ at R = 512 (the tensor path's fixed cost amortised) [corrected 8 October 2026: these per-MAC figures count 1,024 multiply-adds per tile per lane where a tile is 1,024 per warp and 32 per lane, so they are low by 32x: 22 pJ at R = 8 falling to 1.8 pJ at R = 512; the watts and microjoules per hash stand; counter-asic-4-research.md 15.1a at 71fd465b], 0.05 to 0.23 microjoules per hash on a 3.19 microjoule hash (+1.6 to +7.2 percent). The ALU shadow at N = 100,000 costs the 5090 0.6 microjoules per hash (11 pJ per counted op, `latency-shadow-2026-10-06.md` 5, item 4); the tensor block at its free-band ceiling costs a third of that.
|
||||
3. That is the finding, and it is negative for the scheme's purpose (section 5.3): a shadow lever moves the chip's edge only by the joules it makes the HONEST card spend on work the chip cannot do more cheaply. The tensor path is so efficient on the GPU that the block adds 0.23 microjoules at most, so at `k = 1` the chip's edge falls from 6.9x to 4.9x on GDDR7 against this 4090, where the ALU shadow took the 5090 from 5.6x to 2.1x, and the tensor block costs the verifier 26x more per unit of chip-forcing energy (4.39 ms scalar per 0.23 microjoules against 0.17 ms per 0.6 microjoules). The property the design hoped for (`k_mma` near 1 because the GPU's tensor engine is near the floor) is real and is exactly why the lever is weak: there are no joules in it to force.
|
||||
4. The correctness chain holds at every rung: the PTX fragment read and the plain-integer reference agree on all 2^24 lanes at every R, and the CPU interpreter matches the GPU on 1,024 lanes at every R; the probe's fragment layout (`family-probe.cu` mm8 `warp_ref`) was used as written and needed no correction. Registers 29 to 36, occupancy unchanged. This is the first class-shaped evidence that an `mm8` family is cheap and exact for the honest NVIDIA card, which is what the reserve entry R8 needs; it is not evidence for a class v5.
|
||||
|
||||
Consequences per tier (B at R = 128, the largest R whose scalar verifier delta stays near 1 ms):
|
||||
|
||||
| Tier | Meaning | What to do |
|
||||
|---|---|---|
|
||||
| NVIDIA Turing and later (RTX 20 to 50 series), any memory size | rate unchanged, +11.5 W on a 4090 (+5.7 percent), per-pound unchanged, per-watt 0.95x | nothing; the block would not be adopted on this evidence |
|
||||
| NVIDIA Pascal (GTX 10 series) | emulation at about 16 steps per `mm8` (approximate): 1,024 per hash is about 16,000 counted steps, inside the latency shadow of a 1080-class card by the counted-ops rule, approximate; unmeasured | the emulation path exists in the kernel text (`IGNEUM_MM8_REF`) and is bit-exact; a GTX 1080 row is owed if B ever proceeds |
|
||||
| AMD RDNA 3 and 4 | the WMMA layout is unverified (status item 6); emulation otherwise at about 12 to 16 steps per `mm8` | the gate of section 7 rank 2 stays open; not worth running unless B proceeds |
|
||||
| Apple | emulation at about 10 steps per `mm8` (approximate), 10,000 counted steps at R = 128 inside the M5 Max's 130,000 ceiling | owed; not worth running unless B proceeds |
|
||||
| Rig, pool user | nothing changes in shares; a rig pays the block's watts per card | nothing |
|
||||
| Node operator (verifier) | +1.14 ms per unit scalar at R = 128 (+55 percent of 2.06 ms), +4.39 ms at R = 512; a SIMD byte-dot path would cut it 4x to 16x (approximate) | the verifier cost per joule forced is the reason the scheme loses to the ALU shadow |
|
||||
| A chip | must carry a tensor array per 32 lanes in flight; at the measured block energy it pays at most 0.23 microjoules per hash more than today at `k = 1`, 0.07 at `k = 0.3` | the chip's edge barely moves (5.3) |
|
||||
|
||||
### 5.2 `state-dataset` (scheme C), box 2 and igneum-build-1
|
||||
|
||||
Prototype: `proto-newpow/state-dataset/` (README with every command and log; `kernel_sd.cu` = the pack's kernel plus `igneum_leaves` and `igneum_build_sd`, the hash kernel byte for byte the pack's; `verify_sd.c` the 32-lane C interpreter and the item check; `cpu_rows.c` the igneum-build-1 rows). Box 2: RTX 4090 24 GB, driver 595.91, nvcc 12.8, host EPYC 7352; igneum-build-1 EPYC 9454P at load 19 to 32 (shared), one core pinned, nice 19. Run 19:34 to 19:50 UTC. The leaf stand-in: one ChaCha12 block of (sigma, S, t, 0, tag) with `S = K XOR 0x5a5a5a5a` (the README defines it).
|
||||
|
||||
| Row | Control (mx8-genesis) | sd1 | Reading |
|
||||
|---|---|---|---|
|
||||
| Hash rate, 10 batches of 2^24, two passes | 63.083, 63.083 MH/s | 63.088, 63.087 MH/s | equal within 0.01 percent: the kernel is the same binary over a dataset of the same shape |
|
||||
| Watts, mean after 10 s of a 20-s window; SM MHz | 207.6, 205.2 W; 2,745 | 207.9, 206.5 W; 2,745 | equal within the 2 W run-to-run noise; 3.27 microjoules per hash on this 4090 (the 5090 is 2.34 to 2.65) |
|
||||
| MH/s per W | 0.304, 0.307 | 0.303, 0.305 | unchanged |
|
||||
| Cache fill, GPU | 1.81, 1.85 ms | 1.85, 1.85 ms | unchanged |
|
||||
| Leaf array (2^24 ChaCha12 blocks, 1 GiB) on the GPU | none | 5.49 ms | the synthetic stand-in; a real snapshot comes from the node |
|
||||
| Dataset build, second pass | 30.55 ms | 31.98 ms | +1.43 ms (+4.7 percent): one coalesced 64 B read per item |
|
||||
| Chunked build (leaves streamed from pinned host memory in 64 MiB or 256 MiB chunks, one stream) | | 75.8 ms, 75.5 ms | PCIe-bound: 62 ms of copy (17.2 GB/s on this box) plus the build, partly serialised; two streams would hide most of the build (not done) |
|
||||
| Device memory during the build (context 395 MiB included) | 1,675 MiB | 2,699 MiB resident leaves; 1,741 MiB chunked at 64 MiB; 1,933 at 256 MiB | while hashing 1,803 MiB in every mode (leaves freed) |
|
||||
| 2^24 fingerprint at base 0 | 7c28cfb06c5c65a9 (the pack's, both passes) | d5b0c16390cad0e8 (four runs) | |
|
||||
| Pack vectors (3 warps, standalone and in batch); dataset head, word [MASK], 64 Mac samples | PASS | n/a (new values; sd1 base 0 lane 0 = b600edbed969becc) | the harness is the pack's |
|
||||
| Build bit-exact: 1,024 random items, GPU words against the plain C host derivation | 1,024 of 1,024 | 1,024 of 1,024 (also after both chunked rebuilds); 64 device leaves = host leaves | |
|
||||
| Hash bit-exact: 4 dumped warps (128 lanes) against the C interpreter with lazy `mh_word` / `mh_word_sd` | 128 of 128 (and 32 of 32 against the Mac vector at base 0) | 128 of 128 | |
|
||||
| Host cache FNV-1a 64 | 48c4f5bf24166b2e = the Mac's | same | |
|
||||
|
||||
The CPU rows (igneum-build-1, ms per unit of 4,096 items, 100 units; two readings at load 19 and 31):
|
||||
|
||||
| Row | Reading 1 | Reading 2 | Per item |
|
||||
|---|---|---|---|
|
||||
| (i) 4,096 random 64 B reads from a 2 GiB resident leaf array | 0.108 ms (0.092 to 0.172) | 0.163 ms | 26 to 40 ns |
|
||||
| (i) the same from an 8 GiB array (the year-12 size) | 0.139 ms | 0.209 ms | 34 to 51 ns |
|
||||
| (ii) 4,096 leaves derived on the fly (one ChaCha12 block each) | 0.284 ms | 0.285 ms | 69 ns |
|
||||
| (iii) 4,096 x `mh_item` on the host cache, naive, no interleaving | 9.01 ms | 11.24 ms | 2.2 to 2.7 microseconds |
|
||||
| 4,096 x (leaf read + `mh_item_sd`), naive | 9.31 ms | 11.64 ms | |
|
||||
| Daily leaf array on CPU (2^24 blocks): one core / 32 threads | 1.67 s / 0.073 s | 1.68 s / 0.083 s | 100 ns per leaf |
|
||||
| Host cache fill (256 MiB), one core | 0.447 s | 0.452 s | |
|
||||
| Light-client sample: distinct items touched by lane 0's 128 loads | 128 of 2^24 | | 128 openings x 25 levels x 32 B + 128 x 64 B = 108 KiB per lane; 3.4 MiB per 32-lane unit before dedup (arithmetic) |
|
||||
|
||||
Reading, against the gate: the project's Rust verifier does the 4,096 derivations in 2.06 ms on an M5 Max core by interleaving the eight dependent cache misses across items (the naive C figure here, 9 to 11 ms, is an upper bound and is not the production verifier); sd1 adds 0.11 to 0.21 ms per unit when the verifier holds the state in RAM (+5 to +10 percent of 2.06 ms; about +0.3 to +0.5 ms on a 2019-class core by the 2.5x rule), so the class v3 verifier plus sd1 sits at about 2.2 ms on the M5 Max core and about 5.7 ms on the 2019-class core, inside the 10 ms gate with the x8 margin intact. The snapshot's own cost is the state read on the node, not the leaf hashing (1.7 s on one core for 2^24 leaves).
|
||||
|
||||
Consequences per tier (sd1):
|
||||
|
||||
| Tier | Meaning | What to do |
|
||||
|---|---|---|
|
||||
| Home miner, one 8 GB card | hash rate, watts and MH/s per W unchanged (measured equal); the build must stream the leaves (resident leaves at the designed 2 GiB dataset would peak at about 4.6 GiB, approximate; chunked 64 MiB about 2.7 GiB, approximate, measured 1,741 MiB at 1 GiB), so the 8 GB mine-and-prove profiles of `prover-tiers-real-cards.md` keep their memory | ship chunked only; the kernel already takes an item range, so chunking needed no kernel change |
|
||||
| 12, 16, 24, 32 GB cards | unchanged in rate and watts; the daily build +1.4 ms resident or about 76 ms streamed (about 150 ms at 2 GiB, approximate) | nothing |
|
||||
| AMD and Apple | the hash kernel is untouched, so the measured class v3 rates stand (18.9 MH/s on the 9070 XT, 27.1 on the M5 Max); the build kernels gain one 64 B read per item in OpenCL and Metal | the two build kernels in the emitter's other dialects |
|
||||
| Rig | per card as above; one 2 GiB snapshot per day per rig, shared by its cards | the node beside the rig serves it over loopback |
|
||||
| Pool user | a 2 GiB download per day from the pool, or the pool ships the built dataset; today a pool user needs nothing but the day key | the pool protocol (spec 09) gains a daily snapshot or dataset fetch |
|
||||
| Solo miner | must run a node with execution (it already does for templates) and hold the state | nothing new beyond RAM |
|
||||
| Node operator | +2 GiB RAM for the snapshot (+4 GiB at year 4), a daily serialisation pass, the verifier +0.11 to 0.21 ms per unit | the node RAM budget stays inside a 16 GB home node at launch sizes |
|
||||
| Light client, holder | a verifiable sample of 128 state leaves per lane per block exists; checking it costs 108 KiB per lane fetched on demand, so it is an availability spot check, not a light verification of PoW | rank 6 of section 7 |
|
||||
| Prover, rollup customer | nothing | |
|
||||
|
||||
### 5.3 The chip rows on the measured numbers
|
||||
|
||||
Method: `sim/horizon/new-pow/chip_rows.py` (the chip-model-v3 section 5.4 f = 1 chip plus the block's measured energy times `k`; the card figures are 5.1's; every chip figure is arithmetic on `chip-model-v3.md`'s cited and approximate memory figures). The denominator here is the 4090 measured tonight (3.19 microjoules per hash at the control, a card whose memory system is half the 5090's), not the 5090, so the R = 0 column reads 6.9x where `chip-model-v3.md` reads 5.1x for the 5090; the movement with R is the row that matters.
|
||||
|
||||
| Scheme and setting | Honest card microjoules per hash | Chip, GDDR7, k = 1 / 0.5 / 0.3 (microjoules) | Edge over the card, GDDR7 | Chip, one HBM3 stack, k = 1 / 0.5 / 0.3 | Edge, HBM3 |
|
||||
|---|---|---|---|---|---|
|
||||
| Class v3 today (R = 0, the 4090 control) | 3.19 | 0.466 | 6.9x | 0.321 | 9.9x |
|
||||
| B at R = 128 (1,024 tiles per hash, block 0.18 microjoules) | 3.37 | 0.646 / 0.556 / 0.520 | 5.2x / 6.1x / 6.5x | 0.501 / 0.411 / 0.375 | 6.7x / 8.2x / 9.0x |
|
||||
| B at R = 512 (4,096 tiles per hash, block 0.23 microjoules, the free band's top) | 3.42 | 0.696 / 0.581 / 0.535 | 4.9x / 5.9x / 6.4x | 0.551 / 0.436 / 0.390 | 6.2x / 7.8x / 8.8x |
|
||||
| For comparison, the ALU shadow at N = 100,000 on the 5090 (`latency-shadow-2026-10-06.md` 6, measured 3.27 microjoules at the 431 W cap) | 3.27 | 1.59 / 1.03 / 0.80 | 2.1x / 3.2x / 4.1x | 1.44 / 0.88 / 0.66 | 2.3x / 3.7x / 5.0x |
|
||||
| C (sd1) at any size | unchanged (measured equal) | 0.466 | unchanged | 0.321 | unchanged; the f = 0 recompute chip must add 2 GiB of DRAM and becomes this chip |
|
||||
|
||||
Reading: B moves the f = 1 chip's edge by 1.4x to 2x at `k = 1` and by 1.1x at `k = 0.3`, at a verifier cost of 1.1 to 4.4 ms per unit (scalar); the ALU shadow moves it by 2.7x at `k = 1` and 1.4x at `k = 0.3` at 0.17 ms per unit. On every axis the measured tensor block is the weaker lever, for the reason stated in 5.1 item 3. C moves nothing and claims nothing against the chip; its merit is elsewhere.
|
||||
|
||||
## 6. Verdicts
|
||||
|
||||
| Scheme | Verdict | Why, in one line |
|
||||
|---|---|---|
|
||||
| A, mining is proving | NEVER (A1, A2); A0 folds into C | one proof per segment is not a distribution of puzzles; the bytes (2.9 MB of openings per block) or the verify (32 to 40 ms) kill every form that is not "hold the trace", and holding the trace is C with a worse data source |
|
||||
| B, tensor-shaped shadow | NEVER as class v5 content for the anti-chip purpose; the measurement (0.056 to 0.70 pJ per multiply-add as first counted, 1.8 to 22 pJ with the per-lane count corrected on 8 October 2026, 15 W for 4,096 tiles per hash) is the reason, and the correction strengthens it: a chip's MAC at 0.04 to 0.4 pJ (claimed) against the GPU's 1.8 pJ gives a chip k of 0.03 to 0.3 on tile work, below the ALU shadow's. KEEP the `mm8` family as reserve R8 with the two-output correction, for datapath diversity, not for joules |
|
||||
| C, stored state | SHIP AS CLASS v5 CANDIDATE (through the spec items of 4.3 and the Devnet 2 gate): hash rate and watts unchanged by construction and measured equal, build +1.4 ms, verifier +0.11 to 0.21 ms per unit, bit-exact on 1,024 items and 128 lanes; a new property per block (a random sample of state) and a new requirement per mining operation (hold the state); the open decision is what a header verifier is asked to hold |
|
||||
|
||||
**A, in full.** The mandate asked for something never done, and "mining is proving" is the thing everybody has wanted and nobody has shipped; this lane's contribution is the reason, stated as a bound rather than a feeling: the useful fraction of a proving-as-lottery scheme is (proving work per segment) / (network hashes per segment), 8 percent at 1 GH/s and 0.08 percent at 100 GH/s on this chain's measured figures, because gas sets one and the security budget sets the other, and a puzzle whose verifier either recomputes the piece or verifies a 32 to 40 ms proof cannot sit under a 10 ms gate. The 80/20 split stays. Ledger F13's answer stands and gains this bound. What survives (A0) is scheme C.
|
||||
|
||||
**B, in full.** The scheme asked whether a GPU-shaped puzzle could make a chip into a GPU. The answer the 4090 gives is that the tensor-shaped work is free for the honest card (rate unchanged to R = 512, 15 W at most for 4,096 tiles per hash) and therefore nearly free for the chip too: a lever against the f = 1 memory-controller chip works only through joules the honest card is forced to spend on work the chip cannot do more cheaply, and the ALU shadow of class v4 spends 11 pJ per op where the tensor path spends 0.056 to 0.70 pJ per multiply-add. The chip's edge moves from 6.9x to 4.9x at k = 1 (4090 denominator) where the ALU shadow moved the 5090's by 2.7x (5.6x to 2.1x), and the verifier pays 26x more per unit of forcing energy (4.39 ms per 0.23 microjoules against 0.17 ms per 0.6). There is also the vendor split (native on Turing and later and on RDNA 3 and later, emulated on Pascal, RDNA 2 and every Apple card) and the unverified AMD layout. So: never as class v5 content for this purpose. What the measurement does establish, and what should be kept: an mm8 family is exact on NVIDIA at every R tried (2^24 lanes PTX = reference, 1,024 lanes CPU = GPU at five rungs), cheap for the honest card, and bounded in verifier cost by a known law (1.1 microseconds per step per unit, scalar); that is the evidence the reserve entry R8 lacked, and the two-output form (both tile outputs consumed) is a correction R8 needs so a chip cannot halve the tile. The standing rule this leaves for the programme: a shadow lever only works through joules the honest card is forced to spend, so shadow work goes where the GPU is LEAST efficient per op among the operations a chip cannot do much better (RandomX's argument), never where it is most efficient.
|
||||
|
||||
**C, in full.** The scheme does what it says at a cost that is measured and small: the hash kernel is byte for byte the shipped one, the 4090's rate and watts are equal within noise (63.083 against 63.088 MH/s, 205 to 208 W), the daily build grows by 1.4 ms resident or about 76 ms streamed over PCIe (about 150 ms at the designed 2 GiB, approximate), device memory during the build stays at dataset plus cache plus one 64 MiB chunk, the verifier grows by 0.11 to 0.21 ms per unit with the state in RAM (about 2.2 ms on the M5 Max core, about 5.7 ms on a 2019-class core by the 2.5x rule, inside the 10 ms gate), and every GPU word and lane agrees with the plain C derivation (1,024 of 1,024 items, 128 of 128 lanes). It is never weaker than today's dataset against any chip and it removes the f = 0 recompute chip as a category. What it adds has not been done on a shipped chain in this form: the dataset IS the keyed execution state, so every mining operation holds the chain, and every block names 128 random state leaves per lane that any full node can open against the day's state root. Its limits are stated: a pool can ship the dataset (a 2 GiB daily delivery per pool miner, where today a pool miner needs only the day key), a header verifier must hold the state or be shown 3.4 MiB of openings per unit (so light clients still rely on certificates, as spec 10 says), and the canonical serialisation is new consensus-critical code that must cross a day boundary and a finality pause on the fast-time harness before Devnet 2. Verdict: ship as the class v5 candidate, with the three spec items of 4.3 (the serialisation, the pruning-proof witness, the pause rule), the Devnet 2 gate, and the open decision on what a header verifier is asked to hold in front of it.
|
||||
|
||||
**The ranking after measurement.** C, then nothing else from this lane as class content; B retires into the reserve entry it improves; A is recorded as a bound. The brief's hope of "mining is proving" is answered with the reason it cannot be, which is worth more to the project than a design that pretended otherwise.
|
||||
|
||||
## 7. Ranked next steps
|
||||
|
||||
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 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Scheme C as class v5 content: the daily dataset derives from the state snapshot (`sd1`), spec text for 1.8.5 (the leaf line), 1.12 (the snapshot's checkpoint and lead), 10 (the pruning-proof witness), 4.3's pause rule applied to the day boundary | section 5.2: the hash kernel and rate are unchanged by construction, the build and verifier costs are the measured rows; every miner operation must hold state | the verifier row: 2.06 ms + the leaf-read row per unit; node RAM + the dataset size | spec 3 h; emitter and `memhard.rs` leaf line 2 h; node snapshot builder (canonical serialisation, one pass per day, the `prepare` hand-over) 6 h; fast-time harness across a day boundary and a finality pause 3 h; Devnet 2 crossing 2 h | 8 to 32 GB cards: no hash-rate change, +2 GiB device memory during the build only (streamable); rig: the same per card; pool user: a 2 GiB daily download or nothing; solo miner: a full node's state; node operator: +2 GiB RAM (+4 at year 4) and a daily leaf pass; holder: a daily random sample of state per block; prover, rollup customer: nothing | the fast-time 3-node network crosses a day boundary with a non-trivial state and no fork; verifier under 10 ms on a 2019-class core with the snapshot in RAM; Devnet 2 PASS over a day boundary |
|
||||
| 2 | Scheme B's AMD gate: the WMMA iu8 fragment layout on the RX 9070 XT against the spec reference (the two-output tile), a PC 1 job with the card alone | status item 6 (layout unverified); section 5.1's bit-exact rows on NVIDIA | the spec layout as the definition; a lane permutation per vendor | 3 h (the probe exists: `family-probe.cu` mm8 row; the OpenCL twin needs the builtin path and the permutation) | AMD 16 GB tier: decides whether RDNA 3 and 4 run B natively or emulate at about 12 to 16 steps per mm8 | 1,024 random units bit-exact on the 9070 XT, both tile outputs |
|
||||
| 3 | Scheme B's Apple row: the emulated mm8 block in Metal on the M5 Max at R = 32, 128, 512, hash rate and watts under `with-lock.sh measure` | section 3.2's vendor table: Apple is the honest card's worst case; the M5 Max binds at about 130,000 counted ops | 4 shuffles plus 4 dot4 emulations per step, about 10 steps per mm8 (approximate) | 3 h | Apple tier: the 5 percent rule decides the largest R the class can carry | rate within 5 percent of mx8 at the chosen R; bit-exact against the C reference |
|
||||
| 4 | Scheme B as class v5 content beside C (`mx8+sd1+mm8xR`) once ranks 2 and 3 pass: the emitter's three dialects (inline PTX with the `IGNEUM_MM8_REF` fallback for sm_61 to sm_72, the AMD builtin behind a feature test, the Metal emulation), the verifier step in `verify.rs`, the stats and fuzz runs of `TESTS.md` on the class, the v5 switch beside v4 | section 5.1 and 5.3: the block's measured watts and the chip rows at k | the chip rows of 5.3 | emitter 4 h; verifier 1 h; suites 2 h; node seam 2 h; Devnet 2 2 h | NVIDIA Turing and later, AMD RDNA 3 and later: native, the measured watts; Pascal, RDNA 2, Apple: emulation at the measured 5 percent check; pool user: nothing; chip: a tensor array per 32 lanes in flight | the six gates of `counter-asic-2-rollout.md` 7 plus the AMD and Apple rows; verifier under 10 ms on a 2019-class core at the chosen R |
|
||||
| 5 | The reserve text correction: R8 `mm8` consumes both tile outputs (two destinations) so a chip cannot halve the work | section 3.2 (the half-tile shortcut) | arithmetic: 32 of 64 products consumed in the single-output form | 1 h (spec 1.13.2 text and the edge vectors) | none until era 4 or a signal | the R8 edge vectors re-cut for two outputs, bit-exact on the three vendors |
|
||||
| 6 | The light-client state sample: a node RPC that returns, for a block, the 128 leaves of lane n with openings against R_d, and a client check | section 3.3 (the sample is real but modest: availability, not correctness) | 128 x 25 x 32 B = 102 KB per block fetched on demand (arithmetic) | 4 h | holder and light client: a spot check of state availability per block; node: one RPC | 128 openings verify against R_d on the fast-time network |
|
||||
| 7 | The 2019-class core measurement (O-1.14) for the verifier under C and under B at the chosen R | every verifier row here is M5 Max or Zen 4 plus the 2.5x rule | the 2.5x rule stands in | 1 h once a core is found (a 2019 laptop or a rented older CPU box) | every tier: the gate that fixes R and the snapshot read budget | under 10 ms per unit, worst cold |
|
||||
| 8 | Do not build scheme A (mining is proving) in any form; record the sampleability bound (proving work per segment over network hashes per segment: 8 percent at 1 GH/s, 0.08 percent at 100 GH/s) in the ledger beside F13 | section 3.1 and 4.1 | the bound's arithmetic | 0.5 h (a ledger row) | none | none |
|
||||
|
||||
One paragraph each.
|
||||
|
||||
**1. Scheme C first.** It is the cheapest change with a new property: the kernel text, the hash rate and the chip rows are untouched, so every measured number of Counter ASIC 2.0 and 3.0 stands; what changes is the daily build and the node. The cost is a canonical state serialisation in consensus, which is why the fast-time harness must cross a day boundary with state and a finality pause before the Devnet 2 crossing. The pool caveat is stated: the design forces the operation, not the card, to hold state.
|
||||
|
||||
**2 and 3. B's two vendor gates.** B is bit-exact tonight on NVIDIA (section 5.1) and in every emulation; it is not a class candidate until an AMD card reproduces the spec layout and the Apple emulation's hash-rate cost is measured. Both are short jobs with the card alone.
|
||||
|
||||
**4. B beside C.** The order matters: C lands first because it needs no vendor work; B joins the same class byte or the next one once ranks 2 and 3 pass, with R set from 5.1 and the 2019-core row.
|
||||
|
||||
**5 to 8.** Small, named, and each closes a thread this lane opened.
|
||||
|
||||
## 8. Open questions and what could not be run
|
||||
|
||||
| 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 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 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) |
|
||||
|
||||
## 9. Summary for the coordinator
|
||||
|
||||
This lane wrote three candidate proofs of work for Igneum, reviewed each in two personas, prototyped the two that survived on real cards, and measured. (1) Scheme C, the dataset derived from the chain's execution state, is the one to carry forward as the class v5 candidate: measured on an RTX 4090 the hash rate and watts are unchanged to the third decimal (63.08 MH/s, 207 W, the kernel is byte for byte the shipped one), the daily build grows by 1.4 ms resident or about 76 ms streamed, the verifier by 0.11 to 0.21 ms per unit with the state in RAM, every GPU item and lane agrees with the plain C derivation (1,024 of 1,024, 128 of 128); it forces every mining operation to hold state, names 128 random state leaves per lane per block, and removes the f = 0 recompute chip as a category; its costs are a 2 GiB daily delivery for pool miners and three spec items (canonical serialisation, a pruning-proof witness, the pause rule). (2) Scheme B, a tensor-shaped shadow of int8 8x8x16 tiles, is bit-exact on NVIDIA at every rung (2^24 lanes PTX = reference, 1,024 lanes CPU = GPU, R = 8 to 512) and free in hash rate to 4,096 tiles per hash, and that is exactly why it fails as an anti-chip lever: it costs the honest card 0.056 to 0.70 pJ per multiply-add and at most 0.23 microjoules per hash, so the f = 1 chip's edge moves from 6.9x to 4.9x at k = 1 against the 4090 where the class v4 ALU shadow moved the 5090's from 5.6x to 2.1x, at 26x the verifier cost per joule forced; never as class content, kept as the evidence and the two-output correction for reserve family R8. (3) Scheme A, mining is proving, is never: one proof per segment is not a distribution of puzzles; the useful fraction is bounded by proving work per segment over network hashes per segment (8 percent at 1 GH/s, 0.08 percent at 100 GH/s on this chain's measured figures); every form that is not "hold the trace" dies on 2.9 MB of openings per block or a 32 to 40 ms proof verify, and "hold the trace" is scheme C with a worse data source.
|
||||
|
||||
1. Scheme C costs nothing in hash rate or watts (63.083 against 63.088 MH/s, 205 to 208 W on the 4090, measured) and 0.11 to 0.21 ms per unit of verifier time (measured on igneum-build-1), so class v5 can carry it; the model is `item_sd(t) = item(t) with s ^= leaf(t)` over the day's certified state root.
|
||||
2. The tensor shadow moves the f = 1 chip's per-joule edge by at most 1.4x (6.9x to 4.9x at k = 1, R = 512) for 4.39 ms of scalar verifier per unit, against 2.7x for 0.17 ms from the ALU shadow; the model is `chip_rows.py` on the measured block energy of 0.23 microjoules per hash.
|
||||
3. Mining cannot be proving on a lottery with a 10 ms verifier: the useful fraction is bounded by 5 GPU-seconds of proving per 8-second segment over the network's hashes in that segment, 8 percent at 1 GH/s and falling with the hash rate; the model is that ratio on `prover-tiers-real-cards.md` and bench-log 2582.
|
||||
491
docs/analysis/horizon/polish.md
Normal file
491
docs/analysis/horizon/polish.md
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
# Horizon lane 6: polish. Every shipped system against the best in its class
|
||||
|
||||
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 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
|
||||
|
||||
Read, in the worktree: `app/igneum-app` (src, ui, tests), `app/mac/IgneumMiner.swift`, `app/windows/host.cpp`, `docs/plans/miner-ui-2.md`, `miner-ui-3.md`, `miner-ui-3-audit.md`, `ember-tune.md`, `release-0.3.14.md`, `docs/design/app-screens/`, `docs/design/miner-tuning.md`, `docs/fud-ledger.md` (X17, X19, X21 to X28, M25 to M29, G7, G10, G13), `site/` (every page, `api/`, `lib/`, `verify/`, `build.mjs` read not run, `scrub.mjs`, `forbidden-strings.txt`, `downloads.json`, `journey.json`, `vercel.json`, `sitemap.xml`), `docs/design/site-polish-2026-10-04.md`, `docs/plans/site-ui-3-shots/`, `docs/plans/site-miner-2026-10-06.png`, `docs/review/round-4-reddit-2026-10-06.md`, `docs/plans/explorer.md` and `explorer/`, `relay/` (ui, api, lib, clients, playbooks, tests), `tools/relay.mjs`, `tools/console.mjs`, `tools/jobs.mjs`, `docs/design/relay/`, `docs/design/console/`, `pool/` (src, web, tests, README), `docs/plans/pool.md`, `docs/spec/09-pool-protocol.md`, `tools/community/discord-hooks.mjs` and its tests and fixtures, `docs/community/discord-hooks.md`, `tools/fleet/`, `tools/build-job.mjs`, `tools/build-remote.sh`, `tools/ci/`, `docs/plans/build-job.md`, `build-server.md`, `hands-on-build-1.md`, `node-changes.md`, `packaging/README-ship.md`, `packaging/ota/README.md`, `packaging/mac/README.md`, `packaging/windows/README.md`, `tools/observer/` (observer.mjs, README), `docs/spec/03` section 3.9, and this lane's siblings `finality-and-weight.md` (3.1, 6) and `frontier.md` (3.7).
|
||||
|
||||
Read, read-only, in other agents' worktrees: `/Users/joshm/Projects/igneum-wt-ship0315/docs/plans/release-0.3.15.md` (the newest release notes; `release-0.3.15.md` does not exist on `horizon` or `master`), `/Users/joshm/Projects/igneum-wt-wallet*` (wallet 0.1.4 source, UI 3 branch), `/Users/joshm/Projects/igneum-wt-gpu-fleet/tools/fleet/`, `/Users/joshm/Projects/igneum/vendor/igneum-node-0310/consensus/src/processes/finality.rs` (the node's `finality_reason`).
|
||||
|
||||
Run (nothing live, nothing built): `node --test app/igneum-app/ui/*.test.mjs` (35 pass), `node --test relay/test/*.test.mjs` per file (24 pass), `node --test tools/community/discord-hooks.test.mjs` (31 pass, every live poster stubbed), one `curl -s https://igneum.network/` (the allowed single fetch; saved to the scratchpad), and one read-only SQL pass over the observer's Neon tables (`live_checkpoints`, `live_events`, `live_state`) through the same HTTPS SQL endpoint `site/api/_neon.mjs` uses, to see what every surface had to render during tonight's pause. No browser automation.
|
||||
|
||||
Comparator claims are from memory unless a repository or page is named, and are labelled approximate.
|
||||
|
||||
## 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 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 <first look>" 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 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 founder's certificate purchases for Q3.
|
||||
|
||||
## 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:
|
||||
|
||||
| Index | First seen | State | Signed, percent of total | Signed, percent of active | Votes |
|
||||
|---|---|---|---|---|---|
|
||||
| 6820 | 18:28:30 | locked | 88.8 | 100.0 | 80 |
|
||||
| 6840 | 18:42:10 (ingested after the hub came back) | locked | 79.5 | 98.6 | 83 |
|
||||
| 6850 | 18:43:41 | proposed | 55.2 | 76.8 | 75 |
|
||||
| 6880 | 19:00:22 | proposed | 50.1 | 81.2 | 49 |
|
||||
| 6910 | 19:13:55 | proposed | 67.4 | 107.4 | 55 |
|
||||
| 6920 | 19:19:07 | proposed | 74.8 | 113.4 | 78 |
|
||||
| 6960 | 19:40:19 | proposed | 86.0 | 100.0 | 78 |
|
||||
| 7010 | 20:04:32 | proposed | 87.7 | 99.1 | 73 |
|
||||
|
||||
From 6910 (19:13:55Z) every proposed checkpoint carried over two thirds of the sliding total and did not lock: the frozen table of Q5 held it (lane 3, 3.1). No surface could say so, because no field carries it. The "percent of active" above 100 on 6900 to 6930 is a display bug on the hub's checkpoint table (the active denominator lags the signers).
|
||||
|
||||
| Surface | What it showed 18:40Z to 20:05Z (from the code path and the rows above) | Honest? | Fix row |
|
||||
|---|---|---|---|
|
||||
| Ember (`app.js:1350`) | "last lock #6842 · 1 h 25 min ago" with "a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight". No pause word anywhere | No: the age climbed and the note contradicted the state | Q2 |
|
||||
| Site `/live` (`live.html:512-520`) | Status LIVE, blocks flowing, finality bar grey, "finality paused: 45% of weight silent" at 18:50Z falling to "11% of weight silent" by 20:05Z, no final region, no checkpoint ticks; "last lock #6842, 1 h ago" in the stats (`:404`, no active check); a hovered checkpoint block still read "locked" (`:450`) | Half: the pause is named (R4.6.3) but the percentage says the opposite of why it is paused once the stayers passed two thirds | Q1 |
|
||||
| Homepage (`index.html:810, 838, 879`) | Counter "last lock: paused"; the chain scene still drew the dashed line labelled "final" at the newest locked block | No: "final" and "paused" on one screen | Q5 |
|
||||
| Explorer (`explorer.html`, `block.html`) | Identical to a normal hour; stars on locked checkpoint rows; block pages of ordinary blocks "not a checkpoint" | No: nothing marked the pause | Q10 |
|
||||
| Hub (`relay/ui.html:367-368`) | 18:30 to 18:42Z: "observer STALE N s" in red, tiles frozen, finality "active" (the hub itself was down with the Mac). 18:42 to about 18:50Z: "active" in white, no lock forming. About 18:50Z onward: "paused" in amber, "last lock #6842" in amber, eight rows "proposed" at 53 to 89 percent, Events card full of `miner_quiet` and `miner_back` | Half: paused, but no since, no cause, no end; active for ten minutes of no lock | Q6 |
|
||||
| Discord | Nothing: the bot is not live. Had it been: the 20:00Z pulse would have read "Finality paused since 2026-10-06 18:4x UTC: nothing is final until two thirds of the 30-day weight signs again; mining and proving are paid as usual" in the description, and the watcher would have marked the pause `preexisting` and never opened an incident | No | Q7 |
|
||||
| Wallet 0.1.4 (`igneum-wt-wallet` `app.js:185, 340`) | Node card "paused" when a local node reports `finality_active` false, or "not checkpoint-checkable without a node" on the public RPC; transaction rows stay "in block N" with no mention | Half | Q35 |
|
||||
| Pool page (`pool/web/index.html`) | Not deployed; the code has no finality concept; it would have confirmed and paid on blueness throughout | n/a (a design choice, stated in Q50) | Q50 |
|
||||
|
||||
The row for the ledger: **Q1 plus Q2, Q5, Q6, Q7, Q10**: every public and operator surface either hid the pause, named it without a cause, or contradicted it, for two hours, during the first finality pause the project has had in public. The gate across all of them is one forced pause on Devnet 2 with a screenshot of each surface, before the public testnet opens.
|
||||
|
||||
## 3. Per system
|
||||
|
||||
### 3.1 Miner app (Ember)
|
||||
|
||||
**What it does today.** A Rust engine (`app/igneum-app/src/`) runs `igneumd` and one GPU worker per card, serves a tokenised dashboard on 127.0.0.1 (`src/server.rs:11-33`, routes `:184-359`: `api/state`, `api/log`, `detect`, `setup`, `key/saved`, `key/reveal`, `start`, `pause`, `resume`, `cards`, `tune/goal`, `settings`, `prove`, `update/check|install|auto|open`, `jobs/allow|check`, `power/apply|control`, `sweep/*`, `clock/*`, `quit`). Screens (`ui/index.html`): Welcome (line 71), Cards (102), Address (127), the one-time key sheet (438), then a rail with Mine, Earnings, Prove, Settings (164 to 413), a log drawer (460), a status strip (`Notices`, `ui/app.js:7-140`) and an update card (`UpdateCard`, `app.js:154-243`). Ember Tune (`src/ember.rs`, `src/sweep.rs`, `docs/plans/ember-tune.md`) tunes power and clocks per card against a goal. Updates are Ed25519-signed manifests (`src/ota.rs`, `src/manifest.rs`). Hosts: `app/mac/IgneumMiner.swift` (WKWebView, menu bar), `app/windows/host.cpp` (WebView2, tray). The shipped build is 0.3.14 with miner-ui-3; 0.3.15 (miner-ui-4: the big-number hero and the chain scene) is staged, its canary failed on block version 1026 and was rolled back (`release-0.3.15.md` section 5). UI tests: 35 pass.
|
||||
|
||||
**Comparators and the exact gap** (all approximate, from memory of the products).
|
||||
|
||||
| Comparator | Their screen or feature | Ours, and where it falls short |
|
||||
|---|---|---|
|
||||
| NiceHash QuickMiner | Profitability per day in fiat on the main screen; Rig Manager web view of your own rigs | Earnings reads "£0.00 earned: nothing is bought or sold on devnet" (`index.html:250`); mined IGN is never shown, only proving `paid_wei` (`app.js:1364`); no owner-facing remote view (the console is the operator's) |
|
||||
| HiveOS | Hash rate and temperature charts over hours; Telegram or Discord alert on a GPU fault or a rig offline; per-GPU fan and memory clock | A 10-minute blocks strip only (`app.js:1052-1127`); no outbound alert; manual control is the power slider (`app.js:1255`); the memory knob exists in the engine with no control |
|
||||
| lolMiner, T-Rex consoles | Per-GPU accepted, rejected, invalid and faults on every status line | Per card only "N blocks" (`app.js:306`); `rejected_session` a total in the hero sub-line (`app.js:1290`); `mismatched=` and `faults=`, which X21 made the engine read, never reach the row |
|
||||
| Nanominer | Restart-on-hash-drop watchdog with a visible threshold | `src/watchdog.rs` exists; the row says "stopped after repeated faults" (`app.js:296`) with no count or threshold |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| 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 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 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 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 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: <curl error>." (`app.js:1360`, Q11). GPU lost: "Card removed: <name>. 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: "<name>: 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`).
|
||||
|
||||
**Already better than the comparators.** Automatic efficiency tuning against a goal with the money consequence on screen (`app.js:408-419`; 37 to 41 percent more hashes per watt measured on PC 1, `ember-tune.md:254, 271`). Signed, hash-checked, rolled-back updates with a safe-moment rule and machine slots (`ota.rs`, `manifest.rs`). A reason word on every zero rate and hot-plug words (`ember-tune.md` 6a). A virtualised log drawer with time jump and error marks (`app.js:880-1050`). Confirmations in place, no dialogs. A tokenised local API with origin checks (`server.rs:102-125`). Dark-scheme contrast is good: bone 17.4:1, ash 7.0:1, ember 5.6:1, molten 11.0:1 on obsidian (`app.css:19`). Focus rings, `aria-live` strip, labelled switches and sliders, reduced motion honoured on every animation (`app.css:67, 113, 142, 148, 157, 420, 437, 466`; `app.js:1137`). None of the four comparators ship the first two.
|
||||
|
||||
### 3.2 Wallet
|
||||
|
||||
**What it does today.** One desktop wallet (Igneum Wallet, Rust engine plus a local window; `igneum-wt-wallet/app/igneum-wallet/`: `engine.rs`, `vault.rs`, `hd.rs`, `tx.rs`, `finality.rs`, `node.rs`, `updater.rs`, `server.rs`; ui `index.html`, `app.js`, `lock-screen.js`, `update-card.js`), shipped as 0.1.4 (`site/downloads.json` wallet-mac, 19.6 MB; `release-0.3.15.md:90`). 0.1.5 (bundled 0.3.14 node, RPC on localhost) has a DMG and is unpublished (`release-0.3.14.md:82`). UI 3 is commit ab8dfdc on `wallet-ui-3` and `wallet-0.1.5`, not on master. No wallet inside the miner app (it holds the payout key, `wallet.json`, and a button "Open the wallet" to the site, `app/igneum-app/ui/app.js:826`). No web wallet. A MetaMask guide (`site/metamask.html`), an address page (`site/address.html`, noindex, balance plus blocks mined), a marketing page (`site/wallet.html`). Keys: BIP-39 (12 to 24 words, `hd.rs:23`), BIP-44 Ethereum path; vault Argon2id 64 MiB, 3 passes, XChaCha20-Poly1305 (`vault.rs:14-81`); key zeroised on lock (`engine.rs:392`); 24 words shown once, three typed back (`engine.rs:321`); raw key export behind password or Touch ID (`ui/index.html:271-275`). Chain ids 4461, 4462, 4463 (`metamask.html:233`). Signing: EIP-1559 value transfers only; no EIP-712, no `personal_sign`, no contract calls, no dapp provider, no hardware wallet (spec 8.5 item 2 decided, not built; `docs/fud-ledger.md:2058`).
|
||||
|
||||
**Comparators and the exact gap** (approximate unless a standard is named).
|
||||
|
||||
| Comparator | Their feature | Ours |
|
||||
|---|---|---|
|
||||
| MetaMask | Account list and HD derivation; address book; speed up and cancel a pending tx; `window.ethereum` provider; `eth_signTypedData_v4` | One account (`engine.rs:343` refuses a second import); an address field only (`ui/index.html:177`); no replace-by-fee; no provider; no EIP-712 |
|
||||
| Rabby | Pre-sign simulation and recipient risk flags | Review shows amount, fee and total (`app.js:293-295`) |
|
||||
| Frame | Ledger, Trezor, GridPlus signers; injected provider for any browser | Neither (spec 8.5 "MUST offer", no code) |
|
||||
| Monero GUI | View-only wallet; subaddresses; a settable node address with status | Node source engine-chosen, no settable endpoint (`wallet-ui-3.md:112`) |
|
||||
| KDX, Kaspium | Sync bar with peer count; per-transaction DAA score | "reading the chain, N of M blocks" (`app.js:175`), no peer count |
|
||||
| EIP-1193 | `request` provider | Used on the guide (`metamask.html:294`) |
|
||||
| EIP-6963 | Multi-provider discovery | Absent; the guide falls back to whichever extension owns `window.ethereum` (`:292`) |
|
||||
| EIP-3085 | `wallet_addEthereumChain` with `blockExplorerUrls`, `iconUrls` | `blockExplorerUrls: []`, no icon (`:225, 285-286`) |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| 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 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 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 |
|
||||
| Q36 | Reorged-out transaction: "in a block" then the row vanishes; no resend hint | `app.js:340`, `docs/fud-ledger.md:2154-2158` | user-visible | 3 | execution-engineer (node side), app owner (the row) | a transfer unwound in the P23 conformance run shows "reorged out, resending" and ends executed |
|
||||
| Q37 | The address page shows no finality state and "n/a" with a bare reason for balance | `site/address.html:205-209, 242-252` | cosmetic | 2 | site owner | the page shows the latest locked checkpoint and whether finality is active |
|
||||
| Q38 | The permanent phishing sentence differs between surfaces ("words or your key" on the wallet, "your key" in the miner) against spec 8.5's one verbatim sentence | `site/wallet.html:366`, wallet `ui/index.html:54`, `app/igneum-app/ui/index.html:97`, `08-client-security.md:39` | cosmetic | 0.5 | site owner, app owner | one sentence, grep-identical on all three |
|
||||
| Q39 | Vault file permissions not set explicitly (no `0o600` in `vault.rs`) | `vault.rs` | operator-visible | 0.5 | app owner | the file is created 0600; test |
|
||||
|
||||
**Error states.** Node down: pill "no node", balance note "waiting for a node" (`app.js:160, 176`), the node line's raw engine message (`:186`; UI 3 replaces it with "the node stopped answering; trying again", `wallet-ui-3.md:199`). Pending versus final: "PENDING, waiting for a block", "IN A BLOCK, chain block N; final once a verified checkpoint covers it", "FINAL, under checkpoint N, certificate verified by this wallet", "FAILED, the execution failed; the fee was still paid" (`app.js:340`). Address page: "The API did not answer: <message>" (`address.html:270`), "Not found." (`:247`). MetaMask page: "No Ethereum wallet found", "Waiting for the wallet...", "Cancelled in the wallet.", "The wallet refused: <message>" (`metamask.html:292-296`).
|
||||
|
||||
**Security notes.** Plaintext key only in engine memory, zeroised on lock and after each sign (`engine.rs:392, 563`); the window never holds the key (nonce-gated reveal, `server.rs:108`); local API on 127.0.0.1 with a random port, per-run token path and a host token (`server.rs:1, 39, 71`); chain id checked at quote and send (`engine.rs:549`). Testnet RPC is https (`testnet-go.md:13`); devnet is http to 127.0.0.1. Chain ids 4461 to 4463 were absent from chainid.network on 3 October (`docs/design/execution-layer.md:354`), not yet registered (`frontier.md:645`); from memory no listed chain uses them (approximate).
|
||||
|
||||
**Already better.** The wallet verifies finality certificates itself with the node's consensus code (`finality.rs:101-121`) and never trusts the RPC's "locked"; MetaMask and Rabby trust the RPC's block tag (approximate). Touch ID is bound to the exact quote (address, value, nonce, chain id) with a 30 s one-use nonce (`engine.rs:539`). Argon2id at 64 MiB is above MetaMask's PBKDF2 default (approximate). The QR is drawn locally as SVG with the `ethereum:` URI and a checksummed address (`server.rs:140`). The MetaMask page says plainly that nothing on it asks for a seed (`metamask.html:172`).
|
||||
|
||||
### 3.3 Hub and relay
|
||||
|
||||
**What it does today.** One Vercel project (`igneum-relay`, Neon database, Blob store; `relay/README.md:3-5`). `relay/ui.html` (583 lines) is the console at `/r/<token>/` (`relay/vercel.json:5-8`); `relay/index.html` is a placeholder. Functions: `relay/api/relay.mjs` (feed, items, files, inbox, register, name, role), `relay/api/console.mjs` (machines, jobs, builds, chain, log, results, tuning), `relay/api/wake.mjs` (public long-poll for the apps' jobs file, 30 a minute per IP, `relay/lib/wake.mjs:15, 42-54`). Auth: the 20-character token in the path or `x-relay-token`, or any of `RELAY_KEY`, `LOG_INTAKE_KEY`, `LOG_INTAKE_KEY_NEXT` in `x-igneum-key` (`relay/lib/relay.mjs:34-46`); only `task`, a `run` or `task` drop, `name`, `role`, `delete` demand the token (`relay/api/relay.mjs:114-115`). Two job models: relay `run` items polled every 20 s by `relay/clients/igneum-agent.ps1:165` and executed as administrator; and signed app jobs in `igneum-jobs.json`, whose status the console derives from `miner_logs` rows (`relay/api/console.mjs:172-188`). Seven tabs (`relay/ui.html:186-192`): Machines, Jobs, Builds, Chain, Work log, Results, Relay; refreshed every 15 s when visible (`:577`), server cache 10 s (`console.mjs:21`). Chain tiles: blocks, identities, hash estimate, difficulty, last lock, finality, peers, blocks per second (`:363-370`). Mac tools: `tools/relay.mjs`, `tools/console.mjs`, `tools/jobs.mjs`. Tests: 24 pass per file (`node --test relay/test` as a bare directory fails under Node 22.23.2 with MODULE_NOT_FOUND; the README's command needs the glob).
|
||||
|
||||
**Comparators and the exact gap** (approximate).
|
||||
|
||||
| Comparator | Their screen | Ours |
|
||||
|---|---|---|
|
||||
| Tailscale admin console, Machines | Last seen, OS and version, key expiry with "Disable key expiry", tags; an ACL editor with tests | Last seen and a red card after 180 s (`relay/ui.html:270, 273`); no credential state (one static token, `relay/lib/relay.mjs:36`), no per-machine key, no revoke; roles are labels never checked (`relay/api/relay.mjs:191-195`) |
|
||||
| GitHub Actions job view | Live streaming log with line anchors, per-step timing, re-run, artefacts, annotations at the top | One text blob in a modal (`relay/ui.html:319`); `duration_s` and the error lines that `tools/jobs.mjs:41-44` already parses are discarded by `console.mjs:184-187`; no re-run; results are separate items |
|
||||
| Vercel deploy log | Build steps, deployment list, instant rollback, diff between deployments | Builds tab shows the current manifest, the last CI fetch, 40 posts and a folder listing (`relay/ui.html:323-341`); no rollback, no manifest history, no diff |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| 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 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 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 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 |
|
||||
| Q46 | Replay: `register` and `task` have no nonce; a captured `task` POST re-queues a run | `relay/api/relay.mjs:153-167` | operator-visible (security) | 1 | fleet agent | a replayed `task` is refused; test |
|
||||
|
||||
**Error states.** Relay API unreachable: header "offline: Failed to fetch" and a grey dot (`relay/ui.html:564, :32`); the panel is replaced only while it still shows "loading". Node down: a machine card goes red after 180 s with "silent 4h" (`:270, 273`; `SILENT_S`, `console.mjs:35`); a deliberate stop reads "stopped (quit) 2h ago" (`relay/lib/parse.mjs:134-140`); the live API down reads "live feed unreachable: <error>" (`:357`); the observer stale reads "observer STALE N s" in red after 30 s (`:361`, `site/api/live.mjs:8`) while every tile keeps its last value. Finality paused: `tile('finality', f.active ? 'active' : 'paused', ...)` in amber `#FFB35C` (`:368, :17, :65`); the "last lock" tile amber too (`:367`); eight checkpoint rows with state and fraction (`:374`). Job hang: "running" until a `SUMMARY` with `finished_at` arrives (`console.mjs:179-182`); relay `run` items show "read" and never "done" (`tools/relay.mjs:58`).
|
||||
|
||||
**Security, X23 to X28 on this tree.** X23: the tier split exists (`relay/api/relay.mjs:114-115`), no run signature, no per-machine HMAC, the intake key still writes the console (`console.mjs:249`). X24: every client builds `/r/$TOKEN/api` (`agent.sh:10`, `send.sh:11`, `igneum-agent.ps1:13`, `tools/relay.mjs:24`, `tools/console.mjs:27`). X25: `Arm-Restart` at top level and `/RL HIGHEST` (`igneum-agent.ps1:154, 72`). X26: feed to 500 with no retention; `delete` leaves blobs (`relay.mjs:48, 148-152`); `__DL_BASE__` substituted into bodies (`tools/relay.mjs:79`). X27: `from` is free text, `register` for any hostname (`relay.mjs:30, 153-167`). X28: constant-time compare and HSTS present (`relay/lib/auth.mjs:5-11`, `relay/vercel.json:38-41`); still a GET that acks (`relay.mjs:85`), `RELAY-REBOOT` anywhere in output (`igneum-agent.ps1:133`), no rate limit, `NOPASSWD:ALL` (`wsl-setup.ps1:40`), `sudo -S` with the password (`prover-setup.ps1:21`). All of this is what Q8 merges.
|
||||
|
||||
**Already better.** One phone-first page, no login flow, seven tabs, 15 s refresh (`relay/ui.html:5, 577`); Tailscale, Actions and Vercel each take several screens on a phone (approximate). The machine card fuses miner STATUS, node log, app telemetry and OTA state and remembers an OTA line after it scrolls out (`console.mjs:61-84`); no comparator shows workload telemetry. Stale marking at 180 s drops the card from the total; stopped and silent are distinct words (`parse.mjs:16-21, 134-140`). Drop-anywhere upload direct to Blob with a progress bar and a 50 MB cap (`ui.html:506-534`). The wake long-poll is dependency-free and tested with a fake clock (`relay/lib/wake.mjs`). Headers: nosniff, frame DENY, noindex, no-referrer, no-store, HSTS on every path (`relay/vercel.json:21-43`).
|
||||
|
||||
### 3.4 Site
|
||||
|
||||
**What it does today.** Thirteen static pages plus 404 (`site/index.html`, `litepaper.html`, `live.html`, `bench.html` 611 KB rendered from `docs/bench-log.md`, `evidence.html`, `ledger.html` 312 KB generated, `miner.html`, `miners.html` from `miner-bench.json`, `wallet.html`, `metamask.html`, `faucet.html`, `explorer.html`, `block.html`, `address.html`). `site/build.mjs:49-50, 357-372` injects `partials/head.html` (self-hosted fonts, tokens), `nav.html`, `footer.html`, stamps download links from `downloads.json`, inlines `journey.json`. `scrub.mjs` runs on bench and miners only (`build.mjs:171, 401`). Live data: index polls `/api/live` every 2 s (`index.html:704`; 10 s while off); the light client fetches `/api/checkpoint` and imports two noble libraries from cdn.jsdelivr (`verify/verify.js:5-6`); `live.html` polls every 2 s (`:665-666`); faucet POSTs `/api/faucet`. APIs (`site/api/`): `live`, `stats`, `supply`, `explorer`, `checkpoint`, `faucet`, `log`, all reading Neon tables written by `tools/observer`. Edge cache: live 1 s, stats and supply 10 s, explorer 5 s (`vercel.json`). The live HTML (one curl, 20:02Z) is 60,009 bytes, served with HSTS (preload), `X-Frame-Options: DENY`, nosniff, `Referrer-Policy: strict-origin-when-cross-origin`, no CSP; download buttons stamped "v0.3.14 · 45.4 MB"; "Public testnet: not yet open; the devnet build is here for people who want to look."
|
||||
|
||||
**Comparators and the exact gap** (approximate unless a page is named).
|
||||
|
||||
| Comparator | Their page or element | Ours |
|
||||
|---|---|---|
|
||||
| kaspa.org, ethereum.org | A Developers or Docs nav item | Nav is Litepaper, Live devnet, Engineering log, Miner, Wallet, Evidence, GitHub (`partials/nav.html:14-21`); builders land on a litepaper section (`index.html:335`) |
|
||||
| getmonero.org/downloads | Every platform, hashes and signing keys on one page | Split across `index.html:363-365`, `/miner#get`, `/wallet`; the homepage's Linux and HiveOS button links `/miner#get`, not a file; sha256 shown only for the HiveOS package (`miner.html:558`), none beside the Windows and Mac buttons, no signing key, no verify line |
|
||||
| z.cash, aztec.network | Named people, a transparency page | "one founder, pseudonymous" once in the litepaper; the Reddit review's key-powers table (round 4, section 3 row 8) absent |
|
||||
| kaspa.org, getmonero.org | Media kit, press kit, language switch, newsletter | None (`site/` has no `/press` or `/brand`) |
|
||||
| ethereum.org, kaspa.org | Light and dark | Dark only; only the litepaper honours `prefers-color-scheme` |
|
||||
| succinct.xyz, aztec.network | 1200x630 share cards | Home, litepaper, live, explorer use 256 px `og-small.png` with `summary` (`index.html:15-19`); miner, wallet, bench, evidence use the large card |
|
||||
|
||||
**Lighthouse, estimated from source (approximate; no run).** Common: `lang="en"`, a skip link (`partials/nav.html:1`), one `:focus-visible` ring (`head.html:22`), self-hosted fonts with `font-display:swap`, two preloads and fallback metrics (`head.html:1-19`), no render-blocking third-party CSS, canonical, description, OG and twitter tags on every page.
|
||||
|
||||
| Page | Performance | Accessibility | Best practices | SEO | What holds it down |
|
||||
|---|---|---|---|---|---|
|
||||
| index.html | 78 to 85 | 88 | 95 | 92 | 89 KB HTML, 35.0 KB inline JS, 24.0 KB CSS; two rAF canvas loops at display rate with no visibility or intersection stop (`:790, 853, 857`); 2,880 px webp with no srcset; h3 before the first h2 (`:344, 359`); ember on bone 3.08:1 at 15 px in the light section (`:119-120`), eyebrow `#7A776F` on bone 3.97:1 at 12 px (`:130`); 256 px OG card |
|
||||
| explorer.html | 88 to 93 | 90 | 100 | 85 | 32 KB, no images, no third party; `aria-live="polite"` on an 8-tile strip refreshed every 10 s (`:222`); not in the sitemap |
|
||||
| live.html | 85 to 90 | 88 | 100 | 85 | 64 KB, 35.6 KB JS; the canvas loop is capped at 30 fps and stops off screen (`:605-612`), good; `aria-live="polite"` on the 8-cell strip every 2 s (`:225`); canvas `aria-hidden` with no text alternative (`:241`) |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| 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 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 |
|
||||
| Q53 | Sitemap misses `/miner`, `/explorer`, `/faucet`, `/ledger`; every lastmod is 4 or 5 Oct; 404 says "These four pages are everything the site has" | `sitemap.xml`, `404.html:138` | cosmetic | 0.5 | site owner | the sitemap is generated by the build from the page list |
|
||||
| Q54 | `aria-live="polite"` on strips refreshed every 2 s and 10 s (a screen reader hears eight values every refresh) | `live.html:225`, `explorer.html:222` | user-visible | 0.5 | site owner | `aria-live="off"` on the strips plus one polite status line on state change |
|
||||
| Q55 | Homepage canvases never stop off screen or cap their rate | `index.html:790, 853, 857` | user-visible | 1 | site owner | 30 fps cap and IntersectionObserver as `live.html:605-612` |
|
||||
| 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 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 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`).
|
||||
|
||||
**Error states, exact strings.** API unreachable: `/live` after three failed polls shows "OFFLINE", "api unreachable", the canvas frozen with "api unreachable, scene frozen" (`live.html:665, 600`); `/explorer` writes the fetch error into the table on first load and is silent after (`:342-345`); `/block` "The API did not answer: <message>" (`:328`); the homepage "simulated preview · live feed unavailable" and "Live feed unavailable. This is a simulation and its counters count simulated blocks." (`index.html:870-872`). Observer stale over 30 s: "OFFLINE", "observer updated N min ago", "observer offline, last update N min ago" (`live.html:663`); explorer eyebrow "observer offline, last known" (`:318`); homepage "simulated preview · observer offline, last update N s ago" (`:708`). Finality paused: section 2.
|
||||
|
||||
**Already better.** Self-hosted latin subsets with preloads and computed fallback metrics (`head.html:1-19`); ethereum.org and kaspa.org load heavier bundles (approximate). Zero third-party scripts except the two pinned noble modules; no analytics, no cookie banner. Honest live states: "simulated preview · observer offline", "finality paused: N% of weight silent", "api unreachable, scene frozen"; Etherscan shows nothing when its indexer lags (approximate). A browser-side BLS certificate verifier on the homepage (`verify/core.js`), which no comparator homepage has. A public API with a CI field contract (`api/stats.mjs:10-14`) and supply derived from the emission rule with an hourly coinbase check. HSTS with preload, frame DENY, nosniff, immutable font caching, skip link, reduced-motion rule on every page. A dated journey with pass or fail gates inlined at build, and a ledger page of every criticism; none of the six site comparators publish the equivalent.
|
||||
|
||||
### 3.5 Explorer
|
||||
|
||||
**What it does today.** `explorer.html` polls `/api/explorer?blocks=50` every 5 s and `/api/stats` plus `/api/supply` every 10 s (`:342-343`); `block.html` fetches `?block=` or `?height=` once (`:276`); `address.html` fetches `?address=` once; `/block/:id` and `/address/:addr` are Vercel rewrites. Search classification is client-side (`lib/explorer.mjs:19-27`: hash, 0x address, bech32, height) with a server fallback for tx hashes (`api/explorer.mjs:49-59`). The data window is 24 hours of observer rows; balance needs `EXPLORER_EVM_RPC`, unset on the devnet deployment (`docs/plans/explorer.md` section 7).
|
||||
|
||||
**Comparators and the exact gap** (approximate).
|
||||
|
||||
| Comparator | Their feature | Ours |
|
||||
|---|---|---|
|
||||
| Etherscan, Blockscout | A transaction page: decoded input, logs, status, fee | None; a tx hash lands on `/block/<hash>#tx-<hash>` (`api/explorer.mjs:57`); `block.html:310` lists hash, from, to, value, bytes, with from and to "needs an EVM RPC" when unset |
|
||||
| Etherscan, Blockscout | Address page with transactions, token transfers, nonce, contract tab | Blocks mined, 24-hour earnings, balance only with an RPC (`address.html:209, 281`) |
|
||||
| Blockscout | Hosted API reference (Swagger), rate-limit statement | A card on `/explorer` (`explorer.html:232-240`) and `docs/api/public-stats.md` in the repo |
|
||||
| explorer.kaspa.org | DAG view inside the explorer; merge-set order per block | DAG view lives on `/live`; block page has blue work, parents, children, mergeset blues and reds (`block.html:285-325`), no prev and next navigation |
|
||||
| mempool.space | Mempool as the product, fee charts | Mempool count is one tile on `/live` (`live.html:233`); no charts on the explorer |
|
||||
| All four | Per-block status (final, confirmations) | None for an ordinary block (Q10) |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Q10 | No finality state; failures swallowed (named under the ten in section 1) | `api/explorer.mjs:16-25`, `explorer.html:222, 309, 342-345` | user-visible | 3 | site owner | during a forced pause every block row carries "paused" and the strip tile reads the Q1 reason; cut the API: the table dims with "api unreachable, last update N s ago" within 10 s |
|
||||
| Q63 | No tx page, no hosted API reference | `api/explorer.mjs:57`, `explorer.html:232-240` | user-visible | 4 | site owner | `/tx/<hash>` with status, fee, from, to, input bytes, shard and proof state; `/api` page with every field and the cache times |
|
||||
| Q64 | Address page: no transaction list, no finality (Q37 covers the finality half) | `address.html` | user-visible | 3 | site owner | the last 50 transfers in and out, each with its block and state word |
|
||||
| Q65 | No prev and next block navigation; no block-by-height neighbours | `block.html` | cosmetic | 1 | site owner | two links from `selected_parent` and the first child |
|
||||
| Q66 | Blocks per second inverted against the other pages ("1.2 s per block" vs "0.9 blocks / s") and difficulty "126.81 M" with a space against "126.81M" on `/live` | `explorer.html:330-331`, `live.html:329` | cosmetic | 0.5 | site owner | section 4.3's one formatter |
|
||||
|
||||
**Already better.** Block page depth for a DAG: parents, children, mergeset blues and reds, selected parent, shards with prover and lag, checkpoint weight fractions and certificates (`block.html:285-325`); explorer.kaspa.org shows less of the mergeset (approximate). Client-side search that accepts a height, a hash, an 0x or bech32 address with no round trip (`lib/explorer.mjs:19-27`). Five-second edge cache with a stale flag from the observer, so a stale page says so on first load.
|
||||
|
||||
### 3.6 Pool
|
||||
|
||||
**What it does today.** `igneum-pool`, a Rust crate (`pool/Cargo.toml`, 12 source files) speaking the spec 09 protocol in its v0 form: newline JSON over plain TCP (`pool/src/server.rs:1-2`, `protocol.rs:1-9`), the project's own message set (`hello`, `welcome`, `authorize`, `seeds`, `template`, `job`, `share`, `share_result`, `solution`, `stats`; `protocol.rs:21-170`). Every share is CPU-verified on the node's warp verifier (`verify.rs:52-64`; 1.35 ms isolated, 2.1 ms under load, `docs/plans/pool.md` section 5). PPLNS only, window in blocks of weight, default 2 (`pplns.rs:1-5`, `config.rs:67`); fee 1 percent, minimum payout 1 IGN (`config.rs:65-66`); EIP-1559 transfers from the pool's coinbase key, at most 16 per round, failed receipts re-credited (`payout.rs:197-299`). One page, `pool/web/index.html`, baked into the binary (`api.rs:166-174`): a stat strip, connect card, address lookup, blocks, payments, API card. Stats API `/api/stats`, `/api/blocks`, `/api/payments`, `/api/miners/<addr>`, `/api/pool-stats`, `/health` (`api.rs:185-200`) with fixture-pinned field contracts (`api.rs:255-272`). Operator surface: a `STATUS` line every 30 s on stdout (`main.rs:152-174`). Deployment: a written, unexecuted systemd unit behind Caddy (`pool/README.md:75-116`; "not executed", `docs/plans/pool.md` section 8). Nothing is deployed. The horizon tree equals `igneum-wt-pool-rebase` HEAD 269364a.
|
||||
|
||||
**Comparators and the exact gap.**
|
||||
|
||||
| Comparator | Their feature | Ours |
|
||||
|---|---|---|
|
||||
| 2miners | Per-worker hash rate charts over time; payout history per miner; worker-offline alerts by email or Telegram; estimated earnings per day; luck | A single 10-minute and 1-hour number, samples kept 15 minutes and not persisted (`state.rs:704-705, 779-781`); the API returns 50 payments and the page renders none of them (`api.rs:148`, `web/index.html:181-194`); no contact field (`protocol.rs:50-60`); no earnings estimate; luck defined inverted against MiningPoolStats' convention (`api.rs:72-74`) |
|
||||
| WoolyPooly | Solo mode; worker offline alerts | `scheme` hard-coded `"pplns"` (`server.rs:53`, `api.rs:80`); no alerts |
|
||||
| Kaspa acc-pool (approximate) | Prometheus and Grafana | No `/metrics`; stdout only |
|
||||
| Stratum v2 reference (github.com/stratum-mining/stratum, not cloned, approximate) | Noise-encrypted transport, job negotiation, binary framing, header-only mining | Plain TCP while spec 9.3 demands TLS 1.3 and `binding` is sent empty (`server.rs:1-2`, `protocol.rs:58-60`, `09-pool-protocol.md:37, 43, 49`); mode A only; JSON lines, and the `template` ships the whole `RpcRawBlock` to every member every second (`node.rs:147-164`) |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| 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 founder-visible (once deployed) | 2 | miner-community-lead | the page renders "Node unreachable since <time>" against a stopped node |
|
||||
| Q69 | Raw integers (DAA, block count, shares) with no separators; difficulty uses `toLocaleString()` with no locale, so it varies by browser; hash rate 2 decimals against the site's 1 | `web/index.html:152, 163, 172, 190` | the founder-visible | 1 | miner-community-lead | every number formatted as section 4.3 says |
|
||||
| Q70 | Ledger is a JSON file rewritten every 15 s; hash-rate samples and check cost not persisted, so a restart zeroes every rate tile and the 24-hour luck | `state.rs:704-710`, `main.rs:131-144` | user-visible | 4 | miner-community-lead | restart test: tiles recover within one sample window; README names the loss window |
|
||||
| Q71 | Address lookup never shows the payment history the API returns | `api.rs:148`, `web/index.html:181-194` | user-visible | 1 | miner-community-lead | payments table under the lookup |
|
||||
| Q72 | No per-miner history, no charts, no worker-offline notice, no `/metrics`; `/health` always ok | `state.rs:779-781`, `web/index.html:187`, `main.rs:152-174`, `api.rs:195` | user-visible, operator-visible | 4 + 4 + 2 + 1 | miner-community-lead | hourly buckets per address kept 7 days with a sparkline; opt-in webhook after 10 min silence; Prometheus text endpoint; health reflects `net.synced` and template age |
|
||||
| Q73 | Finality: the pool confirms and pays on blueness and has no finality concept; the page says nothing during a pause (a design choice to state) | `node.rs:279-345`, `server.rs:9` | user-visible | 1 | miner-community-lead | the page carries the network's finality state and the sentence "payouts follow blue confirmation, not finality" |
|
||||
|
||||
**Already better.** Every share CPU-verified at the node's own engine, no sampling (`verify.rs:52`; spec 9.8 item 5); most pools trust the miner's claim for low-difficulty shares (approximate). The member's vote key rides in every header it hashes (`node.rs:47`), so pool concentration does not become vote concentration; no Stratum pool does this. PPLNS split snapshotted at find time, credited only on blue confirmation, orphans pay nobody (`state.rs:821-852`). Failed payout receipts re-credit the balance (`payout.rs:288-292`). Exact binary share weights (`vardiff.rs:138-140`). Field contracts pinned by fixture tests.
|
||||
|
||||
### 3.7 Discord
|
||||
|
||||
**What it does today.** `tools/community/discord-hooks.mjs` (798 lines, Node 22, no dependencies). Webhook URLs in `~/.config/igneum/discord` (`:18-19, 41, 520`); state in `~/.config/igneum/discord-hooks-state.json`. Posts: network pulse to #numbers at 03:00, 09:00, 15:00, 21:00 UK (`shapePulse` `:236-273`); daily digest 09:00 (`:276-299`); weekly numbers Monday 09:00 (`:302-352`); release to #announcements by the shipper (`:394-409`); incident open and resolve to #incidents by hand or by the watcher (`:412-433`). Watcher conditions: `finality_paused` (5-minute hold), `proof_lag` (900 s), `observer_silent` (180 s) (`WATCH` `:37`, `:437-472`); one incident per condition, resolved after 120 s clear (`:490-503`). Idempotency keys per post (`:562, 569`); 429 backoff (`:534-551`); a forbidden-string guard before every post (`:47-96`); `allowed_mentions: {parse: []}`. Runner: a systemd timer on igneum-build-1, `tick --live` every minute (`infra/build-server/discord-hooks/`). Per `docs/community/discord-hooks.md:81-82`, no live post has been made and the install has not run. Tests: 31 pass; every live poster is stubbed.
|
||||
|
||||
**Comparators and the exact gap** (approximate).
|
||||
|
||||
| Comparator | Their feature | Ours |
|
||||
|---|---|---|
|
||||
| Ethereum, Kaspa, Monero Discords | Ticker bots renaming channels with height, hash rate, price | None; a webhook cannot rename a channel, a bot token is needed |
|
||||
| The same | Embed colour by severity; a pinned current-status message; "resolved" as an edit of the open message | Single ember colour for every kind (`:33, 158`); no pin; resolve posts a second message (`:706-712`) |
|
||||
| The same | Explorer links per block or lock | Only `/live` and `/miner` links (`:271, 297, 350, 420, 432`) |
|
||||
| The same | Reorg or stall alerts | No condition for a block-production stall (the fixture shows 11 zero minutes at 18:30 to 18:41Z) or a reorg (`:437-472`) |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Q7 | Bot not live; pre-existing never opens; pulse hides the pause (section 1) | `discord-hooks.mjs:481-485, 252-255, 158` | the founder-visible | 3 | miner-community-lead | section 1 |
|
||||
| Q74 | No block-production stall condition, the actual first signal of tonight's 18:30Z gap | `:437-472`, `fixtures/discord/live.json` | user-visible | 2 | miner-community-lead | `blocks_stalled` condition, 3 minutes, fixture test |
|
||||
| Q75 | Resolve posts a new message instead of editing the open one | `:706-712` | cosmetic | 2 | miner-community-lead | PATCH `/messages/<id>` with the resolve fields |
|
||||
| Q76 | Embeds link only `/live` and `/miner`; the last lock could link `/block/<hash>` | `:271, 297, 350` | cosmetic | 1 | miner-community-lead | last lock links its block page |
|
||||
| Q77 | "Last lock" names 93 voters while the hash-rate field says 56 vote keys active; readers will ask | `:259, 262` | user-visible | 1 | miner-community-lead | one clause explains the two counts, or one number |
|
||||
| Q78 | Four semicolon-balanced two-beat sentences in the shipped incident texts ("Blocks keep being produced; the chain is not locking checkpoints", "finality pauses rather than locks on a minority", "Vote keys are not machines; the machine count is owed", "Blocks are produced and final as usual; their proofs arrive late") | `:347, 448, 452, 458` | cosmetic | 0.5 | miner-community-lead | reworded as single statements |
|
||||
|
||||
**Already better.** Release embeds carry the full sha256 of every artefact plus the exact verify commands (`:399-406`); most project bots post a link. Every post passes a guard that refuses the founder's name, hosts, IPs, paths, webhook URLs and mentions (`:47-96`). Every number names its API field and the posts link the raw JSON (`:314-341`). Incident texts state who is affected per tier (`:449, 458, 467`). A dry-run HTML preview renders the exact card before anything goes live (`:602-634`).
|
||||
|
||||
### 3.8 Fleet tooling
|
||||
|
||||
**What it does today.** On the `gpu-fleet` branch (`/Users/joshm/Projects/igneum-wt-gpu-fleet/tools/fleet/`, read-only; the `horizon` tree holds only `publish-fleet.sh`): providers `vast.py` (console.vast.ai v0: filtered bundle search `:47-54`, rent with optional onstart `:61-72`, instances, destroy, a price-stamped `ledger.jsonl` `:88-102`) and `runpod.py` (`:27-44`); the orchestrator `fleet.py` (rent-card, wait, setup, status, run, pull, destroy, tail, sh, dn2-version, list; `:5-13, 183-187`); the shared library the 6 October rule demands, `lib/box.py` (`Box.run/put/alive`, `install_payload` sha-checked, `stop_node/start_node`, `height/daa/peers/synced/wait_synced`, `exec_status/exec_tip/proving_status/paid_segments/paid_shards/state_root`, `node_version`, `rejects_since`, `max_reorg_since`, `start_miner/stop_miners/start_prover/stop_all`, `destroy`; `Registry`) and `lib/standing.py` (roster, install, check, update, rerent, loop, `weight_check`); `lib/test_box.py` runs against dn2-3 with one known-failed case (`:10-12`) and a PASS line (`:37`), by hand, not in CI. Box side: 20 `box-*.sh` scripts (setup, matrix, ember, prover, dn2, standing supervisor, node-swap, exec-snapshot, wave, wave-pool, pool, rig, rig-prover, rehearsal, kill, datadir, floor-v5, segal-host) plus `dn2-kill.sh`, `dn400-worker.sh`, `rehearsal-kill.sh`. Collectors: `collect.py`, `bps-collect.py`, `night.py` (60 s probe, 15-minute rows, exec-reset re-run `:45-55`), `autorun.py`, `restage.py`, `swap.py`. Gates: `devnet2-gate.sh` (PASS = zero rejects, max reorg under 4, version equal, exec roots equal at a common height, one paid segment; `:92-100`), `canary.sh`, `canary-next.sh`. Page: `page.py` builds `fleet.json`; `publish-fleet.sh` copies it and runs a full Vercel production deploy (`:10`); `publish-0315.py` is the first standing publish. PC job runner `tools/build-job.mjs` (run, publish, watch, fetch, verify; targets ae432dc7 and 1ccfe586; signed kinds `run`, `fetch`, `collect`, `restart`, `update-now`, `shard-benchmark`, `build`; `--stop-miners` becomes `stop_miners_first`, consumed at `app/igneum-app/src/jobrun.rs:563`). Console `tools/console.mjs` (post, log, machines, chain, jobs, builds, tuning, results, sync, url). CI checks in `tools/ci` (22): bash-body, check-workflow-shell, commit-string, copied-sources, identity-check, install-hooks, kit-path, link-check, no-conflict-markers, no-foreign-tree-writes, no-secrets, override-json, pinned-guests, playbook-quit, prover-socket, ps-drive-ref, public-api-check, second-engine, signer-pipe, windows-spawn, plus fixtures; `pgrep-self-match-check.sh` exists on `gpu-fleet` only (its `ci.yml:82-83`), not on `horizon` or `master`.
|
||||
|
||||
**Comparators and the exact gap** (approximate unless cited).
|
||||
|
||||
| Comparator | Their feature | Ours |
|
||||
|---|---|---|
|
||||
| Vast CLI | `search offers` with any filter expression; `create instance --onstart`; `logs` without ssh | A fixed filter set (`vast.py:47-52`); `rent-card` never passes onstart (`fleet.py:79`), so install is a second ssh step (`:102-109`); `tail` needs ssh (`:180`) |
|
||||
| SkyPilot | Declarative task YAML, `autostop -i`, spot recovery, `sky status`, `sky logs` | Imperative; one-shot boxes have no autostop (the caps at `page.py:45` are display only; 59 boxes running at 20:02Z); recovery is `standing.loop` after two dead checks at 600 s (`lib/standing.py:126-140`); `check` prints raw dicts (`:148`) |
|
||||
| Nomad | Restart policy: attempts, interval, delay, mode; allocation health in the UI | `box-standing.sh` restarts every 60 s with `pkill -9`, no cap, no backoff, no failure count (`:33, 46-49, 70`) |
|
||||
| HiveOS farm view | Per-rig cards: hash, temps, fan, accepted and rejected, last seen, flight sheets, bulk actions | The fleet page card shows card, state pill, "doing", USD/h, hours only (dlsite `index.html:176`); `standing.py:56` strips the `miner=` field the supervisor writes (`box-standing.sh:64` has MH/s, accepted and rejected, GPU util, watts); `console.mjs:114-116` has the HiveOS-style row for the app machines, not for rented boxes |
|
||||
|
||||
**Rows.**
|
||||
|
||||
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 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's `index.html` references neither (grep 0; it reads boxes, results, phases, night, log, `:243`); a dead box reads "running" for up to 20 minutes because state comes from `boxes.json` (`page.py:38`) and `standing.jsonl` is never read; no fleet or console script reads `finality_active` (only `bps-collect.py:30` tails "Finality:" log lines), so tonight's pause was found by hand (`CLAUDE.md`, the standing-fleet rule) | `page.py:38-47`, dlsite `index.html:243`, `lib/standing.py`, `tools/console.mjs:124` | the founder-visible | 6 (standing block, USD per day, Devnet 2 height and last gate line 2; "unreachable since HH:MM" from `standing.jsonl` within 2 minutes 2; `getFinalityCheckpoints` in the standing check and "finality paused since, reason" on the page and the console 2) | fleet agent | 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 `console.mjs chain` |
|
||||
| Q87 | `rerent` does not set up or supervise the new box: the docstring promises both (`lib/standing.py:14-16`), the code rents and patches rows (`:78-91`); a re-rented card bills idle | `lib/standing.py:78-91` | the founder-visible (spend) | 2 | fleet agent | the new label shows synced and mining on the page within the install time |
|
||||
| Q88 | One-shot boxes have no autostop; `cap_vast 1000` and `cap_runpod 500` enforce nothing (`page.py:45`; `vast.py:61-72` has no cap check) | `page.py:45`, `vast.py:61-72`, `autorun.py` | the founder-visible (spend) | 1.5 | fleet agent | `autorun` destroys a one-shot box N minutes after its done line; `rent` refuses above the cap |
|
||||
| Q89 | Supervisor restart loop without backoff or a failure line | `box-standing.sh:46-49` | operator-visible | 1.5 | fleet agent | after N failures a `standing_node_failed` line and a page flag |
|
||||
| Q90 | Dropped miner and GPU facts: `standing.py:56` strips `miner=`; `gpu=` never parsed | `lib/standing.py:56`, `box-standing.sh:64` | operator-visible | 1 | fleet agent | MH/s, accepted and rejected, watts per box on the page (the HiveOS card) |
|
||||
| Q91 | A transient ssh drop counts as death: `Box.run` returns 124 on timeout (`lib/box.py:49`), `check_one` marks dead on any rc (`standing.py:54`), the loop never asks the provider (`fleet.py:48-60` `refresh_ssh` unused); Vast proxy drops are known (`canary.sh:41`) | `lib/box.py:49`, `lib/standing.py:54` | operator-visible | 1 | fleet agent | provider status consulted before a dead check counts |
|
||||
| Q92 | `lib/test_box.py` pins a stale binary and digest (`:31` igneumd-0313, `:34` digest 4a0b8726; 0.3.15 is 06211d55, `publish-0315.py:15`) and is not scheduled | `lib/test_box.py:31-34` | operator-visible | 1.5 | fleet agent | a nightly PASS line on the page |
|
||||
| Q93 | The Devnet 2 gate reads no peers, no finality, no exec `blocked` (`read_box`, `devnet2-gate.sh:51-53`; `night.py:15` reads `blocked`, the gate does not) | `devnet2-gate.sh:51-53` | operator-visible | 2 | fleet agent | FAIL on 0 peers or finality paused at step 4 |
|
||||
| Q94 | `publish-0315.py` hard-codes one worktree and release (`:14-18`); `page.py:13` points at the master checkout's publish script; every publish is a full Vercel deploy | `publish-0315.py:14-18`, `page.py:13`, `publish-fleet.sh:10` | cosmetic | 2 | fleet agent | `publish.py` reads paths and sha from the release manifest; the page deploys as one file |
|
||||
| Q95 | Five 6 October rule rows have no `tools/ci` check and are therefore OPEN by the project's own rule (table below) | `CLAUDE.md` 6 October rules, `tools/ci/` | operator-visible | 5 (1 each) | fleet agent (consensus-engineer for the gate check) | each check fires on a known-failed fixture and passes a known-good one |
|
||||
|
||||
**The 6 October rules against their checks.**
|
||||
|
||||
| Rule (CLAUDE.md, 6 October 2026) | Check today | What the check is |
|
||||
|---|---|---|
|
||||
| `pgrep -f` with a literal pattern is banned | `pgrep-self-match-check.sh` on `gpu-fleet` only; absent from `horizon` and `master` | merge it; wire it in `ci.yml` on master |
|
||||
| Box operations live in one tested library | none; `devnet2-gate.sh:40-41` and `canary.sh:32-33` define their own `SSH()` and `SCP()` past `lib.box` | fail any file outside `tools/fleet/lib` that runs `ssh -i ~/.ssh/igneum-fleet` |
|
||||
| A watcher verifies the chain-side fact and is trusted only after a known-finished and a known-failed case | none; `devnet2-gate.sh` and `canary.sh` have no `--self-test` | a `--self-test` flag on every gate with both fixtures |
|
||||
| A standing box never leaves the live chain for an experiment; never remove over 10 percent of weight in an hour | `weight_check` exists (`standing.py:108-125`) but nothing enforces its call; `Box.destroy` (`box.py:136`) has no standing guard | destroy refuses standing rows; CI greps that scripts calling `stop_miners` on a label call `weight_check` first |
|
||||
| No live build before the Devnet 2 PASS line | none; `devnet2-status.json last_gate` reads "not run during the block-rate experiment" | the ship script requires `PASS <sha16>` in `devnet2-status.json`, as `commit-string-check` runs inside `build-remote.sh` |
|
||||
| Have checks | commit-string, override-json, ps-drive-ref, no-foreign-tree-writes, playbook-quit, second-engine | |
|
||||
|
||||
**Error states, exact strings.** Console machines (`tools/console.mjs:114`): `STOPPED (<reason>) <ago> ago`, `SILENT`, `live`, then `seen <ago> ago`, `synced` or `not synced`; cards `(stale, not in the total)` and `| <fault>` (`:115`). Console chain (`:124`): `last lock #N (blue N) | lag Ns STALE`; no "paused" word in `console.mjs` (the web tile has it). `fleet.py`: `no ssh` (`:116`), `ssh timeout` (`:118`), `scp failed` (`:106, 133`), `pull failed: ...` (`:145`). `lib/box.py`: `(124, "", "timeout")` (`:49`), `SshError("<label>: rc N: <err>")` (`:53`). `standing.py`: `re-renting <label> -> <new>` (`:134`), `behind: <label> <sha> wanted <sha> (a publish script moves it; the loop only reports)` (`:137`; "behind" is the binary sha, never height), `standing check: N boxes, N alive, N synced, N behind` (`:138`). Fleet page: the state pill (renting, installing, running, done, failed) and `doing` text only; no dead, behind or finality string exists. `devnet2-gate.sh`: `FAIL <box>: N rejected blocks`, `FAIL <box>: a selected-chain reorg of depth N`, `FAIL <box>: version V`, `FAIL <box>: exec root at height H ... differs`, `FAIL no segment record paid on any box during the run` (`:92-100`).
|
||||
|
||||
**Already better.** Every rent and destroy writes a price-stamped ledger line (`vast.py:43-45`, `runpod.py:25-26`), which Vast's CLI does not (approximate). The Devnet 2 gate compares exec state roots across every box at one height (`devnet2-gate.sh:98-99`), beyond any SkyPilot or Nomad health check. Gates read chain facts (height, paid segments, reorg depth), not process names. The supervisor runs the exec-recovery recipe by itself (`box-standing.sh:66-67`). The 10 percent weight rule is a real function (`standing.py:108-125`). The console distinguishes stopped-on-purpose from silent (`parse.mjs:134-140`).
|
||||
|
||||
### 3.9 The node's operator surface
|
||||
|
||||
**What it does today.** Fork-added flags (`igneum-wt-ship0315/vendor/igneum-node-0315/kaspad/src/args.rs`): `--devnet-suffix`, `--evm-rpclisten` (default 26790), `--evm-disable`, `--override-params-file`, `--igneum-exec-snapshot` (`:297`), `--ua-rule` (`:440`), `--rocksdb-wal-dir` (`:502`), `--devnet` alias `--igneum-devnet`. Kept from kaspad: `--loglevel` with per-subsystem `<subsystem>=<level>` (`kaspad/src/args.rs:262-271`), `--perf-metrics` (`:446`, debug lines only), `--utxoindex`, `--rpclisten-borsh`, `--rpclisten-json`, `--externalip`, `--appdir`. Env knobs: `IGNEUM_PROOF_VERIFIER`, `IGNEUM_PROOF_VERIFY`, `IGNEUM_DEVNET_GENESIS_BITS`, `IGNEUMD_DEVNET_BPS`, `IGNEUM_POW_STRIKES` and `IGNEUM_ATTACK_TS_OFFSET_MS` (feature-gated after X19). Finality logs (`consensus/src/processes/finality.rs`): `Finality: checkpoint {i} determined: block {h} (blue score, daa)` (`:625`), `Finality: certificate at index N received: X of Y voters, weight A of B` (`:1127`), `Finality: CONFLICTING certificate at index N` (`:1023`), `Finality: no body tip passes through locked checkpoint ...; fork choice falls back to depth finality` (`:1448`). No pause or resume log line; "paused" exists only as the polled RPC reason (`window filling, N of M`, `active`, `paused`; `igneum-node-0310 finality.rs:1794-1807`); the conflict reason is "O-3.17, not reported yet". RPC `getFinalityCheckpoints` returns `finality_active`, `finality_reason`, `window_filled_daa`, `window_full_daa`, latest lock (`rpc/core/src/model/finality.rs:293-299`); no provisional field, no frozen-table share. Exec JSON-RPC on one listener: `igneum_getExecStatus` (executedTip, blocked, startedFrom), `igneum_getProvingStatus`, `igneum_getFinalityView`, `igneum_submitProofRecord` (`igneum/exec/src/rpc.rs:858-979`). Metrics: no Prometheus endpoint (no hit in the fork's Cargo files). X19 (`docs/fud-ledger.md:1936-1946`): a slow clock disconnecting peers silently, `time_offset` unused, attack env compiled in, a dead override field; fixed on branch `ledger-fixes-0311` fbb0082a (one WARN per minute clock-skew line, attack env behind `attack-switches`, override refuses the field), pending merge; not done: the local-ahead mirror case, the faketime run, the skew field in `getBlockDagInfo` (`docs/plans/node-changes.md:18-19`).
|
||||
|
||||
**Comparators and the exact gap.** kaspad parity holds (`vendor/rusty-kaspa/kaspad/src/args.rs:241` loglevel, `:259` rpclisten-borsh, `:348` utxoindex, `:392` externalip, `:401` perf-metrics) but the fork's own subsystems (finality, exec, proving) got no new knobs. geth (approximate): `--metrics` with Prometheus or InfluxDB, `--log.format json`, `--http.api` allowlist; the fork has none, and writes on the EVM listener share it with reads (`rpc.rs:920`). reth (approximate): `--metrics`, `RUST_LOG` env filter, `reth db stats`, Grafana dashboards in the repo; the fork has no DB tool or dashboards. monerod (approximate): `status`, runtime `set_log`, `limit_rate`, `sync_info`; the fork's log level is start-only, no bandwidth limit, status is `igneum-miner watch` plus `getConnectedPeerInfo`.
|
||||
|
||||
**Rows.**
|
||||
|
||||
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Q96 | No log line on a finality flip: an operator tailing the log never sees the pause begin or end | `consensus/src/processes/finality.rs` (no pause line) | operator-visible | 1.5 | consensus-engineer | fast-time simnet prints `Finality: paused (<reason>)` and `Finality: resumed at index N`, one line per flip |
|
||||
| Q97 | `finality_reason` says `paused` and nothing else: no share that left, no frozen-table hold, no expiry, no conflict reason (O-3.17) (the node half of Q1) | `finality.rs:1794-1807` | the founder-visible (through every surface) | 1.5 (counted in Q1) | consensus-engineer | tonight's pause reads "paused: 42.7 percent of weight left the window; held by the table frozen at lock 6842 until DAA 216,402" |
|
||||
| Q98 | No `/metrics`: finality_active, latest lock, exec tip, blocked, paid segments, peers, DAA as Prometheus gauges (the axum listener exists) | `igneum/exec/src/rpc.rs` | operator-visible | 3 | execution-engineer | `curl :26790/metrics` scraped by a Grafana dashboard committed under `infra/` |
|
||||
| Q99 | Log level is start-only; no runtime change (monerod `set_log`, reth env filter) | `kaspad/src/args.rs:262-271` | operator-visible | 2 | consensus-engineer | an RPC or SIGHUP changes the level without a restart |
|
||||
| Q100 | Clock-skew field in `getBlockDagInfo` and the local-ahead mirror case still owed after X19 | `docs/plans/node-changes.md:18-19` | operator-visible | 2 (after the `ledger-fixes-0311` merge) | consensus-engineer | `faketime -60s` gives one WARN and the field |
|
||||
| Q101 | No `--evm-rpc-api` allowlist: submit and export share the listener with `eth_` reads | `rpc.rs:920` | operator-visible (security) | 2 | execution-engineer | a public listener answers `eth_` and refuses `igneum_submitProofRecord` |
|
||||
| Q102 | No JSON log format: collectors grep four reject patterns (`lib/box.py:119`) | `kaspad` logger | operator-visible | 2 | consensus-engineer | `--log-format json`; the gate reads a field, not a pattern |
|
||||
| Q103 | No `igneumd db` subcommand (column sizes, exec snapshot tip, finality blob layout) | none | operator-visible | 3 | execution-engineer | `igneumd db stats` on a devnet datadir |
|
||||
| Q104 | Merge `ledger-fixes-0311` (X19) into 0.3.16 | `docs/fud-ledger.md` X19 | operator-visible | 1 | consensus-engineer | the faketime WARN on the shipped binary |
|
||||
| Q105 | Eleven fork `.rs` files carry an em dash, one inside a log or help string (upstream origin not verified, approximate) | `igneum-node-0315` (a grep for the character) | cosmetic | 0.5 | consensus-engineer | 0 in any string the operator can see |
|
||||
|
||||
**Already better.** `finality_active` ships with a reason string, which kaspad, geth and reth have no analog for. `--ua-rule` version admission (`args.rs:440-445`) is beyond kaspad. A fixed-height activation is refused by rule; the override file is refused on a dead or duplicate field. Every build binary carries its commit or fails CI.
|
||||
|
||||
### 3.10 Downloads, install and the update flow
|
||||
|
||||
**What it does today.** `site/downloads.json` (updated 2026-10-06T17:48:20Z) lists four artefacts: Windows installer 0.3.14 (45.4 MB), Mac DMG 0.3.14 (41.9 MB), HiveOS package 0.3.14 (24.7 MB), Mac wallet 0.1.4 (19.6 MB), with sha256 and size, under `https://dl.igneum.network/dl/public/` and four stable aliases under `/public/` (`packaging/README-ship.md`, "The public downloads path"). `tools/ship-app.mjs` cuts a version in eleven checked, resumable steps (preflight, bump, inputs, commit, ci, fetch, dmg, copy, manifest, deploy, verify, console; `README-ship.md`). The Windows installer is built only by `windows.yml` on GitHub-hosted runners; since 19:24Z tonight every hosted job dies at start ("recent account payments have failed or your spending limit needs to be increased", `release-0.3.15.md` 6a), so no Windows installer can be cut until billing is fixed. 0.3.15 is staged with `--until manifest`; its Devnet 2 canary failed (block version 1026 on the thirteen-field file) and was rolled back. The ship tool's commit step pushed the release tree to master although the run had `--branch release-0.3.15` (`release-0.3.15.md` 6a).
|
||||
|
||||
**Signing and notarisation state.** macOS: `codesign -s -` (ad hoc) on every binary and the bundle (`build-dmg.sh:118-122`); no Developer ID, no notarisation, no stapling; the engine strips `com.apple.quarantine` from its own bundle on start (`main.rs:78`, `packaging/mac/README.md:51-53`). Windows: unsigned installer and exes; SmartScreen "Windows protected your PC" (`packaging/windows/README.md:76, 111`; `build-installer.ps1:188`); the installer is per-user (`PrivilegesRequired=lowest`) so no UAC for install; the firewall rule asks once, 20 to 50 s in (`ota.rs:981`). Comparators: Signal and Tailscale ship Developer ID-signed, notarised DMGs and Authenticode-signed installers; both open with no interstitial (approximate).
|
||||
|
||||
**The update flow today, precisely** (the baseline for frontier 3.7, reproducible-build attestations with N of M).
|
||||
|
||||
| Step | What happens | Where |
|
||||
|---|---|---|
|
||||
| 1. Build | Node fork and workers built on igneum-build-1 (Linux, Windows cross) and the Mac (macOS); Windows exes reproducible (`-Wl,--no-insert-timestamp`); a binary whose strings lack its commit fails `tools/ci/commit-string-check.sh`. The Windows installer and payload zip are built by `windows.yml` on GitHub-hosted `windows-latest` from `payload-inputs.zip`, whose `payload-inputs.json` (sha256 and size of the zip and every file, fork commit, repo commit) is signed on the Mac with the OTA key and verified in CI against the key compiled into the app and the pin `packaging/windows/node-source.pin` before anything is built (G13, fixed 5 October; 16 local tests pass) | `tools/build-remote.sh`, `cross-remote.sh`, `packaging/windows/push-inputs.sh`, `.github/workflows/windows.yml`, `docs/fud-ledger.md` G13 |
|
||||
| 2. Sign | One Ed25519 key, generated once on the Mac (4 October 2026), seed in `~/.config/igneum/ota-signing-key` (0600), never in the repo or CI; public key the constant `OTA_PUBLIC_KEY_HEX` in `app/igneum-app/src/manifest.rs:28`; `igneum-ota-sign sign` over the canonical manifest bytes (64-byte detached signature, `igneum-app-latest.json.sig`); `publish-manifest.sh` refuses to sign when the embedded key is not the one in `~/.config/igneum`. Rotation = a bridge build with the new constant signed by the old key. The wallet's updater trusts one key with no revocation list (Q31). Key not in hardware (G7 says it will be) | `packaging/ota/README.md` "Keys", `publish-manifest.sh` |
|
||||
| 3. Publish | Manifest (version, per-platform url, sha256, size, kind, `min_supported_version`, notes, `activation_height`) and `.sig` to `dl.igneum.network/dl/<token>/igneum-app-latest.json`, public copy under `dl/public/`; the apps' long-poll on the relay's `/wake` fetches it within seconds; the `igneum-jobs.json` channel uses the same key | `packaging/ota/publish-manifest.sh`, `publish-public.sh`, `relay/api/wake.mjs` |
|
||||
| 4. Check | On start (20 to 50 s in) and hourly with jitter; both files through curl; signature verified over the manifest bytes before parsing; a bad signature reads "manifest signature does not verify" and the manifest is never parsed; a version not newer, or a manifest without this platform, ends the round | `ota.rs:144, 628, 1038-1048`, `manifest.rs:114-120, 175-178` |
|
||||
| 5. Download and verify | Into `<app data>/app/updates/` with resume and `--retry 3`; size and sha256 from the manifest ("sha256 mismatch: the file is not what the manifest signed"); the Mac re-hashes before mounting | `ota.rs:1054-1079, 1124` |
|
||||
| 6. Stage | macOS: mount, copy to `.Igneum Miner.app.new`, run its engine with `--version` and demand the manifest's version; an unwritable folder gives `manual` and "Open the download". Windows: the installer is the staged file | `ota.rs:1096`, `packaging/ota/README.md` "Stage" |
|
||||
| 7. Safe moment | Not urgent: network has not lost over 30 percent of identities in 10 minutes, this machine's minute of the hour, node synced, no hourly boundary within 180 s, no worker starting; a ready update applies anyway after 6 hours. Urgent (fork within 1,800 blocks, unsupported version, Install now) skips every guard. Q4: a passed activation counts as close; no finality input; 0.3.15 adds "never while a remote job is active" | `manifest.rs:322-355` |
|
||||
| 8. Apply | The engine writes `update-pending.json`, starts the helper and exits through its quit path (miners 8 s, node 30 s, last log upload). macOS helper swaps bundles, strips quarantine, reopens, restores `.previous` on failure. Windows helper runs the per-user installer `/VERYSILENT ... /IGNOTA=1` first, with the engine still mining, then the installer stops the engine and relaunches | `packaging/ota/README.md` "Apply", `ota-apply.sh`, `ota-apply.ps1` |
|
||||
| 9. Rollback | The new engine counts starts in `update-pending.json`; a third start without 90 healthy seconds restores the previous version ("rolled back" in Settings); the first Windows update from 0.3.0 has no rollback target | `ota.rs:371` |
|
||||
| 10. What the user sees | "Igneum Miner X is available.", "Downloading X: N%.", "X is ready.", "Installing X. The app restarts itself. Mining continues until then.", "X did not stay up and was rolled back.", "updated to X from Y" in the event feed; Settings: Check now, Install now, automatic switch, the key fingerprint under "Allow remote jobs from Igneum (signed)" | `ui/app.js:29-52`, `index.html` Settings |
|
||||
| 11. Second engine, paused finality | A sweep or `IGNEUM_APP_NO_OTA=1` engine never runs the updater (`engine.rs:601, 778`; the 5 October rule). Nothing in the update path reads finality (Q4) | |
|
||||
|
||||
So the trust today is one key, one builder (the box and the Mac, both the project's), one manifest, no attestation by anyone else, no on-chain record, and the client installs whatever that one key signs at the moment the safe-moment rule allows. That is the baseline frontier 3.7 moves from: N of M attestations in a registry contract, the updater refusing under N, and no install while finality is paused.
|
||||
|
||||
**Rows.**
|
||||
|
||||
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Q3 | Signing and notarisation; first-run prompts (section 1) | `build-dmg.sh:118-122`, `windows.yml` | the founder-visible | 6 plus certificates | app owner, the founder | section 1 |
|
||||
| Q80 | The Windows installer pipeline is dead: every GitHub-hosted job fails at start on billing; the installer and payload zip have no other builder, so 0.3.15 and every hotfix are Mac and HiveOS only until the founder fixes Billing & plans, or the installer step moves to igneum-build-1 (Inno Setup under Wine, or a self-hosted Windows runner on PC 1) | `release-0.3.15.md` 6a, `.github/workflows/windows.yml`, `packaging/windows/build-installer.ps1` | the founder-visible | 0 (the founder: billing) or 4 (installer step on the box or PC 1 as a signed job) | app owner; the founder | a green `windows.yml` run, or `tools/ship-app.mjs --dry-run` showing the fetch step satisfied from the new builder |
|
||||
| Q81 | The ship tool pushes master whatever `--branch` says (the 19:30Z push of the release tree to master) | `tools/ship-app.mjs` commit step | operator-visible | 1 | app owner | `--branch X` pushes X; a test on a scratch repo |
|
||||
| Q82 | The download page shows no hash for Windows and Mac, no OTA key fingerprint, no verify command; the stable aliases are not explained (Q50 covers the page; this row is the artefact side) | `miner.html:549-558`, `packaging/ota/publish-public.sh` | user-visible | 1 | site owner | `igneum-downloads.json` carries the fingerprint; the page prints it with `shasum -a 256` and `certutil -hashfile` lines |
|
||||
| Q83 | The update key is one software key on one Mac, not in hardware, against G7's "held in hardware and its policy published" | `packaging/ota/README.md` "Keys", `docs/fud-ledger.md` G7 | operator-visible (security) | 3 (a YubiKey or Secure Enclave signer behind `igneum-ota-sign`, the policy page) | cryptographer, app owner | a release signed with the hardware key installs; the key policy is a public page; the software seed is retired |
|
||||
| Q84 | An activation height in the manifest outlives its activation and turns every later update urgent (the Q4 class, publisher side) | `publish-manifest.sh --activation-height` | operator-visible | 0.5 | app owner | the publisher refuses a height at or below the live DAA and drops a stale carried-over one |
|
||||
|
||||
**First 60 seconds, fresh Windows PC** (from the code and the READMEs): download `Igneum-Miner-Setup-0.3.14.exe` from `/miner`, SmartScreen interstitial "Windows protected your PC" (More info, Run anyway), per-user install with no UAC, "Start Igneum Miner now" (`Igneum-Miner.iss:86`), tray "Igneum Miner: starting", Welcome, Cards (detect runs on entry, `app.js:641`), Address, the one-time key sheet, "Start mining"; between 20 and 50 s in, a UAC prompt for the `igneumd.exe` inbound firewall rule arrives on top of whatever screen is up (`ota.rs:981`; declined = the node dials out, never asked again); closing the window gives "Igneum Miner keeps mining" (`host.cpp:354`). Fresh Mac: DMG, drag to Applications, Gatekeeper refuses the ad hoc app (macOS 15 has no Right-click > Open; the user must find Privacy and Security > Open Anyway, approximate), "Starting the engine" (`IgneumMiner.swift:146`), quarantine cleared from the bundle (`main.rs:78`), the same three screens, a menu-bar item with "Pause mining". Both tiers of card see the same screens; an 8 GB card gets two vote identities and a 24 GB card eight (`detect.rs`), which no screen explains (the "a card runs several" line lives on the site, not the app).
|
||||
|
||||
## 4. Cross-cutting
|
||||
|
||||
### 4.1 Accessibility (estimated from HTML and CSS)
|
||||
|
||||
| Surface | Contrast | Keyboard | Screen reader | Motion |
|
||||
|---|---|---|---|---|
|
||||
| Ember dark | bone 17.4:1, ash 7.0:1, ember 5.6:1, molten 11.0:1 on obsidian: passes | `:focus-visible` ring (`app.css:66`), rail arrow keys, Escape on the card and quit (`app.js:681-688, 979`) | `aria-live="polite"` strip (`index.html:50`), `role="dialog" aria-modal` (`:419`), `radiogroup` with `aria-checked` (`:194`), labelled switches and sliders; gaps: the blocks canvas `aria-hidden` with no text alternative (`:241`), the drawer a `div` with no landmark, `user-select:none` on body (`app.css:36`) | reduced motion honoured everywhere (`app.css:67, 113, 142, 148, 157, 420, 437, 466`; `app.js:1137`) |
|
||||
| Ember light | molten `#B8731F` on white 3.81:1 and on bone 3.38:1; ember `#E04A14` on bone 3.61:1: fails AA on the key, the address and every live state (Q12) | same | same | same |
|
||||
| Site | dark passes (ash 6.97:1, ember 5.64:1 on obsidian); the homepage light section fails on links 3.08:1 and the eyebrow 3.97:1 (Q56) | skip link and one focus ring on every page (`nav.html:1`, `head.html:22`); explorer rows clickable with the hash link as the keyboard target | `aria-live="polite"` on strips refreshed every 2 and 10 s (Q54); `aria-live="off"` on the homepage live strip, correct; the DAG canvases `aria-hidden` with no alternative | reduced-motion rule on every page (`head.html:62`); `live.html` caps 30 fps and stops off screen; the homepage does not (Q55) |
|
||||
| Hub | amber `#FFB35C` and red on graphite pass (approximate from the tokens, `relay/ui.html:17, 65`) | buttons and tabs are native elements; no skip link | no live regions; the console is an operator tool | none needed |
|
||||
| Pool page | default light tokens, not audited beyond the formats | native | none | none |
|
||||
|
||||
### 4.2 Copy law
|
||||
|
||||
The em dash character, counted per shipped file: 0 in every `site/*.html` and `site/partials/*.html`, 0 in `app/igneum-app/ui/index.html` and `app.js`, 0 in `relay/index.html` and `relay/ui.html`, 0 in `pool/web/index.html`, 0 in `tools/community/discord-hooks.mjs`, 0 in `app/mac/*.swift` and `app/windows/host.cpp`, 0 in every `app/igneum-app/src/*.rs` user-facing string, 0 in the wallet's `ui/`. The rule holds everywhere that ships.
|
||||
|
||||
Forbidden strings (`site/forbidden-strings.txt`, 25 patterns) per shipped file, with what each hit is:
|
||||
|
||||
| File | Pattern | Count | What it is |
|
||||
|---|---|---|---|
|
||||
| `site/index.html` | `dl\.igneum` | 2 | the public download aliases (`:514-515`); the pattern is meant for the token path (Q52) |
|
||||
| `site/miner.html` | `dl\.igneum` | 5 | the same aliases and the HiveOS installation URL (`:549-557`) |
|
||||
| `site/wallet.html` | `dl\.igneum` | 1 | the wallet alias (`:371`) |
|
||||
| `site/downloads.json` | `dl\.igneum` | 1 | the `base` field |
|
||||
| `relay/ui.html` | `Hetzner` | 1 | the "Hetzner network" card title (`:377`); private console, rule does not apply, but one word |
|
||||
| `app/igneum-app/ui/app.js` | `the founder` | 1 | a code comment (`:270`); the ui bundle is outside every check (Q22) |
|
||||
| `tools/community/discord-hooks.mjs` | `MacBook`, `\+0100`, `tailscale`, `ts\.net`, `dl\.igneum` | 1, 1, 1, 1, 2 | the guard's own regexes (`:51, 59, 61`), the read-only fleet URL (`:31`), and `ts\.net` matching "stats.net" in a template (`:255`): false positives, none posted |
|
||||
| `pool/README.md` | `Hetzner`, `/opt/igneum`, `CLAUDE\.md` | 5, 2, 1 | the deploy section; not a site page, but it would fail the scrub if copied |
|
||||
|
||||
Every other pattern is 0 on every file. Two-beat antithesis and aphorisms in shipped copy: the headlines listed in Q62, the two Ember lines in Q23, the four Discord incident sentences in Q78.
|
||||
|
||||
### 4.3 Consistency of numbers across surfaces
|
||||
|
||||
| Quantity | Site home | Site live | Explorer | Ember | Hub | Discord | Pool page |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Hash rate | `fmtHash`: 1 decimal, MH/s or GH/s only, "pending" when null (`index.html:719`) | 1 decimal, kH to TH, unit in `<small>` (`live.html:329, 652`) | 1 decimal, kH to PH, integer under 1 kH/s (`lib/explorer.mjs:62-67`) | 1 decimal MH/s (`app.js:1450`) | 2 decimals GH/s, 1 decimal MH/s (`relay/ui.html:246`) | 1 decimal MH/s, 2 decimals GH/s (`discord-hooks.mjs:101-109`) | 2 decimals once scaled (`web/index.html:152`) |
|
||||
| Blocks per second | `toFixed(1)` "blocks / s" (`index.html:375, 722`) | `toFixed(2)` (`live.html:228, 650`) | inverted: "1.2 s per block" (`explorer.html:331`) | not shown | `toFixed(2)` (`relay/ui.html:370`) | 6-hour window | not shown |
|
||||
| Height, counts | `toLocaleString('en-GB')` "chain block" (`index.html:377`) | `compact()` "110.0k blocks" (`live.html:229, 651`) | full digits "Height" (`explorer.html:212, 328`) | `withCommas` | `nf()` separators | `en-GB` separators (`:99`) | raw integers, no separators (`web/index.html:163, 172`) |
|
||||
| Difficulty | not shown | `compact()` "126.81M" | "126.81 M" with a space | `compact()` | `nf()` | 6-hour delta | `toLocaleString()` with no locale |
|
||||
| Miner count | "vote keys active in 10 min (a card runs several)" (`index.html:374`) | "Identities" (`live.html:232`), table heading "Miner" (`:254`) | not shown | "ids" per card | "identities 10 min" | "56 vote keys active" and "93 voters" in one post (Q77) | "Miners / workers" |
|
||||
| IGN | not shown | not shown | reward 4 decimals, supply integer | 4 decimals (`app.js:1364`) | | 2 decimals (`fmtIgn`) | 4 on tiles, 6 in tables |
|
||||
| Price | none anywhere; "pounds a day" once (`index.html:510`); Ember has a `priceNow()` with a devnet zero | | | | | | |
|
||||
| Finality | "#N" or "paused" (`index.html:879`) | "#N, 3 min ago" plus the bar (`live.html:404, 512`) | none | "#N · 1 h ago" (`app.js:1350`) | "active" or "paused" | "checkpoint 6,842 at 55.x% of weight, 1 h ago, 93 voters" | none |
|
||||
|
||||
Row **Q85** (cross-cutting, 2 hours, site owner with the app owner): one shared formatter module (`site/lib/format.mjs`) for hash rate (1 decimal, unit ladder kH to PH), counts (`en-GB` separators, never `compact()` for the same quantity shown in full elsewhere), blocks per second (2 decimals, never inverted), difficulty (one spelling), IGN (4 decimals on tiles, 6 in tables), and one word for a vote identity ("vote key" on every surface); the app, hub, Discord and pool copy the same table as a Rust and a JS constant, with a test that renders the fixture values identically. Gate: the fixture renders byte-identical on all seven surfaces. Severity: the founder-visible (he reads three of these side by side every evening).
|
||||
|
||||
### 4.4 Error states, one table
|
||||
|
||||
| Condition | Ember | Site home | Site live | Explorer | Hub | Discord | Wallet | Pool |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Node down | "the node is not running", pill "node failed", "waiting for the node to sync" | "simulated preview · observer offline" after 30 s | "OFFLINE", "observer offline, last update N" | "observer offline, last known" | card red "silent 4h"; "observer STALE N s" | `observer_silent` after 180 s (not live) | "no node", "waiting for a node" | "difficulty 0, DAA 0", no word (Q68) |
|
||||
| API or relay unreachable | "The update failed." (Q11) | "live feed unavailable" | "api unreachable, scene frozen" | error on first load, silent after (Q10) | "offline: Failed to fetch", stale panels (Q6) | n/a | "The API did not answer: ..." (address page) | "stats unavailable" |
|
||||
| Finality paused | nothing (Q2) | "paused" counter, "final" label (Q5) | bar "finality paused: N% silent" (Q1) | nothing (Q10) | tile "paused", no cause (Q6) | nothing (not live), `preexisting` (Q7) | "paused" or "not checkpoint-checkable" | nothing (Q73) |
|
||||
| GPU lost | "Card removed: <name>. Its worker stopped." | | | | card telemetry drops | | | member drops from the count |
|
||||
| Job hung | "job running" notice | | | | "running" forever (Q40) | | | |
|
||||
|
||||
### 4.5 First-run and update flow
|
||||
|
||||
Section 3.10 carries both in full. The two facts to carry forward: a fresh machine on either OS meets two warnings before the first screen (Q3), and the update flow is one software key, one builder, one manifest, with a stale activation height that makes every update urgent (Q4, Q84) and no attestation by anyone outside the project (the frontier 3.7 baseline).
|
||||
|
||||
## 5. Already best in class (honest both ways)
|
||||
|
||||
1. Ember Tune: automatic per-card efficiency tuning with the money consequence on screen, measured 37 to 41 percent more hashes per watt on PC 1 (`ember-tune.md:254, 271`). No miner console ships it.
|
||||
2. The update path: Ed25519-signed manifest verified before parsing, sha256 of the artefact, staged swap, safe-moment rule, machine slots, automatic rollback after two failed starts (`ota.rs`, `manifest.rs`). HiveOS and NiceHash update without a safe-moment rule (approximate).
|
||||
3. The wallet verifies finality certificates itself with the node's consensus code (`finality.rs:101-121`); Touch ID is bound to the exact quote (`engine.rs:539`). No mainstream EVM wallet does either (approximate).
|
||||
4. The site: self-hosted fonts with fallback metrics, zero third-party scripts beyond two pinned modules, no analytics, no banner, HSTS preload, honest live states, a browser-side BLS verifier on the homepage (`verify/core.js`), a dated journey with gates and a public ledger of every criticism.
|
||||
5. The block page: parents, children, mergeset blues and reds, selected parent, shards with prover and lag, checkpoint fractions and certificates (`block.html:285-325`).
|
||||
6. The pool: every share CPU-verified at the node's own engine; the member's vote key in every header it hashes (`node.rs:47`), so pool share never becomes vote share.
|
||||
7. Discord: full sha256 and verify commands in every release post; a forbidden-string guard on every post; incident texts that say who is affected per tier.
|
||||
8. The hub: one phone-first page with every PC's miner, node, app and OTA state fused on one card, stopped and silent as different words, drop-anywhere uploads.
|
||||
9. The relay's wake long-poll: dependency-free, fake-clock tested, 30 a minute per IP.
|
||||
10. The ship pipeline: eleven checked, resumable steps with secrets scrubbed from every line and a signed inputs manifest verified in CI before any build (G13).
|
||||
|
||||
## 6. Open questions and what could not be run
|
||||
|
||||
- The pause's end was not observed: at 20:05:03Z (`live_state.updated_at`) no lock after 6842 had formed. Lane 3 expects the frozen table to expire at DAA 216,402, about 20:40Z. Whoever reads this after 21:00Z should add the resume time and whether any surface showed it.
|
||||
- Which commit the deployed relay runs is unknown from this tree (`relay/README.md` does not name it); Q8 assumes master. The deployed site is master by the GitHub integration.
|
||||
- Lighthouse numbers are estimates from source; no browser run was allowed. The gate for every site row is a real run.
|
||||
- The comparator claims are from memory; where a repository or page is named it is cited, otherwise approximate. The Stratum v2 reference was not cloned.
|
||||
- The fleet and node sections depend on the gpu-fleet worktree and the vendor forks, read tonight; the fleet's own page was not opened.
|
||||
- Hours are agent hours (the founder's rule); Q3 and Q80 also need the founder (certificates, GitHub billing).
|
||||
|
||||
## 7. Summary for the coordinator
|
||||
|
||||
Every shipped system was audited against named comparators from the code and the design docs, with 95 ledger rows (ids Q1 to Q105), each with a file or screen, a severity, hours, an owner and a gate; the top ten total 40 agent hours; every row summed is about 230 agent hours. The three findings that matter most: (1) during tonight's two-hour finality pause no public or operator surface could say why, because the node's `finality_reason` is dropped at `tools/observer/observer.mjs:720`, the frozen-table share that held the pause is reported by nothing, Ember has no pause state at all, the homepage drew "final", the explorer showed nothing and Discord was not live; one forced pause on Devnet 2 with a screenshot per surface is the gate (Q1, Q2, Q5, Q6, Q7, Q10). (2) A fresh machine meets two warnings before the first screen (ad hoc Mac signature with a quarantine strip, unsigned Windows installer plus a UAC prompt 20 to 50 s in), and the Windows installer cannot be rebuilt at all tonight because GitHub-hosted runners are blocked on billing (Q3, Q80). (3) The update path treats any passed activation height as "within 1,800 blocks", so every update since 0.3.14's manifest has been urgent, skipping the safe-moment rule (the PC 1 install under a measurement job); the same path has no finality input and is one software key, one builder, one manifest, which is the baseline frontier 3.7 moves from (Q4, Q83, Q84). Rules for main: the observer and every API carry `finality_reason`, `held_by` and `finality_provisional` before the public testnet; the `tools/ci` forbidden-string check covers `app/igneum-app/ui/`; one formatter table for every number on every surface (Q85).
|
||||
94
docs/analysis/income-tiers.md
Normal file
94
docs/analysis/income-tiers.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Mining income per tier on igneum-testnet-1
|
||||
|
||||
Generated by `tools/launch/income-tiers.mjs` from `tools/launch/income-tiers.json` (mission item 10, the consequences rule). Do not edit by hand: `node tools/launch/income-tiers.mjs` rewrites it and `--check` fails CI when the two disagree. Every rate and watt figure is a measurement with its source in the first table; nothing is estimated. Testnet coins have no value and the figures below say nothing about any price: the dollar columns are electricity, a cost the reader computes from a measured watt figure and a tariff they choose.
|
||||
|
||||
## The schedule the figures use
|
||||
|
||||
`EmissionSchedule::TESTNET_1` (the testnet genesis, decided 7 October 2026): 100 IGN a block at one block a second, a 90-day ramp from 10 percent, a monthly glide with a two-year half-life (each month pays 2^(-1/24) of the month before), then 1 percent of supply a year from about year 11.4. Of each block, 80 percent goes to the miner who found it and 20 percent to the proving pool. A miner whose key signs its checkpoints keeps the full 80 percent; an unsigned key gives a tenth of it to the proving pool.
|
||||
|
||||
| Moment | IGN a block to the miner | Of the launch figure |
|
||||
|---|---|---|
|
||||
| day 1 | 8.00 | 10.0% |
|
||||
| day 30 | 32.00 | 40.0% |
|
||||
| day 90 (ramp over) | 75.51 | 94.4% |
|
||||
| month 12 | 58.23 | 72.8% |
|
||||
| year 2 | 41.17 | 51.5% |
|
||||
|
||||
A solo card's expected income is its share of network hash times 86,400 blocks a day times the figure above. The tables use day 90, when the ramp is over. Before that, multiply by the ramp row; after that, by the glide row. A pool user gets the same expected amount minus the pool's fee (pool-0: 1 percent) with the variance taken out; the reference pool holds no balance and pays from the coinbase split.
|
||||
|
||||
## The cards, measured
|
||||
|
||||
6 October 2026, rented single-card Linux boxes (docs/analysis/prover-tiers-real-cards.md, bench log 'Prover tiers on real cards: the rented fleet, 6 October 2026'): the 0.3.12 miner on the ten-field override, the miner alone on the card, nvidia-smi power at the same second. The RX 9070 XT row is the telemetry run of 5 October 2026 on a Windows rig (bench log 'Power, heat, fans and clocks, measured'); the Apple row is the Metal bench of 4 October 2026 (site/miner-bench.json, class v4). The class v4 rates on the live devnet run lower than the ten-field rates on the same card (RTX 5090: 124.2 MH/s class v4 against 98.5 on the rented box under a different driver and container, docs/bench-log.md); the table is re-generated at the testnet cut from that class.
|
||||
|
||||
| Tier | Card | MH/s | W | MH/W | Source |
|
||||
|---|---|---|---|---|---|
|
||||
| 8 GB | NVIDIA RTX 4060 Ti 8 GB | 19.07 | 72.6 | 0.263 | docs/analysis/prover-tiers-real-cards.md, RTX 4060 Ti 8 GB row |
|
||||
| 8 GB | NVIDIA RTX 4060 | 17.07 | owed | owed | docs/analysis/prover-tiers-real-cards.md, RTX 4060 row (the sampler read 0.0 W on that host: power OWED) |
|
||||
| 12 GB | NVIDIA RTX 3060 | 23.78 | 103.7 | 0.229 | docs/analysis/prover-tiers-real-cards.md, RTX 3060 row |
|
||||
| 12 GB | NVIDIA RTX 4070 | 24.99 | 91.1 | 0.274 | docs/analysis/prover-tiers-real-cards.md, RTX 4070 row |
|
||||
| 12 GB | NVIDIA RTX 5070 | 41.89 | 137.0 | 0.306 | docs/analysis/prover-tiers-real-cards.md, RTX 5070 row |
|
||||
| 16 GB | NVIDIA RTX 4060 Ti 16 GB | 17.58 | 72.3 | 0.243 | docs/analysis/prover-tiers-real-cards.md, RTX 4060 Ti 16 GB row |
|
||||
| 16 GB | AMD RX 9070 XT | 18.60 | 199.0 | 0.093 | docs/bench-log.md, 'Power, heat, fans and clocks, measured' (5 October 2026): 18.6 to 19.2 MH/s, 199 W read by the ADLX helper at 100 percent busy |
|
||||
| 24 GB | NVIDIA RTX 3090 | 37.79 | 228.8 | 0.165 | docs/analysis/prover-tiers-real-cards.md, RTX 3090 row |
|
||||
| 24 GB | NVIDIA RTX 4090 | 52.25 | 183.1 | 0.285 | docs/analysis/prover-tiers-real-cards.md, RTX 4090 row |
|
||||
| 32 GB | NVIDIA RTX 5090 | 98.48 | 258.2 | 0.381 | docs/analysis/prover-tiers-real-cards.md, RTX 5090 row (the rented box; 124.2 MH/s class v4 on a desktop, site/miner-bench.json) |
|
||||
| Apple | Apple M5 Max (40 GPU cores) | 26.70 | owed | owed | site/miner-bench.json, Apple M5 Max class v4 row, 4 October 2026 (no power reading: OWED) |
|
||||
|
||||
## IGN a day per card, solo, after the ramp (day 90)
|
||||
|
||||
Three network sizes, because income is a share of the network and nobody knows the network before it exists. 1 GH/s, 10 GH/s, 100 GH/s are reference sizes, not forecasts. The live network's estimate is on /live; divide by it.
|
||||
|
||||
| Tier | Card | at 1 GH/s | at 10 GH/s | at 100 GH/s |
|
||||
|---|---|---|---|---|
|
||||
| 8 GB | NVIDIA RTX 4060 Ti 8 GB | 124,414 | 12,441 | 1,244.1 |
|
||||
| 8 GB | NVIDIA RTX 4060 | 111,366 | 11,137 | 1,113.7 |
|
||||
| 12 GB | NVIDIA RTX 3060 | 155,142 | 15,514 | 1,551.4 |
|
||||
| 12 GB | NVIDIA RTX 4070 | 163,036 | 16,304 | 1,630.4 |
|
||||
| 12 GB | NVIDIA RTX 5070 | 273,293 | 27,329 | 2,732.9 |
|
||||
| 16 GB | NVIDIA RTX 4060 Ti 16 GB | 114,693 | 11,469 | 1,146.9 |
|
||||
| 16 GB | AMD RX 9070 XT | 121,348 | 12,135 | 1,213.5 |
|
||||
| 24 GB | NVIDIA RTX 3090 | 246,544 | 24,654 | 2,465.4 |
|
||||
| 24 GB | NVIDIA RTX 4090 | 340,882 | 34,088 | 3,408.8 |
|
||||
| 32 GB | NVIDIA RTX 5090 | 642,489 | 64,249 | 6,424.9 |
|
||||
| Apple | Apple M5 Max (40 GPU cores) | 174,192 | 17,419 | 1,741.9 |
|
||||
| Rig | 6 x NVIDIA RTX 4070 | 978,217 | 97,822 | 9,782.2 |
|
||||
|
||||
## Electricity a day, and the electricity cost of one mined IGN
|
||||
|
||||
Electricity a day = measured watts x 24 h x the tariff. The cost of one IGN divides that by the day-90 figure at 10 GH/s; at another network size it scales with the network (ten times the network, ten times the cost per coin). The three tariffs: USD 0.05 a kWh is cheap industrial power, USD 0.10 is a US retail rate, USD 0.25 is a UK retail rate at today's level (each approximate; tariffs move, the watt figures do not).
|
||||
|
||||
| Tier | Card | USD/day at 0.05/kWh | USD/day at 0.1/kWh | USD/day at 0.25/kWh | USD per IGN at 0.05/kWh | USD per IGN at 0.1/kWh | USD per IGN at 0.25/kWh |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 8 GB | NVIDIA RTX 4060 Ti 8 GB | 0.087 | 0.174 | 0.436 | 0.000007 | 0.000014 | 0.000035 |
|
||||
| 8 GB | NVIDIA RTX 4060 | owed | owed | owed | owed | owed | owed |
|
||||
| 12 GB | NVIDIA RTX 3060 | 0.124 | 0.249 | 0.622 | 0.00000802 | 0.000016 | 0.0000401 |
|
||||
| 12 GB | NVIDIA RTX 4070 | 0.109 | 0.219 | 0.547 | 0.00000671 | 0.0000134 | 0.0000335 |
|
||||
| 12 GB | NVIDIA RTX 5070 | 0.164 | 0.329 | 0.822 | 0.00000602 | 0.000012 | 0.0000301 |
|
||||
| 16 GB | NVIDIA RTX 4060 Ti 16 GB | 0.087 | 0.174 | 0.434 | 0.00000756 | 0.0000151 | 0.0000378 |
|
||||
| 16 GB | AMD RX 9070 XT | 0.239 | 0.478 | 1.19 | 0.0000197 | 0.0000394 | 0.0000984 |
|
||||
| 24 GB | NVIDIA RTX 3090 | 0.275 | 0.549 | 1.37 | 0.0000111 | 0.0000223 | 0.0000557 |
|
||||
| 24 GB | NVIDIA RTX 4090 | 0.220 | 0.439 | 1.10 | 0.00000645 | 0.0000129 | 0.0000322 |
|
||||
| 32 GB | NVIDIA RTX 5090 | 0.310 | 0.620 | 1.55 | 0.00000482 | 0.00000964 | 0.0000241 |
|
||||
| Apple | Apple M5 Max (40 GPU cores) | owed | owed | owed | owed | owed | owed |
|
||||
| Rig | 6 x NVIDIA RTX 4070 (a six-card rig of the 12 GB card, the rig's own power is six times the card's plus the host (the host is not measured here)) | 0.656 | 1.31 | 3.28 | 0.00000671 | 0.0000134 | 0.0000335 |
|
||||
|
||||
## What it means per tier, and what is being done
|
||||
|
||||
- **One 8 GB card.** NVIDIA RTX 4060 Ti 8 GB: 19.1 MH/s at 73 W, 12,441 IGN a day at 10 GH/s after the ramp, electricity 0.174 a day at USD 0.10 a kWh. Mines; proves only core-only shards (docs/analysis/prover-tiers-real-cards.md). The record says this tier is first under power in every exit (docs/analysis/mission/past.md section 3): the glide never halves it overnight, and the card games when the income goes. Being done: the Cards screen shows the expected MH/s, W and blocks a day before the first share (mission item 6).
|
||||
- **One 12 GB card.** NVIDIA RTX 5070: 41.9 MH/s at 137 W, 27,329 IGN a day at 10 GH/s after the ramp, electricity 0.329 a day at USD 0.10 a kWh. Mines and proves beside the miner on the patched server. Being done: the same Cards line; the proving tier sentence names the shard size the card takes.
|
||||
- **One 16 GB card.** NVIDIA RTX 4060 Ti 16 GB: 17.6 MH/s at 72 W, 11,469 IGN a day at 10 GH/s after the ramp, electricity 0.174 a day at USD 0.10 a kWh. Mines and proves; the AMD row is the measured 9070 XT, 199 W for 18.6 MH/s, so per watt it is about a fifth of the NVIDIA cards of its tier and the electricity cost per coin is the highest in the table. Being done: the AMD read-width experiment (docs/plans/read-width.md); until it lands the AMD tier is told its per-watt figure before it buys.
|
||||
- **One 24 or 32 GB card.** NVIDIA RTX 5090: 98.5 MH/s at 258 W, 64,249 IGN a day at 10 GH/s after the ramp, electricity 0.620 a day at USD 0.10 a kWh. The last GPU standing in every exit on record, and the only tier that proves the full shard beside the miner. Being done: the proving pool is the second income for this tier and the Earnings screen shows shards and IGN beside blocks.
|
||||
- **A rig.** 6 x NVIDIA RTX 4070: cards times the card figure; the host's own draw is not measured and is owed. Rigs followed income across chains inside days on every chain in the record; Igneum expects no loyalty and pays staying keys a vote (30 days of blocks).
|
||||
- **A pool user.** The same expected IGN minus the pool's fee, variance removed; pool-0 holds no balance and the member's own key is in every block it finds, so a pool's vote is the member's weight. Being done: the pool finished (mission item 11).
|
||||
- **Windows, Linux, macOS.** The rates above are Linux containers (NVIDIA) and Windows (AMD); the Mac row is Metal on Apple silicon with no power reading. The same card on another operating system is within the driver's margin, not measured here: owed per platform at the testnet cut.
|
||||
|
||||
## Owed
|
||||
|
||||
- Intel discrete (Arc): no measurement; the tier is listed with no number until one exists
|
||||
- RTX 4060 and Apple M5 Max wall power: the rows carry the rate and no electricity column
|
||||
- every row at class v4 on the testnet cut: the rates above are the ten-field class of 6 October 2026 on rented boxes
|
||||
- a rig host's own draw: a rig row is cards times the card figure, host excluded
|
||||
|
||||
## Check
|
||||
|
||||
`node tools/launch/income-tiers.mjs --check` (in `tools/ci/pre-push.sh`): the file equals the generator's output. `node --test tools/launch/income-tiers.test.mjs`: the schedule arithmetic against the fork's constants (100 MH/s on 100 GH/s after the ramp = 6,912 IGN a day, the figure in docs/analysis/horizon-2026-10.md; day 1 pays 10 percent; one month step pays 97.153 percent of the one before).
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue