Merge origin/master into ca3-coord: Counter ASIC 3.0 complete (every gate green, P2 green, P1 written); the drive-ref check skips single-quoted here-strings and the copied-sources check reads code lines only (master's CI red on 0dc4392); main's decisions and the close in the status file
This commit is contained in:
commit
88f5026d41
138 changed files with 12217 additions and 545 deletions
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
|
@ -73,16 +73,24 @@ jobs:
|
||||||
run: bash tools/ci/copied-sources-check.sh
|
run: bash tools/ci/copied-sources-check.sh
|
||||||
- name: override params files parse with no duplicate key (the duplicate-field class, 6 October 2026)
|
- name: override params files parse with no duplicate key (the duplicate-field class, 6 October 2026)
|
||||||
run: bash tools/ci/override-json-check.sh
|
run: bash tools/ci/override-json-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
|
- name: the signer is never piped into head
|
||||||
run: bash tools/ci/signer-pipe-check.sh
|
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)
|
- 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
|
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)
|
- 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
|
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
|
- name: pinned guest programs match their manifest and are built only by pin-guests.sh
|
||||||
run: bash tools/ci/pinned-guests-check.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)
|
- 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
|
run: bash tools/ci/prover-socket-check.sh
|
||||||
|
- name: commit-string gate self-test (the empty-commit class of 6 October 2026; the gate itself runs in build-remote.sh, cross-remote.sh and cross-build.sh on every node binary)
|
||||||
|
run: bash tools/ci/commit-string-check.sh --self-test
|
||||||
- name: no secret file names and no 64-hex secrets in the tree (self-test first, then the tree)
|
- 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
|
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)
|
- name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors)
|
||||||
|
|
@ -95,6 +103,6 @@ jobs:
|
||||||
- name: ship tool self-test (version bump, the dl-both and public manifest helpers)
|
- name: ship tool self-test (version bump, the dl-both and public manifest helpers)
|
||||||
run: node tools/ship-app.mjs --self-test
|
run: node tools/ship-app.mjs --self-test
|
||||||
- name: relay unit tests (parsers, secret compare, the wake endpoint)
|
- 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
|
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)
|
- 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
|
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
|
||||||
|
|
|
||||||
56
CLAUDE.md
56
CLAUDE.md
|
|
@ -51,22 +51,36 @@ Applied 3 Oct 2026: all 15 Igneum domains at GoDaddy (also .art, .email, .pro, .
|
||||||
## Browser profile (standing rule, 3 October 2026)
|
## Browser profile (standing rule, 3 October 2026)
|
||||||
Every Igneum task in Claude in Chrome runs in the Chrome profile "josh (igneum.network)". Josh also has profiles for VIVANMN, QUANTUM, PEASEHILL, THRSTY, GEMVEN and JBM EXEC. Confirm the active profile before acting; if it is the wrong one, switch or stop and say so. Never carry Igneum sign-ins, posts or purchases through another brand's profile.
|
Every Igneum task in Claude in Chrome runs in the Chrome profile "josh (igneum.network)". Josh also has profiles for VIVANMN, QUANTUM, PEASEHILL, THRSTY, GEMVEN and JBM EXEC. Confirm the active profile before acting; if it is the wrong one, switch or stop and say so. Never carry Igneum sign-ins, posts or purchases through another brand's profile.
|
||||||
|
|
||||||
## Running agents on this Mac (standing rule, 4 October 2026)
|
## Running agents on this Mac (standing rule, 4 October 2026; builds moved to igneum-build-1 on 6 October 2026)
|
||||||
- Code and documents in parallel; BUILDS and MEASUREMENTS one at a time through `tools/lock/with-lock.sh build|measure|run <cmd>`
|
- Code and documents in parallel; the Mac's own BUILDS and MEASUREMENTS one at a time through `tools/lock/with-lock.sh build|measure|run <cmd>`
|
||||||
(`measure` blocks builds too: hash rate, latency in ms, power; `run` is for functional runs such as test networks, attack
|
(`measure` blocks builds too: hash rate, latency in ms, power; `run` is for functional runs such as test networks, attack
|
||||||
harnesses and simulators whose outputs are counts, locks, forks or seconds, and lets builds continue; use the MAIN
|
harnesses and simulators whose outputs are counts, locks, forks or seconds, and lets builds continue; use the MAIN
|
||||||
checkout's script, /Users/joshm/Projects/igneum/tools/lock/with-lock.sh, from any worktree). A number taken while
|
checkout's script, /Users/joshm/Projects/igneum/tools/lock/with-lock.sh, from any worktree). A number taken while
|
||||||
another build or simulation ran is not a number.
|
another build or simulation ran is not a number. `~/.config/igneum/build-slots` (1 to 3) caps how many Mac builds run at once.
|
||||||
- Builds and test suites run on the PCs, not on this Mac (standing rule, 5 October 2026, Josh: "since building on the pc is so
|
- BUILDS GO TO THE BOX (standing rule, 6 October 2026, main's decision after the measurements in docs/plans/build-server.md):
|
||||||
much faster, lets fully utilise both of them"): Linux and Windows binaries and every cargo test suite go out as `build` jobs
|
every Linux and Windows cargo build and every Linux test suite from every agent runs on igneum-build-1 (Hetzner AX162,
|
||||||
(`node tools/build-job.mjs run --target ae432dc7|1ccfe586 ...`, PC 1 = ae432dc7 for builds, PC 2 = 1ccfe586 for test
|
96 threads, 128 GB, `ssh -i ~/.ssh/igneum_ed25519 build@188.40.146.49`) through `tools/build-remote.sh [-- cargo args]`
|
||||||
suites; one job per machine at a time; results come back through the relay, every sha256 checked). Measured 5 October:
|
and `tools/cross-remote.sh` from the crate directory of the agent's own worktree. Measured 6 October: clean node build
|
||||||
PC 1 built the Linux and Windows node and app and ran both test suites in 7 min 38 s cold, 5 min 09 s warm. The Mac
|
1 min 27 s (Mac 12 to 18 min), incremental 7 s (Mac 2 to 15 min), Windows cross 1 min 44 s (Mac 4 min 49 s to 12 min 28 s).
|
||||||
builds only the macOS binaries and the DMG, under the build lock, and `~/.config/igneum/build-slots` (1 to 3) caps how
|
The box has its own slot files (`/srv/builds/_locks/build-<k>`, count in `/srv/builds/_locks/slots`, 1 today); a remote
|
||||||
many Mac builds run at once.
|
build takes one of those, never a Mac slot. Sources travel as HEAD through the bare mirrors `/srv/igneum.git` and
|
||||||
|
`/srv/igneum-node.git` plus an rsync overlay of uncommitted changes (re-stamped); `/srv/builds/<worktree>` mirrors the
|
||||||
|
worktree root; artefacts come back into `<crate>/target-remote/`, never `target/` (they are x86_64 Linux and Windows
|
||||||
|
binaries). Every run writes one line to `/srv/builds/_log/builds.jsonl` (the worker dashboard reads it); `IGNEUM_AGENT=<name>`
|
||||||
|
tags it. The PCs keep only jobs that need their GPUs or the Windows runtime (measurements, Windows test suites, installer
|
||||||
|
smoke runs: `node tools/build-job.mjs run --target ae432dc7|1ccfe586 ...`, PC 1 = ae432dc7, PC 2 = 1ccfe586). The Mac
|
||||||
|
keeps macOS binaries, the DMG and Metal tests, under the build lock. The box never hosts a live-devnet node and never
|
||||||
|
holds a secret; its Devnet 2 seed, when it starts, runs its own unit on 26611 (ufw open). Setup and re-provision:
|
||||||
|
`infra/build-server/run-from-mac.sh <ip>` (idempotent); the rustc pin is `RUST_TOOLCHAIN` in `infra/build-server/provision.sh`
|
||||||
|
(1.99.0; there is no rust-toolchain file, add one) and build-remote.sh refuses a version mismatch. Nobody deletes another
|
||||||
|
worktree's target dir on the box. Windows exes are reproducible (`-Wl,--no-insert-timestamp` in cross-remote.sh, the Mac's
|
||||||
|
cross-build.sh and the PC job alike); a node binary whose strings lack its commit fails `tools/ci/commit-string-check.sh`
|
||||||
|
(the empty-commit class, 6 October 2026: kaspa-build-info embeds the hash only from a `.git` directory on a branch and
|
||||||
|
never re-runs once empty, so every Mac worktree build and every PC job build had shipped without it; build-remote.sh,
|
||||||
|
cross-remote.sh, cross-build.sh and the PC job now carry the two-step: a branch or minimal `.git`, and
|
||||||
|
`cargo clean --release -p kaspa-build-info` on a new commit).
|
||||||
- Every agent works in its own git worktree (`git worktree add ../igneum-wt-<name>`), never the shared checkout, and
|
- Every agent works in its own git worktree (`git worktree add ../igneum-wt-<name>`), never the shared checkout, and
|
||||||
stages only its own files. Builds: `nice -n 19`, at most 4 cargo jobs. Windows builds go to the GitHub runner, Linux
|
stages only its own files. Mac builds (macOS binaries only): `nice -n 19`, at most 4 cargo jobs.
|
||||||
binaries come from `infra/cross/build-linux.sh`; nothing is built on a server.
|
|
||||||
- Never launch /Applications/Google Chrome.app headless (it blocks the owner's Chrome); use the built-in browser pane.
|
- Never launch /Applications/Google Chrome.app headless (it blocks the owner's Chrome); use the built-in browser pane.
|
||||||
- Keep the Mac on mains with the 140 W charger; on 4 October it hibernated at 1% battery and took node 1 and the observer down.
|
- Keep the Mac on mains with the 140 W charger; on 4 October it hibernated at 1% battery and took node 1 and the observer down.
|
||||||
- A source tree copied to another machine (rsync, zip, tar, scp) is re-stamped with `touch` before anything builds it,
|
- A source tree copied to another machine (rsync, zip, tar, scp) is re-stamped with `touch` before anything builds it,
|
||||||
|
|
@ -84,4 +98,20 @@ Every Igneum task in Claude in Chrome runs in the Chrome profile "josh (igneum.n
|
||||||
Josh: "this question and answer should have not needed to be asked ... so that suggestions like this get made without me" (the 12 GB mine-and-prove question, which followed from the 15.6 GB measurement and should have been raised by the agent that measured it). Rule: an agent that measures or reports a number also states, in the same report, what the number means for each user tier and what it will do about it, before anyone asks. The tiers: a home miner with one 8 GB card, one 12 GB card, one 16 GB card, one 24 or 32 GB card; a rig; a pool user; each on Windows, Linux and macOS; each on NVIDIA, AMD and Apple (Intel when it exists). A report that says "peak 15.6 GB" without "so 12 GB cards cannot do both; here is the profile that fits them, measuring now" is incomplete and goes back. The same for hash rates (per watt and per pound for the tiers), times (what deadline it fits), sizes (what disk or memory it needs), and prices. A standing reviewer agent reads every status file, bench-log entry and plan for missed consequences and opens the work; the coordinator does not wait for Josh to notice.
|
Josh: "this question and answer should have not needed to be asked ... so that suggestions like this get made without me" (the 12 GB mine-and-prove question, which followed from the 15.6 GB measurement and should have been raised by the agent that measured it). Rule: an agent that measures or reports a number also states, in the same report, what the number means for each user tier and what it will do about it, before anyone asks. The tiers: a home miner with one 8 GB card, one 12 GB card, one 16 GB card, one 24 or 32 GB card; a rig; a pool user; each on Windows, Linux and macOS; each on NVIDIA, AMD and Apple (Intel when it exists). A report that says "peak 15.6 GB" without "so 12 GB cards cannot do both; here is the profile that fits them, measuring now" is incomplete and goes back. The same for hash rates (per watt and per pound for the tiers), times (what deadline it fits), sizes (what disk or memory it needs), and prices. A standing reviewer agent reads every status file, bench-log entry and plan for missed consequences and opens the work; the coordinator does not wait for Josh to notice.
|
||||||
|
|
||||||
## A job never quits or restarts the installed app it did not start (standing rule, 5 October 2026, 23:05 UTC)
|
## A job never quits or restarts the installed app it did not start (standing rule, 5 October 2026, 23:05 UTC)
|
||||||
Ember Tune's PC 1 playbook read the installed app's `app.url` file and POSTed `/api/quit` to it when its own test budget was spent, taking PC 1 off the network from 22:31Z (141 MH/s gone, the 0.3.11 Windows build blocked) with nobody awake to relaunch. Rule: a test engine started by a job runs on its own port and data dir with its own URL file, and a job may quit, pause, resume or restart only an engine it started itself (the URL it created); the installed app is touched only through the signed `restart` and `update-now` job kinds. `tools/ci` carries a check (`playbook-quit-check`) that fails a playbook which reads `%LOCALAPPDATA%\igneum\app\app.url` or `~/Library/Application Support/Igneum/app/app.url` and sends quit, pause or resume to it; the reviewer's row C35 is the record.
|
Ember Tune's PC 1 playbook started a second engine, that engine's own updater saw itself as 0.3.9 and launched the per-user installer, and the installer's stop step POSTed `/api/quit` to the installed app, taking PC 1 off the network from 22:31Z (141 MH/s gone, the 0.3.11 Windows build blocked) with nobody awake to relaunch (corrected 6 October 2026 from the collected log; the playbook's own quit never fired and pointed at its scratch engine; fixed at e600e63: a second engine never runs the updater). Rule: a test engine started by a job runs on its own port and data dir with its own URL file, and a job may quit, pause, resume or restart only an engine it started itself (the URL it created); the installed app is touched only through the signed `restart` and `update-now` job kinds, and a job that needs the installed app's miners out of the way uses the runner's `--stop-miners` (the runner stops them before the script and restarts them on any exit), never `/api/pause` or `/api/resume` from the script, not even with a finally block (ruling 6 October 2026: a script that dies before its finally leaves the box paused unattended). `tools/ci` carries a check (`playbook-quit-check`) that fails a playbook which reads `%LOCALAPPDATA%\igneum\app\app.url` or `~/Library/Application Support/Igneum/app/app.url` and sends quit, pause or resume to it; the reviewer's row C35 is the record.
|
||||||
|
|
||||||
|
## Devnet 2 gate and activation rules (standing rule, 6 October 2026, 16:4x UTC)
|
||||||
|
|
||||||
|
Josh's ruling after the DAA 198,000 incident (a fixed-height activation crossed while the fleet was still updating: a two-sided chain, a 229-block reorg, execution reset to genesis on every node, proving at zero for an hour): releases stay hourly; what changes is where they land first.
|
||||||
|
- Devnet 2 is the rented fleet as its own staging chain (own genesis and network id, refused by live peers at the handshake). Every release, activation and tuning kit crosses Devnet 2 through `tools/fleet/devnet2-gate.sh` (PASS = zero rejected blocks across the activation, no reorg over depth 3, exec roots agreeing on every box, a segment record paid, every node on the new version) before the live devnet or any of Josh's machines sees it. The shipper does not build the live object before the PASS line.
|
||||||
|
- No fixed-height activation on the live devnet. A consensus change flips when 95 percent of mining weight over a window signals the new object, with a floor height as the backstop; the class v4 cut is the first to carry it (status file gates P1 and P2).
|
||||||
|
- A deep reorg never resets execution; a snapshot a node cannot read fails loudly; the p2p snapshot path refuses a snapshot below the node's tip or the restart; a hands script never pgreps its own command line.
|
||||||
|
- Main checks an agent's number against the log or the chain before relaying it to Josh, or labels it unverified.
|
||||||
|
|
||||||
|
## A rule row closes only with its check (standing rule, 6 October 2026, 18:4x UK)
|
||||||
|
|
||||||
|
Josh, after the pgrep self-match hit twice in one day (the shipper's hands script at lunchtime, the fleet's wave script at 17:1xZ: a `pgrep -f "<pattern>"` whose literal sat in the calling shell's own command line, so the check always passed and no node ever started): "again wasted time". Rules:
|
||||||
|
- A bug class found today gets its `tools/ci` check merged on master the same hour, or the rule row stays OPEN and main says so. A rule row without a check is not closed.
|
||||||
|
- Box operations (ssh to a rented box, install a payload, start a node, wait for sync, read height, peers and exec tip, start a miner or prover) live in ONE shared, tested library (`tools/fleet/lib/`), exercised against a Devnet 2 box by a test; agents call it and never write their own copy in bash or PowerShell.
|
||||||
|
- A watcher or collector verifies the chain-side fact (height, peers, exec tip, paid records), never a reported rate or a process name.
|
||||||
|
- `pgrep -f` / `ps | grep` with a literal pattern is banned in scripts; use the bracket form `[i]gneumd` or `pgrep -x` on the binary name (CI check: pgrep-self-match-check).
|
||||||
|
|
|
||||||
2
app/igneum-app/Cargo.lock
generated
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "igneum-app"
|
name = "igneum-app"
|
||||||
version = "0.3.11"
|
version = "0.3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "igneum-app"
|
name = "igneum-app"
|
||||||
version = "0.3.11"
|
version = "0.3.13"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
1 ICON "igneum.ico"
|
1 ICON "igneum.ico"
|
||||||
|
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,3,11,0
|
FILEVERSION 0,3,13,0
|
||||||
PRODUCTVERSION 0,3,11,0
|
PRODUCTVERSION 0,3,13,0
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
FILEFLAGS 0x0L
|
FILEFLAGS 0x0L
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "Igneum"
|
VALUE "CompanyName", "Igneum"
|
||||||
VALUE "FileDescription", "Igneum Miner engine"
|
VALUE "FileDescription", "Igneum Miner engine"
|
||||||
VALUE "FileVersion", "0.3.11"
|
VALUE "FileVersion", "0.3.13"
|
||||||
VALUE "InternalName", "igneum-app"
|
VALUE "InternalName", "igneum-app"
|
||||||
VALUE "LegalCopyright", "Igneum contributors"
|
VALUE "LegalCopyright", "Igneum contributors"
|
||||||
VALUE "OriginalFilename", "igneum-app.exe"
|
VALUE "OriginalFilename", "igneum-app.exe"
|
||||||
VALUE "ProductName", "Igneum Miner"
|
VALUE "ProductName", "Igneum Miner"
|
||||||
VALUE "ProductVersion", "0.3.11"
|
VALUE "ProductVersion", "0.3.13"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,16 @@ pub struct CardPref {
|
||||||
pub sweep_watts: f64,
|
pub sweep_watts: f64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sweep_mhs: f64,
|
pub sweep_mhs: f64,
|
||||||
|
/// Ember Tune (src/ember.rs): the clock cap the last tune chose (0 = unlocked), the driver and program class it
|
||||||
|
/// ran under (a change makes the card due again), and the plan that produced it (full | confirm | baseline)
|
||||||
|
#[serde(default)]
|
||||||
|
pub sweep_clock_mhz: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sweep_driver: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sweep_class: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sweep_source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -70,9 +80,16 @@ pub struct Settings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prove: bool,
|
pub prove: bool,
|
||||||
/// The efficiency sweep (src/sweep.rs): once after install, then weekly, each NVIDIA card's cap is stepped from
|
/// The efficiency sweep (src/sweep.rs): once after install, then weekly, each NVIDIA card's cap is stepped from
|
||||||
/// 100% to 50% on the live program and held at the best MH per watt. Default on. A pinned card is skipped.
|
/// 100% to 50% on the live program and held at the best MH per watt. Default off; implied by `power_control`
|
||||||
#[serde(default = "yes")]
|
/// (on when that is switched on, never effective while it is off). A pinned card is skipped.
|
||||||
|
#[serde(default)]
|
||||||
pub sweep: bool,
|
pub sweep: bool,
|
||||||
|
/// Power control (Josh, 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,
|
||||||
/// When this install first ran (unix s), for the "first hour after install" sweep.
|
/// When this install first ran (unix s), for the "first hour after install" sweep.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub installed_at: u64,
|
pub installed_at: u64,
|
||||||
|
|
@ -102,16 +119,48 @@ fn yes() -> bool {
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> 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, 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, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
|
/// What a measurement engine (`--sweep`, started by a job beside the installed app) runs with, whatever the copied
|
||||||
|
/// file says: no remote jobs (run 4, 6 October 2026: the second engine fetched the jobs file and ran 96 old jobs
|
||||||
|
/// inside its scratch root), no updates, no proving, not paused, the tune on, Power control off (only an engine
|
||||||
|
/// that is itself elevated controls NVIDIA, through the probe's `direct`), every card due and unpinned. The file
|
||||||
|
/// on disk is never changed: the playbook copies the installed app's settings verbatim (a PowerShell JSON round
|
||||||
|
/// trip rewrote big integers as doubles and the engine read the whole file as defaults: no payout address, every
|
||||||
|
/// card off).
|
||||||
|
pub fn for_measurement(mut self) -> Settings {
|
||||||
|
self.remote_jobs = false;
|
||||||
|
self.auto_update = false;
|
||||||
|
self.prove = false;
|
||||||
|
self.paused = false;
|
||||||
|
self.sweep = true;
|
||||||
|
self.power_control = false;
|
||||||
|
self.setup_done = true;
|
||||||
|
for p in self.cards.values_mut() {
|
||||||
|
p.sweep_at = 0;
|
||||||
|
p.pinned = false;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(path: &Path) -> Settings {
|
pub fn load(path: &Path) -> Settings {
|
||||||
let mut s: Settings = std::fs::read_to_string(path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default();
|
let mut s: Settings = std::fs::read_to_string(path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default();
|
||||||
|
let mut dirty = false;
|
||||||
if s.installed_at == 0 {
|
if s.installed_at == 0 {
|
||||||
// an install from before the sweep existed counts as installed now: it gets its first-hour sweep
|
// an install from before the sweep existed counts as installed now: it gets its first-hour sweep
|
||||||
s.installed_at = crate::platform::unix_now();
|
s.installed_at = crate::platform::unix_now();
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if s.sweep && !s.power_control {
|
||||||
|
// the sweep is implied by power control (5 October 2026): an install from before that setting carried
|
||||||
|
// sweep = true by default; it no longer prompts on its own
|
||||||
|
s.sweep = false;
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
if dirty {
|
||||||
s.save(path);
|
s.save(path);
|
||||||
}
|
}
|
||||||
s
|
s
|
||||||
|
|
@ -347,6 +396,18 @@ mod tests {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_measurement_engine_overrides_the_copied_settings_in_memory() {
|
||||||
|
let mut s = Settings { remote_jobs: true, auto_update: true, prove: true, paused: true, sweep: false, power_control: true, address: "0xabc".into(), ..Default::default() };
|
||||||
|
s.cards.insert("nvidia:0:x".into(), CardPref { enabled: true, identities: 8, sweep_at: 1_791_000_000, pinned: true, power_pct: 70, ..Default::default() });
|
||||||
|
let m = s.for_measurement();
|
||||||
|
assert!(!m.remote_jobs && !m.auto_update && !m.prove && !m.paused && m.sweep && !m.power_control && m.setup_done);
|
||||||
|
assert_eq!(m.address, "0xabc", "the payout address is the installed app's");
|
||||||
|
let c = &m.cards["nvidia:0:x"];
|
||||||
|
assert!(c.enabled && c.identities == 8 && c.power_pct == 70, "the card's choices stay");
|
||||||
|
assert!(c.sweep_at == 0 && !c.pinned, "every card is due and unpinned");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manifest_url_round_trips_through_the_token() {
|
fn manifest_url_round_trips_through_the_token() {
|
||||||
assert_eq!(manifest_url_for_token("abc123"), "https://dl.igneum.network/dl/abc123/igneum-app-latest.json");
|
assert_eq!(manifest_url_for_token("abc123"), "https://dl.igneum.network/dl/abc123/igneum-app-latest.json");
|
||||||
|
|
@ -439,3 +500,18 @@ mod tests {
|
||||||
assert!(p.node_override_params.is_none());
|
assert!(p.node_override_params.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod fixture_tests {
|
||||||
|
/// `IGNEUM_TEST_SETTINGS=<path> cargo test settings_fixture`: parses a real settings.json with this crate's
|
||||||
|
/// struct and prints what it read (6 October 2026: PC 1's copied file read as defaults; this names the field).
|
||||||
|
#[test]
|
||||||
|
fn settings_fixture_parses_when_given() {
|
||||||
|
let Ok(p) = std::env::var("IGNEUM_TEST_SETTINGS") else { return };
|
||||||
|
let t = std::fs::read_to_string(&p).unwrap();
|
||||||
|
match serde_json::from_str::<super::Settings>(&t) {
|
||||||
|
Ok(s) => println!("parsed: address {} cards {} remote_jobs {} setup_done {}", s.address, s.cards.len(), s.remote_jobs, s.setup_done),
|
||||||
|
Err(e) => panic!("the crate refuses the file: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ pub struct Bins {
|
||||||
pub metal: Option<std::path::PathBuf>,
|
pub metal: Option<std::path::PathBuf>,
|
||||||
pub cuda: Option<std::path::PathBuf>,
|
pub cuda: Option<std::path::PathBuf>,
|
||||||
pub opencl: Option<std::path::PathBuf>,
|
pub opencl: Option<std::path::PathBuf>,
|
||||||
|
/// igneum-gpu-telemetry: AMD power, heat, fans and clocks (proto-opencl/gpu-telemetry.c), 5 October 2026
|
||||||
|
pub telemetry: Option<std::path::PathBuf>,
|
||||||
pub dir: std::path::PathBuf,
|
pub dir: std::path::PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +101,7 @@ fn card(index: usize, name: &str, vendor: &str, worker: &str, detail: &str, devi
|
||||||
device: device.into(),
|
device: device.into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
state: "off".into(),
|
state: "off".into(),
|
||||||
|
amd_ordinal: -1,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -725,7 +728,7 @@ pub fn find_bins() -> Result<Bins, String> {
|
||||||
// the prebuilt CUDA worker needs NVIDIA's nvrtc64_*_0.dll next to it (as igneum-common.ps1 checks)
|
// the prebuilt CUDA worker needs NVIDIA's nvrtc64_*_0.dll next to it (as igneum-common.ps1 checks)
|
||||||
let nvrtc = std::fs::read_dir(dir).ok().map(|rd| rd.flatten().any(|e| { let n = e.file_name().to_string_lossy().to_ascii_lowercase(); n.starts_with("nvrtc64_") && n.ends_with("_0.dll") })).unwrap_or(false);
|
let nvrtc = std::fs::read_dir(dir).ok().map(|rd| rd.flatten().any(|e| { let n = e.file_name().to_string_lossy().to_ascii_lowercase(); n.starts_with("nvrtc64_") && n.ends_with("_0.dll") })).unwrap_or(false);
|
||||||
let cuda = opt("igneum-worker-cuda").filter(|_| nvrtc || cfg!(not(windows)));
|
let cuda = opt("igneum-worker-cuda").filter(|_| nvrtc || cfg!(not(windows)));
|
||||||
return Ok(Bins { node, miner, metal: opt("igneum-bench"), cuda, opencl: opt("igneum-worker-opencl"), dir: dir.clone() });
|
return Ok(Bins { node, miner, metal: opt("igneum-bench"), cuda, opencl: opt("igneum-worker-opencl"), telemetry: opt("igneum-gpu-telemetry"), dir: dir.clone() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(format!("igneumd and igneum-miner were not found next to the app (looked in {})", candidates.iter().map(|c| c.display().to_string()).collect::<Vec<_>>().join(", ")))
|
Err(format!("igneumd and igneum-miner were not found next to the app (looked in {})", candidates.iter().map(|c| c.display().to_string()).collect::<Vec<_>>().join(", ")))
|
||||||
|
|
|
||||||
1033
app/igneum-app/src/ember.rs
Normal file
1033
app/igneum-app/src/ember.rs
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -161,6 +161,13 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) {
|
||||||
c.sweep_watts = p.sweep_watts;
|
c.sweep_watts = p.sweep_watts;
|
||||||
c.sweep_mhs = p.sweep_mhs;
|
c.sweep_mhs = p.sweep_mhs;
|
||||||
c.sweep_at = p.sweep_at as f64;
|
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.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 {
|
||||||
|
c.tune_line = crate::ember::tuned_line(p.sweep_mhs, p.sweep_watts, p.sweep_eff);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A card a re-detection added (or brought back): the saved choice if there is one, else the detect defaults it
|
/// A card a re-detection added (or brought back): the saved choice if there is one, else the detect defaults it
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ pub struct Manifest {
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub node_branch: String,
|
pub node_branch: String,
|
||||||
pub node_commit: 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,
|
||||||
pub node_dirty: bool,
|
pub node_dirty: bool,
|
||||||
pub app_version: String,
|
pub app_version: String,
|
||||||
pub builds: Vec<Unit>,
|
pub builds: Vec<Unit>,
|
||||||
|
|
@ -159,7 +161,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 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 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 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_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")?;
|
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() {
|
for (i, b) in builds.iter().enumerate() {
|
||||||
let dir = b.get("dir").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
let dir = b.get("dir").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||||
|
|
@ -243,7 +245,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 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 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_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("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("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");
|
s.push_str("export LIBCLANG_PATH=\"${LLVM_LIB:-/usr/lib/llvm-18/lib}\"\n");
|
||||||
|
|
@ -293,6 +295,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.
|
// 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("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");
|
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
|
// 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
|
// 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");
|
s.push_str("find \"$B/src\" -type f -exec touch {} + 2>/dev/null || true\n");
|
||||||
|
|
@ -323,13 +331,23 @@ pub fn build_script(p: &BuildParams, job_id: &str, m: &Manifest, target: &str) -
|
||||||
s.push_str(&format!("echo \"STAGE {target} start $(now)\"\n"));
|
s.push_str(&format!("echo \"STAGE {target} start $(now)\"\n"));
|
||||||
s.push_str(&format!("mkdir -p \"$OUT/{target}\"\n"));
|
s.push_str(&format!("mkdir -p \"$OUT/{target}\"\n"));
|
||||||
s.push_str("rc_all=0\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)) {
|
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 optional = u.optional_on.iter().any(|t| t == target);
|
||||||
let rel = if windows { "x86_64-pc-windows-gnu/release" } else { "release" };
|
let rel = if windows { "x86_64-pc-windows-gnu/release" } else { "release" };
|
||||||
let tflag = if windows { " --target x86_64-pc-windows-gnu" } else { "" };
|
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!("echo \"STAGE {target} {dir} start $(now)\"\n", dir = u.dir));
|
||||||
s.push_str(&format!("t0=$(date +%s); rc=1\n"));
|
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)));
|
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(&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");
|
s.push_str("if [ \"$rc\" = 0 ]; then\n");
|
||||||
for b in &u.bins {
|
for b in &u.bins {
|
||||||
|
|
|
||||||
|
|
@ -1115,16 +1115,11 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
||||||
.map(|(k, v)| format!("$env:{k} = '{}'\r\n", v.replace('\'', "''")))
|
.map(|(k, v)| format!("$env:{k} = '{}'\r\n", v.replace('\'', "''")))
|
||||||
.collect();
|
.collect();
|
||||||
let wrapper = dir.join("elevated.ps1");
|
let wrapper = dir.join("elevated.ps1");
|
||||||
let w = format!("{env_lines}& '{}' *>&1 | Out-File -FilePath '{}' -Encoding utf8\r\nexit $LASTEXITCODE\r\n", script.display().to_string().replace('\'', "''"), out_file.display().to_string().replace('\'', "''"));
|
let w = elevated_wrapper(&env_lines, &script.display().to_string(), &out_file.display().to_string());
|
||||||
std::fs::write(&wrapper, [b"\xEF\xBB\xBF".as_slice(), w.as_bytes()].concat()).map_err(|e| e.to_string())?;
|
std::fs::write(&wrapper, [b"\xEF\xBB\xBF".as_slice(), w.as_bytes()].concat()).map_err(|e| e.to_string())?;
|
||||||
let _ = std::fs::remove_file(&out_file);
|
let _ = std::fs::remove_file(&out_file);
|
||||||
let inner = format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", wrapper.display());
|
let inner = format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", wrapper.display());
|
||||||
// A refused or unanswered UAC prompt makes Start-Process throw (`$p` stays null) and `exit $p.ExitCode`
|
cmd = crate::platform::elevated_command("powershell.exe", &inner);
|
||||||
// would exit 0: the 5 October 2026 driver job on PC 1 was reported "done" after Windows cancelled its
|
|
||||||
// prompt at 122 s. The launch failure is exit 251 and says so on stderr.
|
|
||||||
let ps = format!("try {{ $p = Start-Process -FilePath powershell.exe -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{ Write-Error ('elevated launch failed (UAC refused, cancelled or timed out): ' + $_.Exception.Message); exit 251 }}; if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}; exit $p.ExitCode", inner.replace('\'', "''"));
|
|
||||||
cmd = Command::new(crate::platform::tool("powershell"));
|
|
||||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]);
|
|
||||||
} else if shell == "powershell" {
|
} else if shell == "powershell" {
|
||||||
cmd = Command::new(crate::platform::tool("powershell"));
|
cmd = Command::new(crate::platform::tool("powershell"));
|
||||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
||||||
|
|
@ -1134,10 +1129,17 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
||||||
}
|
}
|
||||||
cmd.current_dir(&dir);
|
cmd.current_dir(&dir);
|
||||||
job_env(&mut cmd, shared, job, &dir, data_root);
|
job_env(&mut cmd, shared, job, &dir, data_root);
|
||||||
|
// the elevated script's output reaches this side through a file: follow it while the script runs, so the
|
||||||
|
// 5-minute progress reports carry its lines (6 October 2026: a 35-minute run that never mined showed only
|
||||||
|
// "script running" until it ended; the lines that said why were in the file the whole time)
|
||||||
|
let follow = if elevated { Some(follow_file(sink, out_file.clone())) } else { None };
|
||||||
let ran = run_streamed(&mut cmd, sink, ctl, limit, shared, job, started, "script running")?;
|
let ran = run_streamed(&mut cmd, sink, ctl, limit, shared, job, started, "script running")?;
|
||||||
if elevated {
|
if let Some(f) = follow {
|
||||||
|
f.stop.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let seen = f.handle.join().unwrap_or(0);
|
||||||
|
// the tail the follower had not read when the script ended
|
||||||
if let Ok(t) = std::fs::read_to_string(&out_file) {
|
if let Ok(t) = std::fs::read_to_string(&out_file) {
|
||||||
for l in t.lines() {
|
for l in t.lines().skip(seen) {
|
||||||
sink.line(l);
|
sink.line(l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1145,6 +1147,56 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
||||||
finish_ran(ran, "script")
|
finish_ran(ran, "script")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Follows a file another process writes (the elevated script's output), feeding each new complete line to the
|
||||||
|
/// sink every 2 s until stopped; returns how many lines it delivered, so the caller can hand over the remainder.
|
||||||
|
struct Follow {
|
||||||
|
stop: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
handle: std::thread::JoinHandle<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn follow_file(sink: &Sink, path: PathBuf) -> Follow {
|
||||||
|
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let stop2 = stop.clone();
|
||||||
|
let s = Sink { shared: sink.shared.clone(), id: sink.id.clone(), dir: sink.dir.clone(), log_path: sink.log_path.clone(), file: Mutex::new(std::fs::OpenOptions::new().append(true).open(&sink.log_path).ok()), results: Mutex::new(Vec::new()) };
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let mut seen = 0usize;
|
||||||
|
loop {
|
||||||
|
if let Ok(t) = std::fs::read_to_string(&path) {
|
||||||
|
let lines: Vec<&str> = t.lines().collect();
|
||||||
|
// only complete lines (the writer may be mid-line): keep the last one for the next pass unless the
|
||||||
|
// text ends with a newline
|
||||||
|
let complete = if t.ends_with('\n') { lines.len() } else { lines.len().saturating_sub(1) };
|
||||||
|
for l in lines.iter().take(complete).skip(seen) {
|
||||||
|
s.line(l);
|
||||||
|
}
|
||||||
|
seen = seen.max(complete);
|
||||||
|
}
|
||||||
|
if stop2.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
break seen;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_secs(2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Follow { stop, handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PowerShell wrapper an elevated job runs (its own process, its own environment): the IGNEUM_* values, then one
|
||||||
|
/// line about its console (the elevated process cannot inherit the engine's headless console and gets one of its own;
|
||||||
|
/// `-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).
|
||||||
|
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!(
|
||||||
|
"{env_lines}$ErrorActionPreference = 'Continue'\r\n\
|
||||||
|
$igc = ''\r\n\
|
||||||
|
try {{ Add-Type -Name IgCon -Namespace Igneum -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern System.IntPtr GetConsoleWindow(); [DllImport(\"user32.dll\")] public static extern bool IsWindowVisible(System.IntPtr h);'; $h = [Igneum.IgCon]::GetConsoleWindow(); $igc = \"elevated console: hwnd $h visible $([Igneum.IgCon]::IsWindowVisible($h))\" }} catch {{ $igc = \"elevated console: unknown ($_)\" }}\r\n\
|
||||||
|
$igc | Out-File -FilePath '{out}' -Encoding utf8\r\n\
|
||||||
|
& '{script}' *>&1 | Out-File -FilePath '{out}' -Encoding utf8 -Append\r\n\
|
||||||
|
exit $LASTEXITCODE\r\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn finish_ran(ran: Ran, what: &str) -> Result<Done, String> {
|
fn finish_ran(ran: Ran, what: &str) -> Result<Done, String> {
|
||||||
match ran.code {
|
match ran.code {
|
||||||
Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }),
|
Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }),
|
||||||
|
|
@ -1271,10 +1323,24 @@ mod tests {
|
||||||
assert!(d.summary.contains("administrator prompt"), "{}", d.summary);
|
assert!(d.summary.contains("administrator prompt"), "{}", d.summary);
|
||||||
let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap();
|
let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap();
|
||||||
assert_eq!(d.status, "done");
|
assert_eq!(d.status, "done");
|
||||||
// the launcher string itself: a thrown Start-Process must not fall through to `exit $p.ExitCode`
|
// the launcher string itself (platform::elevated_ps_line since 13755b9): a thrown Start-Process must not fall
|
||||||
let src = include_str!("jobrun.rs");
|
// through to `exit $p.ExitCode`
|
||||||
assert!(src.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{"));
|
let l = crate::platform::elevated_ps_line("powershell.exe", "-NoProfile -File x.ps1");
|
||||||
assert!(src.contains("if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}"));
|
assert!(l.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop } catch {"), "{l}");
|
||||||
|
assert!(l.contains("if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }"), "{l}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
assert!(w.starts_with("$env:IGNEUM_JOB_ID = 'j1'\r\n$ErrorActionPreference = 'Continue'\r\n"), "{w}");
|
||||||
|
assert!(w.contains("GetConsoleWindow()") && w.contains("IsWindowVisible("), "{w}");
|
||||||
|
assert!(w.contains("$igc | Out-File -FilePath 'C:\\jobs\\it''s\\elevated-output.log' -Encoding utf8\r\n"), "{w}");
|
||||||
|
assert!(w.contains("& 'C:\\jobs\\j1\\script.ps1' *>&1 | Out-File -FilePath 'C:\\jobs\\it''s\\elevated-output.log' -Encoding utf8 -Append\r\n"), "{w}");
|
||||||
|
assert!(w.ends_with("exit $LASTEXITCODE\r\n"), "{w}");
|
||||||
|
// every line ends in CRLF (the file is written for Windows PowerShell): the env line and six of its own
|
||||||
|
assert_eq!(w.matches("\r\n").count(), 7, "{w:?}");
|
||||||
|
assert_eq!(w.matches('\n').count(), 7, "{w:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1761,6 +1827,7 @@ fn spawn_relaunch_helper(shared: &Arc<Shared>) -> Result<(), String> {
|
||||||
{
|
{
|
||||||
let dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).ok_or("cannot find the install folder")?;
|
let dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).ok_or("cannot find the install folder")?;
|
||||||
let exe = dir.join("igneum-app.exe");
|
let exe = dir.join("igneum-app.exe");
|
||||||
|
// console: igneum-app.exe is a windows-subsystem program in release builds (main.rs), it never gets a console; SW_HIDE would hide the window host it opens
|
||||||
let ps = format!("Start-Sleep 8; Start-Process -FilePath '{}' -ArgumentList '--launch' -WorkingDirectory '{}'", exe.display().to_string().replace('\'', "''"), dir.display().to_string().replace('\'', "''"));
|
let ps = format!("Start-Sleep 8; Start-Process -FilePath '{}' -ArgumentList '--launch' -WorkingDirectory '{}'", exe.display().to_string().replace('\'', "''"), dir.display().to_string().replace('\'', "''"));
|
||||||
c = Command::new(crate::platform::tool("powershell"));
|
c = Command::new(crate::platform::tool("powershell"));
|
||||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", &ps]);
|
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", &ps]);
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,12 @@ mod jobrun;
|
||||||
mod jobbuild;
|
mod jobbuild;
|
||||||
mod prover;
|
mod prover;
|
||||||
mod provedefault;
|
mod provedefault;
|
||||||
|
mod segments;
|
||||||
mod verifier;
|
mod verifier;
|
||||||
mod wslhost;
|
mod wslhost;
|
||||||
mod sweep;
|
mod sweep;
|
||||||
|
mod ember;
|
||||||
|
mod powertask;
|
||||||
mod watchdog;
|
mod watchdog;
|
||||||
|
|
||||||
use std::io::{BufRead, Write};
|
use std::io::{BufRead, Write};
|
||||||
|
|
@ -52,6 +55,12 @@ fn main() {
|
||||||
println!("igneum-app {}", engine::VERSION);
|
println!("igneum-app {}", engine::VERSION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
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"));
|
||||||
|
std::process::exit(powertask::run_helper(&dir));
|
||||||
|
}
|
||||||
if args.iter().any(|a| a == "--launch") {
|
if args.iter().any(|a| a == "--launch") {
|
||||||
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
|
if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) {
|
||||||
let host = dir.join("Igneum Miner.exe");
|
let host = dir.join("Igneum Miner.exe");
|
||||||
|
|
@ -93,6 +102,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
let packaged = config::Packaged::load(&candidates).with_env_overrides();
|
let packaged = config::Packaged::load(&candidates).with_env_overrides();
|
||||||
let settings = config::Settings::load(&runtime.app_dir.join("settings.json"));
|
let settings = config::Settings::load(&runtime.app_dir.join("settings.json"));
|
||||||
|
// a measurement engine runs with the installed app's choices and its own switches (config.rs for_measurement)
|
||||||
|
let settings = if sweep { settings.for_measurement() } else { settings };
|
||||||
|
|
||||||
// the per-launch token: 32 hex characters from the OS
|
// the per-launch token: 32 hex characters from the OS
|
||||||
let mut raw = [0u8; 16];
|
let mut raw = [0u8; 16];
|
||||||
|
|
@ -134,7 +145,7 @@ fn main() {
|
||||||
let Ok(l) = line else { break };
|
let Ok(l) = line else { break };
|
||||||
let t = l.trim();
|
let t = l.trim();
|
||||||
match t {
|
match t {
|
||||||
"quit" => shared.send(engine::Cmd::Quit),
|
"quit" => shared.send(engine::Cmd::Quit("the window host (quit on stdin: the tray menu or the installer)")),
|
||||||
"pause" => shared.send(engine::Cmd::Pause),
|
"pause" => shared.send(engine::Cmd::Pause),
|
||||||
"resume" => shared.send(engine::Cmd::Resume),
|
"resume" => shared.send(engine::Cmd::Resume),
|
||||||
// the window host saw WM_DEVICECHANGE (a card plugged in or out): enumerate now, not at the next minute
|
// the window host saw WM_DEVICECHANGE (a card plugged in or out): enumerate now, not at the next minute
|
||||||
|
|
@ -145,7 +156,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if wrapper {
|
if wrapper {
|
||||||
shared.send(engine::Cmd::Quit);
|
shared.send(engine::Cmd::Quit("the window host went away (stdin closed)"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -546,3 +546,42 @@ mod tests {
|
||||||
assert_eq!(fingerprint("zz"), "");
|
assert_eq!(fingerprint("zz"), "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unix seconds of a manifest `published_at` ("2026-10-04T13:00:00Z", whole seconds, UTC); `None` for any other shape.
|
||||||
|
pub fn unix_from_rfc3339(t: &str) -> Option<u64> {
|
||||||
|
let t = t.trim();
|
||||||
|
let b = t.as_bytes();
|
||||||
|
if b.len() < 20 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T' || b[13] != b':' || b[16] != b':' || !t.ends_with('Z') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let n = |a: usize, z: usize| t[a..z].parse::<i64>().ok();
|
||||||
|
let (y, m, d, hh, mm, ss) = (n(0, 4)?, n(5, 7)?, n(8, 10)?, n(11, 13)?, n(14, 16)?, n(17, 19)?);
|
||||||
|
if !(1..=12).contains(&m) || !(1..=31).contains(&d) || hh > 23 || mm > 59 || ss > 60 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// days from civil (Howard Hinnant), valid for every date after 1970
|
||||||
|
let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
|
||||||
|
let era = y2.div_euclid(400);
|
||||||
|
let yoe = y2 - era * 400;
|
||||||
|
let doy = (153 * m2 + 2) / 5 + d - 1;
|
||||||
|
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||||
|
let days = era * 146097 + doe - 719468;
|
||||||
|
if days < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((days as u64) * 86400 + (hh as u64) * 3600 + (mm as u64) * 60 + ss as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod rfc3339_tests {
|
||||||
|
use super::unix_from_rfc3339;
|
||||||
|
#[test]
|
||||||
|
fn a_manifest_publish_time_parses_to_unix_seconds() {
|
||||||
|
assert_eq!(unix_from_rfc3339("1970-01-01T00:00:00Z"), Some(0));
|
||||||
|
assert_eq!(unix_from_rfc3339("2026-10-05T23:57:49Z"), Some(1791244669));
|
||||||
|
assert_eq!(unix_from_rfc3339("2026-10-04T13:00:00Z"), Some(1791118800));
|
||||||
|
assert_eq!(unix_from_rfc3339(""), None);
|
||||||
|
assert_eq!(unix_from_rfc3339("2026-10-05 23:57:49"), None);
|
||||||
|
assert_eq!(unix_from_rfc3339("2026-13-05T23:57:49Z"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ use std::process::Command;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// A manifest published this long before the engine started is a catch-up: the hourly rollout slot does not apply.
|
||||||
|
const CATCH_UP_AFTER_S: u64 = 3600;
|
||||||
const HEALTHY_AFTER_S: u64 = 90;
|
const HEALTHY_AFTER_S: u64 = 90;
|
||||||
const CHECK_EVERY_S: u64 = 3600;
|
const CHECK_EVERY_S: u64 = 3600;
|
||||||
const RETRY_AFTER_ERROR_S: u64 = 600;
|
const RETRY_AFTER_ERROR_S: u64 = 600;
|
||||||
|
|
@ -117,6 +119,11 @@ pub struct Updater {
|
||||||
deferred_until: Option<Instant>,
|
deferred_until: Option<Instant>,
|
||||||
/// this machine's minute of the hour for applying (manifest::slot_minute of the machine id)
|
/// this machine's minute of the hour for applying (manifest::slot_minute of the machine id)
|
||||||
slot: u64,
|
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 (Josh'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,
|
||||||
/// identity counts from /api/live over the last 10 minutes, sampled while an update is ready
|
/// identity counts from /api/live over the last 10 minutes, sampled while an update is ready
|
||||||
live_samples: Vec<(Instant, u64)>,
|
live_samples: Vec<(Instant, u64)>,
|
||||||
live_next: Instant,
|
live_next: Instant,
|
||||||
|
|
@ -163,6 +170,8 @@ impl Updater {
|
||||||
apply_launched: None,
|
apply_launched: None,
|
||||||
deferred_until: None,
|
deferred_until: None,
|
||||||
slot: manifest::slot_minute(&shared.runtime.id8()),
|
slot: manifest::slot_minute(&shared.runtime.id8()),
|
||||||
|
started_unix: crate::platform::unix_now(),
|
||||||
|
catch_up_logged: false,
|
||||||
live_samples: Vec::new(),
|
live_samples: Vec::new(),
|
||||||
live_next: now,
|
live_next: now,
|
||||||
live_busy: false,
|
live_busy: false,
|
||||||
|
|
@ -175,7 +184,11 @@ impl Updater {
|
||||||
if crate::platform::start_at_login_is_on() {
|
if crate::platform::start_at_login_is_on() {
|
||||||
let _ = crate::platform::set_start_at_login(true);
|
let _ = crate::platform::set_start_at_login(true);
|
||||||
}
|
}
|
||||||
firewall_first_run(shared);
|
// a measurement engine (--sweep) uses the installed app's node and asks for nothing: the rule is the
|
||||||
|
// installed app's (the dry run of 6 October 2026 raised a second UAC prompt from here)
|
||||||
|
if !shared.runtime.sweep_only {
|
||||||
|
firewall_first_run(shared);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
u.failed_versions = std::fs::read_to_string(u.failed_path()).ok().and_then(|t| serde_json::from_str::<Vec<String>>(&t).ok()).unwrap_or_default();
|
u.failed_versions = std::fs::read_to_string(u.failed_path()).ok().and_then(|t| serde_json::from_str::<Vec<String>>(&t).ok()).unwrap_or_default();
|
||||||
// the cached manifest: the rollback floor and the consensus override are known before the first check
|
// the cached manifest: the rollback floor and the consensus override are known before the first check
|
||||||
|
|
@ -519,7 +532,12 @@ impl Updater {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let minute = (crate::platform::unix_now() / 60) % 60;
|
let minute = (crate::platform::unix_now() / 60) % 60;
|
||||||
let slot_ok = minute == self.slot || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
let catch_up = self.manifest.as_ref().and_then(|m| manifest::unix_from_rfc3339(&m.published_at)).map(|p| p + CATCH_UP_AFTER_S <= self.started_unix).unwrap_or(false);
|
||||||
|
if catch_up && !self.catch_up_logged {
|
||||||
|
self.catch_up_logged = true;
|
||||||
|
shared.log(&format!("update: {} was published over an hour before this start, so it installs at the first safe moment (no hourly slot)", self.version()));
|
||||||
|
}
|
||||||
|
let slot_ok = minute == self.slot || catch_up || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
||||||
let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
||||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct };
|
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct };
|
||||||
if !self.auto && !urgent && !self.install_asked {
|
if !self.auto && !urgent && !self.install_asked {
|
||||||
|
|
@ -1264,6 +1282,7 @@ function EngineAlive() { return [bool](Get-Process -Id $EnginePid -ErrorAction S
|
||||||
function Relaunch() {
|
function Relaunch() {
|
||||||
if (EngineAlive) { return }
|
if (EngineAlive) { return }
|
||||||
$exe = Join-Path $InstallDir 'igneum-app.exe'
|
$exe = Join-Path $InstallDir 'igneum-app.exe'
|
||||||
|
# console: igneum-app.exe is a windows-subsystem program (no console); -WindowStyle Hidden would hide the window host it opens
|
||||||
if (Test-Path $exe) { Log 'engine gone and nothing installed: starting the old app again'; Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
if (Test-Path $exe) { Log 'engine gone and nothing installed: starting the old app again'; Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
||||||
}
|
}
|
||||||
Log "$Mode : engine $EnginePid installer '$Installer' version $Version (the engine keeps mining until the installer runs)"
|
Log "$Mode : engine $EnginePid installer '$Installer' version $Version (the engine keeps mining until the installer runs)"
|
||||||
|
|
@ -1278,6 +1297,7 @@ $setupArgs = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CLOSEAPPLICAT
|
||||||
try {
|
try {
|
||||||
# no -Verb RunAs: a per-user installer just runs; an administrator installer makes Windows ask, and a declined or
|
# no -Verb RunAs: a per-user installer just runs; an administrator installer makes Windows ask, and a declined or
|
||||||
# timed-out prompt comes back here as an exception with the engine still mining
|
# timed-out prompt comes back here as an exception with the engine still mining
|
||||||
|
# console: the Inno Setup installer is a GUI program (no console), /VERYSILENT shows nothing
|
||||||
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
||||||
if ($p.ExitCode -eq 0) {
|
if ($p.ExitCode -eq 0) {
|
||||||
if ($Mode -eq 'rollback') { Done $false "Igneum Miner $Version did not stay up twice; the previous version was reinstalled" $true $false }
|
if ($Mode -eq 'rollback') { Done $false "Igneum Miner $Version did not stay up twice; the previous version was reinstalled" $true $false }
|
||||||
|
|
|
||||||
|
|
@ -166,17 +166,28 @@ pub fn lock_permissions(path: &Path, dir: bool) {
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let _ = dir;
|
|
||||||
let user = std::env::var("USERNAME").unwrap_or_default();
|
let user = std::env::var("USERNAME").unwrap_or_default();
|
||||||
if !user.is_empty() {
|
if !user.is_empty() {
|
||||||
let _ = quiet(&mut Command::new(tool("icacls")))
|
let _ = quiet(&mut Command::new(tool("icacls"))).arg(path).args(icacls_lock_args(dir, &user)).output();
|
||||||
.arg(path)
|
|
||||||
.args(["/inheritance:r", "/grant:r", &format!("{user}:F")])
|
|
||||||
.output();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The icacls arguments that lock a path to the user. A folder gets an INHERITABLE grant (`user:(OI)(CI)F`) and
|
||||||
|
/// NO `/T`: Windows propagates the inheritable entry to every child, existing or future, as `(I)(F)`. Measured on
|
||||||
|
/// PC 1, 6 October 2026 (collect ember-acl-2): the old non-inheritable `user:F` cut the folder's inheritance and
|
||||||
|
/// left a file COPIED in before the engine started with no entry at all (the measurement engine's settings.json,
|
||||||
|
/// machine-id and wallet.json read as nothing, so it ran on defaults with no payout address; its own files, written
|
||||||
|
/// after the lock, inherited fine and hid it); the same grant WITH `/T` also left the file empty, because `/T`
|
||||||
|
/// re-applies `/inheritance:r` to the file after the propagation and an `(OI)(CI)` entry on a file is inherit-only.
|
||||||
|
pub fn icacls_lock_args(dir: bool, user: &str) -> Vec<String> {
|
||||||
|
if dir {
|
||||||
|
vec!["/inheritance:r".into(), "/grant:r".into(), format!("{user}:(OI)(CI)F")]
|
||||||
|
} else {
|
||||||
|
vec!["/inheritance:r".into(), "/grant:r".into(), format!("{user}:F")]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Opens a URL in the default browser (the fallback when no window host runs).
|
/// Opens a URL in the default browser (the fallback when no window host runs).
|
||||||
pub fn open_url(url: &str) {
|
pub fn open_url(url: &str) {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|
@ -396,10 +407,7 @@ pub fn sync_clock() -> Result<String, String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let cmd = tool("cmd").display().to_string();
|
let cmd = tool("cmd").display().to_string();
|
||||||
let script = format!("Start-Process -FilePath '{cmd}' -ArgumentList '/c net start w32time & w32tm /resync /force' -Verb RunAs -Wait -WindowStyle Hidden");
|
let mut c = elevated_command(&cmd, "/c net start w32time & w32tm /resync /force");
|
||||||
let mut c = Command::new(tool("powershell"));
|
|
||||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
|
||||||
quiet(&mut c);
|
|
||||||
let out = c.output().map_err(|e| e.to_string())?;
|
let out = c.output().map_err(|e| e.to_string())?;
|
||||||
if out.status.success() {
|
if out.status.success() {
|
||||||
Ok("asked Windows Time to resync (w32tm /resync)".into())
|
Ok("asked Windows Time to resync (w32tm /resync)".into())
|
||||||
|
|
@ -425,18 +433,14 @@ pub fn sync_clock() -> Result<String, String> {
|
||||||
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let escaped = cmdline.replace('\'', "''");
|
|
||||||
let cmd = tool("cmd").display().to_string();
|
let cmd = tool("cmd").display().to_string();
|
||||||
let script = format!("$p = Start-Process -FilePath '{cmd}' -ArgumentList '/c {escaped}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru; exit $p.ExitCode");
|
let mut c = elevated_command(&cmd, &format!("/c {cmdline}"));
|
||||||
let mut c = Command::new(tool("powershell"));
|
|
||||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
|
||||||
quiet(&mut c);
|
|
||||||
let out = c.output().map_err(|e| e.to_string())?;
|
let out = c.output().map_err(|e| e.to_string())?;
|
||||||
if out.status.success() {
|
if out.status.success() {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||||
Err(if err.contains("canceled") || err.contains("cancelled") || err.is_empty() { "the administrator prompt was cancelled".into() } else { err })
|
Err(elevated_failure(out.status.code(), &err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|
@ -451,6 +455,52 @@ pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The reason an elevated step failed, from the launcher's exit code and stderr: exit 251 (the prompt refused,
|
||||||
|
/// cancelled or timed out, `elevated_ps_line`) and the "canceled" wording name the prompt; any other code is the
|
||||||
|
/// step's own exit (the engine then keeps Power control on: rights were given).
|
||||||
|
pub fn elevated_failure(code: Option<i32>, stderr: &str) -> String {
|
||||||
|
if code == Some(ELEVATED_LAUNCH_FAILED) || stderr.contains("canceled") || stderr.contains("cancelled") {
|
||||||
|
"the administrator prompt was refused, cancelled or timed out".into()
|
||||||
|
} else if stderr.is_empty() {
|
||||||
|
format!("the elevated step exited with code {}", code.map(|c| c.to_string()).unwrap_or_else(|| "?".into()))
|
||||||
|
} else {
|
||||||
|
stderr.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Doubles the single quotes of `s` for a single-quoted PowerShell literal.
|
||||||
|
pub fn ps_quote(s: &str) -> String {
|
||||||
|
s.replace('\'', "''")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PowerShell line that starts `file args` as administrator (one UAC prompt), waits, and exits with the child's
|
||||||
|
/// code. Every elevated launch of the app goes through here (the NVIDIA power cap, the sweep helper, the clock sync,
|
||||||
|
/// an elevated remote job) so the console flags live in one place: `-WindowStyle Hidden` is SW_HIDE on the new
|
||||||
|
/// process the AppInfo service creates; the elevated child cannot inherit this process's headless console, so without
|
||||||
|
/// it the child gets a console of its own (5 October 2026, PC 1 watcher, tools/windows/console-watch*.ps1).
|
||||||
|
/// A refused, cancelled or unanswered prompt makes Start-Process throw and `$p` stay null: that is exit 251 with the
|
||||||
|
/// reason on stderr, never `exit $p.ExitCode` = 0 (the 5 October 2026 driver job on PC 1 was reported done after
|
||||||
|
/// Windows cancelled its prompt at 122 s).
|
||||||
|
pub fn elevated_ps_line(file: &str, args: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"try {{ $p = Start-Process -FilePath '{}' -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{ Write-Error ('elevated launch failed (UAC refused, cancelled or timed out): ' + $_.Exception.Message); exit 251 }}; if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}; exit $p.ExitCode",
|
||||||
|
ps_quote(file),
|
||||||
|
ps_quote(args)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exit code `elevated_ps_line` uses when the elevated process never started (the prompt refused, cancelled or
|
||||||
|
/// timed out).
|
||||||
|
pub const ELEVATED_LAUNCH_FAILED: i32 = 251;
|
||||||
|
|
||||||
|
/// The hidden PowerShell that runs `elevated_ps_line(file, args)`: blocking when run, one UAC prompt on the PC.
|
||||||
|
pub fn elevated_command(file: &str, args: &str) -> Command {
|
||||||
|
let mut c = Command::new(tool("powershell"));
|
||||||
|
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &elevated_ps_line(file, args)]);
|
||||||
|
quiet(&mut c);
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds a command that runs without a console window on Windows.
|
/// Builds a command that runs without a console window on Windows.
|
||||||
pub fn quiet(cmd: &mut Command) -> &mut Command {
|
pub fn quiet(cmd: &mut Command) -> &mut Command {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|
@ -461,8 +511,47 @@ pub fn quiet(cmd: &mut Command) -> &mut Command {
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod lock_tests {
|
||||||
|
#[test]
|
||||||
|
fn a_locked_folder_grants_the_user_inheritably_and_covers_what_is_inside() {
|
||||||
|
let d = super::icacls_lock_args(true, "Admin");
|
||||||
|
assert_eq!(d, vec!["/inheritance:r", "/grant:r", "Admin:(OI)(CI)F"], "inheritable, and never /T (it empties the children)");
|
||||||
|
let f = super::icacls_lock_args(false, "Admin");
|
||||||
|
assert_eq!(f, vec!["/inheritance:r", "/grant:r", "Admin:F"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn elevated_line_is_hidden_and_quoted() {
|
||||||
|
let l = super::elevated_ps_line(r"C:\WINDOWS\system32\cmd.exe", "/c echo it's & exit 3");
|
||||||
|
assert!(l.starts_with("try { $p = "), "{l}");
|
||||||
|
assert!(l.contains("-FilePath 'C:\\WINDOWS\\system32\\cmd.exe' -ArgumentList '/c echo it''s & exit 3' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop } catch {"), "{l}");
|
||||||
|
assert!(l.contains("-Verb RunAs"), "{l}");
|
||||||
|
assert!(l.contains("-WindowStyle Hidden"), "{l}");
|
||||||
|
// a thrown Start-Process (the prompt refused) never falls through to `exit $p.ExitCode`
|
||||||
|
assert!(l.contains("exit 251 }; if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }; exit $p.ExitCode"), "{l}");
|
||||||
|
assert!(l.ends_with("exit $p.ExitCode"), "{l}");
|
||||||
|
assert_eq!(super::ELEVATED_LAUNCH_FAILED, 251);
|
||||||
|
assert_eq!(super::elevated_failure(Some(251), "elevated launch failed (UAC refused, cancelled or timed out): ..."), "the administrator prompt was refused, cancelled or timed out");
|
||||||
|
assert_eq!(super::elevated_failure(Some(1), "The operation was canceled by the user."), "the administrator prompt was refused, cancelled or timed out");
|
||||||
|
assert_eq!(super::elevated_failure(Some(2), ""), "the elevated step exited with code 2");
|
||||||
|
assert_eq!(super::elevated_failure(Some(3), "nvidia-smi: bad"), "nvidia-smi: bad");
|
||||||
|
assert_eq!(super::ps_quote("a'b''c"), "a''b''''c");
|
||||||
|
assert_eq!(super::ps_quote("plain"), "plain");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elevated_command_is_a_hidden_powershell() {
|
||||||
|
let c = super::elevated_command("powershell.exe", "-NoProfile -File \"C:\\x y\\elevated.ps1\"");
|
||||||
|
let args: Vec<String> = c.get_args().map(|a| a.to_string_lossy().into_owned()).collect();
|
||||||
|
assert_eq!(&args[..4], ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"]);
|
||||||
|
assert!(args[4].contains("-ArgumentList '-NoProfile -File \"C:\\x y\\elevated.ps1\"' -Verb RunAs -Wait -WindowStyle Hidden"), "{}", args[4]);
|
||||||
|
assert!(c.get_program().to_string_lossy().contains("powershell"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn token_redaction() {
|
fn token_redaction() {
|
||||||
let l = "dashboard at http://127.0.0.1:58776/t/a3a01c537130bceeaa1f6118ba48d63e/ (log x)";
|
let l = "dashboard at http://127.0.0.1:58776/t/a3a01c537130bceeaa1f6118ba48d63e/ (log x)";
|
||||||
|
|
|
||||||
265
app/igneum-app/src/powertask.rs
Normal file
265
app/igneum-app/src/powertask.rs
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
//! One administrator approval, ever (Josh, 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
|
||||||
|
//! start, a reboot, a slider move) and every tune's helper is another elevated launch, so another prompt. This module
|
||||||
|
//! makes the first approval the last: the one elevated step also registers a per-user Windows scheduled task,
|
||||||
|
//! `Igneum Power Helper`, principal = the signed-in user, RunLevel Highest, no trigger, whose action is this very
|
||||||
|
//! executable with `--power-helper`. A task the user owns can be STARTED by the user's unelevated processes without a
|
||||||
|
//! prompt (`Start-ScheduledTask`), and it runs elevated; so every later cap and tune starts the task and talks to it
|
||||||
|
//! through the command file `<app data>/app/sweep/cmd.txt` (the protocol the 0.3.9 helper scripts spoke: `<seq> pl
|
||||||
|
//! <watts>`, `<seq> lgc <MHz>`, `<seq> rgc`, `quit`; plus `remove`, the kill switch). The task survives app restarts,
|
||||||
|
//! updates (the per-user installer replaces the exe in place; the task's action path is the install folder) and
|
||||||
|
//! reboots (a task, not a process). Power control off starts the task once and sends `remove`: the helper unregisters
|
||||||
|
//! the task (elevated) and exits; nothing is left behind.
|
||||||
|
//!
|
||||||
|
//! Threat note (what the helper will and will not run):
|
||||||
|
//! - The action is fixed at registration: the app's own exe in the install folder with `--power-helper`. The task
|
||||||
|
//! has no trigger and no arguments from outside; only `Start-ScheduledTask` by the owning user starts it.
|
||||||
|
//! - The helper reads ONE file, `<app data>/app/sweep/cmd.txt`, in the user's own profile. Every command it accepts
|
||||||
|
//! is a fixed verb with digit-only arguments: `pl <watts>` runs `nvidia-smi -i <dev> -pl <watts>`, `lgc <MHz>` runs
|
||||||
|
//! `nvidia-smi -i <dev> -lgc 0,<MHz>`, `rgc` runs `nvidia-smi -i <dev> -rgc`, `quit` ends it, `remove` unregisters
|
||||||
|
//! the task and ends it. The device index is digits only too (`dev <n>` sets it). No shell, no path, no string from
|
||||||
|
//! the file reaches a process: `Command::new(nvidia-smi).args([...])`, never `cmd /c`.
|
||||||
|
//! - nvidia-smi is resolved to the driver's install path (platform::tool), never from PATH.
|
||||||
|
//! - What an attacker running as the user gains: the power limit and the clock cap of the user's own NVIDIA cards,
|
||||||
|
//! within the ranges the driver allows, which the same user could set with one approved prompt anyway. Nothing
|
||||||
|
//! else: no file, no process, no registry, no other binary.
|
||||||
|
//! - The helper exits after 20 idle minutes; a stale command file is cleared at start (sequence numbers must rise).
|
||||||
|
//! - Linux keeps pkexec per step (no scheduled task); macOS has no cap to set.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// The task name in the Windows Task Scheduler (per user).
|
||||||
|
pub const TASK_NAME: &str = "Igneum Power Helper";
|
||||||
|
/// The helper ends after this long without a new command.
|
||||||
|
pub const IDLE_S: u64 = 20 * 60;
|
||||||
|
|
||||||
|
/// One parsed command from cmd.txt.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum HelperCmd {
|
||||||
|
Dev(String),
|
||||||
|
PowerLimit(u64),
|
||||||
|
ClockCap(u64),
|
||||||
|
ClockReset,
|
||||||
|
Quit,
|
||||||
|
Remove,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses one line: `<seq> <verb> [<digits>]` (the 0.3.9 form `<seq> <watts>` reads as a power limit; `quit` and
|
||||||
|
/// `remove` need no sequence). Anything that is not a fixed verb with digit-only arguments is None.
|
||||||
|
pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
|
||||||
|
let t = line.trim();
|
||||||
|
if t == "quit" {
|
||||||
|
return Some((0, HelperCmd::Quit));
|
||||||
|
}
|
||||||
|
if t == "remove" {
|
||||||
|
return Some((0, HelperCmd::Remove));
|
||||||
|
}
|
||||||
|
let p: Vec<&str> = t.split_whitespace().collect();
|
||||||
|
let digits = |s: &str| !s.is_empty() && s.len() <= 6 && s.chars().all(|c| c.is_ascii_digit());
|
||||||
|
let seq: u64 = p.first().filter(|s| digits(s)).and_then(|s| s.parse().ok())?;
|
||||||
|
match p.as_slice() {
|
||||||
|
[_, w] if digits(w) => Some((seq, HelperCmd::PowerLimit(w.parse().ok()?))),
|
||||||
|
[_, "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)),
|
||||||
|
[_, "dev", d] if digits(d) => Some((seq, HelperCmd::Dev(d.to_string()))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The nvidia-smi arguments a command becomes (None for the verbs that run nothing).
|
||||||
|
pub fn smi_args(dev: &str, c: &HelperCmd) -> Option<Vec<String>> {
|
||||||
|
match c {
|
||||||
|
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()]),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub fn register_script(exe: &Path) -> String {
|
||||||
|
let exe = exe.display().to_string().replace('\'', "''");
|
||||||
|
format!(
|
||||||
|
"$a = New-ScheduledTaskAction -Execute '{exe}' -Argument '--power-helper' -WorkingDirectory '{dir}'\r\n\
|
||||||
|
$p = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest\r\n\
|
||||||
|
$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 1) -MultipleInstances IgnoreNew -Hidden\r\n\
|
||||||
|
Register-ScheduledTask -TaskName '{name}' -Action $a -Principal $p -Settings $s -Force | Out-Null\r\n\
|
||||||
|
exit 0\r\n",
|
||||||
|
dir = exe.rfind(['\\', '/']).map(|i| exe[..i].to_string()).unwrap_or_default(),
|
||||||
|
name = TASK_NAME
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PowerShell that starts the task from an unelevated process (no prompt: the user owns the task).
|
||||||
|
pub fn start_command() -> String {
|
||||||
|
format!("Start-ScheduledTask -TaskName '{TASK_NAME}'; exit 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PowerShell that says whether the task is registered (exit 0) or not (exit 1).
|
||||||
|
pub fn query_command() -> String {
|
||||||
|
format!("if (Get-ScheduledTask -TaskName '{TASK_NAME}' -ErrorAction SilentlyContinue) {{ exit 0 }} else {{ exit 1 }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PowerShell the helper itself runs (elevated) on `remove`: the task goes, nothing is left.
|
||||||
|
pub fn remove_command() -> String {
|
||||||
|
format!("Unregister-ScheduledTask -TaskName '{TASK_NAME}' -Confirm:$false; exit 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the task registered? Windows only; false elsewhere.
|
||||||
|
pub fn registered() -> bool {
|
||||||
|
if !cfg!(windows) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut c = std::process::Command::new(crate::platform::tool("powershell"));
|
||||||
|
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &query_command()]);
|
||||||
|
crate::platform::quiet(&mut c);
|
||||||
|
c.status().map(|s| s.success()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the task (no prompt). Ok when Start-ScheduledTask returned 0.
|
||||||
|
pub fn start() -> Result<(), String> {
|
||||||
|
let mut c = std::process::Command::new(crate::platform::tool("powershell"));
|
||||||
|
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &start_command()]);
|
||||||
|
crate::platform::quiet(&mut c);
|
||||||
|
let out = c.output().map_err(|e| e.to_string())?;
|
||||||
|
if out.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("Start-ScheduledTask failed: {}", String::from_utf8_lossy(&out.stderr).trim()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The helper process (`igneum-app --power-helper`): polls `<dir>/cmd.txt` twice a second, runs the parsed commands
|
||||||
|
/// through nvidia-smi, logs what it ran to `<dir>/helper.log`, ends on `quit`, on `remove` (after unregistering the
|
||||||
|
/// task) or after 20 idle minutes. `dir` is `<app data>/app/sweep`.
|
||||||
|
pub fn run_helper(dir: &Path) -> i32 {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
let cmd_file = dir.join("cmd.txt");
|
||||||
|
let log_file = dir.join("helper.log");
|
||||||
|
let log = |line: &str| {
|
||||||
|
use std::io::Write;
|
||||||
|
if let Ok(mut f) = std::fs::OpenOptions::new().append(true).create(true).open(&log_file) {
|
||||||
|
let _ = writeln!(f, "{} {line}", crate::platform::unix_now());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
log("helper started (scheduled task, elevated)");
|
||||||
|
// a stale file from an earlier run is not a command: only lines after the start count
|
||||||
|
let mut last_seq: u64 = std::fs::read_to_string(&cmd_file).ok().and_then(|t| t.lines().filter_map(parse_line).map(|(s, _)| s).max()).unwrap_or(0);
|
||||||
|
let mut last_text = String::new();
|
||||||
|
let mut dev = "0".to_string();
|
||||||
|
let mut idle = Instant::now();
|
||||||
|
let smi = crate::platform::tool("nvidia-smi");
|
||||||
|
loop {
|
||||||
|
let text = std::fs::read_to_string(&cmd_file).unwrap_or_default();
|
||||||
|
if text != last_text {
|
||||||
|
last_text = text.clone();
|
||||||
|
for (seq, c) in text.lines().filter_map(parse_line) {
|
||||||
|
match c {
|
||||||
|
HelperCmd::Quit => {
|
||||||
|
log("quit");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
HelperCmd::Remove => {
|
||||||
|
let mut p = std::process::Command::new(crate::platform::tool("powershell"));
|
||||||
|
p.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &remove_command()]);
|
||||||
|
crate::platform::quiet(&mut p);
|
||||||
|
let ok = p.status().map(|s| s.success()).unwrap_or(false);
|
||||||
|
log(&format!("remove: the task is {}", if ok { "unregistered" } else { "still registered (Unregister-ScheduledTask failed)" }));
|
||||||
|
return if ok { 0 } else { 1 };
|
||||||
|
}
|
||||||
|
_ if seq <= last_seq => continue,
|
||||||
|
HelperCmd::Dev(d) => {
|
||||||
|
last_seq = seq;
|
||||||
|
idle = Instant::now();
|
||||||
|
dev = d;
|
||||||
|
log(&format!("{seq} dev {dev}"));
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
last_seq = seq;
|
||||||
|
idle = Instant::now();
|
||||||
|
let args = smi_args(&dev, &other).unwrap_or_default();
|
||||||
|
let mut p = std::process::Command::new(&smi);
|
||||||
|
p.args(&args);
|
||||||
|
crate::platform::quiet(&mut p);
|
||||||
|
let out = p.output().map(|o| format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))).unwrap_or_else(|e| e.to_string());
|
||||||
|
log(&format!("{seq} nvidia-smi {} : {}", args.join(" "), out.replace('\n', " ").trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idle.elapsed() >= Duration::from_secs(IDLE_S) {
|
||||||
|
log("idle 20 min: exit (the engine starts the task again when it needs it)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the command file lives for a data root.
|
||||||
|
pub fn sweep_dir(app_dir: &Path) -> PathBuf {
|
||||||
|
app_dir.join("sweep")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_fixed_verbs_with_digit_arguments_parse() {
|
||||||
|
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("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)));
|
||||||
|
// 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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_arguments_reach_nvidia_smi_as_a_list_never_a_shell() {
|
||||||
|
assert_eq!(smi_args("0", &HelperCmd::PowerLimit(460)).unwrap(), vec!["-i", "0", "-pl", "460"]);
|
||||||
|
assert_eq!(smi_args("1", &HelperCmd::ClockCap(2472)).unwrap(), vec!["-i", "1", "-lgc", "0,2472"]);
|
||||||
|
assert_eq!(smi_args("1", &HelperCmd::ClockReset).unwrap(), vec!["-i", "1", "-rgc"]);
|
||||||
|
assert_eq!(smi_args("0", &HelperCmd::Quit), None);
|
||||||
|
assert_eq!(smi_args("0", &HelperCmd::Remove), None);
|
||||||
|
assert_eq!(smi_args("0", &HelperCmd::Dev("1".into())), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_registration_is_per_user_highest_no_trigger_fixed_action() {
|
||||||
|
let s = register_script(Path::new(r"C:\Users\Admin\AppData\Local\Programs\Igneum Miner\igneum-app.exe"));
|
||||||
|
assert!(s.contains("-Execute 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Igneum Miner\\igneum-app.exe' -Argument '--power-helper'"), "{s}");
|
||||||
|
assert!(s.contains("-WorkingDirectory 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Igneum Miner'"), "{s}");
|
||||||
|
assert!(s.contains("-RunLevel Highest") && s.contains("-LogonType Interactive"), "{s}");
|
||||||
|
assert!(s.contains("[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"), "the signed-in user, never a literal");
|
||||||
|
assert!(!s.contains("-Trigger"), "no trigger: only the app starts it");
|
||||||
|
assert!(s.contains("-MultipleInstances IgnoreNew") && s.contains("-ExecutionTimeLimit"), "{s}");
|
||||||
|
assert!(s.contains(&format!("-TaskName '{TASK_NAME}'")));
|
||||||
|
// a quote in the path cannot break out of the literal
|
||||||
|
let q = register_script(Path::new(r"C:\it's\igneum-app.exe"));
|
||||||
|
assert!(q.contains("'C:\\it''s\\igneum-app.exe'"), "{q}");
|
||||||
|
assert!(start_command().starts_with("Start-ScheduledTask -TaskName 'Igneum Power Helper'"));
|
||||||
|
assert!(remove_command().starts_with("Unregister-ScheduledTask -TaskName 'Igneum Power Helper' -Confirm:$false"));
|
||||||
|
assert!(query_command().contains("Get-ScheduledTask -TaskName 'Igneum Power Helper'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
let text = "3 pl 460\n4 lgc 2472\n";
|
||||||
|
let last = text.lines().filter_map(parse_line).map(|(s, _)| s).max().unwrap_or(0);
|
||||||
|
assert_eq!(last, 4);
|
||||||
|
let newer: Vec<_> = "3 pl 460\n4 lgc 2472\n5 rgc\n".lines().filter_map(parse_line).filter(|(s, _)| *s > last).collect();
|
||||||
|
assert_eq!(newer, vec![(5, HelperCmd::ClockReset)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ pub enum Source {
|
||||||
Watch,
|
Watch,
|
||||||
Miner(usize), // card index
|
Miner(usize), // card index
|
||||||
Telemetry, // nvidia-smi -l 5
|
Telemetry, // nvidia-smi -l 5
|
||||||
|
AmdTelemetry, // igneum-gpu-telemetry -l 5 (ADLX or sysfs), 5 October 2026
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Source {
|
impl Source {
|
||||||
|
|
@ -22,6 +23,7 @@ impl Source {
|
||||||
Source::Watch => "watch".into(),
|
Source::Watch => "watch".into(),
|
||||||
Source::Miner(i) => format!("miner{}", i + 1),
|
Source::Miner(i) => format!("miner{}", i + 1),
|
||||||
Source::Telemetry => "gpu".into(),
|
Source::Telemetry => "gpu".into(),
|
||||||
|
Source::AmdTelemetry => "gpu-amd".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ pub struct Work {
|
||||||
pub shard_wei: u128,
|
pub shard_wei: u128,
|
||||||
/// The first of this machine's keys that is assigned (the label that signs).
|
/// The first of this machine's keys that is assigned (the label that signs).
|
||||||
pub key_hash: String,
|
pub key_hash: String,
|
||||||
|
/// The chain block's DAA score (the deadline clock of spec 7.8).
|
||||||
|
pub daa: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the node's work list. Newest first, as the node returns it.
|
/// Parses the node's work list. Newest first, as the node returns it.
|
||||||
|
|
@ -67,6 +69,7 @@ pub fn parse_work(v: &Value) -> Vec<Work> {
|
||||||
in_pool: w["pool"].as_array().map(|p| !p.is_empty()).unwrap_or(false),
|
in_pool: w["pool"].as_array().map(|p| !p.is_empty()).unwrap_or(false),
|
||||||
shard_wei: hexu(&w["shardWei"]),
|
shard_wei: hexu(&w["shardWei"]),
|
||||||
key_hash: w["assignedKeys"].as_array().and_then(|k| k.first()).and_then(|k| k.as_str()).unwrap_or("").to_string(),
|
key_hash: w["assignedKeys"].as_array().and_then(|k| k.first()).and_then(|k| k.as_str()).unwrap_or("").to_string(),
|
||||||
|
daa: hexu(&w["daaScore"]) as u64,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
|
|
@ -344,6 +347,13 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
||||||
let ram_mb = crate::detect::total_ram_mb();
|
let ram_mb = crate::detect::total_ram_mb();
|
||||||
let mut last_probe = Instant::now() - Duration::from_secs(600);
|
let mut last_probe = Instant::now() - Duration::from_secs(600);
|
||||||
let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new();
|
let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new();
|
||||||
|
// proving v1 segment path: (first, last, aggregator wei) of the segment records this machine submitted
|
||||||
|
let mut submitted_segments: Vec<(u64, u64, u128)> = Vec::new();
|
||||||
|
let mut last_segment_secs: Option<f64> = None;
|
||||||
|
// segment records the node refused by the chain rule ("does not chain to ... pending"): held and offered again
|
||||||
|
// every pass until the segment's deadline (the fresh-record window of spec 7.8 is the segment length in DAA on
|
||||||
|
// the rule as shipped, 6 October 2026; from the fresh-rule switch the first retry lands)
|
||||||
|
let mut held_segments: Vec<HeldSegment> = Vec::new();
|
||||||
let mut last_verifier_read = Instant::now() - Duration::from_secs(600);
|
let mut last_verifier_read = Instant::now() - Duration::from_secs(600);
|
||||||
let mut asked_restart = false;
|
let mut asked_restart = false;
|
||||||
// macOS and Linux: the host sits next to the engine, so its pinned ids are read at once, proving on or off
|
// macOS and Linux: the host sits next to the engine, so its pinned ids are read at once, proving on or off
|
||||||
|
|
@ -450,7 +460,7 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let work = match evm_rpc(&shared, "igneum_getAssignedShards", json!([keys.iter().map(|(_, h)| h.clone()).collect::<Vec<_>>(), 60]), Duration::from_secs(10)) {
|
let work = match evm_rpc(&shared, "igneum_getAssignedShards", json!([keys.iter().map(|(_, h)| h.clone()).collect::<Vec<_>>(), crate::segments::WORK_LOOKBACK]), Duration::from_secs(10)) {
|
||||||
Ok(v) => parse_work(&v),
|
Ok(v) => parse_work(&v),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
set(&shared, |p| {
|
set(&shared, |p| {
|
||||||
|
|
@ -477,6 +487,52 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// held segment records: offered again, dropped past the deadline
|
||||||
|
if !held_segments.is_empty() {
|
||||||
|
let tip_daa = evm_rpc(&shared, "igneum_getProvingStatus", json!([]), Duration::from_secs(10)).ok().and_then(|st| st["tipDaa"].as_str().and_then(|x| u64::from_str_radix(x.trim_start_matches("0x"), 16).ok())).unwrap_or(0);
|
||||||
|
let mut keep = Vec::new();
|
||||||
|
for h in held_segments.drain(..) {
|
||||||
|
match retry_held(&shared, &h, tip_daa) {
|
||||||
|
Retry::Accepted => {
|
||||||
|
shared.event("proving", &format!("segment {}..{} record accepted on retry {} (held {} s)", h.first, h.last, h.tries + 1, h.since.elapsed().as_secs()));
|
||||||
|
submitted_segments.push((h.first, h.last, h.agg_wei));
|
||||||
|
set(&shared, |p| {
|
||||||
|
p.segments_submitted += 1;
|
||||||
|
p.aggregated += 1;
|
||||||
|
p.segment_note = format!("segment {}..{} accepted on retry", h.first, h.last);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Retry::Expired(why) => {
|
||||||
|
shared.log(&format!("prover: segment {}..{} record dropped after {} tries: {why}", h.first, h.last, h.tries));
|
||||||
|
}
|
||||||
|
Retry::Again(why) => {
|
||||||
|
let mut h = h;
|
||||||
|
h.tries += 1;
|
||||||
|
if h.tries % 30 == 1 {
|
||||||
|
shared.log(&format!("prover: segment {}..{} record held (try {}): {why}", h.first, h.last, h.tries));
|
||||||
|
}
|
||||||
|
keep.push(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
held_segments = keep;
|
||||||
|
set(&shared, |p| p.segments_held = held_segments.len() as u32);
|
||||||
|
}
|
||||||
|
// paid segments among what we submitted
|
||||||
|
for (first, last, wei) in submitted_segments.clone() {
|
||||||
|
let paid = evm_rpc(&shared, "igneum_getSegmentRecords", json!([format!("{first:#x}")]), Duration::from_secs(10))
|
||||||
|
.ok()
|
||||||
|
.map(|r| !r["paid"].is_null() && r["paid"]["payout"].as_str().map(|a| a.eq_ignore_ascii_case(&payout_address(&shared))).unwrap_or(false))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if paid {
|
||||||
|
submitted_segments.retain(|x| x.0 != first);
|
||||||
|
shared.event("proving", &format!("segment {first}..{last} paid {} IGN to the aggregator", wei as f64 / 1e18));
|
||||||
|
set(&shared, |p| {
|
||||||
|
p.segments_paid += 1;
|
||||||
|
p.segment_paid_wei += wei;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
let assigned = work.iter().filter(|w| w.assigned).count() as u32;
|
let assigned = work.iter().filter(|w| w.assigned).count() as u32;
|
||||||
set(&shared, |p| {
|
set(&shared, |p| {
|
||||||
p.assigned = assigned;
|
p.assigned = assigned;
|
||||||
|
|
@ -496,10 +552,60 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// proving v1 segment path (src/segments.rs, 6 October 2026): a whole segment first, the newest shard only
|
||||||
|
// when no whole segment qualifies
|
||||||
|
let payout = shared.settings.lock().unwrap().address.clone();
|
||||||
|
if payout.len() == 42 {
|
||||||
|
if let Some((seg, prev_file, expected_pv)) = pick_segment(&shared, &work, &keys[0].1, &mut attempted_segments, last_segment_secs) {
|
||||||
|
attempted_segments.insert(seg.first);
|
||||||
|
let started = Instant::now();
|
||||||
|
match prove_segment(&shared, t, &seg, &keys[0].0, &payout, prev_file.as_deref(), &expected_pv, &mut submitted) {
|
||||||
|
Ok(SegmentOutcome::Held(h)) => {
|
||||||
|
let secs = started.elapsed().as_secs_f64();
|
||||||
|
last_segment_secs = Some(secs);
|
||||||
|
shared.event("proving", &format!("segment {}..{}: {} shards proven and submitted in {secs:.0} s; the segment record is held ({})", seg.first, seg.last, seg.shards.len(), h.why));
|
||||||
|
set(&shared, |p| {
|
||||||
|
p.segment_last_s = secs;
|
||||||
|
p.status = "submitted".into();
|
||||||
|
p.message = format!("segment {}..{}: shards submitted, the segment record waits for the chain rule", seg.first, seg.last);
|
||||||
|
p.segment_note = format!("segment {}..{} proven whole in {secs:.0} s; its record is held: {}", seg.first, seg.last, h.why);
|
||||||
|
p.current = String::new();
|
||||||
|
});
|
||||||
|
held_segments.push(h);
|
||||||
|
set(&shared, |p| p.segments_held = held_segments.len() as u32);
|
||||||
|
}
|
||||||
|
Ok(SegmentOutcome::Submitted(agg_wei)) => {
|
||||||
|
let secs = started.elapsed().as_secs_f64();
|
||||||
|
last_segment_secs = Some(secs);
|
||||||
|
submitted_segments.push((seg.first, seg.last, agg_wei));
|
||||||
|
shared.event("proving", &format!("segment {}..{}: {} shards proven, aggregated and submitted in {secs:.0} s", seg.first, seg.last, seg.shards.len()));
|
||||||
|
set(&shared, |p| {
|
||||||
|
p.segments_submitted += 1;
|
||||||
|
p.segment_last_s = secs;
|
||||||
|
p.status = "submitted".into();
|
||||||
|
p.message = format!("segment {}..{} submitted; paid when a block carries it", seg.first, seg.last);
|
||||||
|
p.segment_note = format!("segment {}..{} proven whole in {secs:.0} s", seg.first, seg.last);
|
||||||
|
p.current = String::new();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
shared.log(&format!("prover: segment {}..{}: {e}", seg.first, seg.last));
|
||||||
|
set(&shared, |p| {
|
||||||
|
p.failed += 1;
|
||||||
|
p.status = "idle".into();
|
||||||
|
p.message = e.clone();
|
||||||
|
p.segment_note = e;
|
||||||
|
p.current = String::new();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
let Some(w) = choose(&work, &attempted) else {
|
let Some(w) = choose(&work, &attempted) else {
|
||||||
set(&shared, |p| {
|
set(&shared, |p| {
|
||||||
p.status = if submitted.is_empty() { "idle".into() } else { "submitted".into() };
|
p.status = if submitted.is_empty() { "idle".into() } else { "submitted".into() };
|
||||||
p.message = if assigned == 0 { "no shard assigned to this machine and none open in the last 60 blocks".into() } else { "every assigned and open shard is proven or paid".into() };
|
p.message = if assigned == 0 { format!("no shard assigned to this machine and none open in the last {} blocks", crate::segments::WORK_LOOKBACK) } else { "every assigned and open shard is proven or paid".into() };
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
@ -592,6 +698,218 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A segment record the node refused by the chain rule, kept with its proof for another offer.
|
||||||
|
pub struct HeldSegment {
|
||||||
|
pub first: u64,
|
||||||
|
pub last: u64,
|
||||||
|
pub deadline_daa: u64,
|
||||||
|
pub record: String,
|
||||||
|
pub proof_path: PathBuf,
|
||||||
|
pub agg_wei: u128,
|
||||||
|
pub why: String,
|
||||||
|
pub tries: u32,
|
||||||
|
pub since: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SegmentOutcome {
|
||||||
|
Submitted(u128),
|
||||||
|
Held(HeldSegment),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Retry {
|
||||||
|
Accepted,
|
||||||
|
Again(String),
|
||||||
|
Expired(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offers a held segment record again: accepted, held for another pass, or dropped past the segment's deadline.
|
||||||
|
fn retry_held(shared: &Shared, h: &HeldSegment, tip_daa: u64) -> Retry {
|
||||||
|
if tip_daa > 0 && tip_daa + 1 > h.deadline_daa {
|
||||||
|
return Retry::Expired(format!("past the deadline DAA {} at tip DAA {tip_daa}", h.deadline_daa));
|
||||||
|
}
|
||||||
|
let Ok(proof) = std::fs::read(&h.proof_path) else { return Retry::Expired(format!("proof file {} gone", h.proof_path.display())) };
|
||||||
|
let proof_hex = format!("0x{}", proof.iter().map(|b| format!("{b:02x}")).collect::<String>());
|
||||||
|
match evm_rpc(shared, "igneum_submitSegmentRecord", json!([{ "record": h.record, "proof": proof_hex }]), Duration::from_secs(60)) {
|
||||||
|
Ok(r) if r["accepted"].as_bool().unwrap_or(false) => Retry::Accepted,
|
||||||
|
Ok(r) => Retry::Again(r["reason"].as_str().unwrap_or("?").to_string()),
|
||||||
|
Err(e) => Retry::Again(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proving v1 segment path, the choice: the node's v1 status (active, the grid start, the segment length, the
|
||||||
|
/// deadline clock), the work list grouped into whole untouched segments (`segments::whole_segments`), the
|
||||||
|
/// candidates inside the deadline ranked for this key, then for the best three the node's segment statement:
|
||||||
|
/// executed and pending; the previous segment either paid with its proof in this node's pool (the chain continues,
|
||||||
|
/// `--prev`) or not paid and with no verified record of it waiting in the pool (fresh). Returns the segment, the
|
||||||
|
/// previous proof's host path when the chain continues, and the public values the node expects.
|
||||||
|
fn pick_segment(shared: &Shared, work: &[Work], key_hash: &str, attempted: &mut HashSet<u64>, last_secs: Option<f64>) -> Option<(crate::segments::SegmentWork, Option<String>, String)> {
|
||||||
|
let hexu = |x: &Value| x.as_str().and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()).unwrap_or(0);
|
||||||
|
let st = evm_rpc(shared, "igneum_getProvingStatus", json!([]), Duration::from_secs(10)).ok()?;
|
||||||
|
let v1 = &st["v1"];
|
||||||
|
if !v1["active"].as_bool().unwrap_or(false) || v1["start"].is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (start, n, unproven, tip_daa) = (hexu(&v1["start"]), hexu(&v1["segmentBlocks"]).max(1), hexu(&v1["unprovenDaa"]), hexu(&st["tipDaa"]));
|
||||||
|
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);
|
||||||
|
if cands.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dir = shared.runtime.app_dir.join("proving");
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
let wsl = cfg!(windows);
|
||||||
|
let as_host_path = |p: &Path| if wsl { wsl_path(p) } else { p.display().to_string() };
|
||||||
|
for seg in cands.into_iter().take(3) {
|
||||||
|
let Ok(stmt) = evm_rpc(shared, "igneum_getSegmentStatement", json!([format!("{:#x}", seg.first)]), Duration::from_secs(10)) else { continue };
|
||||||
|
if !stmt["executed"].as_bool().unwrap_or(false) || stmt["status"]["status"].as_str() != Some("pending") {
|
||||||
|
attempted.insert(seg.first);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prev = &stmt["previous"];
|
||||||
|
if prev.is_null() {
|
||||||
|
// fresh only when no record of the previous segment is waiting to be carried (the chain rule would
|
||||||
|
// refuse a fresh record once that one pays)
|
||||||
|
if seg.first >= start + n {
|
||||||
|
let p = evm_rpc(shared, "igneum_getSegmentRecords", json!([format!("{:#x}", seg.first - n)]), Duration::from_secs(10)).unwrap_or(Value::Null);
|
||||||
|
let waiting = p["pool"].as_array().map(|a| a.iter().any(|e| e["verified"] == json!(true) && e["includedIn"].is_null())).unwrap_or(false);
|
||||||
|
if waiting || !p["paid"].is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some((seg, None, stmt["publicValuesFresh"].as_str().unwrap_or("").to_string()));
|
||||||
|
}
|
||||||
|
if prev["proofInPool"] != json!(true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(got) = evm_rpc(shared, "igneum_getSegmentProofBytes", json!([prev["first"], prev["keyHash"]]), Duration::from_secs(60)) else { continue };
|
||||||
|
let hex = got["proof"].as_str().unwrap_or("").trim_start_matches("0x").to_string();
|
||||||
|
if hex.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let bytes: Vec<u8> = (0..hex.len() / 2).map(|k| u8::from_str_radix(&hex[2 * k..2 * k + 2], 16).unwrap_or(0)).collect();
|
||||||
|
let f = dir.join(format!("prev-{}.bin", seg.first));
|
||||||
|
if std::fs::write(&f, bytes).is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Some((seg, Some(as_host_path(&f)), stmt["publicValuesContinuing"].as_str().unwrap_or("").to_string()));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proving v1 segment path, the work: one export of the chain to the segment's last block, one fixture per block,
|
||||||
|
/// one host run (`--mode chain --save-shards`, `--prev` when the chain continues) that proves every shard and
|
||||||
|
/// aggregates the segment, then every shard record signed and submitted (the shard payouts) and the segment
|
||||||
|
/// record signed and submitted (the aggregator share). Returns the segment's aggregator wei.
|
||||||
|
fn prove_segment(shared: &Shared, t: &Tools, seg: &crate::segments::SegmentWork, label: &str, payout: &str, prev_file: Option<&str>, expected_pv: &str, submitted: &mut Vec<(u64, String, u32, u128)>) -> Result<SegmentOutcome, String> {
|
||||||
|
let (first, last) = (seg.first, seg.last);
|
||||||
|
let dir = shared.runtime.app_dir.join("proving").join(format!("seg-{first}"));
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
let as_host_path = |p: &Path| if t.wsl { wsl_path(p) } else { p.display().to_string() };
|
||||||
|
set(shared, |p| {
|
||||||
|
p.status = "proving".into();
|
||||||
|
p.current = format!("segment {first}..{last} ({} shards)", seg.shards.len());
|
||||||
|
p.started_at = crate::platform::unix_now_f();
|
||||||
|
p.message = "exporting the chain and cutting the segment's blocks".into();
|
||||||
|
});
|
||||||
|
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))?;
|
||||||
|
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 {
|
||||||
|
let fixture = dir.join(format!("block-{b}.json"));
|
||||||
|
let (ok, out) = run_tool(shared, t, &t.export, &[as_host_path(&seq), b.to_string(), as_host_path(&fixture)], &[], Duration::from_secs(600), &dir.join(format!("export-{b}.log")));
|
||||||
|
if !ok || !fixture.exists() {
|
||||||
|
return Err(format!("exporter, block {b}: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed")));
|
||||||
|
}
|
||||||
|
fixtures.push(as_host_path(&fixture));
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file(&seq);
|
||||||
|
// 2. the chain: every shard proven, every block aggregated with the previous, in one process
|
||||||
|
set(shared, |p| p.message = format!("proving {} shards and aggregating segment {first}..{last} ({})", seg.shards.len(), if t.cuda { "GPU" } else { "CPU, slow" }));
|
||||||
|
let results = dir.join("chain-results.json");
|
||||||
|
let mut args: Vec<String> = vec!["--mode".into(), "chain".into(), "--chain".into(), fixtures.join(","), "--prover".into(), payout.to_string(), "--save-shards".into(), "--out".into(), as_host_path(&results)];
|
||||||
|
if let Some(pf) = prev_file {
|
||||||
|
args.push("--prev".into());
|
||||||
|
args.push(pf.to_string());
|
||||||
|
}
|
||||||
|
let (ok, out) = run_tool(shared, t, &t.host, &args, &[("SP1_PROVER", if t.cuda { "cuda" } else { "cpu" }), ("RUST_LOG", "off")], Duration::from_secs(3 * 3600), &dir.join("chain.log"));
|
||||||
|
if !ok || !results.exists() {
|
||||||
|
let last_line = out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed").to_string();
|
||||||
|
let hint = if last_line.contains("PermissionDenied") { " (a GPU-server socket /tmp/sp1-cuda-*.sock owned by another user: the root-socket class)" } else { "" };
|
||||||
|
return Err(format!("chain: {last_line}{hint}"));
|
||||||
|
}
|
||||||
|
let res: Value = serde_json::from_str(&std::fs::read_to_string(&results).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||||
|
let to_win = |f: &str| if t.wsl { PathBuf::from(f.replace("/mnt/c/", "C:/")) } else { PathBuf::from(f) };
|
||||||
|
// 3. the shard records
|
||||||
|
let chain = chain_name(shared);
|
||||||
|
let mut shard_ok = 0usize;
|
||||||
|
for b in res["blocks"].as_array().cloned().unwrap_or_default() {
|
||||||
|
for r in b["shard_records"].as_array().cloned().unwrap_or_default() {
|
||||||
|
let (number, hash, shard) = (r["number"].as_u64().unwrap_or(0), r["block_hash"].as_str().unwrap_or("").to_string(), r["shard"].as_u64().unwrap_or(0) as u32);
|
||||||
|
let statement = r["statement"].as_str().unwrap_or("").to_string();
|
||||||
|
let proof_sha = r["proof_sha256"].as_str().unwrap_or("").to_string();
|
||||||
|
let proof_file = r["proof_file"].as_str().unwrap_or("").to_string();
|
||||||
|
let sg = crate::detect::run_timeout(crate::platform::quiet(&mut Command::new(&t.miner)).args(["sign-record", label, &chain, &hash, &number.to_string(), &shard.to_string(), payout, &statement, &proof_sha]), None, Duration::from_secs(20)).ok_or("sign-record did not run")?;
|
||||||
|
let signed: Value = serde_json::from_str(sg.lines().last().unwrap_or("")).map_err(|_| format!("sign-record: {}", sg.trim()))?;
|
||||||
|
let record = signed["record"].as_str().ok_or("sign-record gave no record")?.to_string();
|
||||||
|
let proof = std::fs::read(to_win(&proof_file)).map_err(|e| format!("proof file {proof_file}: {e}"))?;
|
||||||
|
let proof_hex = format!("0x{}", proof.iter().map(|b| format!("{b:02x}")).collect::<String>());
|
||||||
|
let out = evm_rpc(shared, "igneum_submitProofRecord", json!([{ "record": record, "proof": proof_hex }]), Duration::from_secs(60))?;
|
||||||
|
if out["accepted"].as_bool().unwrap_or(false) {
|
||||||
|
shard_ok += 1;
|
||||||
|
let wei = seg.shards.iter().position(|(n, _, s)| *n == number && *s == shard).map(|_| seg.shard_wei / seg.shards.len().max(1) as u128).unwrap_or(0);
|
||||||
|
submitted.push((number, hash.clone(), shard, wei));
|
||||||
|
} else {
|
||||||
|
shared.log(&format!("prover: segment {first}..{last}: block {number} shard {shard} record refused: {}", out["reason"].as_str().unwrap_or("?")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shard_ok != seg.shards.len() {
|
||||||
|
return Err(format!("{shard_ok} of {} shard records accepted; the segment record is not submitted", seg.shards.len()));
|
||||||
|
}
|
||||||
|
set(shared, |p| {
|
||||||
|
p.proved += shard_ok as u32;
|
||||||
|
p.submitted += shard_ok as u32;
|
||||||
|
});
|
||||||
|
// 4. the segment record: the aggregated statement against the node's native one (every field but provers)
|
||||||
|
let pv = res["segment_public_values"].as_str().ok_or("no public values in the chain results")?.to_string();
|
||||||
|
let proof_sha = res["segment_proof_sha256"].as_str().ok_or("no segment proof hash in the chain results")?.to_string();
|
||||||
|
let proof_file = res["segment_proof_file"].as_str().ok_or("no segment proof file in the chain results")?.to_string();
|
||||||
|
let strip = |h: &str| { let h = h.trim_start_matches("0x"); if h.len() == 680 { format!("{}{}", &h[..472], &h[536..]) } else { h.to_string() } };
|
||||||
|
if strip(&pv) != strip(expected_pv) {
|
||||||
|
return Err(format!("the aggregated statement differs from the node's native statement (it would be vetoed); ours {} node {}", &pv[..66.min(pv.len())], &expected_pv[..66.min(expected_pv.len())]));
|
||||||
|
}
|
||||||
|
let last_hash = seg.shards.iter().rev().find(|(n, _, _)| *n == last).map(|(_, h, _)| h.clone()).ok_or("no last block hash")?;
|
||||||
|
let sg = crate::detect::run_timeout(crate::platform::quiet(&mut Command::new(&t.miner)).args(["sign-segment-record", label, &chain, &first.to_string(), &last.to_string(), &last_hash, payout, &pv, &proof_sha]), None, Duration::from_secs(20)).ok_or("sign-segment-record did not run")?;
|
||||||
|
let signed: Value = serde_json::from_str(sg.lines().last().unwrap_or("")).map_err(|_| format!("sign-segment-record: {}", sg.trim()))?;
|
||||||
|
let record = signed["record"].as_str().ok_or("sign-segment-record gave no record")?.to_string();
|
||||||
|
let proof = std::fs::read(to_win(&proof_file)).map_err(|e| format!("segment proof file {proof_file}: {e}"))?;
|
||||||
|
let proof_hex = format!("0x{}", proof.iter().map(|b| format!("{b:02x}")).collect::<String>());
|
||||||
|
let r = evm_rpc(shared, "igneum_submitSegmentRecord", json!([{ "record": record, "proof": proof_hex }]), Duration::from_secs(60))?;
|
||||||
|
let hexu = |x: &Value| x.as_str().and_then(|s| u128::from_str_radix(s.trim_start_matches("0x"), 16).ok()).unwrap_or(0);
|
||||||
|
let stmt = evm_rpc(shared, "igneum_getSegmentStatement", json!([format!("{first:#x}")]), Duration::from_secs(10)).unwrap_or(Value::Null);
|
||||||
|
let agg_wei = hexu(&stmt["aggregatorWei"]);
|
||||||
|
// the fixtures and the export go; the proofs stay (the next segment's chain link, and a held record's offer)
|
||||||
|
for b in first..=last {
|
||||||
|
let _ = std::fs::remove_file(dir.join(format!("block-{b}.json")));
|
||||||
|
}
|
||||||
|
if !r["accepted"].as_bool().unwrap_or(false) {
|
||||||
|
let why = r["reason"].as_str().unwrap_or("?").to_string();
|
||||||
|
// the chain rule's refusal ("does not chain to ... pending until DAA ..."): held, not failed; anything else
|
||||||
|
// (a bad statement, a late carrier) is an error
|
||||||
|
if why.contains("does not chain") {
|
||||||
|
let deadline = hexu(&stmt["status"]["deadline_daa"]) as u64;
|
||||||
|
return Ok(SegmentOutcome::Held(HeldSegment { first, last, deadline_daa: if deadline > 0 { deadline } else { u64::MAX }, record, proof_path: to_win(&proof_file), agg_wei, why, tries: 0, since: Instant::now() }));
|
||||||
|
}
|
||||||
|
return Err(format!("segment record refused: {why}"));
|
||||||
|
}
|
||||||
|
set(shared, |p| p.aggregated += 1);
|
||||||
|
Ok(SegmentOutcome::Submitted(agg_wei))
|
||||||
|
}
|
||||||
|
|
||||||
/// Proving v1 (spec 7.8): one aggregation attempt. When the node reports v1 active, takes the newest executed
|
/// Proving v1 (spec 7.8): one aggregation attempt. When the node reports v1 active, takes the newest executed
|
||||||
/// segment that is still pending and not yet attempted here, needs one shard proof per shard of every block in
|
/// segment that is still pending and not yet attempted here, needs one shard proof per shard of every block in
|
||||||
/// this node's pool (`igneum_getProofBytes`, a verified one when there is one) and, when the previous segment is
|
/// this node's pool (`igneum_getProofBytes`, a verified one when there is one) and, when the previous segment is
|
||||||
|
|
|
||||||
214
app/igneum-app/src/segments.rs
Normal file
214
app/igneum-app/src/segments.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
//! Proving v1 (spec 7.8): segment-aligned work for the prover loop (6 October 2026).
|
||||||
|
//!
|
||||||
|
//! The shipped loop took the newest open shard each pass, so one prover scattered one block in about 45 across
|
||||||
|
//! the segment grid and no segment ever had all its blocks proven (node 1, 04:16Z: pending 55, proven 0). Here a
|
||||||
|
//! free prover claims a whole segment (`proving_v1_segment_blocks` consecutive chain blocks), proves every shard
|
||||||
|
//! of it in order from one export in one host run (`--mode chain --save-shards`), submits the shard records and the
|
||||||
|
//! aggregated segment record, then takes the next. One card completes whole segments at its own rate instead of
|
||||||
|
//! completing none.
|
||||||
|
//!
|
||||||
|
//! The choice is deterministic per prover: among the untouched whole segments still inside their deadline by a
|
||||||
|
//! margin, the lowest FNV-1a of (first block, this prover's key hash) wins, so several provers spread over the
|
||||||
|
//! candidates without a coordinator; the per-block fallback (`prover::choose`) stays for the passes where no whole
|
||||||
|
//! segment qualifies.
|
||||||
|
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
|
use crate::prover::Work;
|
||||||
|
|
||||||
|
/// The least time a claimed segment is given before its deadline (DAA units, about one a second on devnet): the
|
||||||
|
/// chain of 8 empty blocks took 135.6 s cold beside the miner (bench-log, 5 October 2026), so 240 leaves the
|
||||||
|
/// submission and the carrying block inside the window.
|
||||||
|
pub const SEGMENT_MARGIN_MIN_DAA: u64 = 240;
|
||||||
|
/// The margin grows with what the last segment actually took, times this.
|
||||||
|
pub const SEGMENT_MARGIN_FACTOR: f64 = 1.5;
|
||||||
|
/// How far back the work list reaches (chain blocks): the record window, so every open segment inside the
|
||||||
|
/// deadline is visible.
|
||||||
|
pub const WORK_LOOKBACK: u64 = 600;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SegmentWork {
|
||||||
|
pub first: u64,
|
||||||
|
pub last: u64,
|
||||||
|
/// the last block's DAA score; the deadline is it plus `proving_v1_unproven_daa`
|
||||||
|
pub last_daa: u64,
|
||||||
|
pub deadline_daa: u64,
|
||||||
|
/// (chain block number, block hash, shard index) in chain order
|
||||||
|
pub shards: Vec<(u64, String, u32)>,
|
||||||
|
/// the shard payouts summed (what the shards earn when carried)
|
||||||
|
pub shard_wei: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The segment holding chain block `number` on the grid that starts at `start`.
|
||||||
|
pub fn segment_of(start: u64, n: u64, number: u64) -> (u64, u64) {
|
||||||
|
let n = n.max(1);
|
||||||
|
let k = number.saturating_sub(start) / n;
|
||||||
|
(start + k * n, start + k * n + n - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The DAA margin a segment must have before its deadline: the floor, or 1.5 times the last segment's wall time.
|
||||||
|
pub fn need_daa(last_segment_secs: Option<f64>) -> u64 {
|
||||||
|
let from_last = last_segment_secs.map(|s| (s * SEGMENT_MARGIN_FACTOR).ceil() as u64).unwrap_or(0);
|
||||||
|
from_last.max(SEGMENT_MARGIN_MIN_DAA)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Groups the node's work list into whole, untouched segments: every block of the segment is in the list, every
|
||||||
|
/// listed shard is open (past its exclusive window), unpaid and not in this node's pool from us. A segment with a
|
||||||
|
/// block missing (inside the exclusive window, or outside the lookback) or a shard already paid is not a candidate.
|
||||||
|
pub fn whole_segments(start: u64, n: u64, unproven_daa: u64, work: &[Work]) -> Vec<SegmentWork> {
|
||||||
|
let n = n.max(1);
|
||||||
|
let mut by_block: BTreeMap<u64, Vec<&Work>> = BTreeMap::new();
|
||||||
|
for w in work.iter().filter(|w| w.number >= start) {
|
||||||
|
by_block.entry(w.number).or_default().push(w);
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
for number in by_block.keys() {
|
||||||
|
let (first, last) = segment_of(start, n, *number);
|
||||||
|
if !seen.insert(first) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut shards = Vec::new();
|
||||||
|
let mut wei: u128 = 0;
|
||||||
|
let mut last_daa = 0;
|
||||||
|
let mut whole = true;
|
||||||
|
for b in first..=last {
|
||||||
|
let Some(entries) = by_block.get(&b) else {
|
||||||
|
whole = false;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let mut e: Vec<&&Work> = entries.iter().collect();
|
||||||
|
e.sort_by_key(|w| w.shard);
|
||||||
|
e.dedup_by_key(|w| w.shard);
|
||||||
|
if e.iter().any(|w| !w.open || w.paid || w.in_pool) {
|
||||||
|
whole = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for w in e {
|
||||||
|
shards.push((w.number, w.hash.clone(), w.shard));
|
||||||
|
wei = wei.saturating_add(w.shard_wei);
|
||||||
|
if b == last {
|
||||||
|
last_daa = w.daa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if whole && !shards.is_empty() {
|
||||||
|
out.push(SegmentWork { first, last, last_daa, deadline_daa: last_daa.saturating_add(unproven_daa), shards, shard_wei: wei });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FNV-1a 64 of the segment's first block and this prover's key hash: the per-prover rank.
|
||||||
|
pub fn rank(first: u64, key_hash: &str) -> u64 {
|
||||||
|
let mut h: u64 = 0xcbf29ce484222325;
|
||||||
|
for b in first.to_be_bytes().iter().chain(key_hash.as_bytes()) {
|
||||||
|
h ^= *b as u64;
|
||||||
|
h = h.wrapping_mul(0x100000001b3);
|
||||||
|
}
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The segments to try, best first: inside the deadline by `need` DAA at `tip_daa`, not attempted, ranked by
|
||||||
|
/// `rank(first, key)` (ties by the older first block).
|
||||||
|
pub fn candidates(segs: &[SegmentWork], tip_daa: u64, need: u64, key_hash: &str, attempted: &HashSet<u64>) -> Vec<SegmentWork> {
|
||||||
|
let mut c: Vec<SegmentWork> = segs.iter().filter(|s| !attempted.contains(&s.first) && s.deadline_daa >= tip_daa.saturating_add(1).saturating_add(need)).cloned().collect();
|
||||||
|
c.sort_by(|a, b| rank(a.first, key_hash).cmp(&rank(b.first, key_hash)).then(a.first.cmp(&b.first)));
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn w(number: u64, shard: u32, daa: u64, open: bool, paid: bool, in_pool: bool) -> Work {
|
||||||
|
Work { number, hash: format!("0x{number:064x}"), shard, pgas: 0, tx_count: 0, assigned: false, open, paid, in_pool, shard_wei: 10, key_hash: String::new(), daa }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid(start: u64, n: u64, segments: u64, daa0: u64) -> Vec<Work> {
|
||||||
|
(0..segments * n).map(|i| w(start + i, 0, daa0 + i, true, false, false)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_grid_is_counted_from_the_first_v1_block() {
|
||||||
|
assert_eq!(segment_of(100, 8, 100), (100, 107));
|
||||||
|
assert_eq!(segment_of(100, 8, 107), (100, 107));
|
||||||
|
assert_eq!(segment_of(100, 8, 108), (108, 115));
|
||||||
|
assert_eq!(segment_of(100, 8, 123), (116, 123));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_whole_open_unpaid_untouched_segments_qualify() {
|
||||||
|
let mut work = grid(100, 4, 3, 1000); // 100..111, three segments
|
||||||
|
work.retain(|x| x.number != 105); // 104..107 has a block missing (inside its exclusive window, say)
|
||||||
|
work.iter_mut().find(|x| x.number == 110).unwrap().paid = true; // 108..111 has a paid shard
|
||||||
|
let segs = whole_segments(100, 4, 600, &work);
|
||||||
|
assert_eq!(segs.len(), 1);
|
||||||
|
assert_eq!((segs[0].first, segs[0].last), (100, 103));
|
||||||
|
assert_eq!(segs[0].shards.len(), 4);
|
||||||
|
assert_eq!(segs[0].last_daa, 1003);
|
||||||
|
assert_eq!(segs[0].deadline_daa, 1603);
|
||||||
|
assert_eq!(segs[0].shard_wei, 40);
|
||||||
|
// a shard of ours already in the pool, or one still exclusive, also disqualifies
|
||||||
|
let mut work = grid(100, 4, 1, 1000);
|
||||||
|
work[1].in_pool = true;
|
||||||
|
assert!(whole_segments(100, 4, 600, &work).is_empty());
|
||||||
|
let mut work = grid(100, 4, 1, 1000);
|
||||||
|
work[3].open = false;
|
||||||
|
assert!(whole_segments(100, 4, 600, &work).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_block_with_several_shards_lists_them_in_order() {
|
||||||
|
let mut work = grid(100, 2, 1, 1000);
|
||||||
|
work.push(w(101, 1, 1001, true, false, false));
|
||||||
|
work.push(w(100, 1, 1000, true, false, false));
|
||||||
|
let segs = whole_segments(100, 2, 600, &work);
|
||||||
|
assert_eq!(segs[0].shards.iter().map(|(n, _, s)| (*n, *s)).collect::<Vec<_>>(), vec![(100, 0), (100, 1), (101, 0), (101, 1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_deadline_margin_and_the_attempted_set_filter_the_candidates() {
|
||||||
|
let work = grid(100, 8, 4, 1000); // 100..131, deadlines 1607, 1615, 1623, 1631
|
||||||
|
let segs = whole_segments(100, 8, 600, &work);
|
||||||
|
assert_eq!(segs.len(), 4);
|
||||||
|
// at tip DAA 1380 with a 240 margin only the segments with a deadline at or past 1621 remain
|
||||||
|
let c = candidates(&segs, 1380, 240, "0xkey", &HashSet::new());
|
||||||
|
let firsts: Vec<u64> = c.iter().map(|s| s.first).collect();
|
||||||
|
assert_eq!(firsts.len(), 2);
|
||||||
|
assert!(firsts.contains(&116) && firsts.contains(&124));
|
||||||
|
let mut attempted = HashSet::new();
|
||||||
|
attempted.insert(firsts[0]);
|
||||||
|
let c2 = candidates(&segs, 1380, 240, "0xkey", &attempted);
|
||||||
|
assert_eq!(c2.len(), 1);
|
||||||
|
assert_eq!(c2[0].first, firsts[1]);
|
||||||
|
// past every deadline: nothing
|
||||||
|
assert!(candidates(&segs, 1700, 240, "0xkey", &HashSet::new()).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_order_is_deterministic_per_key_and_differs_between_keys() {
|
||||||
|
let work = grid(100, 8, 6, 1000);
|
||||||
|
let segs = whole_segments(100, 8, 600, &work);
|
||||||
|
let a = candidates(&segs, 1000, 240, "0xaaaa", &HashSet::new());
|
||||||
|
let a2 = candidates(&segs, 1000, 240, "0xaaaa", &HashSet::new());
|
||||||
|
assert_eq!(a, a2);
|
||||||
|
assert_eq!(a.len(), 6);
|
||||||
|
// two provers rank the six candidates differently (the spread); the sets are the same
|
||||||
|
let b = candidates(&segs, 1000, 240, "0xbbbb", &HashSet::new());
|
||||||
|
let (fa, fb): (Vec<u64>, Vec<u64>) = (a.iter().map(|s| s.first).collect(), b.iter().map(|s| s.first).collect());
|
||||||
|
let mut sa = fa.clone();
|
||||||
|
let mut sb = fb.clone();
|
||||||
|
sa.sort();
|
||||||
|
sb.sort();
|
||||||
|
assert_eq!(sa, sb);
|
||||||
|
assert_ne!(fa, fb, "two keys should not rank six segments identically");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_margin_follows_the_last_segment_time() {
|
||||||
|
assert_eq!(need_daa(None), 240);
|
||||||
|
assert_eq!(need_daa(Some(100.0)), 240);
|
||||||
|
assert_eq!(need_daa(Some(190.0)), 285);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -319,6 +319,12 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
||||||
shared.send(Cmd::SweepPin(key, pinned));
|
shared.send(Cmd::SweepPin(key, pinned));
|
||||||
Ok(json!({ "ok": true }))
|
Ok(json!({ "ok": true }))
|
||||||
}
|
}
|
||||||
|
// Power control (config.rs power_control): on = one administrator prompt now for the cap, off = nothing asks
|
||||||
|
"/api/power/control" => {
|
||||||
|
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||||
|
shared.send(Cmd::PowerControl(on));
|
||||||
|
Ok(json!({ "ok": true }))
|
||||||
|
}
|
||||||
"/api/sweep/enable" => {
|
"/api/sweep/enable" => {
|
||||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||||
shared.send(Cmd::SweepEnable(on));
|
shared.send(Cmd::SweepEnable(on));
|
||||||
|
|
@ -333,7 +339,8 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
||||||
Ok(json!({ "ok": true }))
|
Ok(json!({ "ok": true }))
|
||||||
}
|
}
|
||||||
"/api/quit" => {
|
"/api/quit" => {
|
||||||
shared.send(Cmd::Quit);
|
// the caller is on 127.0.0.1 and holds the token: the installer, the OTA apply, a script that read app.url
|
||||||
|
shared.send(Cmd::Quit("POST /api/quit (a local caller with the token: the installer, the OTA apply, or a script that read app.url)"));
|
||||||
Ok(json!({ "ok": true }))
|
Ok(json!({ "ok": true }))
|
||||||
}
|
}
|
||||||
_ => Err("unknown api".into()),
|
_ => Err("unknown api".into()),
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,11 @@ pub struct CardState {
|
||||||
pub temp_gpu: f64,
|
pub temp_gpu: f64,
|
||||||
pub temp_mem: f64,
|
pub temp_mem: f64,
|
||||||
pub telemetry_at: f64,
|
pub telemetry_at: f64,
|
||||||
|
// AMD through igneum-gpu-telemetry (ADLX on Windows, amdgpu sysfs on Linux), 5 October 2026; 0 = unknown
|
||||||
|
pub fan_pct: f64,
|
||||||
|
pub fan_rpm: f64,
|
||||||
|
pub mclk_mhz: f64,
|
||||||
|
pub util_pct: f64,
|
||||||
// hash per watt (src/sweep.rs)
|
// hash per watt (src/sweep.rs)
|
||||||
pub eff_mhw: f64, // live: hash_now over power_w, MH per watt; 0 = unknown
|
pub eff_mhw: f64, // live: hash_now over power_w, MH per watt; 0 = unknown
|
||||||
pub sweep_supported: bool, // NVIDIA with readable limits; the note says why not otherwise
|
pub sweep_supported: bool, // NVIDIA with readable limits; the note says why not otherwise
|
||||||
|
|
@ -108,6 +113,18 @@ pub struct CardState {
|
||||||
pub sweep_mhs: f64,
|
pub sweep_mhs: f64,
|
||||||
pub sweep_at: f64, // unix s of the last sweep
|
pub sweep_at: f64, // unix s of the last sweep
|
||||||
pub pinned: bool, // the user set the cap by hand; the sweep records but does not change it
|
pub pinned: bool, // the user set the cap by hand; the sweep records but does not change it
|
||||||
|
// Ember Tune (src/ember.rs): the two-knob tune, 5 October 2026
|
||||||
|
pub clock_max_mhz: u32, // the vendor's maximum core clock (0 = unknown)
|
||||||
|
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 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
|
||||||
|
pub tune_control: bool, // both knobs reach the card (else measure only; sweep_note says why)
|
||||||
|
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
|
||||||
// the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose
|
// the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose
|
||||||
pub variant: String,
|
pub variant: String,
|
||||||
pub race_mhs: f64,
|
pub race_mhs: f64,
|
||||||
|
|
@ -172,6 +189,9 @@ pub struct ProvingState {
|
||||||
pub submitted: u32,
|
pub submitted: u32,
|
||||||
pub paid: u32,
|
pub paid: u32,
|
||||||
pub failed: u32,
|
pub failed: u32,
|
||||||
|
/// wei, serialised as a decimal string: serde_json's `to_value` refuses a u128 over u64::MAX (about 18.45 IGN,
|
||||||
|
/// 15 paid shards at 1.23 IGN), and that refusal emptied the whole `/api/state` reply to "{}" (6 October 2026)
|
||||||
|
#[serde(serialize_with = "u128_string")]
|
||||||
pub paid_wei: u128,
|
pub paid_wei: u128,
|
||||||
pub current: String,
|
pub current: String,
|
||||||
pub started_at: f64,
|
pub started_at: f64,
|
||||||
|
|
@ -201,6 +221,15 @@ pub struct ProvingState {
|
||||||
/// proving v1: segment records this machine aggregated and submitted, and the aggregator's last line
|
/// proving v1: segment records this machine aggregated and submitted, and the aggregator's last line
|
||||||
pub aggregated: u32,
|
pub aggregated: u32,
|
||||||
pub segment_note: String,
|
pub segment_note: String,
|
||||||
|
/// proving v1 segment path (6 October 2026): whole segments this machine proved and submitted, paid, and what
|
||||||
|
/// they paid (wei as a decimal string, see `paid_wei`); the last segment's wall time
|
||||||
|
pub segments_submitted: u32,
|
||||||
|
pub segments_paid: u32,
|
||||||
|
#[serde(serialize_with = "u128_string")]
|
||||||
|
pub segment_paid_wei: u128,
|
||||||
|
pub segment_last_s: f64,
|
||||||
|
/// segment records the node refused by the chain rule and this machine offers again each pass
|
||||||
|
pub segments_held: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Default)]
|
#[derive(Clone, Serialize, Default)]
|
||||||
|
|
@ -244,8 +273,16 @@ pub struct SettingsState {
|
||||||
pub remote_jobs: bool,
|
pub remote_jobs: bool,
|
||||||
/// the prover service (src/prover.rs)
|
/// the prover service (src/prover.rs)
|
||||||
pub prove: bool,
|
pub prove: bool,
|
||||||
/// the efficiency sweep (src/sweep.rs): once after install, then weekly
|
/// the efficiency sweep (src/sweep.rs): once after install, then weekly; effective only with `power_control`
|
||||||
pub sweep: bool,
|
pub sweep: bool,
|
||||||
|
/// the NVIDIA power cap and the sweep may ask for administrator rights (config.rs: default off, one prompt when
|
||||||
|
/// switched on)
|
||||||
|
pub power_control: bool,
|
||||||
|
/// the line beside the Power control switch: why it is off, or that the rights were given
|
||||||
|
pub power_note: String,
|
||||||
|
/// 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,
|
||||||
/// the miner software's dev fee switch (settings; `--dev-fee 0` when off)
|
/// the miner software's dev fee switch (settings; `--dev-fee 0` when off)
|
||||||
pub dev_fee: bool,
|
pub dev_fee: bool,
|
||||||
/// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`)
|
/// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`)
|
||||||
|
|
@ -401,3 +438,22 @@ impl Rings {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A u128 as a decimal JSON string (the dashboard reads it with `Number()`).
|
||||||
|
pub fn u128_string<S: serde::Serializer>(v: &u128, s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
s.serialize_str(&v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod paid_wei_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_paid_total_over_u64_max_still_serialises_the_whole_state() {
|
||||||
|
let mut st = State::default();
|
||||||
|
st.proving.paid_wei = u64::MAX as u128 + 1;
|
||||||
|
let v = serde_json::to_value(&st).expect("the state serialises");
|
||||||
|
assert_eq!(v["proving"]["paid_wei"], serde_json::Value::String("18446744073709551616".into()));
|
||||||
|
assert!(v["mining"].is_object());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -340,10 +340,12 @@ impl Run {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The elevated helper that sets caps for a sweep (one administrator prompt per sweep, not one per step). It polls
|
/// The elevated helper that sets limits for a tune (one administrator prompt per tune, not one per step). It polls
|
||||||
/// `<dir>/cmd.txt` twice a second: a line `<seq> <watts>` runs `nvidia-smi -i <device> -pl <watts>`, `quit` ends it.
|
/// `<dir>/cmd.txt` twice a second; each line is `<seq> pl <watts>` (`nvidia-smi -i <device> -pl <watts>`; the
|
||||||
/// After 20 minutes without a new command it restores `<restore watts>` and exits by itself, so an engine that died
|
/// 0.3.9 form `<seq> <watts>` still works), `<seq> lgc <mhz>` (`-lgc 0,<mhz>`, the core clock cap; the memory clock
|
||||||
/// mid-sweep leaves the card on its old limit. It writes what it ran to `<dir>/helper.log`.
|
/// is never touched) or `<seq> rgc` (`-rgc`, unlocked); `quit` ends it. After 20 minutes without a new command it
|
||||||
|
/// restores `<restore watts>`, resets the clocks and exits by itself, so an engine that died mid-tune leaves the
|
||||||
|
/// card on its old limits. It writes what it ran to `<dir>/helper.log`.
|
||||||
pub fn helper_script_windows() -> &'static str {
|
pub fn helper_script_windows() -> &'static str {
|
||||||
r#"param([string]$Dir, [string]$Smi, [string]$Device, [string]$Restore)
|
r#"param([string]$Dir, [string]$Smi, [string]$Device, [string]$Restore)
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
|
|
@ -359,15 +361,27 @@ while ($true) {
|
||||||
$last = $c
|
$last = $c
|
||||||
$idle = Get-Date
|
$idle = Get-Date
|
||||||
if ($c -eq 'quit') { "$(Get-Date -Format o) quit" | Out-File -FilePath $log -Append -Encoding utf8; break }
|
if ($c -eq 'quit') { "$(Get-Date -Format o) quit" | Out-File -FilePath $log -Append -Encoding utf8; break }
|
||||||
$w = ($c -split ' ')[-1]
|
foreach ($line in ($c -split "`n")) {
|
||||||
if ($w -match '^\d+$') {
|
$p = ($line.Trim() -split ' ')
|
||||||
$out = (& $Smi -i $Device -pl $w 2>&1 | Out-String).Trim()
|
if ($p.Count -lt 2) { continue }
|
||||||
"$(Get-Date -Format o) -pl $w : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
$op = $p[1]; $v = $p[-1]
|
||||||
|
if ($p.Count -eq 2 -and $v -match '^\d+$') { $op = 'pl' }
|
||||||
|
if ($op -eq 'pl' -and $v -match '^\d+$') {
|
||||||
|
$out = (& $Smi -i $Device -pl $v 2>&1 | Out-String).Trim()
|
||||||
|
"$(Get-Date -Format o) $($p[0]) -pl $v : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
||||||
|
} elseif ($op -eq 'lgc' -and $v -match '^\d+$') {
|
||||||
|
$out = (& $Smi -i $Device -lgc "0,$v" 2>&1 | Out-String).Trim()
|
||||||
|
"$(Get-Date -Format o) $($p[0]) -lgc 0,$v : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
||||||
|
} 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (((Get-Date) - $idle).TotalMinutes -gt 20) {
|
if (((Get-Date) - $idle).TotalMinutes -gt 20) {
|
||||||
$out = (& $Smi -i $Device -pl $Restore 2>&1 | Out-String).Trim()
|
$out = (& $Smi -i $Device -pl $Restore 2>&1 | Out-String).Trim()
|
||||||
"$(Get-Date -Format o) idle 20 min: restored $Restore W and quit: $out" | Out-File -FilePath $log -Append -Encoding utf8
|
$out2 = (& $Smi -i $Device -rgc 2>&1 | Out-String).Trim()
|
||||||
|
"$(Get-Date -Format o) idle 20 min: restored $Restore W, clocks reset, and quit: $out / $out2" | Out-File -FilePath $log -Append -Encoding utf8
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 500
|
||||||
|
|
@ -388,11 +402,20 @@ while true; do
|
||||||
if [ -n "$c" ] && [ "$c" != "$last" ]; then
|
if [ -n "$c" ] && [ "$c" != "$last" ]; then
|
||||||
last="$c"; idle=$(date +%s)
|
last="$c"; idle=$(date +%s)
|
||||||
if [ "$c" = "quit" ]; then echo "$(date -u +%FT%TZ) quit" >> "$dir/helper.log"; break; fi
|
if [ "$c" = "quit" ]; then echo "$(date -u +%FT%TZ) quit" >> "$dir/helper.log"; break; fi
|
||||||
w="${c##* }"
|
printf '%s\n' "$c" | while IFS= read -r line; do
|
||||||
case "$w" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) -pl $w : $("$smi" -i "$dev" -pl "$w" 2>&1)" >> "$dir/helper.log";; esac
|
set -- $line
|
||||||
|
[ $# -ge 2 ] || continue
|
||||||
|
op="$2"; v="${line##* }"
|
||||||
|
[ $# -eq 2 ] && op=pl
|
||||||
|
case "$op" in
|
||||||
|
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" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
if [ $(( $(date +%s) - idle )) -gt 1200 ]; then
|
if [ $(( $(date +%s) - idle )) -gt 1200 ]; then
|
||||||
echo "$(date -u +%FT%TZ) idle 20 min: restored $restore W: $("$smi" -i "$dev" -pl "$restore" 2>&1)" >> "$dir/helper.log"; break
|
echo "$(date -u +%FT%TZ) idle 20 min: restored $restore W, clocks reset: $("$smi" -i "$dev" -pl "$restore" 2>&1) / $("$smi" -i "$dev" -rgc 2>&1)" >> "$dir/helper.log"; break
|
||||||
fi
|
fi
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
done
|
done
|
||||||
|
|
@ -405,8 +428,9 @@ pub fn unsupported_reason(vendor: &str, power_default_w: f64, device: &str) -> O
|
||||||
match vendor {
|
match vendor {
|
||||||
"nvidia" if power_default_w > 0.0 && !device.is_empty() => None,
|
"nvidia" if power_default_w > 0.0 && !device.is_empty() => None,
|
||||||
"nvidia" => Some("not available: nvidia-smi did not report this card's power limits"),
|
"nvidia" => Some("not available: nvidia-smi did not report this card's power limits"),
|
||||||
"apple" => Some("not available on Apple silicon: there is no power cap to set, and powermetrics needs administrator rights for the draw"),
|
// Ember Tune (src/ember.rs, 5 October 2026): AMD is tuned through igneum-gpu-telemetry, Apple measures only;
|
||||||
"amd" => Some("not available for AMD in this version: the app has no power reading or cap for AMD cards (nothing like nvidia-smi ships with the driver)"),
|
// the tune itself says which at its start (the card row's note)
|
||||||
|
"apple" | "amd" => None,
|
||||||
_ => Some("not available: no power reading or cap for this card"),
|
_ => Some("not available: no power reading or cap for this card"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -580,14 +604,16 @@ mod tests {
|
||||||
fn unsupported_reasons() {
|
fn unsupported_reasons() {
|
||||||
assert!(unsupported_reason("nvidia", 575.0, "0").is_none());
|
assert!(unsupported_reason("nvidia", 575.0, "0").is_none());
|
||||||
assert!(unsupported_reason("nvidia", 0.0, "0").unwrap().contains("power limits"));
|
assert!(unsupported_reason("nvidia", 0.0, "0").unwrap().contains("power limits"));
|
||||||
assert!(unsupported_reason("apple", 0.0, "").unwrap().contains("powermetrics"));
|
assert!(unsupported_reason("apple", 0.0, "").is_none(), "measure only, said by the tune");
|
||||||
assert!(unsupported_reason("amd", 0.0, "1").unwrap().contains("AMD"));
|
assert!(unsupported_reason("amd", 0.0, "1").is_none(), "tuned through igneum-gpu-telemetry");
|
||||||
|
assert!(unsupported_reason("other", 0.0, "1").is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn helper_scripts_carry_the_protocol() {
|
fn helper_scripts_carry_the_protocol() {
|
||||||
for s in [helper_script_windows(), helper_script_unix()] {
|
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("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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ pub fn bash_line(file: &Path, login: bool, args: &[&str]) -> String {
|
||||||
/// command line exactly as `bash_line` wrote it; elsewhere the words are ordinary arguments (nothing runs wsl there).
|
/// command line exactly as `bash_line` wrote it; elsewhere the words are ordinary arguments (nothing runs wsl there).
|
||||||
/// The caller adds stdio, the hidden-window flag and the timeout.
|
/// The caller adds stdio, the hidden-window flag and the timeout.
|
||||||
pub fn command(wsl_exe: &Path, distro: &str, user: Option<&str>, file: &Path, login: bool, args: &[&str]) -> Command {
|
pub fn command(wsl_exe: &Path, distro: &str, user: Option<&str>, file: &Path, login: bool, args: &[&str]) -> Command {
|
||||||
|
// console: a builder; every caller runs it through run_capture, run_streamed or platform::quiet (tools/ci/windows-spawn-check.mjs)
|
||||||
let mut c = Command::new(wsl_exe);
|
let mut c = Command::new(wsl_exe);
|
||||||
c.args(["-d", distro]);
|
c.args(["-d", distro]);
|
||||||
if let Some(u) = user.filter(|u| !u.is_empty()) {
|
if let Some(u) = user.filter(|u| !u.is_empty()) {
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,21 @@ var View = (function () {
|
||||||
function shortHex(h, head, tail) { h = String(h || ''); head = head || 8; tail = tail || 6; return h.length > head + tail + 2 ? h.slice(0, head) + '…' + h.slice(-tail) : h; }
|
function shortHex(h, head, tail) { h = String(h || ''); head = head || 8; tail = tail || 6; return h.length > head + tail + 2 ? h.slice(0, head) + '…' + h.slice(-tail) : h; }
|
||||||
var kindWord = Notices.kindWord;
|
var kindWord = Notices.kindWord;
|
||||||
// hot-plug (src/hotplug.rs): a removed card's row hides after five minutes (gone); a faulty one has no switch
|
// hot-plug (src/hotplug.rs): a removed card's row hides after five minutes (gone); a faulty one has no switch
|
||||||
function shownCards(cards) { return (cards || []).filter(function (c) { return !c.gone; }); }
|
// The list is ordered by performance (Josh, 6 October 2026): usable cards first, then by the measured rate since the
|
||||||
|
// start (5 MH/s buckets so the order does not flicker), then discrete, external and Apple before integrated, then
|
||||||
|
// memory; removed and unusable cards last. Ties keep the detection order.
|
||||||
|
function perfRank(c) {
|
||||||
|
var usable = !(c.removed_at > 0) && !c.problem ? 0 : 1;
|
||||||
|
var integrated = c.kind === 'integrated' ? 1 : 0;
|
||||||
|
var bucket = -Math.floor(((c.enabled ? (c.hash_avg || c.hash_now || 0) : 0)) / 5);
|
||||||
|
return [usable, integrated, bucket, -(c.vram_mb || 0)];
|
||||||
|
}
|
||||||
|
function shownCards(cards) {
|
||||||
|
return (cards || []).map(function (c, i) { return { c: c, i: i, r: perfRank(c) }; }).sort(function (a, b) {
|
||||||
|
for (var k = 0; k < a.r.length; k++) { if (a.r[k] !== b.r[k]) return a.r[k] - b.r[k]; }
|
||||||
|
return a.i - b.i;
|
||||||
|
}).map(function (x) { return x.c; }).filter(function (c) { return !c.gone; });
|
||||||
|
}
|
||||||
// listed, driver fine, not unplugged: a worker can run on it
|
// listed, driver fine, not unplugged: a worker can run on it
|
||||||
function present(c) { return !(c.removed_at > 0) && !c.problem; }
|
function present(c) { return !(c.removed_at > 0) && !c.problem; }
|
||||||
// the tooltip on a card's name: what the tool calls it (gfx1201), its device index, the OpenCL platform, the PCI address
|
// the tooltip on a card's name: what the tool calls it (gfx1201), its device index, the OpenCL platform, the PCI address
|
||||||
|
|
@ -372,7 +386,35 @@ var View = (function () {
|
||||||
}
|
}
|
||||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, toggle: toggle, nodeWords: nodeWords, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote };
|
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, toggle: toggle, nodeWords: nodeWords, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote };
|
||||||
})();
|
})();
|
||||||
if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; module.exports.View = View; }
|
/* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ----------
|
||||||
|
One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned
|
||||||
|
("Tuned: 122.3 MH/s at 290 W (0.422 MH/W)" plus the point and when), measure only (Apple, NVIDIA without Power
|
||||||
|
control: the measured line and why nothing is set), stopped (the reason), or not run yet. */
|
||||||
|
var TuneLine = (function () {
|
||||||
|
'use strict';
|
||||||
|
function point(cd) {
|
||||||
|
var pct = cd.sweep_pct || cd.power_pct || 0;
|
||||||
|
if (cd.tune_clock_mhz > 0) return cd.tune_clock_mhz + ' MHz at ' + pct + '%';
|
||||||
|
return pct ? pct + '%, clock unlocked' : '';
|
||||||
|
}
|
||||||
|
// the model: {kind: running|tuned|measured|stopped|idle|off, text, note}
|
||||||
|
function model(cd, now) {
|
||||||
|
cd = cd || {};
|
||||||
|
if (cd.vendor !== 'nvidia' && cd.vendor !== 'amd' && cd.vendor !== 'apple') return { kind: 'off', text: cd.sweep_note || 'tuning: no power or clock control for this card' };
|
||||||
|
if (cd.sweep_state === 'running') return { kind: 'running', text: cd.sweep_note || 'tuning: running' };
|
||||||
|
var when = cd.sweep_at && now ? ', ' + rel(now - cd.sweep_at) + ' ago' : '';
|
||||||
|
if (cd.tune_line) {
|
||||||
|
if (cd.tune_source === 'baseline' || !cd.tune_control) return { kind: 'measured', text: cd.tune_line + ' (measured as it runs' + when + ')', note: cd.tune_control ? '' : (cd.sweep_note || '') };
|
||||||
|
var src = cd.tune_source === 'confirm' ? 'from the fleet prior, confirmed' : 'full tune';
|
||||||
|
return { kind: 'tuned', text: cd.tune_line, note: point(cd) + ', ' + src + when + (cd.pinned ? '; your setting stays pinned' : '') + (cd.sweep_note && cd.sweep_note.indexOf('stopped') === 0 ? '; ' + cd.sweep_note : '') };
|
||||||
|
}
|
||||||
|
if (cd.sweep_note && cd.sweep_note.indexOf('tuning stopped') === 0) return { kind: 'stopped', text: cd.sweep_note };
|
||||||
|
return { kind: 'idle', text: cd.sweep_note || 'tuning: not run yet (starts after 120 s of steady mining)' };
|
||||||
|
}
|
||||||
|
function rel(s) { s = Math.max(0, Math.floor(s)); return s < 60 ? s + ' s' : s < 3600 ? Math.floor(s / 60) + ' min' : s < 86400 ? Math.floor(s / 3600) + ' h' : Math.floor(s / 86400) + ' d'; }
|
||||||
|
return { model: model, point: point, rel: rel };
|
||||||
|
})();
|
||||||
|
if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; module.exports.View = View; module.exports.TuneLine = TuneLine; }
|
||||||
|
|
||||||
if (typeof document !== 'undefined') (function () {
|
if (typeof document !== 'undefined') (function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -558,7 +600,8 @@ if (typeof document !== 'undefined') (function () {
|
||||||
$('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); });
|
$('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); });
|
||||||
$('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); });
|
$('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); });
|
||||||
$('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); });
|
$('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); });
|
||||||
$('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Sweep on: once after install, then weekly' : 'Sweep off'); }); });
|
$('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Ember Tune on: once after install, then weekly' : 'Ember Tune off'); }); });
|
||||||
|
$('s-power-control').addEventListener('change', function () { var on = this.checked; api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off; nothing asks'); }); });
|
||||||
$('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); });
|
$('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); });
|
||||||
$('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); });
|
$('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); });
|
||||||
$('s-log-open').addEventListener('click', function () { setDrawer(true); });
|
$('s-log-open').addEventListener('click', function () { setDrawer(true); });
|
||||||
|
|
@ -591,8 +634,8 @@ if (typeof document !== 'undefined') (function () {
|
||||||
$('s-cards').addEventListener('click', function (e) {
|
$('s-cards').addEventListener('click', function (e) {
|
||||||
var b = e.target.closest('button'); if (!b) return;
|
var b = e.target.closest('button'); if (!b) return;
|
||||||
if (b.dataset.d) { var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1; inp.value = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); sendCards('Identities set; that card’s worker restarts'); }
|
if (b.dataset.d) { var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1; inp.value = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); sendCards('Identities set; that card’s worker restarts'); }
|
||||||
else if (b.dataset.sweepStart) api('api/sweep/start', { key: b.dataset.sweepStart }).then(function () { toast('Sweep queued: 100% down to 50%, 75 s a step'); });
|
else if (b.dataset.sweepStart) api('api/sweep/start', { key: b.dataset.sweepStart }).then(function () { toast('Tune queued: the power limit and the core clock, 75 s a step'); });
|
||||||
else if (b.dataset.sweepStop) api('api/sweep/stop', {}).then(function () { toast('Sweep stopped; cap restored'); });
|
else if (b.dataset.sweepStop) api('api/sweep/stop', {}).then(function () { toast('Tune stopped; the card is back where it was'); });
|
||||||
else if (b.dataset.sweepPin) api('api/sweep/pin', { key: b.dataset.sweepPin, pinned: b.dataset.pinned === '1' }).then(function () { toast(b.dataset.pinned === '1' ? 'Cap pinned' : 'The sweep chooses the cap again'); });
|
else if (b.dataset.sweepPin) api('api/sweep/pin', { key: b.dataset.sweepPin, pinned: b.dataset.pinned === '1' }).then(function () { toast(b.dataset.pinned === '1' ? 'Cap pinned' : 'The sweep chooses the cap again'); });
|
||||||
else if (b.dataset.powerRetry) api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); });
|
else if (b.dataset.powerRetry) api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); });
|
||||||
});
|
});
|
||||||
|
|
@ -919,7 +962,7 @@ if (typeof document !== 'undefined') (function () {
|
||||||
if (s.detecting) { det.hidden = false; $('detect-text').textContent = 'asking the graphics cards to report in'; $('btn-cards-next').disabled = true; $('cards-sub').textContent = 'Asking the graphics cards to report in.'; cardsRendered = ''; return; }
|
if (s.detecting) { det.hidden = false; $('detect-text').textContent = 'asking the graphics cards to report in'; $('btn-cards-next').disabled = true; $('cards-sub').textContent = 'Asking the graphics cards to report in.'; cardsRendered = ''; return; }
|
||||||
det.hidden = true;
|
det.hidden = true;
|
||||||
var cards = shownCards(s.mining.cards);
|
var cards = shownCards(s.mining.cards);
|
||||||
var sig = cards.map(function (c) { return c.key + ':' + (c.problem || '') + ':' + (c.removed_at > 0 ? 'r' : '') + ':' + (c.enabled ? 'on' : 'off'); }).join('|');
|
var sig = cards.map(function (c) { return c.key + ':' + (c.problem || '') + ':' + (c.removed_at > 0 ? 'r' : '') + ':' + (c.enabled ? 'on' : 'off'); }).join('|'); // cards is already in performance order, so a reorder changes the signature
|
||||||
if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); }
|
if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); }
|
||||||
if (!cards.length) {
|
if (!cards.length) {
|
||||||
$('cards-sub').textContent = 'No GPU this app can drive was found.';
|
$('cards-sub').textContent = 'No GPU this app can drive was found.';
|
||||||
|
|
@ -1011,6 +1054,11 @@ if (typeof document !== 'undefined') (function () {
|
||||||
setText('pv-submitted', String(pv.submitted || 0));
|
setText('pv-submitted', String(pv.submitted || 0));
|
||||||
setText('pv-paid', String(pv.paid || 0));
|
setText('pv-paid', String(pv.paid || 0));
|
||||||
setText('pv-paid-sub', pv.paid_wei ? (Number(pv.paid_wei) / 1e18).toFixed(4) + ' IGN earned' : 'shards paid out');
|
setText('pv-paid-sub', pv.paid_wei ? (Number(pv.paid_wei) / 1e18).toFixed(4) + ' IGN earned' : 'shards paid out');
|
||||||
|
// proving v1 segments (6 October 2026): whole segments this machine proved, and the segment path's last line
|
||||||
|
var segLine = '';
|
||||||
|
if (pv.segments_submitted) segLine = 'Segments: ' + pv.segments_submitted + ' proven whole, ' + (pv.segments_paid || 0) + ' paid' + (pv.segment_paid_wei && Number(pv.segment_paid_wei) ? ' (' + (Number(pv.segment_paid_wei) / 1e18).toFixed(4) + ' IGN to the aggregator)' : '') + (pv.segment_last_s ? ', the last in ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' record' + (pv.segments_held > 1 ? 's' : '') + ' held for the chain rule' : '') + '.';
|
||||||
|
if (pv.segment_note && enabled) segLine += (segLine ? ' ' : '') + pv.segment_note + '.';
|
||||||
|
$('pv-seg-note').hidden = !segLine; setText('pv-seg-note', segLine);
|
||||||
var v = View.verifierWords(pv);
|
var v = View.verifierWords(pv);
|
||||||
setText('pv-verifier', v.word); $('pv-verifier').className = 'big-word ' + v.tone;
|
setText('pv-verifier', v.word); $('pv-verifier').className = 'big-word ' + v.tone;
|
||||||
$('pv-verifier-note').hidden = !pv.verifier_note; setText('pv-verifier-note', pv.verifier_note || '');
|
$('pv-verifier-note').hidden = !pv.verifier_note; setText('pv-verifier-note', pv.verifier_note || '');
|
||||||
|
|
@ -1097,7 +1145,8 @@ if (typeof document !== 'undefined') (function () {
|
||||||
var s = state;
|
var s = state;
|
||||||
var sw = function (id, on) { if (document.activeElement !== $(id)) $(id).checked = !!on; };
|
var sw = function (id, on) { if (document.activeElement !== $(id)) $(id).checked = !!on; };
|
||||||
sw('s-vote', s.settings.vote); sw('s-devfee', s.settings.dev_fee); sw('s-login', s.settings.start_at_login);
|
sw('s-vote', s.settings.vote); sw('s-devfee', s.settings.dev_fee); sw('s-login', s.settings.start_at_login);
|
||||||
sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', s.settings.sweep); sw('s-trust', s.settings.proof_verify_trust);
|
sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', s.settings.sweep); sw('s-trust', s.settings.proof_verify_trust); sw('s-power-control', s.settings.power_control);
|
||||||
|
setText('s-power-note', s.settings.tuning_off ? s.settings.tuning_note : (s.settings.power_note || (s.settings.power_control ? '' : 'Off: NVIDIA cards measure only; AMD cards need no rights.')));
|
||||||
setText('s-devfee-text', View.devFeeText(s));
|
setText('s-devfee-text', View.devFeeText(s));
|
||||||
if (document.activeElement !== $('s-name')) $('s-name').value = s.display_name || '';
|
if (document.activeElement !== $('s-name')) $('s-name').value = s.display_name || '';
|
||||||
setText('s-mid', (s.machine_id || '').slice(0, 8));
|
setText('s-mid', (s.machine_id || '').slice(0, 8));
|
||||||
|
|
@ -1113,27 +1162,30 @@ if (typeof document !== 'undefined') (function () {
|
||||||
if (unusable) return h + '<p class="help">' + esc(cd.reason || 'No worker can drive this card.') + '</p></div>';
|
if (unusable) return h + '<p class="help">' + esc(cd.reason || 'No worker can drive this card.') + '</p></div>';
|
||||||
if (nv) {
|
if (nv) {
|
||||||
h += '<div class="ctl"><span class="k">Power cap</span><input type="range" min="50" max="100" step="5" value="' + pct + '" aria-label="Power cap for ' + esc(cd.name) + '"><span class="pv">' + pct + '% · ' + Math.round(cd.power_default_w * pct / 100) + ' W</span>' +
|
h += '<div class="ctl"><span class="k">Power cap</span><input type="range" min="50" max="100" step="5" value="' + pct + '" aria-label="Power cap for ' + esc(cd.name) + '"><span class="pv">' + pct + '% · ' + Math.round(cd.power_default_w * pct / 100) + ' W</span>' +
|
||||||
'<p class="help">Lower is cooler and quieter; most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so the sweep leaves it.' : 'The sweep may move it.') + '</p></div>';
|
'<p class="help">Lower is cooler and quieter; most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so the tune leaves it.' : 'Ember Tune may move it.') + '</p></div>';
|
||||||
var want = Math.round(cd.power_default_w * pct / 100);
|
var want = Math.round(cd.power_default_w * pct / 100);
|
||||||
h += cd.power_applied ? '<div class="line ok">cap applied: <b>' + want + ' W</b>' + (cd.pinned ? ' <span class="pin">pinned</span>' : cd.sweep_pct === pct && cd.sweep_pct ? ' <span class="pin">chosen by the sweep</span>' : '') + '</div>' : '<div class="line hot">cap not applied (needs the administrator prompt) <button class="btn tiny" data-power-retry="1">Retry</button></div>';
|
h += cd.power_applied ? '<div class="line ok">cap applied: <b>' + want + ' W</b>' + (cd.pinned ? ' <span class="pin">pinned</span>' : cd.sweep_pct === pct && cd.sweep_pct ? ' <span class="pin">chosen by the sweep</span>' : '') + '</div>' : '<div class="line hot">cap not applied (needs the administrator prompt) <button class="btn tiny" data-power-retry="1">Retry</button></div>';
|
||||||
if (cd.telemetry_at > 0) h += '<div class="line">draw <b>' + (cd.power_w ? Math.round(cd.power_w) + ' W' : 'n/a') + '</b><span>GPU <b>' + (cd.temp_gpu ? Math.round(cd.temp_gpu) + ' °C' : 'n/a') + '</b></span><span>memory <b>' + (cd.temp_mem ? Math.round(cd.temp_mem) + ' °C' : 'n/a') + '</b></span><span>efficiency <b>' + (cd.eff_mhw ? cd.eff_mhw.toFixed(3) + ' MH/W' : 'n/a') + '</b></span></div>';
|
if (cd.telemetry_at > 0) h += '<div class="line">draw <b>' + (cd.power_w ? Math.round(cd.power_w) + ' W' : 'n/a') + '</b><span>GPU <b>' + (cd.temp_gpu ? Math.round(cd.temp_gpu) + ' °C' : 'n/a') + '</b></span><span>memory <b>' + (cd.temp_mem ? Math.round(cd.temp_mem) + ' °C' : 'n/a') + '</b></span><span>efficiency <b>' + (cd.eff_mhw ? cd.eff_mhw.toFixed(3) + ' MH/W' : 'n/a') + '</b></span></div>';
|
||||||
var running = cd.sweep_state === 'running', t;
|
|
||||||
if (!cd.sweep_supported) t = esc(cd.sweep_note || 'sweep not available on this card');
|
|
||||||
else if (running) t = esc(cd.sweep_note || 'sweep running');
|
|
||||||
else if (cd.sweep_pct) t = 'sweep: best <b>' + cd.sweep_pct + '%</b> · <b>' + cd.sweep_eff.toFixed(3) + ' MH/W</b> (' + cd.sweep_mhs.toFixed(1) + ' MH/s at ' + Math.round(cd.sweep_watts) + ' W' + (cd.sweep_at ? ', ' + rel(state.now - cd.sweep_at) : '') + ')';
|
|
||||||
else t = esc(cd.sweep_note || 'sweep: not run yet');
|
|
||||||
var btn = !cd.sweep_supported ? '' : running ? '<button class="btn tiny" data-sweep-stop="1">Stop</button>' : '<button class="btn tiny" data-sweep-start="' + esc(cd.key) + '">Sweep now</button>' + (cd.pinned ? ' <button class="btn tiny" data-sweep-pin="' + esc(cd.key) + '" data-pinned="0">Unpin</button>' : '');
|
|
||||||
h += '<div class="line' + (running ? ' on' : '') + '">' + t + ' ' + btn + '</div>';
|
|
||||||
} else if (cd.vendor === 'apple') {
|
} else if (cd.vendor === 'apple') {
|
||||||
h += '<p class="help">Apple silicon manages its own power; there is no cap to set.</p>';
|
h += '<p class="help">Apple silicon manages its own power; there is no cap to set. Ember Tune measures the card as it runs.</p>';
|
||||||
}
|
}
|
||||||
|
// Ember Tune's line (TuneLine.model): running, tuned, measured, stopped or not run yet, for every card
|
||||||
|
h += tuneLineHtml(cd);
|
||||||
h += '<div class="ctl"><span class="k">Identities</span><p class="help" style="grid-column:2">Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
h += '<div class="ctl"><span class="k">Identities</span><p class="help" style="grid-column:2">Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
||||||
return h + '</div>';
|
return h + '</div>';
|
||||||
}
|
}
|
||||||
|
function tuneLineHtml(cd) {
|
||||||
|
var m = TuneLine.model(cd, state ? state.now : 0);
|
||||||
|
if (m.kind === 'off') return m.text ? '<div class="line dim">' + esc(m.text) + '</div>' : '';
|
||||||
|
var running = m.kind === 'running';
|
||||||
|
var t = m.kind === 'tuned' || m.kind === 'measured' ? '<b>' + esc(m.text) + '</b>' + (m.note ? ' <span class="dim">' + esc(m.note) + '</span>' : '') : esc(m.text);
|
||||||
|
var btn = running ? '<button class="btn tiny" data-sweep-stop="1">Stop</button>' : '<button class="btn tiny" data-sweep-start="' + esc(cd.key) + '">Tune now</button>' + (cd.pinned ? ' <button class="btn tiny" data-sweep-pin="' + esc(cd.key) + '" data-pinned="0">Unpin</button>' : '');
|
||||||
|
return '<div class="line' + (running ? ' on' : '') + '">' + t + ' ' + btn + '</div>';
|
||||||
|
}
|
||||||
function renderSetCards(s) {
|
function renderSetCards(s) {
|
||||||
var cards = shownCards(s.mining.cards), box = $('s-cards');
|
var cards = shownCards(s.mining.cards), box = $('s-cards');
|
||||||
if (box.querySelector('input[type=range]:active') || box.contains(document.activeElement)) return;
|
if (box.querySelector('input[type=range]:active') || box.contains(document.activeElement)) return;
|
||||||
var sig = JSON.stringify(cards.map(function (c) { return [c.key, c.enabled, c.identities, c.power_pct, c.pinned, c.power_applied, c.sweep_state, c.sweep_pct, c.sweep_note, Math.round(c.power_w || 0), Math.round(c.temp_gpu || 0), Math.round(c.temp_mem || 0), c.problem || '', c.removed_at > 0]; }));
|
var sig = JSON.stringify(cards.map(function (c) { return [c.key, c.enabled, c.identities, c.power_pct, c.pinned, c.power_applied, c.sweep_state, c.sweep_pct, c.sweep_note, c.tune_line, c.tune_source, c.tune_control, Math.round(c.power_w || 0), Math.round(c.temp_gpu || 0), Math.round(c.temp_mem || 0), c.problem || '', c.removed_at > 0]; }));
|
||||||
if (sig === setCardsSig) return;
|
if (sig === setCardsSig) return;
|
||||||
setCardsSig = sig;
|
setCardsSig = sig;
|
||||||
setText('s-cards-eyebrow', cards.length ? cards.length + ' card' + (cards.length === 1 ? '' : 's') : '');
|
setText('s-cards-eyebrow', cards.length ? cards.length + ' card' + (cards.length === 1 ? '' : 's') : '');
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@
|
||||||
<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">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 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>
|
</div>
|
||||||
|
<p class="note" id="pv-seg-note" hidden></p>
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-head"><h3>Verifier</h3><div class="eyebrow">the node's check</div></div>
|
<div class="card-head"><h3>Verifier</h3><div class="eyebrow">the node's check</div></div>
|
||||||
|
|
@ -366,8 +367,10 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-head"><h3>Graphics cards</h3><div class="eyebrow" id="s-cards-eyebrow"></div></div>
|
<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>
|
<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>Find each NVIDIA card's best efficiency</span></label>
|
<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, the power cap steps from 100% down to 50% and holds the step with the most hashes per watt. A cap you set by hand is left alone.</p>
|
<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>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-head"><h3>This machine</h3></div>
|
<div class="card-head"><h3>This machine</h3></div>
|
||||||
|
|
|
||||||
44
app/igneum-app/ui/tune-line.test.mjs
Normal file
44
app/igneum-app/ui/tune-line.test.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// node --test app/igneum-app/ui/tune-line.test.mjs (no dependencies; CI runs it in the site job)
|
||||||
|
// The card row's Ember Tune line (app.js TuneLine): what a user sees per state, from the card state fields.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.js'), 'utf8');
|
||||||
|
const mod = { exports: {} };
|
||||||
|
new Function('module', src)(mod);
|
||||||
|
const { model, point } = mod.exports.TuneLine;
|
||||||
|
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 });
|
||||||
|
|
||||||
|
test('tuned: the line the brief asks for, with the point, the source and when', () => {
|
||||||
|
const m = model(card({ 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 - 3600 }), NOW);
|
||||||
|
assert.equal(m.kind, 'tuned');
|
||||||
|
assert.equal(m.text, 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)');
|
||||||
|
assert.equal(m.note, '2470 MHz at 100%, full tune, 1 h ago');
|
||||||
|
const c = model(card({ tune_line: 'Tuned: 17.7 MH/s at 177 W (0.100 MH/W)', tune_source: 'confirm', sweep_pct: 90, sweep_at: NOW - 120, vendor: 'amd' }), NOW);
|
||||||
|
assert.equal(c.note, '90%, clock unlocked, from the fleet prior, confirmed, 2 min ago');
|
||||||
|
const p = model(card({ tune_line: 'Tuned: 1 MH/s at 1 W (1.000 MH/W)', tune_source: 'full', sweep_pct: 70, pinned: true }), NOW);
|
||||||
|
assert.match(p.note, /your setting stays pinned$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('measure only: Apple and NVIDIA without Power control say so beside the measured line', () => {
|
||||||
|
const a = model(card({ vendor: 'apple', tune_control: false, tune_line: 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)', tune_source: 'baseline', sweep_note: 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed', sweep_at: NOW - 60 }), NOW);
|
||||||
|
assert.equal(a.kind, 'measured');
|
||||||
|
assert.equal(a.text, 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W) (measured as it runs, 1 min ago)');
|
||||||
|
assert.match(a.note, /^measure only on Apple silicon/);
|
||||||
|
const n = model(card({ tune_control: false, tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'baseline', sweep_note: 'measure only until Power control is on in Settings (Windows asks for administrator rights once)' }), NOW);
|
||||||
|
assert.equal(n.kind, 'measured');
|
||||||
|
assert.match(n.note, /Power control/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running, stopped, idle and off', () => {
|
||||||
|
assert.deepEqual(model(card({ sweep_state: 'running', sweep_note: 'tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)' }), NOW), { kind: 'running', text: 'tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)' });
|
||||||
|
assert.equal(model(card({ sweep_note: 'tuning stopped: a remote job took the GPU' }), NOW).kind, 'stopped');
|
||||||
|
assert.equal(model(card({}), NOW).text, 'tuning: not run yet (starts after 120 s of steady mining)');
|
||||||
|
assert.equal(model(card({ sweep_note: 'tuning: waits for 120 s of steady mining' }), NOW).text, 'tuning: waits for 120 s of steady mining');
|
||||||
|
assert.equal(model(card({ vendor: 'other' }), NOW).kind, 'off');
|
||||||
|
assert.equal(point({ tune_clock_mhz: 0, sweep_pct: 0, power_pct: 0 }), '');
|
||||||
|
});
|
||||||
|
|
@ -172,6 +172,24 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
||||||
// a row that is gone (five minutes after removal) is not shown at all; the big button counts only present cards
|
// a row that is gone (five minutes after removal) is not shown at all; the big button counts only present cards
|
||||||
assert.deepEqual(V.shownCards([card(), card({ key: 'x', gone: true })]).map((c) => c.key), ['nvidia:0:RTX 5090']);
|
assert.deepEqual(V.shownCards([card(), card({ key: 'x', gone: true })]).map((c) => c.key), ['nvidia:0:RTX 5090']);
|
||||||
assert.equal(V.present(card()), true);
|
assert.equal(V.present(card()), true);
|
||||||
|
// performance order (6 October 2026): PC 1 detects 5090, integrated AMD, 9070 XT; the list shows the 5090, the 9070 XT,
|
||||||
|
// then the integrated card, whatever the detection order and whatever the integrated card's rate
|
||||||
|
const pc1 = [
|
||||||
|
card({ key: 'nvidia:0:RTX 5090', hash_avg: 122 }),
|
||||||
|
card({ key: 'amd:gfx1036', name: 'AMD Radeon(TM) Graphics', vendor: 'amd', kind: 'integrated', vram_mb: 512, hash_avg: 2.1, enabled: true }),
|
||||||
|
card({ key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', vendor: 'amd', kind: 'discrete', vram_mb: 16384, hash_avg: 18.2 }),
|
||||||
|
];
|
||||||
|
assert.deepEqual(V.shownCards(pc1).map((c) => c.key), ['nvidia:0:RTX 5090', 'amd:gfx1201', 'amd:gfx1036']);
|
||||||
|
// before any rate (first start): discrete by memory, integrated last; a removed card last of all; ties keep detection order
|
||||||
|
const fresh = [
|
||||||
|
card({ key: 'amd:gfx1036', kind: 'integrated', vram_mb: 512, hash_avg: 0, hash_now: 0, state: 'off' }),
|
||||||
|
card({ key: 'amd:gfx1201', kind: 'discrete', vram_mb: 16384, hash_avg: 0, hash_now: 0, state: 'waiting' }),
|
||||||
|
card({ key: 'nvidia:0:RTX 5090', hash_avg: 0, hash_now: 0, state: 'waiting' }),
|
||||||
|
card({ key: 'nvidia:1:RTX 5090', hash_avg: 0, hash_now: 0, state: 'waiting', removed_at: 5 }),
|
||||||
|
];
|
||||||
|
assert.deepEqual(V.shownCards(fresh).map((c) => c.key), ['nvidia:0:RTX 5090', 'amd:gfx1201', 'amd:gfx1036', 'nvidia:1:RTX 5090']);
|
||||||
|
// a small rate change does not reorder (5 MH/s buckets): 122 and 124 sort as equal and keep detection order
|
||||||
|
assert.deepEqual(V.shownCards([card({ key: 'a', hash_avg: 122 }), card({ key: 'b', hash_avg: 124 })]).map((c) => c.key), ['a', 'b']);
|
||||||
assert.equal(V.present(gone), false);
|
assert.equal(V.present(gone), false);
|
||||||
assert.equal(V.present(bad), false);
|
assert.equal(V.present(bad), false);
|
||||||
const t = V.toggle({ state: 'mining', paused: false, cards: [gone, bad] }, { synced: true }, {});
|
const t = V.toggle({ state: 'mining', paused: false, cards: [gone, bad] }, { synced: true }, {});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
|
// 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
|
#ifndef IGNEUM_HOST_VERSION_H
|
||||||
#define IGNEUM_HOST_VERSION_H
|
#define IGNEUM_HOST_VERSION_H
|
||||||
#define IGNEUM_HOST_VERSION_STR "0.3.11"
|
#define IGNEUM_HOST_VERSION_STR "0.3.13"
|
||||||
#define IGNEUM_HOST_VERSION_RC 0,3,11,0
|
#define IGNEUM_HOST_VERSION_RC 0,3,13,0
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1551,6 +1551,125 @@ What is measured: one BLS12-381 aggregate signature over 16 summed G1 keys plus
|
||||||
|
|
||||||
Reading (the NEW finding, ledger C4). With the module off GHOSTDAG alone converges on the heavier chain and the losing side's records re-determine (F24 works when the chain moves). With the module on the overlay holds during the split (A, with 30% of the frozen table, locks nothing; B locks 7 and 8) and then fails at the heal in the shipped node: B's certificates for blocks off n0's chain are "kept pending until the chain decides (no lock at this index)", n0's chain never decides because GHOSTDAG keeps its heavier tip and nothing turns the certificate into a fork-choice constraint, and once n0's last lock (index 7, DAA 209) is one window old (DAA 329) the frozen table stops applying on A's chain ("no frozen table (no lock on this chain inside the window)"), A's two keys are 100% of A's own window (B's post-cut blocks are red there) and n0 locks 10, 11, 12 alone; B's certificates for 10 and 11 then log CONFLICTING on n0 (n0 log, 17:27:04 to 17:29:54 BST). A finality fork from a 96-s honest partition, no attacker, table intact at the heal; the 150-s run and the v2 control end the same way. The spec's fork choice ("GHOSTDAG among tips through all certified checkpoints", 3.5) is therefore implemented only for certificates over blocks already on the node's chain. Fix named in the ledger entry: verify an off-chain certificate against the table at its own block and let it constrain fork choice (a certificate-driven reorg), then re-determine. Raw: `scratchpad fud-a/c4-results-*.md`, node logs `c4-on90-tmp/`, `c4-v2-control-tmp/`.
|
Reading (the NEW finding, ledger C4). With the module off GHOSTDAG alone converges on the heavier chain and the losing side's records re-determine (F24 works when the chain moves). With the module on the overlay holds during the split (A, with 30% of the frozen table, locks nothing; B locks 7 and 8) and then fails at the heal in the shipped node: B's certificates for blocks off n0's chain are "kept pending until the chain decides (no lock at this index)", n0's chain never decides because GHOSTDAG keeps its heavier tip and nothing turns the certificate into a fork-choice constraint, and once n0's last lock (index 7, DAA 209) is one window old (DAA 329) the frozen table stops applying on A's chain ("no frozen table (no lock on this chain inside the window)"), A's two keys are 100% of A's own window (B's post-cut blocks are red there) and n0 locks 10, 11, 12 alone; B's certificates for 10 and 11 then log CONFLICTING on n0 (n0 log, 17:27:04 to 17:29:54 BST). A finality fork from a 96-s honest partition, no attacker, table intact at the heal; the 150-s run and the v2 control end the same way. The spec's fork choice ("GHOSTDAG among tips through all certified checkpoints", 3.5) is therefore implemented only for certificates over blocks already on the node's chain. Fix named in the ledger entry: verify an off-chain certificate against the table at its own block and let it constrain fork choice (a certificate-driven reorg), then re-determine. Raw: `scratchpad fud-a/c4-results-*.md`, node logs `c4-on90-tmp/`, `c4-v2-control-tmp/`.
|
||||||
|
|
||||||
|
## 5 October 2026 (evening), the 9070 XT on the eGPU: why 17.9 MH/s, and what moved
|
||||||
|
|
||||||
|
PC 1 (ae432dc7, Windows 11, Ryzen 7 9800X3D with its gfx1036, RTX 5090 on CUDA), an AMD Radeon RX 9070 XT (gfx1201, RDNA 4) in a Sonnet Breakaway Box 850T5 over USB4, Adrenalin 26.9.2 (OpenCL driver string `3683.0 (PAL,LC)`, platform `OpenCL 2.1 AMD-APP (3683.0)`). Branch `opencl-rdna4`. Josh: "the hashrate is low" (17.9 MH/s with one worker; two workers on the card earlier gave 8.9 and 9.4).
|
||||||
|
|
||||||
|
**Before, from PC 1's own app log** (`node tools/logs.mjs win-ae432dc7-20261005-181046`, the miner's STATUS line for the card `amd:1:gfx1201`, 2^21-nonce jobs): `hash=17.82 MH/s wall (17.83 MH/s inside jobs) ... idle=0.3%`. Wall equals inside, so the host loop (template fetch, job line, read-back, scan) costs nothing measurable; the dispatch itself is slow. The worker's `ready` line: `exchange 0` (local memory: AMD lists `cl_khr_subgroups` and no shuffle extension), `batch 4194304`, `dataset-log2 28` (1 GiB), device `[1] gfx1201` on the 3683.0 platform, `AMD wavefront width 32`. The same card was listed again as `[3] gfx1201` on the older platform `3652.0` (the 32.0.21042 driver's OpenCL registration is still present after the update): that is the two-worker run.
|
||||||
|
|
||||||
|
**Hypotheses, each with its number** (the measurement job `rdna4-bench-1`, 18:39:25 to 18:41:17 UTC, the card switched off in the app through `POST /api/cards` for key `amd:1:gfx1201` only, the 5090 untouched; worker exe sha256 `53c7e8c9…5403e10` built from this branch by `proto-cuda/nvrtc/build-windows.sh`; read back with `node tools/jobs.mjs rdna4-bench-1`):
|
||||||
|
|
||||||
|
| # | Hypothesis | Measured | Verdict |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | The dataset or program is re-sent over the eGPU link per job | Nothing is re-sent: the dataset (1 GiB) and cache (256 MiB) are built on the device once per pair (`info first pack ... cache 11 dataset 51 ms` on the Mac check); per 2^21-nonce job the old path sent 32 B up and read 16 MiB down; the serve A/B below puts a number on that read-back | Not the cause |
|
||||||
|
| 2 | Work-group, occupancy, wave width, the exchange | `clGetKernelSubGroupInfoKHR`: sub-group 32 for a 32-item work-group (wave32), private memory 0 (no spills), preferred multiple 32; `--group-warps 1, 2, 4, 8` = 18.024, 18.063, 18.070, 18.039 MH/s (`--batches 3`, 2^24, device event time); `--batch-log2 21` (the app's job size) = 18.108 | Not the cause: the shape does not move the number |
|
||||||
|
| 3 | The wrong AMD platform | The app's worker runs on `[1]`, the 3683.0 platform (ready line). The old platform's `[3]` gives 18.049 MH/s: the same. The duplicate listing is real and is the two-worker halving | Not the cause of 17.9; fixed anyway (below) |
|
||||||
|
| 4 | The card's own random-read rate | `--memprobe`: dependent random 4-byte loads over 1024 MiB top out at 2.42 to 2.68 G loads/s from 4,096 lanes up (table below); 128 loads per hash gives a ceiling of 18.9 to 20.9 MH/s; the hash runs at 18.0 to 18.1 | THE CAUSE: the hash is at 87 to 95% of what this card does for this access pattern |
|
||||||
|
|
||||||
|
**The memprobe on the 9070 XT** (`igneum-worker-opencl.exe --device 1 --memprobe`, device event time, best of 3, 256 dependent steps per lane; `chase` = one dependent random 4-byte load per step, `indep x8` = eight independent chains per lane):
|
||||||
|
|
||||||
|
| Buffer | Work-group | Lanes in flight | chase G loads/s | ns per dependent load | indep x8 G loads/s |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 4 MiB (inside the 8 MB L2, approximate size) | 256 | 4,096 | 34.95 | 117 | |
|
||||||
|
| 4 MiB | 256 | 262,144 | 64.63 | 4,056 | 63.8 (262k lanes) |
|
||||||
|
| 64 MiB (the 64 MB Infinity Cache, approximate size) | 256 | 4,096 | 9.17 | 447 | |
|
||||||
|
| 64 MiB | 256 | 262,144 | 9.18 | 28,561 | 8.8 (262k lanes) |
|
||||||
|
| 1024 MiB (GDDR6) | 32 | 4,096 | 2.64 | 1,552 | |
|
||||||
|
| 1024 MiB | 32 | 65,536 | 2.60 | 25,181 | |
|
||||||
|
| 1024 MiB | 32 | 4,194,304 | 2.43 | 1,729,136 | |
|
||||||
|
| 1024 MiB | 256 | 4,096 | 2.64 | 1,552 | |
|
||||||
|
| 1024 MiB | 256 | 262,144 | 2.45 | 106,831 | 2.46 (262k lanes) |
|
||||||
|
| 1024 MiB | 256 | 4,194,304 | 2.42 | 1,732,023 | 2.42 (4M lanes) |
|
||||||
|
| ALU chain, 1,048,576 lanes x 4,096 steps | 256 | | 6,219 G int ops/s (5 ops per step counted, approximate) | | |
|
||||||
|
|
||||||
|
Reading: at the dataset size the card delivers about 2.5 G random 4-byte reads per second whatever the parallelism (4,096 lanes already saturate it; more lanes only queue, the ns column is Little's law on a fixed throughput). Eight independent loads per lane give the same 2.4 G/s, so it is not a latency-hiding problem in the kernel. Inside the Infinity Cache the same chain runs 3.7x faster and inside L2 26x faster, so the cap is the path to GDDR6 for random reads. The ALU chain says the shader clock is not parked (approximate: 6.2 T int ops/s is of the order of 64 CUs x 64 lanes x 2.46 GHz with quarter-rate multiplies).
|
||||||
|
|
||||||
|
**Against the other two cards** (same probe; the 5090 through NVIDIA's OpenCL `[4]` WHILE its CUDA worker was mining, so a lower bound; the Mac through Apple OpenCL, wall time, a Mac at high load, approximate):
|
||||||
|
|
||||||
|
| Card | 1024 MiB chase at 4,096 lanes | 1024 MiB chase ceiling | indep x8 ceiling | ceiling / 128 = hash ceiling | measured hash rate |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| RX 9070 XT, eGPU over USB4 | 2.64 G/s, 1,552 ns | 2.42 to 2.68 G/s | 2.42 G/s | 18.9 to 20.9 MH/s | 18.0 to 18.1 MH/s (bench), 17.8 (app) |
|
||||||
|
| RTX 5090, PCIe 5 x16, contended | 9.09 G/s, 451 ns | 16.4 to 18.0 G/s | 16.2 to 16.7 G/s | 128 to 141 MH/s | 127 MH/s (app, Josh), 139.7 alone (M11) |
|
||||||
|
| Apple M5 Max, Apple OpenCL | 2.10 G/s, 1,949 ns | 3.41 to 3.49 G/s | 3.45 to 3.47 G/s | 26.6 to 27.3 MH/s | 27.9 Mhash/s (README, Apple OpenCL) |
|
||||||
|
|
||||||
|
Reading: on all three cards the hash runs within a few percent of 1/128 of the card's dependent random-read ceiling, which is what a 128-load program should do; the probe is a good model of the hash. The 5090 does 6.6x the random reads of the 9070 XT for 2.8x the rated bandwidth (1,792 against 640 GB/s, vendor figures): the rest is access granularity and DRAM behaviour on random 4-byte reads, which the kernel cannot change.
|
||||||
|
|
||||||
|
**Power, heat, fans and clocks, measured** (branch `opencl-rdna4-telemetry`; Josh watched the 9070 XT at 90% usage with its fans barely turning and the app had no AMD reading, the MH/W line came from nvidia-smi only; a new helper `proto-opencl/gpu-telemetry.c` reads ADLX on Windows and the amdgpu sysfs on Linux. Job `tele-measure-1`, 20:27:45 to 20:29:41 UTC, both cards mining in the app, nothing touched: `igneum-gpu-telemetry -l 5` (sha256 `703cf69c…a9c69b`) and `nvidia-smi --query-gpu=index,name,power.draw,temperature.gpu,fan.speed,clocks.mem,clocks.gr,utilization.gpu -l 5` side by side, the app's `hash_now` every 5 s; `node tools/jobs.mjs tele-measure-1`):
|
||||||
|
|
||||||
|
| Card | Samples | Watts (mean, min to max) | Temperature | Fan | Memory clock | Shader clock | Busy | Hash (mean of 24) | MH/W, measured |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| RX 9070 XT, bus 98, ADLX `GPUPower` | 12 (the helper's buffered tail was lost at the kill; fixed, `fflush` per sample) | 198.9 (193 to 212) | 64 C | 657 rpm (ADLX gives rpm; no percent) | 2,505 MHz | 3,290 MHz | 100% | 17.73 MH/s | 0.089 |
|
||||||
|
| RTX 5090, nvidia-smi, 450 W cap | 24 | 307.6 (306.3 to 308.7) | 69 C | 44% | 13,801 MHz | 2,850 MHz | 94% | 122.30 MH/s | 0.398 |
|
||||||
|
| gfx1036 (integrated, idle) | 12 | 42.7 (32 to 56; the package, not the GPU alone) | 62 C | none | 2,800 MHz | 600 MHz | 0% | off | |
|
||||||
|
|
||||||
|
Reading: the 9070 XT draws 199 W of its 304 W board rating (vendor figure) at 100% busy with the shader clock at its top, so the die is waiting on memory, which is the ceiling finding again; the fans at 657 rpm and 64 C are the card's own curve at that load, not a fault. Per watt the 5090 is 4.5x the 9070 XT on this program class (0.398 against 0.089 MH/W). The earlier per-watt claim from the board rating (304 W) would have read 0.058 MH/W; the measured number is 1.5x that.
|
||||||
|
|
||||||
|
**Is it the eGPU link?** No. 2.42 G loads/s x 64 B lines = 155 GB/s of DRAM traffic, forty times what a USB4 PCIe tunnel carries (about 4 GB/s, approximate); the 1 GiB buffer sits in the card's own memory (the 4 and 64 MiB cases show the card's caches at work above it, and a buffer in host memory would run below 0.1 G/s). A PCIe slot would move the per-job read-back (16 MiB per 2^21-nonce job on the old path, now gone) and nothing else; the random-read ceiling is the card's. What a PCIe slot would give: the same 18 MH/s.
|
||||||
|
|
||||||
|
**What changed on `opencl-rdna4`** (`proto-opencl/host.c`, `app/igneum-app/src/detect.rs`):
|
||||||
|
|
||||||
|
| Change | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| Duplicate platform | `--list` showed the card twice ([1] 3683.0 and [3] 3652.0); the app made two cards and ran two workers (8.9 + 9.4 MH/s) | the older platform's entry prints as ` dup [3] ... hidden, use [1]`, the default pick skips it, the app's parser (`parse_opencl_list`, 3 tests) never makes a card of it; `--device 3` still works for comparison. Verified on PC 1: `platforms: 2 device(s) hidden ...`, cards `amd:0:gfx1036` and `amd:1:gfx1201` only |
|
||||||
|
| Kernel report | work-group and local memory | plus preferred multiple, private memory (spills), sub-group size on every exchange path (`info kernel:` in serve mode) |
|
||||||
|
| Read-back per dispatch | 8 B per nonce (16 MiB per job) and a host scan of 2^21 words | a GPU select pass: the hits (index, hash) behind an atomic counter plus 34 sentinel words; 276 B per chunk plus 16 B per hit; found lines in nonce order; `--readback full` / `IGNEUM_READBACK=full` keeps the old path; a chunk with over 256 hits falls back to the full read |
|
||||||
|
| Transfer accounting | none | bytes up and down per chunk and the mean device time of kernel, select, read-back and scan in the stats line every 200 jobs and at quit |
|
||||||
|
| `--memprobe` | none | the tables above, no pack needed |
|
||||||
|
|
||||||
|
Correctness: `proto-opencl/test-generic.sh` on the Mac (Apple OpenCL) PASS on both paths: "15 sampled hashes (both packs, both sides of the 32-bit nonce boundary) equal igneum-pow hash-bound"; select path transfers `5 chunks, up 180 B, down 4452 B`, full path `up 160 B, down 1536 B` (the check's jobs are 32 to 64 nonces with every nonce a hit). The bench on the 9070 XT: cache check PASS, dataset self-test PASS, 6 of 6 vector warps PASS, batch fingerprint `3cc4fbf90fa6366c` at 2^24 for the devnet pack (the Apple OpenCL value in the README), at every `--group-warps`.
|
||||||
|
|
||||||
|
**The serve-mode A/B on the card** (job `rdna4-serve-4`, 19:11 UTC, card off in the app, worker exe sha256 `324a6d9b…2bfdfff`; 200 real `job` lines of 2,097,152 nonces each, the app's `--job-nonces`, against the emulator test pack `pack-a` (epoch `edc4fa84…`, self-test PASS, 96 of 96 vector lanes), target `0000100000000000` so that 408 hits fall in 200 jobs on both paths; `done` ms over jobs 11 to 200; `node tools/jobs.mjs rdna4-serve-4`):
|
||||||
|
|
||||||
|
| Read-back | Bytes down per job | Kernel (device, mean) | Select pass | Read-back (wall) | Host scan | Mean job | Inside-job rate |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| full (before) | 16,777,216 | 116.12 ms | 0 | 7.28 ms | 0.55 ms | 124.22 ms | 16.88 MH/s |
|
||||||
|
| select (after) | 309 | 116.00 ms | 0.039 ms | 0.78 ms | 0.00 ms | 117.38 ms | 17.87 MH/s |
|
||||||
|
| select (repeat) | 309 | 115.96 ms | 0.038 ms | 0.76 ms | 0.00 ms | 117.33 ms | 17.87 MH/s |
|
||||||
|
|
||||||
|
Reading: the kernel is the same 116.0 ms on both paths (18.08 MH/s pure kernel, the bench's number). The old path paid 7.8 ms per job for 16 MiB over the eGPU link (2.3 GB/s, the USB4 tunnel's rate; a PCIe slot would read it in about 1 ms, approximate) and the host scan. The select pass removes it: +5.9% per job on this link, nothing on the kernel. Both paths found the same 408 hits. The `--group-warps` and exchange levers were already shown flat above, so this is the whole host-side gain available on the 9070 XT.
|
||||||
|
|
||||||
|
**Probes with a fresh seed per repetition** (the first probe round replayed the same addresses on repeats, so its low-lane rows were cache hits; fixed in `probeLaunch`, job `rdna4-serve-4`): 1024 MiB chase at 256 lanes 276 ns per dependent load, at 1,024 lanes 422 ns, at 4,096 lanes 1,560 ns (2.63 G/s, the cap). Random 64-byte lines (four `uint4` loads per step) at 1024 MiB: 2.46 to 2.88 G lines/s = 158 to 184 GB/s in lines, the same count per second as the 4-byte chase: every random 4-byte read costs this card a 64-byte line fetch. Coalesced stream over the whole 1024 MiB: 635.2 GB/s against the vendor's 640 GB/s, so the memory clock is in its full state and the card is not parked. Inside the 64 MiB buffer the line probe reaches 8.3 to 14.0 G lines/s (533 to 894 GB/s in lines: the Infinity Cache, approximate).
|
||||||
|
|
||||||
|
**A second defect found on the way: the pack export race.** PC 1's app log since its 19:02 UTC restart (`node tools/logs.mjs win-ae432dc7-20261005-190232`): `worker error: error 0 pack packs\devnet: the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT` at 19:07:03, 19:07:19 and 19:08:07, so the 9070 XT was not mining at all in the app while this entry was written (my job `rdna4-serve-1` at 18:43 hit the same folder in the same state). Cause, from `app/igneum-app/src/engine.rs` `prepare_worker`: one thread per card, each running `igneum-miner export-pack` into the one folder `packs\devnet`; across an epoch change the two exports interleave and the folder keeps one epoch's `program.h` with the other's `seeds.txt` until the next export. Fix on this branch: a process-wide mutex around both export sites (`EXPORT_LOCK`); the second export rewrites the same pack. Not measured in the app yet: it ships with the branch.
|
||||||
|
|
||||||
|
**Answer to Josh.** The 9070 XT does 2.5 G random 4-byte reads per second from its memory for this access pattern, and the hash needs 128 of them, so about 19 MH/s is this card's ceiling for the current program class, on any slot; it was running at 92% of that. The eGPU link cost 6% per job through the read-back, now removed (17.87 against 16.88 MH/s inside jobs standalone). The duplicate platform that halved it to 8.9 + 9.4 is folded away. The pack race that stopped it is serialised. Nothing else in the worker's control moves the number: the next step for this card is the program class itself (fewer, wider loads per hash would favour AMD's 64-byte lines), which is a consensus question, not a worker one.
|
||||||
|
|
||||||
|
## 5 October 2026 (night), Ember Tune: the two-knob efficiency tune, the fleet prior, and what PC 1 could measure tonight (miner-community-lead)
|
||||||
|
|
||||||
|
Branch `ember-tune` (54ff1bc), docs/plans/ember-tune.md. Every card tuned for MH per watt out of the box: the power limit and the core clock cap stepped on the live kernel (memory clock never touched), the point with the best MH per watt within 1% of the top rate kept and pinned, every result uploaded as a `TUNE {json}` record (a hash of the install id, no address) and folded per (card model, driver major, program class) into a prior the signed manifest carries back, so a new card of a known model starts there and confirms it in two steps.
|
||||||
|
|
||||||
|
**What was measured tonight (PC 1, machine ae432dc7, from its own uploads to the intake):**
|
||||||
|
|
||||||
|
| Fact | Where it was read | Consequence |
|
||||||
|
|---|---|---|
|
||||||
|
| The installed 0.3.9 app runs as `DESKTOP-KMCV30N\Admin` with `elevated=False` (account line, 19:02:33 UTC) | app log `win-ae432dc7-20261005-190232` | `nvidia-smi -pl` and `-lgc` need administrator rights; the one prompt is the Power control switch (3562f26), which the app never raises by itself |
|
||||||
|
| Two in-app sweep attempts aborted at 20:09 UTC: `the_elevated_helper_did_not_run_(the_administrator_prompt_was_cancelled)` | the same log | no stored sweep result from today exists; the 5090's two-knob tune is owed to the morning (one click on Power control, then it runs by itself within 2 minutes of steady mining) |
|
||||||
|
| The RX 9070 XT left PC 1's bus at about 20:40 UTC, was back at 21:09 and gone again at 21:22:59 UTC (the eGPU link, third drop today) | the telemetry agent and the PC 1 scheduler | the AMD path (ADLX, no prompt) is unit-tested on the helper's captured line shapes; its end-to-end run waits for the card |
|
||||||
|
|
||||||
|
**The pipeline, verified without a card:** 9 `ember` unit tests (plans, clamps, the choice rule, the five marks, a faulted step reverted inside a fake-clock run, the confirm verdicts, the baseline plan, the record and prior shapes, the vendor reasons), the AMD `tune` line and the 0.3.10 sample line parsed (`engine::amd_telemetry_tests`), the helper protocol (`sweep::tests`), 6 relay aggregation tests (five samples converge on 2,470 MHz at 100%; an outlier at 0.908 MH/W moves the median by nothing; baseline records make no prior; de-duplication; the manifest merge keeps lever 2's cards; the canonical round trip), 3 UI line tests. A test manifest was signed on this Mac with `packaging/ota/publish-manifest.sh --tuning` from fixture priors: `tuning.priors["NVIDIA_GeForce_RTX_5090|581|l128w16"]` = 2,470 MHz at 100%, 5 samples, beside the kernel-variant `cards` entry and `tuning.ember {enabled: true, min_samples: 5, rate_tolerance_pct: 1}`, signature verified by the signer, 21:25 UTC.
|
||||||
|
|
||||||
|
**Tier consequences** (docs/plans/ember-tune.md section 7): a 9-step full tune costs about 12 minutes once and 3 minutes a week per card, under 1% of the hour, the worker never stops; a rig tunes one card at a time and every card of a known model after the first takes the 3-minute confirm; a pool user gives up the same 1% of shares at most; Apple silicon and AMD on Linux measure only and the row says so.
|
||||||
|
|
||||||
|
**The PC 1 run, 22:30 UTC (job ember-tune-pc1-1, engine aeea3228..., PC 1 on 0.3.10):** the job published at 22:29:40Z, the installed app stopped its miners and started the second engine at 22:30:21Z, and at 22:31:06Z the installed app quit (its log: `quit: stopping the miners, then the node`, then `job ember-tune-pc1-1: aborted (the app is quitting)`), 46 s in, before any step. Nothing was set. Corrected the same night (C35), then named the next morning from the second engine's own log (collect ember-c35-collect-1, 06:59Z): the second engine, reporting 0.3.9 (the branch's Cargo version) under the manifest's `min_supported_version`, took the 0.3.10 update as urgent (the "urgent" rule beats the copied `auto_update = false`), downloaded it at 22:31:02Z and started `ota-apply.ps1` with the per-user installer at 22:31:05Z; the installer's PrepareToInstall sent `POST /api/quit` to the installed app, which logged `quit:` at 22:31:06Z. So the source was my own second engine's updater, through the installer, one second before: a second install of 0.3.10 over the 0.3.10 PC 1 had taken through the shipper's update-now at 21:40:41Z (release-0.3.10.md section 8), whose only effect was the quit and the hang. The first reading (the 0.3.11 rollout) was wrong in the cause and right in the class: an installer. What else is established: the engine's quit then HUNG for 24 minutes in the jobs runner's abort, waiting for EOF on the script's stdout pipe whose write end the second engine and its miners had inherited, and those miners (2 igneum-miner, 2 CUDA workers, 1 OpenCL worker) mined on, orphaned, until the relay lane killed them at about 23:00Z; the second engine also raised one administrator prompt at about 22:30:25Z (`apply_power_limits` at start counted `--sweep` as Power control), 41 s before the quit; PC 2's unexplained quit at 20:01:09Z came 20 s after a cancelled prompt of the same class, so the prompt is the common factor and the morning's test (one prompt raised beside the mining app on PC 2, the stamped quit line read). Fixed on the branch: b671c8b (quit sources, Power control alone decides, no cap at start under `--sweep`), 8ab9068 (no pipe into a second engine, its tree ended, the CI check), and the third close: a second engine never runs the updater (`IGNEUM_APP_NO_OTA=1`, implied by `--sweep`; the playbooks set it; the CI check demands it). What the run did record, the "before" snapshots with the miners stopped:
|
||||||
|
|
||||||
|
| Card | Read back at 22:30:20Z | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| RTX 5090 (driver 617.14) | limit 450 W of 575 W default (min 400, max 600), draw 259.9 W idle-after-stop, core 2,850 MHz, `clocks.max.gr` 3,090 MHz, memory 14,001 MHz | the two-knob plan for this card is 5 power steps (575, 518, 460, 403, 400 W) and 4 clock steps (2,781, 2,472, 2,163, 1,854 MHz); it needs the one administrator prompt (Power control) |
|
||||||
|
| RX 9070 XT (bus 98, present again) | `tune 1 ... gmax 0 gmax_range -500 1000 plimit 0 plimit_range -30 10 factory 1 ok` | the helper's clock range is an OFFSET from stock in MHz, not a ceiling: a probe reading it as a 1,000 MHz maximum would have asked for `--set-gmax 900`, an overclock. Fixed at 054e041: an offset range closes the clock knob (until the stock clock is known) and the power ladder runs on the percent scale bounded by the range, so the 9070 XT's plan is 100, 90, 80, 70% (the -30 floor), 4 steps |
|
||||||
|
| Radeon(TM) Graphics (integrated) | `tune 0 ... gmax - ... factory 0 ok` | no manual tuning: measure only, and it is off by default anyway |
|
||||||
|
|
||||||
|
**Run 2, 6 October 2026, 07:21 to 07:56Z (job ember-tune-pc1-2, elevated on Josh's word, engine 25113f52..., PC 1 on 0.3.11):** Josh answered the one prompt; the installed app stopped its miners at 07:21:16Z; the second engine ran for the whole 35-minute budget at "waiting, 0.00 MH/s" and no step ran. Cause: the playbook wrote the engine's copy of settings.json with PowerShell 5.1's `Set-Content -Encoding utf8`, which adds a UTF-8 BOM; the engine's JSON parser refuses it, `Settings::load` fell back to defaults (no payout address, no cards), the engine logged `[error] no payout address` and never started a miner. Run 1's scratch log carried the same line the night before. Readbacks, idle both times: the 5090 at 90.6 W before and 69.9 W after (2,505 then 2,407 MHz core, 14,001 MHz memory, limit 450 W of 575), the 9070 XT at factory (`gmax 0`, `plimit 0`). Nothing set on either card. The installed app's runner released the miners-stopped hold by itself on the failed exit (`job finished; the miners restart` at 07:56:50Z, both miners up by 07:57:04Z, `mining` at 07:57:29Z): mining paused 36 min 13 s. Fix 8273494: the copy is written without a BOM, the address is read back and the job fails within seconds if it is empty (`RESULT TUNE scratch settings: address ..., cards N, first bytes ...`), and the CI check fails any playbook writing JSON with `Set-Content -Encoding utf8`. The re-run needs one more click on the prompt.
|
||||||
|
|
||||||
|
**Dry run 3, 6 October 2026, 14:56 to 15:02Z (job ember-dryrun-pc1-3, unelevated, no prompt, measure only; engine from ember-tune 07d5a72, kit sha256 36b522c9...):** the first measurement engine on PC 1 that mined. Both cards, one 60 s row each at the installed app's 80% cap, clocks unlocked, rate = the worker's STATUS wall rate, draw = nvidia-smi every 5 s:
|
||||||
|
|
||||||
|
| Card | MH/s | W | MH/W | core | memory | GPU C | limit |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| RTX 5090 | 127.31 | 316.5 | 0.402 | 2,850 MHz | 13,801 MHz | 68 | 460 W of 575 |
|
||||||
|
| RTX 4070 | 28.68 | 102.7 | 0.279 | 2,805 MHz | 10,251 MHz | 46 | 160 W of 200 |
|
||||||
|
|
||||||
|
Nothing set; the installed app's miners back after 350 s. Why every earlier run (5 and 6 October, runs 1 to 4 and dry runs 1 and 2) read its copied settings as defaults, measured on PC 1 (collect ember-acl-2): the engine's own start locks its app folder with `icacls /inheritance:r /grant:r <user>:F`; cutting the folder's inheritance propagates down, the non-inheritable grant gives the children nothing, so a file COPIED in before the start (settings.json, machine-id, wallet.json) is left with no access entry and its owner cannot read it (`ReadAllText`: access denied), while the engine's own files written after the lock inherit fine, which hid it for a day. A first fix with `(OI)(CI)F /T` left the file empty too: `/T` re-applies `/inheritance:r` to each file after the propagation and an `(OI)(CI)` entry on a file is inherit-only. The right form is the inheritable grant without `/T` (07d5a72). Consequence for every tier on Windows: nothing changes for the installed app (its files were always its own); any tool that drops files into the app folder before the app starts (an installer's seed, a migration, a support script) was unreadable to the app until now and is readable from 0.3.13 on.
|
||||||
|
|
||||||
|
Consequence for the tiers: an AMD card is tuned on its power limit alone until its stock core clock is read (a 9070 XT at -30% is the floor the driver allows, 4 steps, 5 minutes); every NVIDIA card's two-knob plan waits on the user's one click on Power control; the re-run on PC 1 is held until the quit's source is named (the event-log collect) and follows the 0.3.11 rollout (the update clears the jobs folder, so the engine and the helper are fetched again), with the scheduler's slot.
|
||||||
## 5 October 2026 (night), read width of the lottery hash: 4, 16 and 64-byte loads, a per-load mix, a written scratch; three cards (gate 1 experiment, cryptographer)
|
## 5 October 2026 (night), read width of the lottery hash: 4, 16 and 64-byte loads, a per-load mix, a written scratch; three cards (gate 1 experiment, cryptographer)
|
||||||
|
|
||||||
Branch `readwidth` (commits 019b014, b970dda, 4badcee, a9e002c, d0018cf and the entry commit); plan and recommendation in `docs/plans/read-width.md`. Nothing here changes consensus: every class sits behind `igneum-pow --class` and the default class is generator version 2 byte for byte (`igneum-pow/tests/packs.rs` passes on the four pinned packs after every commit). Question (Josh, after "the 9070 XT on the eGPU" above): would wider reads keep the latency-bound random-access property while closing the vendor gap. Additions from the coordinator: a per-load width drawn from an era-fixed mix, and a written per-warp scratch (measurement only, no soundness claim).
|
Branch `readwidth` (commits 019b014, b970dda, 4badcee, a9e002c, d0018cf and the entry commit); plan and recommendation in `docs/plans/read-width.md`. Nothing here changes consensus: every class sits behind `igneum-pow --class` and the default class is generator version 2 byte for byte (`igneum-pow/tests/packs.rs` passes on the four pinned packs after every commit). Question (Josh, after "the 9070 XT on the eGPU" above): would wider reads keep the latency-bound random-access property while closing the vendor gap. Additions from the coordinator: a per-load width drawn from an era-fixed mix, and a written per-warp scratch (measurement only, no soundness claim).
|
||||||
|
|
@ -2198,3 +2317,194 @@ The PC 2 job (run-ca3-v4-gates-pc2-20261006, `tools/ca3-v4/pc2-v4-gates.ps1`, 15
|
||||||
Per tier: 73 microseconds per hash on an M5 Max core, so a pool checks about 13,700 shares per second per such core and about 5,500 per 2019-class core (approximate); a node on any tier verifies a warp inside the gate; no tier is slower than under x8 by more than 8.5 percent.
|
Per tier: 73 microseconds per hash on an M5 Max core, so a pool checks about 13,700 shares per second per such core and about 5,500 per 2019-class core (approximate); a node on any tier verifies a warp inside the gate; no tier is slower than under x8 by more than 8.5 percent.
|
||||||
|
|
||||||
**Findings.** (1) Under the chain's path a generator-3 program's id is `program_id(3, seed, attempt)`, class-independent: the seven exported packs carry 73bcbfe8ccf988f1 with and without the shadow, and 50 of 50 fuzz seeds agree. A node and a miner could agree on the id while running different classes, and 2.0's G4 check `program_ids_differ_across_the_switch` would not fire across a v4 activation: the v4 seam (G4, G6) must stamp its own generator version or put the class in the id. The hash needs nothing for it; the cut must not go without it. (2) The report cap: a G2 playbook that prints 8,192 found lines loses its own G1 lines; the tooling fix is a found-lines file plus a count and digest on stdout, with a collect. Owed: AMD (PC 1 job 1), the 2019-class core (O-1.14).
|
**Findings.** (1) Under the chain's path a generator-3 program's id is `program_id(3, seed, attempt)`, class-independent: the seven exported packs carry 73bcbfe8ccf988f1 with and without the shadow, and 50 of 50 fuzz seeds agree. A node and a miner could agree on the id while running different classes, and 2.0's G4 check `program_ids_differ_across_the_switch` would not fire across a v4 activation: the v4 seam (G4, G6) must stamp its own generator version or put the class in the id. The hash needs nothing for it; the cut must not go without it. (2) The report cap: a G2 playbook that prints 8,192 found lines loses its own G1 lines; the tooling fix is a found-lines file plus a count and digest on stdout, with a collect. Owed: AMD (PC 1 job 1), the 2019-class core (O-1.14).
|
||||||
|
|
||||||
|
### 6 October 2026, 00:4xZ, the empty `/api/state` reply (proving v1 branch)
|
||||||
|
|
||||||
|
Reported by the aggregation-cost agent: PC 2's `/api/state` answered `{}` (2 bytes) at 22:22Z, 22:41Z and 00:18Z. Not measured on PC 2 (no job); derived from the app source and node 1's RPC, read-only on the Mac:
|
||||||
|
|
||||||
|
| Figure | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| Paid shards, devnet, all provers | 663 | `curl -s 127.0.0.1:26790 -d '{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}'` at tip DAA 0x22caf |
|
||||||
|
| Paid wei, all provers | 0x2c2961a69990745400 = 814.64 IGN | same call |
|
||||||
|
| Average per paid shard | 1.23 IGN (approximate: the mean over 663) | 814.64 / 663 |
|
||||||
|
| u64::MAX in IGN | 18.45 | 2^64 - 1 over 1e18 |
|
||||||
|
| Paid shards per app start before the reply empties | 15 (approximate: at the mean payout) | 18.45 / 1.23 |
|
||||||
|
|
||||||
|
Cause: `ProvingState.paid_wei: u128` and serde_json `to_value` (1.0.151, `value/ser.rs` `serialize_u128`: u64 range or an error); the error became `json!({})`. Fix: the field serialises as a decimal string; `state_json` logs the error once. Test `a_paid_total_over_u64_max_still_serialises_the_whole_state` (`cargo test --offline -q paid_wei`, 1 passed).
|
||||||
|
|
||||||
|
| The fast-time 3-node harness (`tools/proving-v1/net.mjs`, 29950+, suffix 956, every node in trust mode, three vmine voters, v0 at DAA 60, v1 at DAA 120, 4 blocks a segment, unproven after 60 DAA, a tenth to the aggregator; fork b177718e built on this Mac) | run 2, 19:13:01Z to 19:16:19Z, under the run lock: PASSED, 21 checks in 197.3 s (`tools/proving-v1/report-2026-10-05.json`). v1 start = chain block 119 on all three nodes; the native statement identical on all three. Known-finished: segment 119..122's fresh-chain record submitted to n1 at t=131.1 s, relayed, verified (trust) and PAID on n0 1.0 s later at chain block 129, 253,611,648,000,000,000 wei = a tenth of the four credits, the same on every node, the payout address holding it. Chain rule: segment 123..126's fresh-chain record refused ("does not chain to segment 119..122 ... proven (record paid at chain block 129)"), the continuing one (chain_len 8) accepted and paid. Known-failed: segment 127..130 left without a record: a fresh-chain record for 131..134 refused while 127..130 was pending ("pending until DAA 191"); at DAA 192 the status read unproven, a late record for 127..130 refused ("unproven: carried after the deadline"), the fresh-chain record for 131..134 accepted and paid with chain_len 4; `segmentsInWindow` proven 3, unproven 1. The shard side: a v1 shard's `shardWei` = 90% of its block's credit. Run 1 (19:10Z) failed in its own tooling (the signer's argument order), fixed |
|
||||||
|
|
||||||
|
## 5 October 2026 (night), aggregation cost on the RTX 5090: what a per-block aggregation spends and what each lever gives (proving engineer, agg-cost)
|
||||||
|
|
||||||
|
Josh, 5 October 2026: "fix everything else in the numbers tonight". The number under test: the chained segment aggregation cost 9.6 to 9.7 s a block on PC 2's 5090 while the card mined (`chain-pc2-pv1c`, the entry above), 2.2 s on 4 October with the card to itself. Target: under 3 s a block, the miner's slowdown of the prover under 1.5x, the proof statement unchanged. Branch `agg-cost` (worktree `igneum-wt-agg-cost`, from `proving-v1` 219517f). Host changes (statement untouched, `elf/` untouched): the aggregation's stdin build timed apart from the prove call, the deferred-proof count and the SP1 knobs in the RESULT lines, `--mode chain --save-shards` (every shard's compressed proof written next to the results, so `--mode aggregate` re-runs the same proofs under other settings). Jobs: `agg-cost-pc2-1` (21:01:20Z to 21:25:11Z, `tools/proving-v1/pc2-agg-cost.ps1`, the package `igneum-prove-wsl2-aggcost.zip` fetched by `fetch-prove-aggcost` 20:55:39Z, built in WSL2 against the live target dir in 5 s, installed to `/opt/igneum-aggcost`, the live `/opt/igneum` untouched, `--mode id` the pinned pair) and `agg-cost-pc2-2` (21:34:00Z, the same script). The live prover was switched OFF for the runs (its sp1-gpu-server would otherwise be shared through `/tmp/sp1-cuda-0.sock` and carry its own environment; `gpu_server_before running=0`) and ON again at the end. Fixtures: four consecutive live blocks cut from PC 2's own node (86165..86168 at tip 86195, one empty shard each, every one MATCHES natively), the same four for every phase of job 1. App 0.3.9 on PC 2 throughout.
|
||||||
|
|
||||||
|
Known-finished case of the host changes before the GPU (this Mac, CPU, run lock, 20:41Z to 20:44Z): `--mode chain` over `fixtures/chain/block-81046.json` with `--save-shards` (shard 38.5 s, aggregate 43.4 s, the proof file written), then `--mode aggregate` over that saved shard proof with `SP1_WORKER_VERIFY_INTERMEDIATES=false` (46.6 s, the same statement `0x3dedb8ea...`), `--mode verify-segment` VERIFIED in 0.027 s; known-failed: a wrong statement NOT VERIFIED in 0.027 s. Unit tests: `cargo test --release -p igneum-prove-core -p igneum-prove-host`: core 8 passed, host 9 passed and 1 ignored (build lock, 20:53Z).
|
||||||
|
|
||||||
|
### Lever 1, the profile: where a per-block aggregation goes
|
||||||
|
|
||||||
|
| What | Measured (job `agg-cost-pc2-1`) |
|
||||||
|
|---|---|
|
||||||
|
| The host's own share of an aggregation (the stdin build: the AggInput, the proof clones into the request) | 0.000 s on every block, mining or idle (the `stdin` field of every `RESULT chain block` line): everything is inside the one `prove().compressed()` call to the GPU server |
|
||||||
|
| The GPU server's log at `RUST_LOG=info` (phase A0, the same chain of 1, stderr captured) | 1 line: sp1-gpu-server 6.8.1 prints no spans and no timings, so the step costs below are read from the deferred-proof count, not from a profiler |
|
||||||
|
| Aggregation with 1 deferred proof (the first block, no previous proof) against 2 (every chained block), the card mining | 7.9 s against 9.6, 9.6, 9.8 s: the second deferred proof costs 1.7 to 1.9 s under the miner |
|
||||||
|
| The same, the miners paused (phase C, the same fixtures, 21:05:51Z) | 1.7 s against 2.1, 2.1, 2.2 s: the second deferred proof costs 0.4 to 0.5 s alone |
|
||||||
|
| The shard proof of an empty shard | 7.4 to 7.8 s mining, 1.9 to 2.2 s alone |
|
||||||
|
| A whole block (one empty shard plus its aggregation) | 17.1 to 17.3 s mining (end to end 67.4 s for 4 blocks), 4.1 s alone (16.4 s for 4) |
|
||||||
|
| GPU utilisation over the phase (1-s `nvidia-smi` samples) | 93.9% mining (80 samples, the miner's), 15.8% alone (32 samples): the prover alone keeps the card busy a sixth of the time. Its work is short GPU bursts between CPU phases (the executor, the witness and recursion-program generation run on the CPU inside the server), and the miner's kernels fill the gaps |
|
||||||
|
| GPU memory peak | 16,195 MiB mining (the miner's 3.4 GB resident), 14,483 MiB alone |
|
||||||
|
| The slowdown by the miner, same fixtures, same host, 2 min apart | shards 3.6x, the first aggregation 4.6x, a chained aggregation 4.5x, a block 4.2x |
|
||||||
|
| Setup per host process (client plus two key setups) | 13.0 to 15.7 s, mining or not |
|
||||||
|
|
||||||
|
Reading. An aggregation is three or four recursion steps on the card (the aggregator guest's one core shard, its lift, one deferred program per verified proof, the compose), each a burst of under half a second when the card is free. The chained aggregation's extra deferred proof is the only part that grows with the chain rule, 0.4 to 0.5 s alone. Everything else the 9.7 s holds is the miner: with the card at 94% from the lottery kernels, every prover burst waits for a time slice, and a 2.1-s aggregation becomes 9.7 s. The 4 October 2.2 s (two shards, no previous proof, the card to itself) and tonight's 1.7 s (one shard) and 2.1 s (one shard plus the previous proof) agree within the deferred count.
|
||||||
|
|
||||||
|
### Lever 2, batch and tree folds (estimate from the measured step costs; the statement is pinned, no guest was changed tonight)
|
||||||
|
|
||||||
|
A fold of K blocks' shard proofs plus the previous segment proof in ONE aggregator call would cost one core shard, one lift, K + 1 deferred programs and the compose tree in place of K chained aggregations. From the measured rows (alone: a 1-deferred aggregation 1.7 s, each further deferred proof 0.45 s; mining: 7.9 s and 1.8 s):
|
||||||
|
|
||||||
|
| Fold | Deferred proofs per call | Per block, card alone (estimate) | Per block, card mining (estimate) | Rule |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| chained, as pinned (measured) | 2 | 2.1 s | 9.7 s | one call per block |
|
||||||
|
| batch of 4 | 5 | (1.7 + 4 x 0.45) / 4 = 0.9 s | (7.9 + 4 x 1.8) / 4 = 3.8 s | one call per 4 blocks |
|
||||||
|
| batch of 8 | 9 | (1.7 + 8 x 0.45) / 8 = 0.7 s | (7.9 + 8 x 1.8) / 8 = 2.8 s | one call per 8 blocks |
|
||||||
|
| tree of 4 (2 + 2, then the pair) | 3 per call, 3 calls | 3 x (1.7 + 2 x 0.45) / 4 = 1.9 s | 3 x (7.9 + 2 x 1.8) / 4 = 8.6 s | no gain over the chain: every call pays the fixed part |
|
||||||
|
|
||||||
|
Reading. A batch fold halves to quarters the per-block aggregation but changes the aggregator's statement (`AggInput` carries one block's shards and the guest asserts one block hash), so it is a new pinned guest and a new program id: a provers-off drain and a rollout (proving/README.md, pinned guests). It does not reach 3 s on a mining card by itself (2.8 s at K = 8 is on the line), and the shard proof beside it stays 7.4 s a block on a mining card. The lever that moves both is the card's other job, lever 4. A tree fold gains nothing here because the fixed part of a call (the core shard and the lift) dominates the per-proof part 4 to 1.
|
||||||
|
|
||||||
|
### Levers 3 and 4, two streams and the miner's kernels (job `agg-cost-pc2-2` and the re-run)
|
||||||
|
|
||||||
|
Job `agg-cost-pc2-2` (21:34:00Z to 21:49:22Z) ran with the 5090 idle throughout: job 1's `/api/resume` had left the worker off (below), so the rows that needed the miner (the batch-log2 curve, the two streams beside the miner, the time-slice policy, the chosen combination) are void and wait for a re-run; the idle rows are measured.
|
||||||
|
|
||||||
|
| What | Measured (job `agg-cost-pc2-2`, card idle) |
|
||||||
|
|---|---|
|
||||||
|
| Aggregate-only over job 1's four saved shard proofs (`--mode aggregate --proofs b1;b2;b3;b4 --parent ...`, one process, the same statement `0x3a995f24...` as the chain run), default knobs (phase B0, then C1) | 1.7, 2.0, 2.0, 2.0 s (1, 2, 2, 2 deferred proofs), 8.1 s for four; C1: 1.8, 2.1, 2.1, 2.1 s, 8.3 s |
|
||||||
|
| The same with `SP1_WORKER_VERIFY_INTERMEDIATES=false` (phase B; the server inherits the host's environment, the knob printed in the `sp1 knobs` line) | 1.7, 2.0, 2.0, 2.0 s, 7.8 s for four: no gain (0.3 s over four, inside the run-to-run spread of 0.2 s). The knobs that change the recursion shape (`SP1_WORKER_MAX_COMPOSE_ARITY`, `MAX_REDUCE_ARITY`) were not tried: a different shape is a different recursion key set and the pinned verifier would refuse the proof |
|
||||||
|
| A 4-deferred aggregation (block-344-shards4, four prototype shards of 6.75 M pgas, phase C2) | shards 42.8 s (10.7 s each, the 4 October 10.2 to 10.7 s), aggregation 2.4 s with 4 deferred proofs; GPU peak 28,402 MiB (the prototype shard's 28.3 GB), utilisation 27.7% over the phase. With 1.7 s at one deferred proof and 2.0 to 2.1 s at two: 0.25 s per further deferred proof alone, so a batch of 8 would cost about 3.5 s a call, 0.45 s a block (estimate, the pinned statement forbids it) |
|
||||||
|
| Two host processes at once on the one card (phase G0: chains of 2 on disjoint blocks, started 2 s apart) | both connected to ONE sp1-gpu-server (the first process's child; the socket is per device, `/tmp/sp1-cuda-0.sock`): process 1 shard 2.2 and 3.5 s, aggregation 3.0 and 4.0 s (12.9 s for 2 blocks against 8.2 s alone); process 2 shard 3.3 s, aggregation 3.6 s, then its second block died with `CudaClientError: Failed to read the response: early eof` when process 1 finished and its server exited. GPU 24,911 MiB, utilisation 12.4% and 13.1%. Two streams through SP1 6.8.1's server are serialised on one socket and the second dies with the first: no throughput gain (3 blocks in 33 s against 4 in 16.4 s) and a failure mode; lever 3 is closed on this SP1 version |
|
||||||
|
| Job 3 (`agg-cost-pc2-3`, 22:41:15Z, app 0.3.10, the same script with the socket rule and a card switch): phase A, the app's 5090 miner at 117.0 MH/s mean (n 3, STATUS lines 22:44:45Z to 22:46:11Z), four fresh live blocks 90896..90899 | shards 8.0, 7.8, 7.6, 7.8 s; aggregations 8.0 s (1 deferred), 10.0, 10.0, 10.0 s (2 deferred); 69.5 s for four, 17.8 s a block; GPU 93.8%, peak 16,245 MiB: the job-1 baseline reproduced 100 min later on other blocks |
|
||||||
|
| Job 3's own-miner phases | void: the state reads came back empty (the class below), the card switch did nothing, phase D launched my miner beside the app's (the app's dropped to 62.2 MH/s, mine read 60.6 MH/s), then PC 2's app restarted at 23:03:30Z and the job died with it; no curve point |
|
||||||
|
| The GPU time-slice policy (`nvidia-smi compute-policy --set-timeslice`, the restore job `agg-cost-restore-1`, 23:16:53Z) | "Not Supported" on PC 2 (RTX 5090, driver 13.3, the Windows nvidia-smi, not elevated): the lever is closed on this driver; an elevated try is not worth a slot, the error is the driver's, not a permission's |
|
||||||
|
| The own-miner phases of job 2 | void: no 5090 miner was running to copy the command line from (the worker off since 21:25Z) |
|
||||||
|
|
||||||
|
The curve, job `agg-cost-pc2-6` (01:12:09Z to 01:24:14Z, app 0.3.11, PC 2 to itself; every phase closed before the next job landed on PC 2 at 01:24:21Z). The app's 5090 miner switched off through `/api/cards` (the keys from `settings.json`; the worker was still alive after 120 s, `/api/pause` as the fallback stopped it in 5 s), then the job's OWN miner on the 5090 with the app's command line (`igneum-miner mine ... --worker igneum-worker-cuda.exe --identities 8 --worker-args "--device 0 --pack packs\devnet --race off [--batch-log2 B]"`, the base variant, its STATUS line every 10 s), the same four live blocks 96556..96559 (one empty shard each) proven by `--mode chain` under it, the miner's rate from its own `now=` field (the first two lines skipped). `--batch-log2 B` sets the worker's nonces per kernel launch (2^B; 22 is the worker's default, 4,194,304 nonces, about 35 ms a launch at 120 MH/s; `proto-cuda/nvrtc/worker.cpp`).
|
||||||
|
|
||||||
|
| batch-log2 | Shard proof (4, s) | Aggregation (1 deferred, then 2) (s) | A block (s) | GPU util. (%) | GPU peak (MiB) | Own miner (MH/s wall, n) | Against the card alone (4.1 s a block) |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| 22 (the default), phase D | 8.1, 7.8, 7.9, 7.8 | 8.4; 10.3, 10.0, 10.4 | 18.1 | 95.5 | 16,580 | 103.9 (9) | 4.4x |
|
||||||
|
| 20, E20 | 8.1, 7.8, 7.8, 7.8 | 8.4; 10.3, 10.1, 10.1 | 18.0 | 94.9 | 16,461 | 103.7 (8) | 4.4x |
|
||||||
|
| 18, E18 | 7.0, 6.7, 6.7, 6.7 | 7.2; 8.9, 8.8, 8.8 | 15.6 | 91.5 | 16,487 | 99.3 (7), minus 4.4% | 3.8x |
|
||||||
|
| 16, E16 | 5.1, 4.9, 4.9, 4.9 | 5.0; 6.1, 6.2, 6.2 | 11.1 | 85.3 | 16,519 | 83.8 (6), minus 19% | 2.7x |
|
||||||
|
| 16 again, phase H (the job's own choice: the shortest chain) | 5.0, 4.9, 4.8, 4.9 | 4.9; 6.1, 6.2, 6.2 | 11.1 | 85.7 | 16,487 | 84.0 (6) | 2.7x |
|
||||||
|
|
||||||
|
Reading. Between 2^22 and 2^20 nothing moves: the card's time-slice scheduler alternates the two contexts whatever the kernel length above a few milliseconds. From 2^18 down the miner's launches get short enough (about 2 ms at 2^18, 0.5 ms at 2^16) that the prover's bursts find the card sooner, and the miner pays in launch overhead and idle gaps: at 2^16 the prover runs 1.6x faster (18.1 to 11.1 s a block, the chained aggregation 10.2 to 6.2 s) for a fifth of the hash rate, and it is still 2.7x slower than on a card to itself. The trade is about 1 MH/s per 0.37 s of block time at the 2^16 point, and the 3-s aggregation and the 1.5x slowdown are not reachable on a mining card by the kernel length; a 2^14 point (approximate, extrapolated) would be about 8 s a block at about 65 MH/s. The phase E0 (a 4-deferred aggregation under the miner) failed in 0.1 s: its proof paths pointed at `/` where job 1 had left its shard proofs, but job 2's block-344 proofs sit in job 2's own folder (`$JOB` was exported from job 2 on); the 4-deferred cost under the miner stays an estimate (lever 2 above). The app's own 5090 miner ran at 117 MH/s (job 3, 22:44Z) and 110 to 129 MH/s (its STATUS lines at 01:10Z) with the prover beside it, against my miner's 104 MH/s at the default batch: my miner runs the base variant with `--race off` (no tuning file on PC 2), so the curve's rates are relative to each other, not to the app's.
|
||||||
|
|
||||||
|
### Lever 5, the host side under WSL2 (what the chain-mode numbers leave out)
|
||||||
|
|
||||||
|
| What | Measured |
|
||||||
|
|---|---|
|
||||||
|
| The export (`igneum_exportSegments` 0..tip, 75 to 77 MB over curl.exe to a file on `C:`) | 1.1 to 1.5 s |
|
||||||
|
| The cut (`igneum-prove-export` replaying from genesis, then `--mode native`), four blocks | 18 s for four including the native checks (21:01:28Z to 21:01:46Z), about 4 s a block; the export's file sits on `/mnt/c` |
|
||||||
|
| The key setup per host process | 13.0 to 15.7 s on PC 2 (8.0 to 8.5 s on the Mac CPU): `--mode chain` and `--mode aggregate` pay it once per process, the app's loop pays it per shard |
|
||||||
|
| The proof file write through the WSL2 bridge | the 4 October entry ("shard proving on the RTX 5090"): 24 min of unbuffered `save` across `/mnt/c`, fixed by the 4 MB buffer; tonight `--save-shards` wrote the four 1.27 MB proofs inside the chain phase with no visible gap (the A phase's 80.4 s wall against 67.4 s of proving plus 13.0 s of setup) |
|
||||||
|
| Native Linux | not measured: no native Linux machine with an NVIDIA card exists in the project tonight, and the 4 October numbers were also WSL2 (Ubuntu 24.04 under PC 2's Windows). The WSL2 cost inside a `prove()` call is not separable from here; the host-side pieces above are what a native box would also skip or keep |
|
||||||
|
|
||||||
|
### What went wrong, measured
|
||||||
|
|
||||||
|
| What | Fixed |
|
||||||
|
|---|---|
|
||||||
|
| Job 1's per-phase command ran with `$JOB` empty (the bash variables of `vars.sh` were set, not exported, and the command runs in a child bash): `--out /results-A.json`, the saved shard proofs in `/` on the WSL root, so the aggregate-only phases B0, B, C1 and the prototype-shard phase C2 failed in 0.0 s ("No such file") | `export` in `vars.sh`; job 2 reads the proofs from `/` |
|
||||||
|
| Job 1's own-miner phases launched the iGPU miner (the first `igneum-miner mine` process matched; the 5090's is the second) and `if (StartMiner ...)` was always true (PowerShell: a function's emitted RESULT strings are part of its output), so D and E ran with the 5090 idle and the AMD iGPU at 3.4 MH/s: three more idle replicates of the chain (2.0 to 2.2 s shards, 1.8 and 2.2 s aggregations), no curve | the miner matched on `igneum-worker-cuda`, the outcome in a script-scope flag, `--race off` for the own miner (no tuning file on PC 2; a race costs up to 120 s a start) |
|
||||||
|
| Job 1's `/api/resume` at 21:25:11Z answered ok and the 5090 miner stayed off (card state `off`, hash 0.0, 1,760 MiB on the card) until the 0.3.10 restart; job 2 waited its full 600 s for a hash rate and ran its mining phases void | the restore job `tools/proving-v1/pc2-agg-cost-restore.ps1` also posts `/api/start`; the Counter ASIC coordinator opened a task chip for the resume defect |
|
||||||
|
| Jobs 3 and 4 (`agg-cost-pc2-3` 22:41Z on app 0.3.10, `agg-cost-pc2-4` 00:18Z on 0.3.11): every `/api/state` read came back as the two bytes `{}` (job 4's raw-body print: `raw_len=2`; the same reads gave the full state on 0.3.9 at 21:01Z and the AMD agent saw the empty reply at 22:22Z), so the card switch found no card, the app's 5090 miner kept mining, and job 3 ran a second miner beside it (two miners at about 60 MH/s each) while job 4's double-mining guard voided its own-miner phases. The class is the app's, not the reader's: `state_json()` (engine.rs:180) does `serde_json::to_value(st).unwrap_or(json!({}))`, and the value that fails is `ProvingState.paid_wei: u128` (serde_json 1.0.151 refuses a u128 over u64::MAX, 18.45 IGN; the proving-v1 agent's diagnosis): a paid shard averages 1.23 IGN, so the reply empties about 15 paid shards after every app start and comes back at the next restart, which matches the times (full at 21:01Z with paid_wei 0, empty from 22:22Z after the prover had paid from 22:02Z). Fixed on the app branch proving-v1 at 6714a45 (paid_wei as a decimal string, the error logged, an `{"error":...}` reply on any future failure) | job 5 reads the card keys from the app's `settings.json` (`cards`: key to enabled and identities), restores the 5090's 8 identities first (the restore job of 23:16:53Z had set 2: its parser read the next card's value), refuses before any pause when it cannot name the card, waits on the CUDA worker process count for the card to stop, and checks the worker is back at the end |
|
||||||
|
| Job 5 (`agg-cost-pc2-5`, 01:10:44Z) failed at PowerShell's parse in 1 s: `$RestoreIdentities:` inside a double-quoted string (a drive-qualified variable); no card or miner touched | `${RestoreIdentities}:`; the other `$name:` shapes are inside single-quoted bash here-strings |
|
||||||
|
| Job 6's identities step found `settings.json` already at 8 identities under the active key `nvidia:0:NVIDIA GeForce RTX 5090` (a stale key `nvidia:NVIDIA GeForce RTX 5090` carries 2), so no change was sent; job 6's `/api/cards` with the 5090 disabled answered ok but the worker ran on for 120 s, `/api/pause` stopped it in 5 s, and at the end `/api/resume` brought it back in 5 s on 0.3.11 | the card switch keeps the pause as its fallback; the resume path works on 0.3.11 |
|
||||||
|
| PC 2 ran three jobs at once from 01:24Z (`run-prover-on-pc2-20261006` at 01:24:21Z, the ledger suites build at 01:26:15Z, while agg-cost-pc2-6's closing report was still being uploaded): the app does not serialise jobs, "one job per machine at a time" holds only by the coordinator's word; job 6 had closed at 01:24:14Z, so its rows are clean | nothing of mine to fix; a rule for the job runner |
|
||||||
|
| The make-package gate ran the exporter's side files (`block-N.json.node-plan.json`) as fixtures and failed; its execute step took the exclusive `measure` lock for a cycle count and queued 25 min behind a packbench run | the glob skips `.node-plan.json`; the execute step runs under the `run` lock (a count, not a time) |
|
||||||
|
|
||||||
|
### 6 October 2026, 07:12Z to 07:17Z, the host's chain mode with --save-shards records and --prev, on the Mac's CPU
|
||||||
|
|
||||||
|
`tools/lock/with-lock.sh run`, `SP1_PROVER=cpu igneum-prove-host --mode chain --chain proving/fixtures/chain/block-81046.json,block-81047.json --save-shards --out chain-a.json`, then `--chain block-81048.json --save-shards --prev segment-81047-aggregated.bin --out chain-b.json` (the app branch at ce8f34a, Apple M5 Max, CPU prover). The flags the app's segment path needs, before PC 2 (approximate figures: a CPU run, one sample each):
|
||||||
|
|
||||||
|
| Step | Value |
|
||||||
|
|---|---|
|
||||||
|
| Shard proof, CPU, empty block | 34.7 s and 36.3 s |
|
||||||
|
| Aggregation, CPU, 1 then 2 deferred proofs | 39.1 s, 50.8 s |
|
||||||
|
| Chain of 2, end to end | 160.9 s |
|
||||||
|
| Per-shard records written | 2 (number, block_hash, shard, statement, proof_sha256, proof_bytes 1,272,897, proof_file, prove_seconds) |
|
||||||
|
| `--prev` run: base_chain_len, final chain_len | 2, 3 (the chain continued; a wrong previous proof is refused by number and parent hash) |
|
||||||
|
|
||||||
|
### 6 October 2026, 07:52Z to 08:24Z, the segment-aligned prover beside the miner on PC 2's RTX 5090 (job `segments-pc2-pv1c`)
|
||||||
|
|
||||||
|
`tools/proving-v1/pc2-segments.ps1` (app branch 330207d; the host from the package `igneum-prove-wsl2-segal`, built on PC 2 in 7 s warm to `/opt/igneum-segal`, pinned guests unchanged); the app's own prover OFF for the run through `/api/prove`, ON again at the end; the app's miner running (8 identities, batch-log2 22); `SP1_PROVER=cuda`, the stock 6.8.1 GPU server; a 1-s nvidia-smi sampler under every chain. Payouts read on node 1 (read-only, `igneum_getProofRecords` per block at 08:30Z). The miner's rate from the app's uploaded log (`status: ... MH/s` every 30 s, run win-1ccfe586-20261005-235130).
|
||||||
|
|
||||||
|
| Figure | Value | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| Segments claimed in 30 min | 9 (114470, 114654, 114862, 115022, 115198, 115366, 115542, 115710, 115870) | one every 210 s; 32.1 min of loop |
|
||||||
|
| Candidates per pass | 32 to 38 whole segments inside the margin | margin 580 to 589 DAA at claim |
|
||||||
|
| Export (the chain to the segment's last block) | 99.6 to 100.6 MB in 1.4 to 1.6 s | once per segment |
|
||||||
|
| Cut (8 fixtures, the exporter) | 45.1 to 46.0 s | the exporter replays from genesis per block; the next lever |
|
||||||
|
| Chain run wall (8 shards, 8 aggregations, one key setup) | 159.7 to 160.6 s | host `--mode chain --save-shards` |
|
||||||
|
| Shard proofs, 8 per segment | 63.0 to 63.5 s (7.9 s a shard) | empty blocks |
|
||||||
|
| Aggregation, 8 chained | 80.4 to 81.1 s (10.1 s a block) | the fixed cost per block beside the miner |
|
||||||
|
| End to end per segment (export, cut, chain, sign, submit) | 210.0 to 211.2 s | |
|
||||||
|
| GPU memory peak during a chain | 16,484 to 17,573 MiB (miner resident) | the 24 GB tier's gate holds |
|
||||||
|
| GPU utilisation during a chain | 94.9 to 95.3% | |
|
||||||
|
| Shard records accepted | 72 of 72 | 8 per segment |
|
||||||
|
| Shard records paid on chain | 72 of 72 | 0.905 to 2.719 IGN a shard (90% of the credit); carried 180 to 226 blocks after the block |
|
||||||
|
| Segment records accepted | 0 of 9 | every one refused: "does not chain to segment N-8..N-1 (chain_len 8), which is pending until DAA ..." |
|
||||||
|
| Miner alone (the app's prover off), 07:25 to 07:51Z | 117.86 MH/s mean (n=52) | min 46.37 is the switch-off dip at 07:22Z |
|
||||||
|
| Miner beside the segment prover, 07:55 to 08:24Z | 104.90 MH/s mean (n=58, min 98.39, max 119.24) | 12.96 MH/s = 11.0% of the miner, at 95% GPU utilisation from the prover |
|
||||||
|
| The 0.3.11 prover as shipped beside the miner (5 October row) | 5.0 MH/s = 4.0% | one shard per 46 s; this run proves 8 shards per 210 s, 2.8x the shards |
|
||||||
|
| Node 1's v1 window at 08:24Z | pending 59, proven 0, unproven 16, paid segments 0 | unchanged by the run: the chain rule |
|
||||||
|
|
||||||
|
What the refusal is (the fork, `igneum/exec/src/proving.rs` `check_segment_record`): a fresh record (chain_len = N) is valid only when the previous segment is UNPROVEN at the carrier, and the record's own deadline is the previous segment's deadline plus one segment length in DAA, so a fresh record is valid for 8 DAA (about 8 s) per segment and must be carried inside them. With one prover every previous segment is pending at proof time. Fixed on the fork branch behind `proving_v1_fresh_rule_daa` (0f0dda95): from the switch a fresh record is valid whenever the previous segment is not proven; the app holds a refused record and offers it again every pass until the deadline (272b025).
|
||||||
|
|
||||||
|
Run b (`segments-pc2-pv1b`, 07:20Z to 07:51Z) claimed nothing in 88 passes: the driver's segment keys were doubles against int64 hashtable keys (fixed in 330207d); its 30 minutes are the miner-alone baseline above.
|
||||||
|
|
||||||
|
### 6 October 2026, 08:26Z to 08:35Z, the fast-time harness on the fresh-record rule (Mac, `tools/lock/with-lock.sh run`)
|
||||||
|
|
||||||
|
`IGNEUM_PV1_BIN=vendor/igneum-node/target-pv1/release node tools/proving-v1/net.mjs --segment 8 --unproven 10 [--fresh-rule 0]` (fork 0f0dda95, 3 nodes at 60x, ports 29950+):
|
||||||
|
|
||||||
|
| Case | Checks | Time |
|
||||||
|
|---|---|---|
|
||||||
|
| The rule as shipped (no switch): fresh refused while the previous segment is pending (known-failed), accepted after it is unproven | 22 passed | 166.2 s |
|
||||||
|
| `--fresh-rule 0`: fresh accepted while the previous segment is pending, `freshAdmissible` true, still refused after a proven one, the second offer a duplicate ("segment already paid") | 23 passed | 139.9 s |
|
||||||
|
|
||||||
|
|
||||||
|
## 6 October 2026, 12:25 to 13:20Z, the finality route: why 26 fresh nodes lost the seed every checkpoint (fork `fin-route-0313` 5a339733 on 83089544; release engineer)
|
||||||
|
|
||||||
|
The fleet agent's finding (12:25Z): every rented node logged `P2P, route error: incoming route capacity for message type IgneumFinality has
|
||||||
|
been reached (peer: 188.245.5.161:26611)` every 20 to 60 s and reconnected at the checkpoint cadence (every 30 s); on a Vast box the seed
|
||||||
|
is the only peer, so each drop cost the node its only peer until the next dial.
|
||||||
|
|
||||||
|
**The cause is an echo, not the burst.** A certificate for an index below a node's window (`next_index` minus `KEEP_CHECKPOINTS` 2,000:
|
||||||
|
trimmed history) finds no record, goes through the off-chain path (`ingest_off_chain`), is LOCKED, pushed to gossip and sent to every peer,
|
||||||
|
trimmed again on the next pass, and comes back from every peer that held it. The seed's journal (`igneumd-v4`, 12:40 to 12:47Z):
|
||||||
|
|
||||||
|
| Line shape | Count in 7 min |
|
||||||
|
|---|---|
|
||||||
|
| `Finality: checkpoint N LOCKED by certificate: block <hash> ... is off this node's selected chain (not determined here yet)` | 13,354 (index 2954: 2,811; 2956: 2,799; 2957: 2,790; 2955: 2,778; 3897: 1,234; 1464: 942; the seed's next index was 6,127) |
|
||||||
|
| `route error: incoming route capacity for message type IgneumFinality` (the seed dropping ITS peers) | 13 |
|
||||||
|
| the real work (determined, received, LOCKED, folded, replaced by a heavier one) | 13 + 13 + 13 + 8 + 20 |
|
||||||
|
|
||||||
|
A fresh node on the Mac against the seed only (the 0.3.12 binary 83089544, 300 s, `kaspa_p2p_flows=debug`): 11,700 `Finality relay:
|
||||||
|
certificate` lines, every one `new=false`, 15 distinct indices, 240 per second at the peak (2,530 per 10 s), 203 votes; no route error on
|
||||||
|
the Mac (it drains 240/s with a 256-deep route) and one connection, where the fleet's slower boxes filled the route and lost the peer.
|
||||||
|
|
||||||
|
**The fix (four changes, 5a339733):** `ingest_certificate` ignores an index below `keep_from` (counted, debug: the echo stops at its source
|
||||||
|
once the seed runs it); the router's overflow policy for `IgneumFinality` is `Drop` with a counted warn once per 10 s per peer, never a
|
||||||
|
disconnect; the finality route is subscribed with 4,096 (a checkpoint's worst case is `MAX_VOTES_PER_BLOCK` 48 votes on each of 30 blocks
|
||||||
|
plus the certificates); the relay flow skips votes while IBD runs (counted, said once per 30 s; certificates still go in and land pending).
|
||||||
|
No consensus change, no digest change. Tests: the overflow-policy table (p2p 33 of 33), the flows crate (19 of 19), a certificate below
|
||||||
|
the window submitted twice (ignored, no gossip, counter 2; an index inside goes the normal way) with the finality tests (12 of 12).
|
||||||
|
|
||||||
|
**After, on the fixed binary against the still-unfixed seed** (203ae727, same run, 13:15:31 to 13:20:31Z): 63,628 certificates received
|
||||||
|
(the seed's echo had grown to 3,032 per 10 s at the peak as more fleet nodes joined), 0 route errors, 0 drops, 4 connections kept (the seed
|
||||||
|
and three peers learned from it), 168 votes skipped during IBD. The receiver side of the fix holds under a storm five times the morning's;
|
||||||
|
the source side (the guard) cannot show on the seed until 0.3.13 runs there, and the fresh node's own guard never fires during IBD (its
|
||||||
|
window starts at genesis), which is correct. Harness s7 on the fixed binary (`--quick --live-only`): PASS, 192 blocks accepted in 60 s under
|
||||||
|
a 50 blocks/s flood from one peer, honest template p50/p95/max 0.4/0.6/1.4 ms, rss 306 to 321 MB.
|
||||||
|
|
||||||
|
**Per tier:** a home miner joining today sees the warning and the peers=0 flicker every checkpoint until the seed runs 0.3.13; a rig the
|
||||||
|
same once; a pool user nothing; a fleet operator gets a node that keeps its only peer, and a seed that stops amplifying old certificates to
|
||||||
|
every peer (13,354 lines of work it did not need in seven minutes). Owed: the fleet agent's synced-node reading; a receiver-side limit on
|
||||||
|
certificates per index per minute as a second belt once the seed is fixed; the formatter's reflow of `finality.rs` (taken out of the commit).
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ shard run reported as exit 0, 7a7e873).
|
||||||
| Date | Symptom | Cause | Fix | Proven by |
|
| Date | Symptom | Cause | Fix | Proven by |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 4 Oct 2026 | Every `ci` run on master red since 67bf226 (eleven pushes), unnoticed | `sim/difficulty/records/testnet-v2-2026-10-04.schedule.log` carried a home path; `.log` was outside the identity scrub's extension list in `tools/ci/identity-check.sh` (and in the mirror's `tools/sync.sh`) | 2996cca: `.log` scrubbed like the other text files; the record rewritten with `~`; the same list in igneum-public `tools/sync.sh` (local commit e18256d, not pushed) | `bash tools/ci/identity-check.sh` 0 hits locally; run 37226816xxx on master green |
|
| 4 Oct 2026 | Every `ci` run on master red since 67bf226 (eleven pushes), unnoticed | `sim/difficulty/records/testnet-v2-2026-10-04.schedule.log` carried a home path; `.log` was outside the identity scrub's extension list in `tools/ci/identity-check.sh` (and in the mirror's `tools/sync.sh`) | 2996cca: `.log` scrubbed like the other text files; the record rewritten with `~`; the same list in igneum-public `tools/sync.sh` (local commit e18256d, not pushed) | `bash tools/ci/identity-check.sh` 0 hits locally; run 37226816xxx on master green |
|
||||||
|
| 5 Oct 2026 | PC 1 (Windows 11 Pro 26200, default terminal Windows Terminal 1.24): "Windows Command Processor" windows whenever a remote job runs (Josh) | measured, not guessed: `tools/windows/console-watch.ps1` (job run-20261005-182528) started every candidate child from the app's job runner, whose console is headless (`conhost.exe 0x4`, hwnd 0), with a user32 EnumWindows sampler every 30 ms: powershell, cmd, query, curl, nvidia-smi, wsl --status, a distro, interop cmd and powershell, `powershell -WindowStyle Hidden`, `Start-Process -WindowStyle Hidden`: 0 windows each; `Start-Process cmd` in a new console: a Terminal window and a cmd PseudoConsoleWindow (the known-failed case fires). The 25-minute background watcher (console-watch-bg.ps1, run-20261005-184330, 18:44 to 19:09 UTC, every 200 ms) across an app restart, a build job, two run jobs, two collect jobs and the sweep helper's elevated launch at 19:04:43: 0 console or Terminal windows, 69 conhost starts (every one `conhost.exe 0x4`, headless, under curl, wsl, wslhost, powershell), 1 cmd.exe (under wslhost, WSL interop, no window). The one road that creates a console of its own is the elevated launch (`Start-Process -Verb RunAs`, the AppInfo service: the power cap, the sweep helper, the clock sync, an elevated job); it carried `-WindowStyle Hidden` in four copies, and "Windows Command Processor" is also the name on the UAC prompt the engine raises for cmd.exe (the sweep helper prompted at 17:00, 17:30 and 18:12 UTC, the power cap at every start; the elevated watcher's own prompt, run-20261005-184610, timed out unanswered at 122 s) | `platform::elevated_ps_line` + `elevated_command`: one builder for every elevated launch, hidden by construction, exit 251 when the prompt is refused; the elevated job wrapper reports its own console (`elevated console: hwnd N visible False`) on every elevated job; `tools/ci/windows-spawn-check.mjs` fails CI on a Command::new without the quiet flag, a creation_flags other than CREATE_NO_WINDOW, a Start-Process without -WindowStyle Hidden/-NoNewWindow, or a host.cpp spawn without CREATE_NO_WINDOW / SW_HIDE | the watcher's known-failed case (2 windows) and known-finished case (0); the CI check's self-test (9 cases) and the tree (0 hits); the igneum-app test suite on PC 1 |
|
||||||
| 4 Oct 2026 | `collect-pc1-board3` printed PowerShell parse errors (`.Name`, `.AdapterRAM`) | the publishing shell expanded `$_` inside double quotes to nothing before the command reached the jobs file; nothing to do with Format-List or Out-String (board2 and board4 printed their values) | publish-jobs.sh refuses a collect command that pipes into a script block without `$_` or `$PSItem` | the eaten form refused with the reason, the single-quoted form published to a test folder |
|
| 4 Oct 2026 | `collect-pc1-board3` printed PowerShell parse errors (`.Name`, `.AdapterRAM`) | the publishing shell expanded `$_` inside double quotes to nothing before the command reached the jobs file; nothing to do with Format-List or Out-String (board2 and board4 printed their values) | publish-jobs.sh refuses a collect command that pipes into a script block without `$_` or `$PSItem` | the eaten form refused with the reason, the single-quoted form published to a test folder |
|
||||||
| 4 Oct 2026 | the same job reported `done (exit 0)` over `command exit Some(1)` | `run_collect` in `app/igneum-app/src/jobrun.rs` builds `Done` from the upload count only; the command's exit code is logged and dropped | branch `bugfix-collect-exit`, 35ccdc8 rebased on c257444 (app engine; merge by the main session) | `cargo test --bin igneum-app`: all 28 tests pass on the rebased branch; the new one covers the board3 shape (`Some(1)` is failed exit 1), `Some(0)` done, the cap as timeout, failed uploads still failing |
|
| 4 Oct 2026 | the same job reported `done (exit 0)` over `command exit Some(1)` | `run_collect` in `app/igneum-app/src/jobrun.rs` builds `Done` from the upload count only; the command's exit code is logged and dropped | branch `bugfix-collect-exit`, 35ccdc8 rebased on c257444 (app engine; merge by the main session) | `cargo test --bin igneum-app`: all 28 tests pass on the rebased branch; the new one covers the board3 shape (`Some(1)` is failed exit 1), `Some(0)` done, the cap as timeout, failed uploads still failing |
|
||||||
| 4 Oct 2026 | `publish-jobs.sh --deploy` said "not reachable, differs from the local one, or does not verify yet" after a deploy that had succeeded | one check the instant the CLI returned, while the edge still served the previous file; the deploy's own exit status was hidden by `\|\| true` | `verify_live`: up to `--tries` (12) checks 5 s apart, each failure names its condition; `publish-jobs.sh verify` re-checks on its own; a failed deploy stops before the check | finished: `verify --tries 2` against the live file (try 1 of 2); failed: a local server with an older file ("differs", both publish stamps named) and a closed port ("is not reachable") |
|
| 4 Oct 2026 | `publish-jobs.sh --deploy` said "not reachable, differs from the local one, or does not verify yet" after a deploy that had succeeded | one check the instant the CLI returned, while the edge still served the previous file; the deploy's own exit status was hidden by `\|\| true` | `verify_live`: up to `--tries` (12) checks 5 s apart, each failure names its condition; `publish-jobs.sh verify` re-checks on its own; a failed deploy stops before the check | finished: `verify --tries 2` against the live file (try 1 of 2); failed: a local server with an older file ("differs", both publish stamps named) and a closed port ("is not reachable") |
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,19 @@ A proof-of-work chain mined on consumer GPUs, where the same cards prove every I
|
||||||
| A versioned interface | Jobs run against the `ProofSystem` trait, version 1 of which is SP1. A later version is a release with its own test-vector set and a three-month overlap, so your integration survives a prover swap | Design document, execution layer, section 5.6 |
|
| A versioned interface | Jobs run against the `ProofSystem` trait, version 1 of which is SP1. A later version is a release with its own test-vector set and a three-month overlap, so your integration survives a prover swap | Design document, execution layer, section 5.6 |
|
||||||
| Verification you can run | A job proof is a single proof your contract verifies on your own chain; Igneum's own segment proofs recursively verify it, so no relayer or committee is in the path | Designed |
|
| Verification you can run | A job proof is a single proof your contract verifies on your own chain; Igneum's own segment proofs recursively verify it, so no relayer or committee is in the path | Designed |
|
||||||
|
|
||||||
|
## Every payment route
|
||||||
|
|
||||||
|
One row per route, so operator income and protocol income never blur. Rows 1 to 5 are the protocol. Row 6 is the project's software, outside the protocol, and is never added to the other five. Rules: specification sections 2.5 and 5.1 to 5.4; the fuller version with the diagram is `docs/design/payment-routes.md`.
|
||||||
|
|
||||||
|
| Route | Currency | Recipient | Fee | Burn |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1. Emission, per block | IGN, new coins on the published schedule | 80% the block's miner, 20% the proving pool for the provers of that block | None | None. Implemented in consensus on the devnet |
|
||||||
|
| 2. Base fee, both gas dimensions | IGN | Nobody | The base fee the chain sets per block | All of it. Implemented on the devnet |
|
||||||
|
| 3. Priority fee | IGN | 80% the block's miner and provers; 20% the apps whose code ran, per call frame | The tip the sender sets | The share of any frame in an unregistered contract. Implemented on the devnet |
|
||||||
|
| 4. External job, at launch | Your currency, on your chain | The miner who delivered, through a payout contract keyed by miner address | Priced in dollars per proof; your chain's own bond and slashing apply | None; Igneum cannot see the payment. Designed |
|
||||||
|
| 5. External job, after the proof bridge | IGN, on Igneum | 90% the provers who delivered | The job fee | 10%. Designed, phase two |
|
||||||
|
| 6. The official client's dev fee | IGN | The project, as operator income, never the protocol | 1 block template in 100 requested with the dev address; off with one flag | None. Implemented, measured on a test network 4 October 2026 |
|
||||||
|
|
||||||
## What you must do
|
## What you must do
|
||||||
|
|
||||||
1. Integrate against the versioned prover interface: the guest program hash (`program_id`) your batches are proven under, the public-input layout, and the proof encoding for version 1.
|
1. Integrate against the versioned prover interface: the guest program hash (`program_id`) your batches are proven under, the public-input layout, and the proof encoding for version 1.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Four rules for reading the table:
|
||||||
1. Nothing on this chain has been reproduced externally or reviewed independently. Every row's last column says "none yet". The repository is private until the public testnet (decision of 5 October 2026), so the first three labels are the ceiling today.
|
1. Nothing on this chain has been reproduced externally or reviewed independently. Every row's last column says "none yet". The repository is private until the public testnet (decision of 5 October 2026), so the first three labels are the ceiling today.
|
||||||
2. A status applies to the exact version in the row. An audit of one version never covers a newer one; when the version changes, the status falls back to "tested by the team" until the new version is reproduced or reviewed again.
|
2. A status applies to the exact version in the row. An audit of one version never covers a newer one; when the version changes, the status falls back to "tested by the team" until the new version is reproduced or reviewed again.
|
||||||
3. "Tested by the team" on one machine is one machine. The rows say which. Discrete AMD, Intel and a 2019-class CPU core have not run anything.
|
3. "Tested by the team" on one machine is one machine. The rows say which. Discrete AMD, Intel and a 2019-class CPU core have not run anything.
|
||||||
4. The 12-node cloud network of 4 October 2026 (`infra/cloud-devnet`, Hetzner VMs in five locations) is the project's own. Rows that cite it are tested by the team, not reproduced externally.
|
4. The 12-node cloud network of 4 October 2026 (`infra/cloud-devnet`, cloud VMs in five locations) is the project's own. Rows that cite it are tested by the team, not reproduced externally.
|
||||||
|
|
||||||
Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`, version 0.2.0 since 4 October 2026 (generator version 2; 0.1.0 rows are marked). "Repo" commits are this repository's. "Fork" commits are `vendor/igneum-node` and its worktrees (`-v4`, `-diff`, `-exec`, `-harness`, `-fin-fixes`), which are not in this repository's history; the row names the fork commit or branch as the bench log does. The live devnet is devnet v4 (genesis 10:05 BST, 4 October 2026, branch `devnet-v4`). The spec is `docs/spec/` version 0.1.
|
Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`, version 0.2.0 since 4 October 2026 (generator version 2; 0.1.0 rows are marked). "Repo" commits are this repository's. "Fork" commits are `vendor/igneum-node` and its worktrees (`-v4`, `-diff`, `-exec`, `-harness`, `-fin-fixes`), which are not in this repository's history; the row names the fork commit or branch as the bench log does. The live devnet is devnet v4 (genesis 10:05 BST, 4 October 2026, branch `devnet-v4`). The spec is `docs/spec/` version 0.1.
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`
|
||||||
| 14 | Ethereum bytecode runs unchanged, with the documented differences of spec 7.1 | Homepage Build card; litepaper Building | tested by the team | as row 13; fixes `F-exec-A`, `F-exec-B` (spec 7.5) | `tools/evm-smoke/smoke.mjs`: deploy via viem, `increment`, `hashLoop`, `eth_estimateGas`, `eth_getLogs`; `tools/exec-attacks` scenarios 1 and 3; bench-log "execution layer attack fixes" | Deployment, calls, reverts, logs and gas estimates behave as viem expects; chain id 4463; the prototype pgas table gives 0.0095 to 0.028 pgas per gas, below the design's band before calibration, 3 October 2026. 4 October 2026: a transaction that would cross the block's proving budget is refused by the mempool and, if forced in, aborted and charged with its nonce advanced (25 of 25 checks; 30 of 30 malformed cases). Apple M5 Max. The `Prover` precompile, proof records and the shard planner are not in the node | none yet |
|
| 14 | Ethereum bytecode runs unchanged, with the documented differences of spec 7.1 | Homepage Build card; litepaper Building | tested by the team | as row 13; fixes `F-exec-A`, `F-exec-B` (spec 7.5) | `tools/evm-smoke/smoke.mjs`: deploy via viem, `increment`, `hashLoop`, `eth_estimateGas`, `eth_getLogs`; `tools/exec-attacks` scenarios 1 and 3; bench-log "execution layer attack fixes" | Deployment, calls, reverts, logs and gas estimates behave as viem expects; chain id 4463; the prototype pgas table gives 0.0095 to 0.028 pgas per gas, below the design's band before calibration, 3 October 2026. 4 October 2026: a transaction that would cross the block's proving budget is refused by the mempool and, if forced in, aborted and charged with its nonce advanced (25 of 25 checks; 30 of 30 malformed cases). Apple M5 Max. The `Prover` precompile, proof records and the shard planner are not in the node | none yet |
|
||||||
| 15 | Every block is proven, with the proof landing within about a minute at launch | Homepage stats ("~60 s to a proof"); litepaper Proving; roadmap phase 3 gate | implemented | repo `d7e1f89` (GPU proof), `e01a3cc`, `292e800`, `eedd136` (`proving/igneum-prove`: shard cutter, MPT witnesses, shard and aggregator guests); SP1 6.8.1; spec 7.2, 7.6 | `proving/windows-wsl2` (SETUP-PROVER, PROVE-BLOCK) on the RTX 5090; `igneum-prove-host --mode block` on `proving/fixtures/`; bench-log "proving v0 on the RTX 5090" and "proving: devnet v4 shards" | First GPU proof of an Igneum block, 4 October 2026, RTX 5090 (WSL2, SP1 cuda, mining paused): fixture `block-78-increment` (2 transactions), core proof 1.4 s (7.3 MB, verify 0.221 s), compressed proof 2.7 s (1.27 MB, verify 0.038 s), post-state and receipts roots identical to the node's; 15.7x and 20.6x faster than a loaded M5 Max CPU. The same day on that CPU (load 38 to 47): a three-shard block proved shard by shard and aggregated by recursion, 19 min (1,139 s) end to end, 245 to 337 s per compressed shard proof, every proof verified. What is not there: no proof is produced, carried or checked on the chain (the devnet prover is a stub that signs claims), the proving pool pays nobody (row 21), the block proven is far below one shard, and the 60-second figure remains a design target; the pass mark is the standard in `docs/benchmarks/proving-e2e.md`. Second RTX 5090 run, 4 October 2026 evening (job run-20261004-173115): a full shard at the provisional S_p (6.75 M pgas, 60.8 M cycles) executed in 1.63 s, core proof 8.3 s (18.1 MB), compressed proof 10.9 s (1.27 MB, verify 0.040 s); a two-shard block (13.5 M pgas) proved shard by shard (11.7 s and 10.0 s) and aggregated in 2.2 s, 24 s of GPU stages end to end, every proof verified, six tampered witnesses rejected. The two host defects (an abort after the upload, an idle wait that turned out to be an unbuffered 18 MB proof save through the WSL2 file bridge, 24 minutes) are fixed (ledger P20) 5 October 2026, live devnet with real transactions (bench-log "real transactions, the first non-empty shard proven and paid"): block 72704 shard 0, 29 transfers, 5,800 pgas, proven on PC 2 in 34 s, verified on the Mac in 0.297 s and paid 1.7623 IGN, 53 s after the chain block executed; of about 1,400 blocks in the 20-minute window 36 were proven (the one prover takes the newest shard assigned to it), so "every block" is not yet true; a second content shard (72803, all copies skipped) failed the native-execution veto on the exporter's block structure, fixed with fixtures the same day, the node side pending the 0.3.9 rollout 5 October 2026, evening (bench-log "proving v1"): the aggregated segment record, the chain rule and the unproven rule are implemented behind `proving_v1_activation_daa` (branch proving-v1, not on the devnet before 0.3.11); on the RTX 5090 a chain of 8 consecutive live blocks proved and aggregated by recursion in 135.6 s with the miner on the card (17 s a block, one proof of 1,272,909 bytes attesting all 8, verified in 0.04 s); the 3-node fast-time harness paid a segment record 1.0 s after submission and refused a late one after its deadline (21 checks); the devnet itself, with one prover, carried proofs for 2.4% of blocks over 30 minutes at a block-to-record latency p50 44 s, p99 52 s. The "within about a minute" holds per proven block; "every block" needs 18 mining 5090s or 6 proving-only cards at empty blocks on the measured rates, and the mandatory rule stays off until the share is one | none yet |
|
| 15 | Every block is proven, with the proof landing within about a minute at launch | Homepage stats ("~60 s to a proof"); litepaper Proving; roadmap phase 3 gate | implemented | repo `d7e1f89` (GPU proof), `e01a3cc`, `292e800`, `eedd136` (`proving/igneum-prove`: shard cutter, MPT witnesses, shard and aggregator guests); SP1 6.8.1; spec 7.2, 7.6 | `proving/windows-wsl2` (SETUP-PROVER, PROVE-BLOCK) on the RTX 5090; `igneum-prove-host --mode block` on `proving/fixtures/`; bench-log "proving v0 on the RTX 5090" and "proving: devnet v4 shards" | First GPU proof of an Igneum block, 4 October 2026, RTX 5090 (WSL2, SP1 cuda, mining paused): fixture `block-78-increment` (2 transactions), core proof 1.4 s (7.3 MB, verify 0.221 s), compressed proof 2.7 s (1.27 MB, verify 0.038 s), post-state and receipts roots identical to the node's; 15.7x and 20.6x faster than a loaded M5 Max CPU. The same day on that CPU (load 38 to 47): a three-shard block proved shard by shard and aggregated by recursion, 19 min (1,139 s) end to end, 245 to 337 s per compressed shard proof, every proof verified. What is not there: no proof is produced, carried or checked on the chain (the devnet prover is a stub that signs claims), the proving pool pays nobody (row 21), the block proven is far below one shard, and the 60-second figure remains a design target; the pass mark is the standard in `docs/benchmarks/proving-e2e.md`. Second RTX 5090 run, 4 October 2026 evening (job run-20261004-173115): a full shard at the provisional S_p (6.75 M pgas, 60.8 M cycles) executed in 1.63 s, core proof 8.3 s (18.1 MB), compressed proof 10.9 s (1.27 MB, verify 0.040 s); a two-shard block (13.5 M pgas) proved shard by shard (11.7 s and 10.0 s) and aggregated in 2.2 s, 24 s of GPU stages end to end, every proof verified, six tampered witnesses rejected. The two host defects (an abort after the upload, an idle wait that turned out to be an unbuffered 18 MB proof save through the WSL2 file bridge, 24 minutes) are fixed (ledger P20) 5 October 2026, live devnet with real transactions (bench-log "real transactions, the first non-empty shard proven and paid"): block 72704 shard 0, 29 transfers, 5,800 pgas, proven on PC 2 in 34 s, verified on the Mac in 0.297 s and paid 1.7623 IGN, 53 s after the chain block executed; of about 1,400 blocks in the 20-minute window 36 were proven (the one prover takes the newest shard assigned to it), so "every block" is not yet true; a second content shard (72803, all copies skipped) failed the native-execution veto on the exporter's block structure, fixed with fixtures the same day, the node side pending the 0.3.9 rollout 5 October 2026, evening (bench-log "proving v1"): the aggregated segment record, the chain rule and the unproven rule are implemented behind `proving_v1_activation_daa` (branch proving-v1, not on the devnet before 0.3.11); on the RTX 5090 a chain of 8 consecutive live blocks proved and aggregated by recursion in 135.6 s with the miner on the card (17 s a block, one proof of 1,272,909 bytes attesting all 8, verified in 0.04 s); the 3-node fast-time harness paid a segment record 1.0 s after submission and refused a late one after its deadline (21 checks); the devnet itself, with one prover, carried proofs for 2.4% of blocks over 30 minutes at a block-to-record latency p50 44 s, p99 52 s. The "within about a minute" holds per proven block; "every block" needs 18 mining 5090s or 6 proving-only cards at empty blocks on the measured rates, and the mandatory rule stays off until the share is one | none yet |
|
||||||
| 16 | A 12 GB card proves one shard in about 20 s (WITHDRAWN 5 October 2026: a 24 GB card proves a full shard at the adopted size in 4.3 s; 32 GB mines and proves) | Litepaper Proving ("The proving budget"); roadmap gate 2 | designed | spec 5.1 (Target), 7.6 (`S_p` provisional, 7,500,000 pgas = `B_p` / 4) | `PROVE-SHARD.bat` on the RTX 5090 (pending); the end-to-end standard in `docs/benchmarks/proving-e2e.md`; bench-log "proving: devnet v4 shards" | Measured on a 32 GB card, not yet on a 12 GB card. A shard at the provisional `S_p` is 60.8 M SP1 cycles on the prototype pgas table (9 cycles per pgas, 44 per EVM gas; the modexp entry about 100x its SP1 cost); on an RTX 5090 (4 October 2026 evening, job run-20261004-173115) it executed in 1.63 s and its compressed proof took 10.9 s, verified in 0.040 s, so the 32 GB card is inside the 20 s target with margin. Whether a 12 GB card proves it at all, and in what time, is the next measurement (an RTX 3060 and an RTX 5060 Ti 16 GB are on order). A per-shard time can be met by shrinking the shard, so the project does not use it as a pass mark 5 October 2026, evening (bench-log "proving v1", the S_p curve): measured on the RTX 5090 with SP1 6.8.1's GPU prover, the card to itself, 1-s nvidia-smi samples: an empty shard 13,874 MiB and 2.2 s; a full shard at the ADOPTED v1 budget (30,000 pgas, 4.7 M cycles) 20,434 MiB and 4.3 s; the full prototype shard (6.75 M pgas, 60 M cycles) 28,307 MiB and 10.8 s; beside the miner 15,670 and 30,039 MiB. No environment knob of SP1 moves the 13.9 GB floor and the GPU server has no options of its own, so on this build a 12 GB card proves nothing, a 16 GB card only empty shards, a 24 GB card the adopted full shard alone and beside the miner (22,210 MiB and 13.2 s, measured on the 32 GB card: the 5090's allocation pattern, not yet a run on a 24 GB card) and a 32 GB card the prototype shard beside the miner with 2.5 GB spare. The litepaper line now says so; the 12 GB gate returns when a prover build with a smaller floor is measured on a 12 GB card | none yet |
|
| 16 | A 12 GB card proves one shard in about 20 s (WITHDRAWN 5 October 2026: a 24 GB card proves a full shard at the adopted size in 4.3 s; 32 GB mines and proves) | Litepaper Proving ("The proving budget"); roadmap gate 2 | designed | spec 5.1 (Target), 7.6 (`S_p` provisional, 7,500,000 pgas = `B_p` / 4) | `PROVE-SHARD.bat` on the RTX 5090 (pending); the end-to-end standard in `docs/benchmarks/proving-e2e.md`; bench-log "proving: devnet v4 shards" | Measured on a 32 GB card, not yet on a 12 GB card. A shard at the provisional `S_p` is 60.8 M SP1 cycles on the prototype pgas table (9 cycles per pgas, 44 per EVM gas; the modexp entry about 100x its SP1 cost); on an RTX 5090 (4 October 2026 evening, job run-20261004-173115) it executed in 1.63 s and its compressed proof took 10.9 s, verified in 0.040 s, so the 32 GB card is inside the 20 s target with margin. Whether a 12 GB card proves it at all, and in what time, is the next measurement (an RTX 3060 and an RTX 5060 Ti 16 GB are on order). A per-shard time can be met by shrinking the shard, so the project does not use it as a pass mark 5 October 2026, evening (bench-log "proving v1", the S_p curve): measured on the RTX 5090 with SP1 6.8.1's GPU prover, the card to itself, 1-s nvidia-smi samples: an empty shard 13,874 MiB and 2.2 s; a full shard at the ADOPTED v1 budget (30,000 pgas, 4.7 M cycles) 20,434 MiB and 4.3 s; the full prototype shard (6.75 M pgas, 60 M cycles) 28,307 MiB and 10.8 s; beside the miner 15,670 and 30,039 MiB. No environment knob of SP1 moves the 13.9 GB floor and the GPU server has no options of its own, so on this build a 12 GB card proves nothing, a 16 GB card only empty shards, a 24 GB card the adopted full shard alone and beside the miner (22,210 MiB and 13.2 s, measured on the 32 GB card: the 5090's allocation pattern, not yet a run on a 24 GB card) and a 32 GB card the prototype shard beside the miner with 2.5 GB spare. The litepaper line now says so; the 12 GB gate returns when a prover build with a smaller floor is measured on a 12 GB card | none yet |
|
||||||
| 17 | The chip resistance target: a chip gains under 2x over a GPU | Homepage hero and litepaper abstract ("a custom chip gains under 2x, and the model and the bounty are public"), litepaper "What Igneum does not claim" | tested by the team (the model), designed (the target) | program class v3 (Counter ASIC 2.0, 5 October 2026): branches ca2-v3 d233fa1 and after, ca2-mixer 1ab8b21, ca2-era 78c0ee4; `docs/analysis/chip-model-v3.md`, `docs/analysis/sram-mirror.md`, `docs/analysis/scratch-soundness.md` | The m16 recompute model re-run on the measured v3 rates and verifier times; the on-die-cache chip row | The on-die-cache recompute chip against the RTX 5090's measured 136.1 MH/s: class v2 2.4x; class v3 (mixer x8) 0.31x bare, 0.92x with a 3x fixed-function allowance (approximate), 0.76x at equal silicon; margin 8% on the allowance, 9% on the budget. 5 October 2026, M5 Max, RTX 5090, RX 9070 XT. The 2x target is a target: no chip has been built; the bounty stands (O-1.17) | none yet |
|
| 17 | The chip resistance claim: the strongest recompute chip under 1x per chip against an RTX 5090; the stored-dataset chip 1.2x per chip and 5x to 9x per joule in the model (2.1x to 4.8x by the Ethash precedent); the latency-shadow lever, measured and in its gates, brings it to about 2x | Homepage hero and litepaper abstract (draft (a) of `docs/plans/counter-asic-3-status.md` section 6, chosen 6 October 2026), litepaper "What Igneum does not claim" | tested by the team (the model), designed (the target) | program class v3 (Counter ASIC 2.0, 5 October 2026): branches ca2-v3 d233fa1 and after, ca2-mixer 1ab8b21, ca2-era 78c0ee4; `docs/analysis/chip-model-v3.md`, `docs/analysis/sram-mirror.md`, `docs/analysis/scratch-soundness.md` | The m16 recompute model re-run on the measured v3 rates and verifier times; the on-die-cache chip row | The on-die-cache recompute chip against the RTX 5090's measured 136.1 MH/s: class v2 2.4x; class v3 (mixer x8) 0.31x bare, 0.92x with a 3x fixed-function allowance (approximate), 0.76x at equal silicon; margin 8% on the allowance, 9% on the budget. 5 October 2026, M5 Max, RTX 5090, RX 9070 XT. The 2x target is a target: no chip has been built; the bounty stands (O-1.17) | none yet |
|
||||||
| 18 | The chip resistance measurements: the program is latency-bound (random reads), not bandwidth-bound, on every card we own, and sits beyond a card's on-chip cache | Litepaper Mining ("waits on memory latency, not on maths or bandwidth"), vs RandomX; the numbers page | tested by the team | readwidth e752fc7 (`docs/plans/read-width.md`), ca2-era 78c0ee4, ca2-cache 2de19e5 (`docs/plans/hot-table.md`) | The dependent-read probes at 32 to 1,024 MiB and the hash rate per class on the three cards; the latency-bound share = rate over the probe ceiling per load | Latency-bound share at the 1 GiB dataset: RTX 5090 0.96 (v2) and 1.01 (v3), RX 9070 XT 0.87 and 0.95, M5 Max 1.01 and 1.06; wider reads do not close the AMD gap (the 9070 XT does 2.4 G dependent reads per second at every width; the 5090 goes bandwidth-bound at 64 B, share 0.58); a 32 to 96 MiB hot table is not kept resident by any card while the dataset streams (g 0.80 to 0.87 in the added form). 5 October 2026 | none yet |
|
| 18 | The chip resistance measurements: the program is latency-bound (random reads), not bandwidth-bound, on every card we own, and sits beyond a card's on-chip cache | Litepaper Mining ("waits on memory latency, not on maths or bandwidth"), vs RandomX; the numbers page | tested by the team | readwidth e752fc7 (`docs/plans/read-width.md`), ca2-era 78c0ee4, ca2-cache 2de19e5 (`docs/plans/hot-table.md`) | The dependent-read probes at 32 to 1,024 MiB and the hash rate per class on the three cards; the latency-bound share = rate over the probe ceiling per load | Latency-bound share at the 1 GiB dataset: RTX 5090 0.96 (v2) and 1.01 (v3), RX 9070 XT 0.87 and 0.95, M5 Max 1.01 and 1.06; wider reads do not close the AMD gap (the 9070 XT does 2.4 G dependent reads per second at every width; the 5090 goes bandwidth-bound at 64 B, share 0.58); a 32 to 96 MiB hot table is not kept resident by any card while the dataset streams (g 0.80 to 0.87 in the added form). 5 October 2026 | none yet |
|
||||||
| 19 | The lottery hash is sound as a hash: uniform output, deterministic, no out-of-bounds read, fuzzed; class v3 bit-exact on the three vendors | Litepaper vs RandomX ("Every number above is measured and logged"), the numbers page | tested by the team | ca2-mixer 1ab8b21 (`tests/mixer.rs`, `tests/scratch.rs`), ca2-era 78c0ee4, ca2-soundness a465881 (`docs/analysis/scratch-soundness.md`), `igneum-pow/tests/packs.rs` | The crate suite (53 + 4 + 19 + 7), the Metal fuzz, edge, stats and determinism runs on the v3 construction, the pack vectors and 2^24 fingerprints on Metal, Apple OpenCL, the RTX 5090 and the RX 9070 XT, the 1,024-hash CPU re-check per card | Class v3 (mixer x8 + era): 200-program fuzz 200 of 200 on Metal, every tenth on Apple OpenCL; the pinned v3 packs 3/3 + 3/3 and 96 of 96 lanes on Metal and Apple OpenCL; the six era packs' fingerprints equal on the three vendors (PC 1 job run-ca2-era-pc1-20261005, 5 October 2026); the v2 exports byte-identical on the v3 crate; the final-class PC rows and the G2 re-check: job run-ca2-era-pc1b-20261005 (pending at the time of writing) | none yet |
|
| 19 | The lottery hash is sound as a hash: uniform output, deterministic, no out-of-bounds read, fuzzed; class v3 bit-exact on the three vendors | Litepaper vs RandomX ("Every number above is measured and logged"), the numbers page | tested by the team | ca2-mixer 1ab8b21 (`tests/mixer.rs`, `tests/scratch.rs`), ca2-era 78c0ee4, ca2-soundness a465881 (`docs/analysis/scratch-soundness.md`), `igneum-pow/tests/packs.rs` | The crate suite (53 + 4 + 19 + 7), the Metal fuzz, edge, stats and determinism runs on the v3 construction, the pack vectors and 2^24 fingerprints on Metal, Apple OpenCL, the RTX 5090 and the RX 9070 XT, the 1,024-hash CPU re-check per card | Class v3 (mixer x8 + era): 200-program fuzz 200 of 200 on Metal, every tenth on Apple OpenCL; the pinned v3 packs 3/3 + 3/3 and 96 of 96 lanes on Metal and Apple OpenCL; the six era packs' fingerprints equal on the three vendors (PC 1 job run-ca2-era-pc1-20261005, 5 October 2026); the v2 exports byte-identical on the v3 crate; the final-class PC rows and the G2 re-check: job run-ca2-era-pc1b-20261005 (pending at the time of writing) | none yet |
|
||||||
| 20 | No premine, no pre-sale, no allocation: every coin is minted by the schedule and every coin goes to the block producer (80%) and the proving pool (20%) | Homepage stats and Economics tiles; litepaper Supply, Economics | implemented | repo `6ac80a3`; fork "igneum-node devnet v0"; `consensus/core/src/igneum.rs`, `coinbase.rs` | `cargo test -p kaspa-consensus-core igneum` (8 pass: subsidy table, ramp, split, cap) and `cargo test -p kaspa-consensus coinbase` (8 pass); `igneum-miner inspect 40`; bench-log "igneum-node devnet v0" | Coinbases on the devnet: 80/20 exact on 39 of 39 single-payee blocks, the 20% to the `igneum-proving-pool-v0` output; the per-second schedule sums to under the 4,000,000,000 cap by less than 100 coins; 3,168,808,781 units per DAA second in years 0 to 2, halving at 63,115,200 DAA s. 3 October 2026, Apple M5 Max. The devnet genesis carries no allocation; the mainnet genesis does not exist yet, so the claim is about the code and the stated rule, not a launch that has happened | none yet |
|
| 20 | No premine, no pre-sale, no allocation: every coin is minted by the schedule and every coin goes to the block producer (80%) and the proving pool (20%) | Homepage stats and Economics tiles; litepaper Supply, Economics | implemented | repo `6ac80a3`; fork "igneum-node devnet v0"; `consensus/core/src/igneum.rs`, `coinbase.rs` | `cargo test -p kaspa-consensus-core igneum` (8 pass: subsidy table, ramp, split, cap) and `cargo test -p kaspa-consensus coinbase` (8 pass); `igneum-miner inspect 40`; bench-log "igneum-node devnet v0" | Coinbases on the devnet: 80/20 exact on 39 of 39 single-payee blocks, the 20% to the `igneum-proving-pool-v0` output; the per-second schedule sums to under the 4,000,000,000 cap by less than 100 coins; 3,168,808,781 units per DAA second in years 0 to 2, halving at 63,115,200 DAA s. 3 October 2026, Apple M5 Max. The devnet genesis carries no allocation; the mainnet genesis does not exist yet, so the claim is about the code and the stated rule, not a launch that has happened | none yet |
|
||||||
|
|
@ -53,8 +53,9 @@ Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`
|
||||||
| 26 | A phone or browser verifies the chain from a locked checkpoint, at about 3.44 MB per day in checkpoint mode | Homepage "Browser checks Igneum" card; litepaper Building ("Light clients"), firsts row 6 | designed | spec 10 (10.5 bytes per day: 3.44 MB at 1,000 voters, 6.68 MB at 10,000, derived, approximate); repo `f874f80` for the browser card; `site/api/checkpoint.mjs` | None for the byte figure; `site/verify/` for the card against `/api/checkpoint`. BLS verification on a phone and in WebAssembly is O-10.3; the full-header mode on a phone is O-10.4 | Since 12:03 BST on 4 October 2026 the homepage card verifies the live devnet's own certificates in the tab (index 522 with 27 voters at 13:42 UTC), BLS aggregate against the voter list the node serves, light client v0; before that it verified the 3 October test network's. The byte figure is arithmetic on designed sizes (header 400 bytes, proof 400 bytes), measured nowhere; the execution proof the card would also check is not on the chain (row 15) | none yet |
|
| 26 | A phone or browser verifies the chain from a locked checkpoint, at about 3.44 MB per day in checkpoint mode | Homepage "Browser checks Igneum" card; litepaper Building ("Light clients"), firsts row 6 | designed | spec 10 (10.5 bytes per day: 3.44 MB at 1,000 voters, 6.68 MB at 10,000, derived, approximate); repo `f874f80` for the browser card; `site/api/checkpoint.mjs` | None for the byte figure; `site/verify/` for the card against `/api/checkpoint`. BLS verification on a phone and in WebAssembly is O-10.3; the full-header mode on a phone is O-10.4 | Since 12:03 BST on 4 October 2026 the homepage card verifies the live devnet's own certificates in the tab (index 522 with 27 voters at 13:42 UTC), BLS aggregate against the voter list the node serves, light client v0; before that it verified the 3 October test network's. The byte figure is arithmetic on designed sizes (header 400 bytes, proof 400 bytes), measured nowhere; the execution proof the card would also check is not on the chain (row 15) | none yet |
|
||||||
| 27 | The node survives malformed input, floods, withholding, partitions and eclipses | Litepaper Speed ("GHOSTDAG, the BlockDAG consensus proven on Kaspa"); spec 2 | tested by the team | repo `394030c`, `8dae48b`, `6b5bd92`; fork worktree `vendor/igneum-node-harness` and `devnet-v4`; `tools/harness/`; `infra/cloud-devnet/experiments/partition.sh` | `tools/harness/` against a private `igneumd` test network; the merged node's harness scenarios 2 and 5; the cloud network's 10-minute partition of Singapore (`results/2026-10-04/partition-sin-20261004-110906/partition.md`); bench-log "consensus attack harness", "devnet-v4 integration" | 3 October 2026, Apple M5 Max: 63 malformed cases, node up on every one; withholding at 10% to 45% within 2 sigma of share; partitions of 120 s to 3,700 s healed to one chain in 10 s; eclipse victims rejoined in 10 s; 50x floods left template p95 under 4 ms; one FAIL, a 45% withholder releasing every 20 blocks took 50.7% of blues (bound 47.4%). Merged node, 4 October 2026: 63 cases, node up, 0 cache builds; the 10 s timestamp floor and future bound exact. Cloud network, 4 October 2026: 12 nodes in five locations on their own chain, Singapore cut off by iptables for 10 minutes; the two minority nodes adopted the majority chain 10 and 14 s after the heal with reorgs of 445 and 516 blocks, the majority's deepest reorg was 2 blocks, 0 conflicting locks (none were possible: the weight window stood at DAA 3,030 of 7,200). CPU miners only; the finality rules under partition are row 10 | none yet |
|
| 27 | The node survives malformed input, floods, withholding, partitions and eclipses | Litepaper Speed ("GHOSTDAG, the BlockDAG consensus proven on Kaspa"); spec 2 | tested by the team | repo `394030c`, `8dae48b`, `6b5bd92`; fork worktree `vendor/igneum-node-harness` and `devnet-v4`; `tools/harness/`; `infra/cloud-devnet/experiments/partition.sh` | `tools/harness/` against a private `igneumd` test network; the merged node's harness scenarios 2 and 5; the cloud network's 10-minute partition of Singapore (`results/2026-10-04/partition-sin-20261004-110906/partition.md`); bench-log "consensus attack harness", "devnet-v4 integration" | 3 October 2026, Apple M5 Max: 63 malformed cases, node up on every one; withholding at 10% to 45% within 2 sigma of share; partitions of 120 s to 3,700 s healed to one chain in 10 s; eclipse victims rejoined in 10 s; 50x floods left template p95 under 4 ms; one FAIL, a 45% withholder releasing every 20 blocks took 50.7% of blues (bound 47.4%). Merged node, 4 October 2026: 63 cases, node up, 0 cache builds; the 10 s timestamp floor and future bound exact. Cloud network, 4 October 2026: 12 nodes in five locations on their own chain, Singapore cut off by iptables for 10 minutes; the two minority nodes adopted the majority chain 10 and 14 s after the heal with reorgs of 445 and 516 blocks, the majority's deepest reorg was 2 blocks, 0 conflicting locks (none were possible: the weight window stood at DAA 3,030 of 7,200). CPU miners only; the finality rules under partition are row 10 | none yet |
|
||||||
| 28 | Headers are validated cheaply before the lottery engine runs, so forged timestamps cannot force 256 MiB cache builds | Spec 2.4; ledger M15 | tested by the team | repo `0953ec7`, `8dae48b`; fork worktree `vendor/igneum-node-r3` branch `r3-fixes` at `5166ee26`, merged into `devnet-v4` | `measure_m15_attack_before_and_after` (ignored test, release, `--features igneum-pow`); kaspa-pow 8, header_processor 1, p2p `pow_guard` 2 tests; harness scenario 5 on the merged node | 50 forged headers: before, 50 cold builds in 10,595 ms and the live day evicted; after, 0 builds, all 50 rejected in 14 ms, 3 October 2026, Apple M5 Max under load 60 to 110. Merged node, 4 October 2026: 63 harness cases with 0 cache builds (the node log shows one build, the honest day) and the M15 p2p cases disconnected by the strike guard; the live devnet v4 runs it. Measured through the validate path with `skip_proof_of_work`, not the daemon RPC | none yet |
|
| 28 | Headers are validated cheaply before the lottery engine runs, so forged timestamps cannot force 256 MiB cache builds | Spec 2.4; ledger M15 | tested by the team | repo `0953ec7`, `8dae48b`; fork worktree `vendor/igneum-node-r3` branch `r3-fixes` at `5166ee26`, merged into `devnet-v4` | `measure_m15_attack_before_and_after` (ignored test, release, `--features igneum-pow`); kaspa-pow 8, header_processor 1, p2p `pow_guard` 2 tests; harness scenario 5 on the merged node | 50 forged headers: before, 50 cold builds in 10,595 ms and the live day evicted; after, 0 builds, all 50 rejected in 14 ms, 3 October 2026, Apple M5 Max under load 60 to 110. Merged node, 4 October 2026: 63 harness cases with 0 cache builds (the node log shows one build, the honest day) and the M15 p2p cases disconnected by the strike guard; the live devnet v4 runs it. Measured through the validate path with `skip_proof_of_work`, not the daemon RPC | none yet |
|
||||||
| 29 | Blocks reach every node well inside GHOSTDAG's delay bound across continents | Litepaper Speed (GHOSTDAG at one block a second); spec 03 C1 (lock latency); `infra/cloud-devnet/README.md` | tested by the team | repo `6b5bd92`; `infra/cloud-devnet/experiments/latency.sh`, `analyze.py`; the Linux cross-build `infra/cross/build-linux.sh` | 12 `igneumd` nodes on Hetzner VMs in Helsinki, Falkenstein, Ashburn, Hillsboro and Singapore (own chain `igneum-devnet-20`, one CPU trickle miner each), a ping matrix, then 10 minutes of per-node arrival logs joined on block hash; `results/2026-10-04/latency/propagation.md` and `rtt-by-region.md` | 644 blocks in the window, 642 seen by at least 80% of nodes; arrival at a node minus the first arrival anywhere: p50 343 ms, p90 497 ms, p99 666 ms, max 2,313 ms; by region p50 239 ms (Falkenstein) to 413 ms (Singapore), p90 455 to 632 ms; inter-region RTT 35 ms (Helsinki to Falkenstein) to 289 ms (Ashburn to Singapore); first arrival minus header time median 490 ms. 4 October 2026. The network is the project's own: 12 nodes not 20 (a new account's limits), CPU hash rate only, clocks by chrony, one evening of data; the 5 s bound behind GHOSTDAG k is a design parameter this run did not challenge | none yet |
|
| 29 | Blocks reach every node well inside GHOSTDAG's delay bound across continents | Litepaper Speed (GHOSTDAG at one block a second); spec 03 C1 (lock latency); `infra/cloud-devnet/README.md` | tested by the team | repo `6b5bd92`; `infra/cloud-devnet/experiments/latency.sh`, `analyze.py`; the Linux cross-build `infra/cross/build-linux.sh` | 12 `igneumd` nodes on cloud VMs in Helsinki, Falkenstein, Ashburn, Hillsboro and Singapore (own chain `igneum-devnet-20`, one CPU trickle miner each), a ping matrix, then 10 minutes of per-node arrival logs joined on block hash; `results/2026-10-04/latency/propagation.md` and `rtt-by-region.md` | 644 blocks in the window, 642 seen by at least 80% of nodes; arrival at a node minus the first arrival anywhere: p50 343 ms, p90 497 ms, p99 666 ms, max 2,313 ms; by region p50 239 ms (Falkenstein) to 413 ms (Singapore), p90 455 to 632 ms; inter-region RTT 35 ms (Helsinki to Falkenstein) to 289 ms (Ashburn to Singapore); first arrival minus header time median 490 ms. 4 October 2026. The network is the project's own: 12 nodes not 20 (a new account's limits), CPU hash rate only, clocks by chrony, one evening of data; the 5 s bound behind GHOSTDAG k is a design parameter this run did not challenge | none yet |
|
||||||
| 30 | One click: install, press start, the card mines; the app looks after its node | Homepage Mine section ("One click: install, press start"); litepaper "One click, for everyone else"; journey phase 5 | tested by the team | repo `3bb50d6`, `2c4b30f`, `6461540` (package 0.3.0: prebuilt NVRTC CUDA worker and generic OpenCL worker, driver only), `a1a33cb`, `7c794df`, `0d4498e`, `6c083db` (Igneum Miner 0.3.0), `78903cd` (0.3.1, over-the-air updates) | `Igneum-Miner-Setup-0.3.0.exe` (runner-built, unsigned) on a Windows PC with an RTX 5090 and no toolchain; `proto-cuda/nvrtc/emu/serve-check.sh` on the Mac; `proto-cuda/windows-app/TEST.md`; bench-log "one-click Windows workers", "first machine on the Igneum Miner app", "a node 60 s behind the clock is silently dead", "the gfx1036 worker fault" | Four machines by 15:45 BST on 4 October 2026: PC 2, then PC 1 (RTX 5090 at 110 MH/s under the 80% power cap), the project's Apple M5 Max (25 MH/s) and the outside Apple silicon laptop (row 29), all on Igneum Miner 0.3.1. The NVRTC worker compiled the pack on the card with no toolchain installed and mined at 124.2 MH/s, equal to the nvcc-built worker, 0 rejected, CPU re-check clean; inside the app 117 to 119 MH/s with 34 accepted blocks in the first minute, the integrated AMD chip at 3.3 MH/s beside it (row 9). Two defects found by the install, both fixed the same hour: a clock 62 s slow after a power cut made the node reject every relayed block for 12 minutes with no visible reason (the app now reads the skew from the node's warnings, the block timestamps over the EVM RPC and an HTTPS Date header, warns over 5 s and blocks Start over 10 s, with a one-click clock sync; checked on the Mac with a fake 60 s skew; a one-line node warning is filed), and the node card said "syncing" while the miner was already accepted. The Mac could only emulate the NVIDIA path (17 of 17 sampled hashes) and the AMD path on Apple OpenCL (15 of 15). Over-the-air updates were dry-run on a private devnet (0.3.0 to 0.3.1 and back), not on a user's machine. The installer is unsigned (SmartScreen "run anyway"). Second machine, the same afternoon: a friend of the project installed Igneum Miner 0.3.1 from the DMG on an Apple silicon laptop with no toolchain and no instructions beyond five steps; the node synced from the seed, the Metal worker reported ready, 33 accepted blocks and 0 rejected in 7 minutes at 21.0 MH/s average, CPU re-check OK on every share, uploads arriving every minute under its per-install id. That laptop is not the project's hardware, but the result is observed through the project's own log intake and reported by the project, so it stays tested by the team until an outsider publishes a run of their own. The devnet's other GPU machines (PC 1 and the Mac) run the same workers through the launcher, not the app | none yet |
|
| 30 | One click: install, press start, the card mines; the app looks after its node | Homepage Mine section ("One click: install, press start"); litepaper "One click, for everyone else"; journey phase 5 | tested by the team | repo `3bb50d6`, `2c4b30f`, `6461540` (package 0.3.0: prebuilt NVRTC CUDA worker and generic OpenCL worker, driver only), `a1a33cb`, `7c794df`, `0d4498e`, `6c083db` (Igneum Miner 0.3.0), `78903cd` (0.3.1, over-the-air updates) | `Igneum-Miner-Setup-0.3.0.exe` (runner-built, unsigned) on a Windows PC with an RTX 5090 and no toolchain; `proto-cuda/nvrtc/emu/serve-check.sh` on the Mac; `proto-cuda/windows-app/TEST.md`; bench-log "one-click Windows workers", "first machine on the Igneum Miner app", "a node 60 s behind the clock is silently dead", "the gfx1036 worker fault" | Four machines by 15:45 BST on 4 October 2026: PC 2, then PC 1 (RTX 5090 at 110 MH/s under the 80% power cap), the project's Apple M5 Max (25 MH/s) and the outside Apple silicon laptop (row 29), all on Igneum Miner 0.3.1. The NVRTC worker compiled the pack on the card with no toolchain installed and mined at 124.2 MH/s, equal to the nvcc-built worker, 0 rejected, CPU re-check clean; inside the app 117 to 119 MH/s with 34 accepted blocks in the first minute, the integrated AMD chip at 3.3 MH/s beside it (row 9). Two defects found by the install, both fixed the same hour: a clock 62 s slow after a power cut made the node reject every relayed block for 12 minutes with no visible reason (the app now reads the skew from the node's warnings, the block timestamps over the EVM RPC and an HTTPS Date header, warns over 5 s and blocks Start over 10 s, with a one-click clock sync; checked on the Mac with a fake 60 s skew; a one-line node warning is filed), and the node card said "syncing" while the miner was already accepted. The Mac could only emulate the NVIDIA path (17 of 17 sampled hashes) and the AMD path on Apple OpenCL (15 of 15). Over-the-air updates were dry-run on a private devnet (0.3.0 to 0.3.1 and back), not on a user's machine. The installer is unsigned (SmartScreen "run anyway"). Second machine, the same afternoon: a friend of the project installed Igneum Miner 0.3.1 from the DMG on an Apple silicon laptop with no toolchain and no instructions beyond five steps; the node synced from the seed, the Metal worker reported ready, 33 accepted blocks and 0 rejected in 7 minutes at 21.0 MH/s average, CPU re-check OK on every share, uploads arriving every minute under its per-install id. That laptop is not the project's hardware, but the result is observed through the project's own log intake and reported by the project, so it stays tested by the team until an outsider publishes a run of their own. The devnet's other GPU machines (PC 1 and the Mac) run the same workers through the launcher, not the app | none yet |
|
||||||
|
| 31 | Card lifetime: a 4 GB card mines about four years and an 8 GB card about twelve, under the dataset's step schedule (2 GB at genesis, doubling at years 4, 12, 28, 60) with the cache freed after the daily build | Litepaper Hardware and vs RandomX ("Dataset" row); homepage Mine card and "Memory" row | designed | `docs/analysis/card-lifetime-2026-10-05.md` (branch card-lifetime 1fecfe2); spec 1.13.3 option (b) recommended to the owner 5 October 2026 (`docs/plans/counter-asic-2-rollout.md` 6c) | The per-tier working-set arithmetic of that document (GTX 1650, RTX 3050, RTX 3060, RTX 4090 tiers) against the step schedule | A design claim: under the continuous mapping (a) a 4 GB card is out within 1 to 1.5 years and an 8 GB card at 6 to 7.5 years, so the sentence is true only under the step schedule (b), which the spec has not yet fixed (O-1.13) | none yet |
|
||||||
|
|
||||||
## Count by status
|
## Count by status
|
||||||
|
|
||||||
|
|
@ -66,7 +67,7 @@ Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`
|
||||||
| reproduced externally | 0 |
|
| reproduced externally | 0 |
|
||||||
| reviewed independently | 0 |
|
| reviewed independently | 0 |
|
||||||
|
|
||||||
30 rows. The rendered page is `site/evidence.html`, kept in step by hand with this file; the bench page is generated, this one is not, because its text is judgement, not a log.
|
31 rows. The rendered page is `site/evidence.html`, generated from this file by `site/build.mjs` (since 6 October 2026); the text is judgement, so this file is edited by hand and the page follows.
|
||||||
|
|
||||||
## What moved on 4 October 2026
|
## What moved on 4 October 2026
|
||||||
|
|
||||||
|
|
@ -87,7 +88,6 @@ Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 15 | implemented | implemented, with a live result | the first non-empty shard (block 72704, 29 transfers) proven, verified and paid on the devnet; not every block is proven yet |
|
| 15 | implemented | implemented, with a live result | the first non-empty shard (block 72704, 29 transfers) proven, verified and paid on the devnet; not every block is proven yet |
|
||||||
| 21 | designed | tested by the team | 388 shards paid from the pool on the live devnet, the rule in `proving.rs`, the numbers in the bench log |
|
| 21 | designed | tested by the team | 388 shards paid from the pool on the live devnet, the rule in `proving.rs`, the numbers in the bench log |
|
||||||
| 22 | Card lifetime: a 4 GB card mines about four years and an 8 GB card about twelve, under the dataset's step schedule (2 GB at genesis, doubling at years 4, 12, 28, 60) with the cache freed after the daily build | Litepaper Hardware and vs RandomX ("Dataset" row); homepage Mine card and "Memory" row | designed | `docs/analysis/card-lifetime-2026-10-05.md` (branch card-lifetime 1fecfe2); spec 1.13.3 option (b) recommended to Josh 5 October 2026 (`docs/plans/counter-asic-2-rollout.md` 6c) | The per-tier working-set arithmetic of that document (GTX 1650, RTX 3050, RTX 3060, RTX 4090 tiers) against the step schedule | A design claim: under the continuous mapping (a) a 4 GB card is out within 1 to 1.5 years and an 8 GB card at 6 to 7.5 years, so the sentence is true only under the step schedule (b), which the spec has not yet fixed (O-1.13) | none yet |
|
|
||||||
|
|
||||||
## What would move a row
|
## What would move a row
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,29 @@ Added by `docs/review/ledger-sweep-2026-10-05.md`, which holds what ran, what di
|
||||||
| 126 | E12 | The devnet half of O-5.9 (profit-only prover clients, `f_p` and `B_p` paths) | Phase 4 devnet run as the ledger entry states (4 h) | execution engineer | before testnet | no |
|
| 126 | E12 | The devnet half of O-5.9 (profit-only prover clients, `f_p` and `B_p` paths) | Phase 4 devnet run as the ledger entry states (4 h) | execution engineer | before testnet | no |
|
||||||
| 127 | M25 | Confirmed 5 October 2026 (sweep batch 3): a miner started with a different `IGNEUM_POW_DAY_MS` builds its cache for another day and every block is rejected as `BlockInvalid` / `block has invalid proof-of-work`, with no line naming the day (0 of 4 accepted against 7 of 7 for the control) | The day length (or the day index) in the template beside `pow_epoch`, the miner takes it from there and ignores the environment; the node's PoW rejection names the engine's day and the header's day (1.5 h) | miner lead, consensus engineer | before testnet | no |
|
| 127 | M25 | Confirmed 5 October 2026 (sweep batch 3): a miner started with a different `IGNEUM_POW_DAY_MS` builds its cache for another day and every block is rejected as `BlockInvalid` / `block has invalid proof-of-work`, with no line naming the day (0 of 4 accepted against 7 of 7 for the control) | The day length (or the day index) in the template beside `pow_epoch`, the miner takes it from there and ignores the environment; the node's PoW rejection names the engine's day and the header's day (1.5 h) | miner lead, consensus engineer | before testnet | no |
|
||||||
|
|
||||||
|
### 2.7 Close round 1 (5 October 2026, night)
|
||||||
|
|
||||||
|
Appended by the public-text closer (worktree `igneum-wt-ledger-text`, branch `ledger-text`, group A of `docs/plans/ledger-close-plan.md`). Every sentence named here was grepped in the public text before the ledger row moved. Rows name the earlier row they touch; nothing above is rewritten.
|
||||||
|
|
||||||
|
| Row touched | Ledger | What changed (5 October 2026, night) | Who next | When |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 17, 48, 53, 56, 15 | M2, M4, M7, M9, M10 | Verified and moved to "Conceded, stated" (M10: "Answered with evidence, stated"). M10's overclaim 14 was still live at 18:20 UTC and is applied now: "bound by random memory access", 95 GB/s of useful loads against 1,638 GB/s sequential, RTX 5090 | none | done |
|
||||||
|
| 19 | M13 | "a fifth" confirmed in For miners, Hardware (26.7 against 123 MH/s, 4 October 2026); moved | none | done |
|
||||||
|
| 10, 11 | F5, F10 | Verified and moved | none | done |
|
||||||
|
| 57, 21, 22, 24, 25, 26 | P1, P3, P4, P6, P7, P10 | Verified and moved. P3: overclaim 25 applied ("Wrapped for light clients ... phase two measurement") with the certificate-half numbers of bench-log round 6 (139 to 155 ms cold, 58 to 68 ms warm, laptop core, no phone); the wrapper stays Open for phase two | measurement (the wrapper) | before public repo |
|
||||||
|
| 8, 27 | E5, E6 | E5 verified and moved. E6: one sentence in Security after the subsidy, "The schedule is a bet, not a measurement", Kaspa's reduction marked approximate; moved | none | done |
|
||||||
|
| 28, 29, 9, 30, 32 | G1, G2, G3, G4, G6 | Verified and moved. G3: the entry's first Status line is now the Decided line (no team page; "The team is pseudonymous and there is no team page" in the litepaper), the older line kept prefixed "Was:" | none | done |
|
||||||
|
| 14, 12, 13, 34, 49 | C2, C3, C5, C6, C8, C10, C11 | Verified and moved | none | done |
|
||||||
|
| (C13, M18, L8) | C13, M18, L8 | C13: "they say nothing about the price of a chip with the 256 MB cache on its die" in What Igneum does not claim. M18 verified. L8: "whether it is issued is Circle's decision" in Questions builders ask; Canto and Blast absent. All moved | none | done |
|
||||||
|
| 6 | L2 (text half) | "so the people who show up early get the most" removed from Supply; schedule fact only. Counsel half unchanged, decision owner Josh | Josh, counsel | before public repo |
|
||||||
|
| 110 | L9 | "Devnet: coins have no value and the chain may be reset." beside every download control on index, miner and wallet; the homepage tiles read "of emission to miners and provers; the protocol carries no fee" and "0% anyone else in the protocol". Not done: the same line on `/live` (outside this round's files) | Claude (live page) | now |
|
||||||
|
| 111 | M29 | The litepaper's app paragraph rewritten to Ember 0.3.9 from the shipped UI; earnings, currency, game pause and the hardware wallet moved to a roadmap sentence; nothing from an unmerged branch | none | done |
|
||||||
|
| 109 | E16 (text half) | The 20% row and the homepage bar now say the coinbase's 20% output is burned under `igneum-proving-pool-v0` and provers are paid from the execution-state escrow credited with the same 20%; the single coinbase payout stays Open (code half, group D) | consensus engineer (payout) | before testnet |
|
||||||
|
| 115 | E17 (text half) | "a million 100,000-gas calls a day" with the base unit marked Open; the fleet-size dependence sentence (4.9x today, 930x at 10,000 cards, approximate). Draw lines still owed | measurement (draw lines) | when convenient |
|
||||||
|
| 94 | E13 | The six-row route table in the litepaper Economics ("Every payment route") and in the customer brief; source `docs/design/payment-routes.md`; moved to "Conceded, stated". That file's section 4 states are of 3 October and now lag the litepaper | Claude (refresh payment-routes.md section 4) | when convenient |
|
||||||
|
| 1, 2, 39, 3, 4 | X1, X2, X3, X7, X8 | Verified and moved. X3: "Live rows arrive with the public testnet, August 2027" under the proofs feed (overclaim 61); the old sentence was already gone. X7 closes on the ledger's submission line (hello@igneum.network, spec issues) | none | done |
|
||||||
|
| 36, 5 | X9, X10, D2 | Verified and moved; D2's sentence now reads "100,000-gas calls" (E17) | none | done |
|
||||||
|
|
||||||
## 4. What the 3 October decisions close or change
|
## 4. What the 3 October decisions close or change
|
||||||
|
|
||||||
Closed:
|
Closed:
|
||||||
|
|
@ -362,3 +385,4 @@ Nothing below is optional. The history, not just the working tree, carries the n
|
||||||
9. Put the GitHub links back on HP and the /bench page (row 1) on the day the repository opens, at the public testnet.
|
9. Put the GitHub links back on HP and the /bench page (row 1) on the day the repository opens, at the public testnet.
|
||||||
|
|
||||||
What is already clean: `docs/bench-log.md` and the /bench page name machines, not people (commit 1769eda); the live site returns 404 for everything under `docs/` and 307 for `/ledger`; `site/.env.local`, `site/.vercel/` and `vendor/` are ignored; no tracked file carries a home-directory path; no connection string or token appears anywhere in the history.
|
What is already clean: `docs/bench-log.md` and the /bench page name machines, not people (commit 1769eda); the live site returns 404 for everything under `docs/` and 307 for `/ledger`; `site/.env.local`, `site/.vercel/` and `vendor/` are ignored; no tracked file carries a home-directory path; no connection string or token appears anywhere in the history.
|
||||||
|
| 128 | P23 | The EVM pool has `on_chain_block` and no reorg hook, so a transaction unwound by a selected-chain reorg leaves the node until its sender resends (found by the P17 conformance run, 6 October 2026) | A reorg hook: unwound transactions handed back to the pool as pending with the usual checks; unit test; the conformance driver's `reorged out` case ends in `executed` without a resend (2 h) | execution engineer (round 3 `ledger-rebase` carries it) | before a public RPC | no |
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
94
docs/plans/build-server.md
Normal file
94
docs/plans/build-server.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# Build server: igneum-build-1 (plan, benchmarks, rules)
|
||||||
|
|
||||||
|
6 October 2026. Josh ordered a Hetzner dedicated server at 18:2x UK: AX162-1-LTD, EPYC 9454P (48 cores, 96 threads),
|
||||||
|
128 GB, 2x 3.84 TB NVMe, Falkenstein (FSN1-DC24, Hetzner #3088308), 188.40.146.49, IPv6 2a01:4f8:2240:205a::/64.
|
||||||
|
Purpose: this Mac is the compile queue for ten agents (8-minute rebuilds under contention) and the Windows cross-builds;
|
||||||
|
the box takes over Rust builds, cross-builds and a shared compile cache, and later a CI runner and the Devnet 2 seed.
|
||||||
|
|
||||||
|
## 1. What is in place (all on branch `build-server`)
|
||||||
|
|
||||||
|
| Piece | File | State 6 Oct 2026 |
|
||||||
|
|---|---|---|
|
||||||
|
| Install + provision | `infra/build-server/provision.sh` | ran: installimage 17:16 to 17:20 UTC (Ubuntu 24.04, RAID 1, no swap), provision 17:24 to 17:27 UTC, second run 2 s with zero changes (idempotent) |
|
||||||
|
| Mac side | `infra/build-server/run-from-mac.sh <ip>` | ran: `~/.config/igneum/build-server` = `build@188.40.146.49`, `build` remotes added, 124 igneum branches and 48 fork branches pushed to the bare mirrors |
|
||||||
|
| Shared library | `infra/build-server/lib.sh` | host line, ssh options, mirror push, remote checkout, overlay, remote slot runner |
|
||||||
|
| Remote runner | `infra/build-server/remote-run.sh` | runs on the box: slot, sccache, RESULT line, one JSON line per run in `/srv/builds/_log/builds.jsonl` (the worker dashboard's feed, fields as main asked on 6 Oct) |
|
||||||
|
| Remote cargo | `tools/build-remote.sh` | any crate dir, any worktree: `tools/build-remote.sh [-- cargo args]`; `IGNEUM_AGENT=<name>` tags the slot label and the log |
|
||||||
|
| Remote Windows cross | `tools/cross-remote.sh` | fork worktree or app/igneum-app: `tools/cross-remote.sh [--compare <dir of the Mac's exes>]` |
|
||||||
|
|
||||||
|
ssh line: `ssh -i ~/.ssh/igneum_ed25519 build@188.40.146.49` (root works with the same key; passwords are off).
|
||||||
|
|
||||||
|
Box facts (provision summary): rustc 1.99.0 (the Mac's version; NO rust-toolchain file exists in the repo or the fork, the
|
||||||
|
pin is `RUST_TOOLCHAIN` in provision.sh), targets x86_64-unknown-linux-gnu and x86_64-pc-windows-gnu, sccache 0.18.0 with a
|
||||||
|
100 GB disk cache at /srv/sccache, mingw-w64 GCC 13 posix threads (the PC job's recipe), clang and lld 18.1.3, Node v22.23.3,
|
||||||
|
git 2.43.0, tmux 3.4, ufw 22 + 26611 + 26811 (the devnet and testnet seed p2p ports; RPC stays on loopback as on every seed),
|
||||||
|
md0 /boot and md1 / as RAID 1, 3.3 TB free, swap none, 1 build slot in `/srv/builds/_locks/slots`.
|
||||||
|
|
||||||
|
## 2. How a build travels
|
||||||
|
|
||||||
|
1. `bs_context` (lib.sh) reads the crate: a fork worktree under `vendor/` (kind node, mirror `/srv/igneum-node.git`) or a crate
|
||||||
|
of the igneum repo (kind repo, mirror `/srv/igneum.git`). `cargo metadata` lists every path dependency.
|
||||||
|
2. HEAD is pushed to the mirror; the box clones or fetches it at `/srv/builds/<worktree>/<same relative path>` and checks out
|
||||||
|
that commit. A real `.git` is needed: the fork's `kaspa-build-info` runs `git rev-parse HEAD` at build time and every release
|
||||||
|
plan checks the commit in the binary's strings.
|
||||||
|
3. Uncommitted changes go by `rsync --checksum` without `-t` (files that changed are written with the box's clock) and the
|
||||||
|
written files are re-stamped with `touch` (the copied-sources rule, `tools/ci/copied-sources-check.sh` passes).
|
||||||
|
4. The cargo command runs in the crate dir under one of the box's slot files (`flock` on `/srv/builds/_locks/build-<k>`,
|
||||||
|
never the Mac's `~/.config/igneum/build-slots`), with sccache and `-j 90`, logging wall time, compile count and cache hits.
|
||||||
|
5. Artefacts come back into `<crate>/target-remote/...` with size and sha256. NEVER into `target/`: the box's native
|
||||||
|
binaries are x86_64 Linux (glibc 2.39) and do not run on the Mac.
|
||||||
|
|
||||||
|
`/srv/builds/<worktree>` mirrors the Mac's igneum worktree ROOT because the fork's path dependency is
|
||||||
|
`../../../../igneum-pow` (vendor/igneum-node/consensus/pow/Cargo.toml).
|
||||||
|
|
||||||
|
## 3. Benchmarks
|
||||||
|
|
||||||
|
The Mac numbers are from the logs (docs/bench-log.md, docs/plans/release-0.3.10.md, release-0.3.11.md); no fresh Mac run
|
||||||
|
was made, because a Mac build would take a slot from the agents and the logs already hold several readings per case.
|
||||||
|
Mac = Apple M5 Max (18 cores), builds at `nice -n 19` with 4 to 6 jobs under the build lock, usually loaded by other agents.
|
||||||
|
Box = igneum-build-1, `-j 90`, nothing else running.
|
||||||
|
|
||||||
|
| Case | Mac (logged) | Box | Box run |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Clean node build (`cargo build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow`, every crate) | 12 min 36 s (0.3.10, new target dir, -j 4); 17 min 53 s at load 140 and 8 min 58 s second time (fud ledger, -j 4); rusty-kaspa kaspad alone 2 min 36 s on an idle Mac | **1 min 27 s** cargo wall (1 min 34 s end to end from the Mac: push and sync 1 s, build, fetch of both binaries) | cold: 518 crates downloaded inside that time, sccache 0 hits / 993 misses, 563 crates compiled; igneumd 48,220,896 B, igneum-miner 9,107,232 B, ELF x86-64 PIE; `igneumd --version` runs on the box; 17:30:58 to 17:32:32 UTC |
|
||||||
|
| Incremental rebuild (one file changed, same target dir) | 2 min 08 s (0.3.10, 17:49Z); 3 min 19 s (0.3.11); 5 min 06 s (txgossip); 15 min 18 s at load 110 to 134 (M31); 35 s to 4 min (execution layer); "8 min under contention" (main, 6 Oct) | **7 s** cargo wall (10 s end to end: sync 1 s, build 6.97 s, fetch) | one line appended to kaspad/src/main.rs in the fork worktree, uncommitted, carried by the overlay (1 file written and re-stamped); 1 crate compiled, igneumd relinked; box idle (load 12 from the clean build a minute earlier); 17:33:11 to 17:33:21 UTC |
|
||||||
|
| Windows cross-build (`--target x86_64-pc-windows-gnu`, igneumd.exe + igneum-miner.exe) | 8 min 25 s clean (-j 6, 3 Oct); 4 min 49 s and 4 min 53 s with warm dependencies (4 Oct); 12 min 28 s (finality-fixes, 4 Oct) | **1 min 44 s** cargo wall (1 min 48 s end to end) | cold: 995 compiles, sccache 0 hits; igneumd.exe 49,434,624 B, igneum-miner.exe 10,201,600 B; mingw GCC 13 posix, libclang 18; igneumd.exe imports libstdc++-6.dll (this fork head predates the housekeeping commit that made the C++ runtime static), so cross-remote.sh now fetches the three GCC 13 runtime DLLs beside the exes as the PC job does; 17:33:58 to 17:35:46 UTC. Second run on the same target dir, no source change: 8 s |
|
||||||
|
|
||||||
|
Also logged for context: the Mac's Linux cross-build with zig (`infra/cross/build-linux.sh`) took 30 min 56 s cold and
|
||||||
|
3 min 20 s incremental; PC 1 built Linux + Windows node and app and ran both test suites in 7 min 38 s cold, 5 min 09 s warm
|
||||||
|
(CLAUDE.md, 5 Oct).
|
||||||
|
|
||||||
|
### What the numbers mean and what follows
|
||||||
|
|
||||||
|
| Number | Means | Done or proposed |
|
||||||
|
|---|---|---|
|
||||||
|
| Clean build 1 min 27 s against 12 to 18 min on the loaded Mac (8 to 12x) | a new worktree costs an agent a minute and a half, not a slot for a quarter of an hour; the first build of every one of the 123 worktree dirs on the box is this cold case, later ones are the 7 s case | R1 proposed; sccache was cold (0 hits of 1,982 requests) because every run so far was the first of its kind; the cache fills as agents build the same crate versions from different worktrees, so the second worktree's clean build will be mostly hits (measure when it happens, write the number here) |
|
||||||
|
| Incremental 7 s against 2 to 15 min on the Mac (the "8 min under contention") | an edit-build loop of seconds for Linux targets; end to end 10 s because sync is 1 s and the two binaries (57 MB) come back in 2 s | R1; the slot count stays 1 until two agents collide, then 2 with `-j 48` each (`SLOTS=2 run-from-mac.sh` and `--jobs 48`) |
|
||||||
|
| Windows cross 1 min 44 s against 4 min 49 s to 12 min 28 s on the Mac (3 to 7x), 8 s incremental | a Windows exe per commit is cheap enough to build on every push; the PC `build` job (7 min 38 s cold, 5 min 09 s warm for Linux + Windows + tests) stays the second source | R2 proposed |
|
||||||
|
| Mac arm64 binaries: not built here | agents who run nodes on the Mac (local devnets, the DMG) still take Mac slots; the box cannot remove that contention | R3; the real relief is to move test networks to the fleet or to a Devnet 2 seed on this box (its unit, ports and ufw are ready) |
|
||||||
|
| The commit hash is EMPTY in every Mac worktree build and was empty in the first box builds | `kaspa-build-info` (build-info/build.rs) needs `.git` to be a directory AND HEAD to be a symbolic ref to a loose branch file; a worktree's `.git` is a file and a detached HEAD is not a ref; and once it has found nothing it emits no `rerun-if-changed`, so cargo never runs it again in that target dir (release-0.3.11: `cargo clean -p kaspa-build-info`) | fixed on the box: the remote checkout is `git checkout -B <branch> <sha>` and build-remote.sh runs `cargo clean --release -p kaspa-build-info` whenever the commit differs from the last one built in that target dir (`.build-remote-sha-<target dir>`); verified 17:40 UTC: 2 string hits for 3bfe346f, binary +1,024 B. The release plans' "commit in its strings" checks were passing against builds from the fork's MAIN checkout (a real .git directory on a branch), not from worktrees. DONE (main's decision): the PC job writes a minimal `node/.git` (HEAD -> refs/heads/build holding the manifest's new `commit_full`, written by push-build-inputs.sh) at extract and cleans kaspa-build-info on a new commit (jobbuild.rs, 4 unit tests pass on the box); the Mac's cross-build.sh refuses a worktree and cleans on a new commit; the gate `tools/ci/commit-string-check.sh` runs on every igneumd the three scripts produce (self-test in ci.yml; shown firing on the Mac's worktree-built igneumd and passing on the box's). Only kaspad depends on kaspa-build-info, so igneum-miner is out of the gate's scope |
|
||||||
|
| igneumd.exe hash changed build to build with no source change (c424aae0 then bfbff015) while the Linux igneumd stayed byte-identical across three builds | the mingw linker wrote a timestamp into the PE header; the Linux ELF has none | DONE (main's decision): `-C link-arg=-Wl,--no-insert-timestamp` in cross-remote.sh, the Mac's cross-build.sh and the PC job (jobbuild.rs); verified 17:48 and 17:49 UTC: two builds, igneumd.exe c38b7570... and igneum-miner.exe c3a0fc2b... identical both times |
|
||||||
|
| Second worktree's clean build (vendor/igneum-node-v4 from the main checkout, cold target dir, warm sccache): 1 min 18 s, 604 hits of 993 compiles (61 percent), 389 misses | the first build of each of the 123 worktree dirs costs 1 min 18 s to 1 min 27 s, not the Mac's 12 to 18 min; sccache saves 9 s of the 87 because the clean build is dominated by the fork's own 74 crates and the C++ (rocksdb) objects, which differ per tree or do not cache; the cache is 1.0 GB after four builds, capped at 100 GB | nothing; the hit rate is in every RESULT line and every JSONL line |
|
||||||
|
| Disk 3.3 TB free, RAM 125 GB, load peaked at 24 during the Windows build with 96 threads | room for 2 slots and the Devnet 2 seed without contention | nothing now |
|
||||||
|
|
||||||
|
## 4. Rules (ADOPTED by main on 6 October 2026, written into CLAUDE.md "Running agents on this Mac"; R2 narrowed: the PCs keep only GPU and Windows-runtime jobs from now, not two releases)
|
||||||
|
|
||||||
|
| Rule | Text |
|
||||||
|
|---|---|
|
||||||
|
| R1 | Every agent's `cargo build`, `cargo test`, `cargo check` and `cargo clippy` for Linux goes through `tools/build-remote.sh` from the crate directory. The box takes one remote slot per build; nobody runs cargo over ssh by hand. |
|
||||||
|
| R2 | Windows exes come from `tools/cross-remote.sh` (fork worktree: igneumd.exe, igneum-miner.exe; app/igneum-app: igneum-app.exe and the two tools). The PC `build` job stays as the second source until two releases have shipped from the box. |
|
||||||
|
| R3 | The Mac keeps what only it can do: aarch64-apple-darwin binaries (the DMG, nodes that agents run locally), tests that need Metal (proto-metal, the Metal worker), and measurements. Those still use `tools/lock/with-lock.sh` and the Mac's build slots. |
|
||||||
|
| R4 | A worktree builds once on the box per commit plus overlay; the next build is incremental in `/srv/builds/<worktree>/.../target`. Nobody deletes another worktree's target dir on the box. |
|
||||||
|
| R5 | `RUST_TOOLCHAIN` in provision.sh is bumped in the same commit as the Mac's `rustup update`; build-remote.sh refuses a mismatch. Add a `rust-toolchain.toml` to the fork and the repo (none exists today) so both sides pin from one file. |
|
||||||
|
| R6 | The box is never a node host for the live devnet and never holds a secret (no `~/.config/igneum` there). The Devnet 2 seed on it runs under its own unit with `--devnet --devnet-suffix=<n>` on 26611 (ufw already open) when that work starts. |
|
||||||
|
| R7 | CLAUDE.md line "nothing is built on a server" and "Windows builds go to the GitHub runner, Linux binaries come from infra/cross/build-linux.sh" are rewritten when R1 and R2 are adopted; until then the box is the measured option, not the rule. |
|
||||||
|
|
||||||
|
## 5. What the box does not do yet
|
||||||
|
|
||||||
|
| Gap | Why it matters | Next step |
|
||||||
|
|---|---|---|
|
||||||
|
| No zig / cargo-zigbuild | the devnet seed (Debian 12, glibc 2.36) takes the Mac's zig build; a native box build links glibc 2.39, which Debian 13 seeds accept and HiveOS (Ubuntu 18/20 base) does not | install zig 0.17 + cargo-zigbuild in provision.sh, add `--target x86_64-unknown-linux-gnu.2.36` mode to build-remote.sh |
|
||||||
|
| No macOS target | agents who run nodes on the Mac still build there | out of scope (needs the macOS SDK on Linux); the fleet or the box's own Devnet 2 seed takes the test-network runs instead |
|
||||||
|
| No CI runner | GitHub `ci.yml` and `windows.yml` run on GitHub's machines | install a self-hosted runner as user build once R1 is in |
|
||||||
|
| Byte identity with the Mac's exes | different C/C++ toolchain (Homebrew mingw vs Ubuntu GCC 13) and embedded source paths | not a goal; the box is identical with itself build to build, cross-remote.sh reports sha256 and the DLL list per exe |
|
||||||
|
| Robot API | `~/.config/igneum/hetzner-token` is the Cloud token (hcloud); the dedicated box lives in Robot, a separate credential | main sets the Robot server name in the UI; a webservice user goes to `~/.config/igneum/robot-credentials` when needed |
|
||||||
|
|
@ -4,7 +4,7 @@ Josh, 5 October 2026 (night): "not an information overload". Four levels; the la
|
||||||
|
|
||||||
## Level 1: one sentence (site hero, litepaper abstract)
|
## Level 1: one sentence (site hero, litepaper abstract)
|
||||||
|
|
||||||
Built for graphics cards. A custom chip gains under 2x, and the model is public. (The bounty is named only once it is escrowed: docs/plans/funding.md rule 3; D11 for Josh.)
|
Built for graphics cards. A custom chip gains under 2x, and the model is public. (Josh's decision of 6 October 2026, 17:35 UTC, ledger M1: no device bounty; the claim is backed by the paid independent cryptanalysis (CA 3.0 item 3, four paid reviews) and the public benchmark with M22's metrics; an optional USD 50,000 cryptanalysis prize may follow later, escrowed before it is named.)
|
||||||
|
|
||||||
## Level 2: one site card, one short litepaper section
|
## Level 2: one site card, one short litepaper section
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ Three ideas, no layer names, no widths, no SRAM.
|
||||||
|
|
||||||
**Miners hold the switch.** Spare defences are written into the rules, switched off. A 90% miner signal turns one on. No fork.
|
**Miners hold the switch.** Spare defences are written into the rules, switched off. A 90% miner signal turns one on. No fork.
|
||||||
|
|
||||||
A custom chip gains under 2x. Model published; a bounty follows the external review. [link: the numbers page]
|
A custom chip gains under 2x. Model published; tested by paid independent cryptanalysis and the public benchmark. [link: the numbers page]
|
||||||
|
|
||||||
Litepaper only, a fourth paragraph: No hash has stayed free of chips forever. Igneum does not claim to. It claims the gain is small, the response takes a week, and both are measured.
|
Litepaper only, a fourth paragraph: No hash has stayed free of chips forever. Igneum does not claim to. It claims the gain is small, the response takes a week, and both are measured.
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ Site card placement: the Mine section of `site/index.html` beside "no chip can b
|
||||||
|
|
||||||
Headline of the chip model (5 October 2026, night): the strongest chip holds the whole 256 MiB cache on-die (about 128 mm^2 and $46 of silicon at N5 by shipped cache-die density, approximate) and computes dataset items on the fly; its gain over the RTX 5090 is 2.4x as the parameters stand, and no write-scratch share within an 8 GB card's budget changes that. The lever that does is the dataset item's mixer cost (x4: 1.8x with a 3x fixed-function factor, verifier 1.6 to 4.8 ms per warp). Decided 5 October 2026 (delegated): the mixer x4 and the cache growth rule enter class v3, so the headline row is the on-die-cache chip against v3 with everything combined. [owed: the combined row from docs/analysis/chip-model-v3.md; if it reads 1.8x, the claim is "under 2x" with the margin stated as thin, and the next levers are named: the mixer x8 and the hot table.]
|
Headline of the chip model (5 October 2026, night): the strongest chip holds the whole 256 MiB cache on-die (about 128 mm^2 and $46 of silicon at N5 by shipped cache-die density, approximate) and computes dataset items on the fly; its gain over the RTX 5090 is 2.4x as the parameters stand, and no write-scratch share within an 8 GB card's budget changes that. The lever that does is the dataset item's mixer cost (x4: 1.8x with a 3x fixed-function factor, verifier 1.6 to 4.8 ms per warp). Decided 5 October 2026 (delegated): the mixer x4 and the cache growth rule enter class v3, so the headline row is the on-die-cache chip against v3 with everything combined. [owed: the combined row from docs/analysis/chip-model-v3.md; if it reads 1.8x, the claim is "under 2x" with the margin stated as thin, and the next levers are named: the mixer x8 and the hot table.]
|
||||||
|
|
||||||
Per card, the bench table: the v2 class and the v3 class, hash rate, bytes per hash, the latency-bound share (rate over the card's random-read ceiling per load), the CPU verifier per warp, with machine, date and command. The chip model before and after Counter ASIC 2.0 (the m16 model's gain arithmetic at the v2 class and at the v3 class, with the SRAM a mirror needs, cited or approximate as the analysis says). The bounty terms (spec O-1.17: the leaderboard by card model, the standing bounty for any chip design beating a GPU by more than 2x, January 2027). Here the layers are named next to their numbers: read width, per-program mix, scratch, era layout, working set, hot table, cache schedule, the reserved integer-matrix family.
|
Per card, the bench table: the v2 class and the v3 class, hash rate, bytes per hash, the latency-bound share (rate over the card's random-read ceiling per load), the CPU verifier per warp, with machine, date and command. The chip model before and after Counter ASIC 2.0 (the m16 model's gain arithmetic at the v2 class and at the v3 class, with the SRAM a mirror needs, cited or approximate as the analysis says). The benchmark terms (spec O-1.17: the leaderboard by card model and the paid independent cryptanalysis's published findings, January 2027; no device bounty by Josh's decision of 6 October 2026). Here the layers are named next to their numbers: read width, per-program mix, scratch, era layout, working set, hot table, cache schedule, the reserved integer-matrix family.
|
||||||
|
|
||||||
| Card | v2 MH/s | v3 MH/s (era packs, six eras) | Bytes per hash | Latency-bound share | Verifier ms per warp (v2 / v3, one loaded M5 Max core) | Daily 1 GiB build (v2 / v3) |
|
| Card | v2 MH/s | v3 MH/s (era packs, six eras) | Bytes per hash | Latency-bound share | Verifier ms per warp (v2 / v3, one loaded M5 Max core) | Daily 1 GiB build (v2 / v3) |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,18 @@ Either replaces "under 2x" on the site and in the litepaper once Josh chooses; u
|
||||||
8. PC 1: the 9070 XT rows for items 2, 6 and 8, the detector's band and the FPGA ranking's AMD line are all owed on PC 1's release.
|
8. PC 1: the 9070 XT rows for items 2, 6 and 8, the detector's band and the FPGA ranking's AMD line are all owed on PC 1's release.
|
||||||
9. The job tooling: one card-key form everywhere (the settings.json key carries the device index) and a CI check that fails a job script posting a key without it (the class fix for item 2's loaded-card run).
|
9. The job tooling: one card-key form everywhere (the settings.json key carries the device index) and a CI check that fails a job script posting a key without it (the class fix for item 2's loaded-card run).
|
||||||
|
|
||||||
|
### Main's decisions on section 6 (18:0x UTC, 6 October 2026)
|
||||||
|
|
||||||
|
| # | Decision | Record |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | The public claim: the sentence deployed on the hero at 16:21Z ("In our public model the strongest chip reaches 5x to 9x per joule against an RTX 5090 today, about 2x once the lever now in its gates ships") plus the litepaper's long form; "under 2x" nowhere | `docs/evidence.md` row 17 (the claim, its sources, draft (a) of this file chosen); the review finding behind it: `docs/review/round-4-reddit-2026-10-06.md` item 3 (Serious), flipped by the measured fact |
|
||||||
|
| 2 | The cut: yes, after P1's rehearsal passes and 0.3.14 has run a clean day; the fleet agent owns P1's run | section 5a |
|
||||||
|
| 3 | R0 (the per-day derivation, dr368 the safe draw) and the reserve order R1 perm to R8 mm8: GO as proposed, with the once-a-day item module recorded as a requirement on every vendor (NVRTC +1.1 s, AMD OpenCL +2.0 s per pack) | `counter-asic-3-derivation.md`, `counter-asic-3-reserve.md` |
|
||||||
|
| 4 | The cryptanalysis spend, USD 80,000 to 160,000: AWAITING JOSH (his money decision; put to him with the brief on a quieter day, not tonight) | funding.md |
|
||||||
|
| 5 | Push: ca3-coord merged into master through CI (nothing activates: v4 sits behind the override; the detector and vendor-share hooks go live on the observer's next restart) | the merge commit and the CI run, below |
|
||||||
|
| 6 | The AMD watts: on the runner's `--cards-off` mechanism, next cut | section 6a |
|
||||||
|
| 7 | The 2019-class core (O-1.14): a Windows igneum-pow job on the US laptop when it next appears on the relay | owed list |
|
||||||
|
|
||||||
## 6a. A rule from the run (main, 17:5x UTC, from the watts job's failure)
|
## 6a. A rule from the run (main, 17:5x UTC, from the watts job's failure)
|
||||||
|
|
||||||
A script never switches the installed app's cards: a POST of enabled false to /api/cards from a script is the same class as /api/pause from a script (a script that dies leaves the box degraded, unattended). A job that needs a card alone asks the runner for it: the runner's `--stop-miners` grows a `--cards-off <keys>` that posts the exact settings entries off before the script and restores them with their own flags and identities on ANY exit, as it restarts miners; `tools/ci/playbook-quit-check` gains the api/cards enabled-false pattern so the shape fails CI. Both land tonight (the PC 1 worker, branch ca3-pc1-amd); the four PC 1 scripts that carry the shape move to the flag; the AMD watts row re-runs only on the runner mechanism, after the app that carries it is installed (owed to the next cut).
|
A script never switches the installed app's cards: a POST of enabled false to /api/cards from a script is the same class as /api/pause from a script (a script that dies leaves the box degraded, unattended). A job that needs a card alone asks the runner for it: the runner's `--stop-miners` grows a `--cards-off <keys>` that posts the exact settings entries off before the script and restores them with their own flags and identities on ANY exit, as it restarts miners; `tools/ci/playbook-quit-check` gains the api/cards enabled-false pattern so the shape fails CI. Both land tonight (the PC 1 worker, branch ca3-pc1-amd); the four PC 1 scripts that carry the shape move to the flag; the AMD watts row re-runs only on the runner mechanism, after the app that carries it is installed (owed to the next cut).
|
||||||
|
|
@ -278,3 +290,7 @@ A script never switches the installed app's cards: a POST of enabled false to /a
|
||||||
| 6 | mm8 as a chain on Apple (Metal 4 matmul2d; the Mac's Swift toolchain has no tensor API); mm8's exactness on AMD (the gfx12 WMMA fragment layout; an empirical layout probe would settle it in one job); the 5 percent rule per family with the family live (argued only); the RDNA ISA guides unread (mnemonics from LLVM's tables). The 9070 XT step costs themselves are CLOSED (run e) | |
|
| 6 | mm8 as a chain on Apple (Metal 4 matmul2d; the Mac's Swift toolchain has no tensor API); mm8's exactness on AMD (the gfx12 WMMA fragment layout; an empirical layout probe would settle it in one job); the 5 percent rule per family with the family live (argued only); the RDNA ISA guides unread (mnemonics from LLVM's tables). The 9070 XT step costs themselves are CLOSED (run e) | |
|
||||||
| 1 | the DRAM energy figures are streaming figures applied to random 32-byte reads; the GDDR7 burst and HBM3 tFAW are behind the JEDEC paywall; no chip has been built or torn down | |
|
| 1 | the DRAM energy figures are streaming figures applied to random 32-byte reads; the GDDR7 burst and HBM3 tFAW are behind the JEDEC paywall; no chip has been built or torn down | |
|
||||||
| all | every AMD RDNA 4 number in this run | PC 1 is Josh's desk today. 16:0x UTC: the RX 9070 XT is back on PC 1 (amd:gfx1201, 16,304 MiB) beside the 5090 and an RTX 4070; main has asked for the PC 1 jobs to be PREPARED, not published: (1) G1 AMD plus item 8's ladder on the 9070 XT (about 15 min, the card alone), (2) item 6's family step costs on AMD (about 3 min), (3) item 2's dr736 build and compile on AMD (about 3 min), (4) optional: the 4070 ladder (about 10 min); branch ca3-pc1-amd (1f33cc6, e054ed7, merged): the four scripts pass every CI check, the kit zip sha256 a70fce5b... verified, the AMD watts readback is igneum-gpu-telemetry.exe (ADLX board watts); the commands and the row map in tools/ca3-pc1-amd/README.md; published one at a time on "go PC 1 AMD" after the 0.3.13 update and the Ember table run, about 33 min of PC 1 in all |
|
| all | every AMD RDNA 4 number in this run | PC 1 is Josh's desk today. 16:0x UTC: the RX 9070 XT is back on PC 1 (amd:gfx1201, 16,304 MiB) beside the 5090 and an RTX 4070; main has asked for the PC 1 jobs to be PREPARED, not published: (1) G1 AMD plus item 8's ladder on the 9070 XT (about 15 min, the card alone), (2) item 6's family step costs on AMD (about 3 min), (3) item 2's dr736 build and compile on AMD (about 3 min), (4) optional: the 4070 ladder (about 10 min); branch ca3-pc1-amd (1f33cc6, e054ed7, merged): the four scripts pass every CI check, the kit zip sha256 a70fce5b... verified, the AMD watts readback is igneum-gpu-telemetry.exe (ADLX board watts); the commands and the row map in tools/ca3-pc1-amd/README.md; published one at a time on "go PC 1 AMD" after the 0.3.13 update and the Ember table run, about 33 min of PC 1 in all |
|
||||||
|
|
||||||
|
## 8. Close
|
||||||
|
|
||||||
|
Closed 6 October 2026, 18:1x UTC. The lanes: item 1 (ca3-analysis), item 2 (ca3-derive), items 4 and 5 (ca3-detector), item 3 (ca3-crypto-brief), items 6 and 7 (ca3-reserve), item 8 (ca3-shadow), the PC 1 AMD jobs (ca3-pc1-amd), the hash gates (ca3-v4-hash) and the node gates with both preconditions (ca3-v4-node and the fork): thank you, every one of you, for the numbers and for the faults you found in your own work and in mine before they reached a cut.
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ The third set of chip-resistance layers, from the ASIC-history agent's audit of
|
||||||
| 1 | The partial-store chip and the time-memory curve: price a chip that holds a fraction f of the dataset (f = 0.25, 0.5, 1) on HBM3 or GDDR7 with 4-byte access granularity and recomputes the rest, scored in energy per hash | the only chip class that beat a memory-bound GPU hash (Ethash: 2.1x Linzhi 2020, 2.9x E9 2022, 4.8x per joule Jasminer X4 2021) did it with custom memory controllers and on-package memory, not an on-die dataset; chip-model-v3.md prices only f = 0 | `docs/analysis/chip-model-v3.md`, O-1.6, MEMHARD.md section 3 item 2 (the curve never drawn) | analysis, before the public testnet's vectors freeze; the first item |
|
| 1 | The partial-store chip and the time-memory curve: price a chip that holds a fraction f of the dataset (f = 0.25, 0.5, 1) on HBM3 or GDDR7 with 4-byte access granularity and recomputes the rest, scored in energy per hash | the only chip class that beat a memory-bound GPU hash (Ethash: 2.1x Linzhi 2020, 2.9x E9 2022, 4.8x per joule Jasminer X4 2021) did it with custom memory controllers and on-package memory, not an on-die dataset; chip-model-v3.md prices only f = 0 | `docs/analysis/chip-model-v3.md`, O-1.6, MEMHARD.md section 3 item 2 (the curve never drawn) | analysis, before the public testnet's vectors freeze; the first item |
|
||||||
| 2 | A random item-derivation program per day in place of the fixed-shape mixer (RandomX's SuperscalarHash idea) | the fixed mixer shape IS the 3x fixed-function allowance that turns x8's 0.31x into 0.92x; removing it is worth more than x16 (0.46x with the factor) | a reserve family now; genesis if the per-day compiled derivation verifies under the 10 ms gate (unmeasured); risks: cryptanalysis of random ARX, weak draws, bit-exact compilation on three vendors; the daily build about doubles (23 to 77 ms, approximate) | design and the verifier measurement |
|
| 2 | A random item-derivation program per day in place of the fixed-shape mixer (RandomX's SuperscalarHash idea) | the fixed mixer shape IS the 3x fixed-function allowance that turns x8's 0.31x into 0.92x; removing it is worth more than x16 (0.46x with the factor) | a reserve family now; genesis if the per-day compiled derivation verifies under the 10 ms gate (unmeasured); risks: cryptanalysis of random ARX, weak draws, bit-exact compilation on three vendors; the daily build about doubles (23 to 77 ms, approximate) | design and the verifier measurement |
|
||||||
| 3 | External cryptanalysis of the mixer M_r, the chained cache and the acceptance rule, with the x8 shape as the target | MTP fell from 2 GB to under 1 MB before launch (Dinur and Nadler 2017), Catena's proofs were flawed, Argon2i's parameters were attackable; RandomX bought four audits for about $141,000 before launch; x8 multiplies the mixer's weight in the chip model, so a structural shortcut is worth 8x more | ledger M7, raised to a genesis gate | commission before genesis |
|
| 3 | External cryptanalysis of the mixer M_r, the chained cache and the acceptance rule, with the x8 shape as the target | MTP fell from 2 GB to under 1 MB before launch (Dinur and Nadler 2017), Catena's proofs were flawed, Argon2i's parameters were attackable; RandomX bought four audits for about $141,000 before launch; x8 multiplies the mixer's weight in the chip model, so a structural shortcut is worth 8x more | ledger M7, raised to a genesis gate | commission before genesis |
|
||||||
| 4 | The clock and the detector: (a) a share-pattern detector on the observer (per-program hash-rate spread, nonce-group patterns, per-card-model rate bands; alert when a population behaves like one fixed design: how MoneroCrusher found Monero's secret chips at 85% of the hashrate, February 2019); (b) the bounty's trigger as daily issuance in dollars, not a date (compute-bound hashes got chips at $20K to $30K a day: Radiant, Kadena, Handshake; Vorick's 2018 rule about $55K a day) | not a layer: the response time | the observer (`tools/observer`), D11 (the bounty is unfunded) | 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, per-card-model rate bands; alert when a population behaves like one fixed design: how MoneroCrusher found Monero's secret chips at 85% of the hashrate, February 2019); (b) the audit-and-benchmark trigger as daily issuance in dollars, not a date (no device bounty: Josh, 6 October 2026, 17:35 UTC, ledger M1; the trigger brings forward the paid cryptanalysis and the benchmark's next round) (compute-bound hashes got chips at $20K to $30K a day: Radiant, Kadena, Handshake; Vorick's 2018 rule about $55K a day) | not a layer: the response time | the observer (`tools/observer`), D11 (no device bounty; the trigger brings forward the paid cryptanalysis and the benchmark round) | before the public testnet |
|
||||||
| 5 | Rank layer 9 (the epoch length) above layer 7 and measure the FPGA lane: a soft-overlay FPGA with HBM (reads in flight per watt against the 5090's 17.5 G/s) added to the compile-ahead measurement | FPGAs were the first adversary of Lyra2REv2 (2018) and X16R (1.3x, September 2019) and came back within weeks of X16Rv2; Xelis forked for FPGA resistance (July 2024); a per-hour compiled program is a bitstream target | `docs/plans/epoch-length.md` | measurement before the public testnet |
|
| 5 | Rank layer 9 (the epoch length) above layer 7 and measure the FPGA lane: a soft-overlay FPGA with HBM (reads in flight per watt against the 5090's 17.5 G/s) added to the compile-ahead measurement | FPGAs were the first adversary of Lyra2REv2 (2018) and X16R (1.3x, September 2019) and came back within weeks of X16Rv2; Xelis forked for FPGA resistance (July 2024); a per-hour compiled program is a bitstream target | `docs/plans/epoch-length.md` | measurement before the public testnet |
|
||||||
| 6 | Order the reserve by chip-unfriendliness: the 32-bit datapath families first (byte permute, bit-field extract, variable shifts, popcount, select, the second shuffle), mm8 last | int8 matrix blocks are licensable IP at every node; Apple pays 1.6x to 4.7x per emulated dot4; Least Authority's ProgPoW suggestion 5 was "watch ML hardware" | spec 1.13.2 | a decision for Josh with the 3.0 measurements |
|
| 6 | Order the reserve by chip-unfriendliness: the 32-bit datapath families first (byte permute, bit-field extract, variable shifts, popcount, select, the second shuffle), mm8 last | int8 matrix blocks are licensable IP at every node; Apple pays 1.6x to 4.7x per emulated dot4; Least Authority's ProgPoW suggestion 5 was "watch ML hardware" | spec 1.13.2 | a decision for Josh with the 3.0 measurements |
|
||||||
| 7 | A vendor-share metric (hashrate by vendor) published with the benchmark | the 7.5x AMD gap is a softer form of the capture the history records (Kaspa's GPU share went to nothing within months of KS0) | the numbers page, the observer | with the public benchmark |
|
| 7 | A vendor-share metric (hashrate by vendor) published with the benchmark | the 7.5x AMD gap is a softer form of the capture the history records (Kaspa's GPU share went to nothing within months of KS0) | the numbers page, the observer | with the public benchmark |
|
||||||
|
|
@ -18,7 +18,7 @@ Placed nowhere, with the reasons in the history document's section 4.3: per-hash
|
||||||
|
|
||||||
## Decisions for Josh raised by the history
|
## Decisions for Josh raised by the history
|
||||||
|
|
||||||
Add the partial-store rows before the vectors freeze; name the random derivation as a reserve family and fund its verifier measurement; commission the mixer cryptanalysis; escrow the bounty on an issuance trigger and build the detector; rank the epoch-length reserve above mm8.
|
Add the partial-store rows before the vectors freeze; name the random derivation as a reserve family and fund its verifier measurement; commission the mixer cryptanalysis; put the paid cryptanalysis and the benchmark round on an issuance trigger and build the detector; rank the epoch-length reserve above mm8.
|
||||||
|
|
||||||
## The plan, in order
|
## The plan, in order
|
||||||
|
|
||||||
|
|
|
||||||
204
docs/plans/ember-tune.md
Normal file
204
docs/plans/ember-tune.md
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
# Ember Tune: every card tuned for MH per watt, out of the box
|
||||||
|
|
||||||
|
5 October 2026, night. Josh: "make sure we have ember tuning every single card for efficiency out of the box, the
|
||||||
|
more data = the better the tune, make an awesome system." Branch `ember-tune`, worktree `../igneum-wt-ember-tune`,
|
||||||
|
on top of the AMD telemetry commit (7adcd4c, branch `opencl-rdna4-telemetry`) and the Power control commit (3562f26,
|
||||||
|
branch `job-console`), both cherry-picked. Lever 3 of docs/plans/miner-eff.md grows two knobs and a fleet memory;
|
||||||
|
lever 2 (docs/design/miner-tuning.md) carries the priors in the same signed `tuning` section.
|
||||||
|
|
||||||
|
## 1. What a user sees
|
||||||
|
|
||||||
|
| Moment | The card row says | What happened |
|
||||||
|
|---|---|---|
|
||||||
|
| First 2 minutes of mining | `tuning: waits for 120 s of steady mining` | The worker warms up; nothing is touched. |
|
||||||
|
| Tuning | `tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)` with a Stop button | One card at a time, on the live kernel, never restarting the worker. |
|
||||||
|
| Tuned | **Tuned: 122.3 MH/s at 290 W (0.422 MH/W)**, then `2470 MHz at 100%, full tune, 1 h ago` | The point is pinned on the card; the result went to the fleet. |
|
||||||
|
| Known model | the same line, `from the fleet prior, confirmed, 2 min ago` | The card started at its model's prior and confirmed it in two steps instead of nine. |
|
||||||
|
| Apple silicon | **Tuned: 26.7 MH/s at 38 W (0.703 MH/W)** `(measured as it runs)`, and `measure only on Apple silicon: the system sets the clocks and the power; no control exposed` | Nothing can be set; the number is still reported so the row and the fleet know what the card does. |
|
||||||
|
| NVIDIA, Power control off | the measured line and `measure only until Power control is on in Settings (Windows asks for administrator rights once)` | The app never raises the prompt by itself (5 October 2026). One switch, one prompt, and the full tune runs. |
|
||||||
|
| Slider moved | `your setting stays pinned` | A manual point is never overridden; the tune still measures and reports. |
|
||||||
|
| Stopped | `tuning stopped: a remote job took the GPU` and the card back where it was | Any fault reverts the step and the run. |
|
||||||
|
| Fleet pause | Settings: `tuning paused fleet-wide by the signed manifest` | The kill switch. |
|
||||||
|
|
||||||
|
Settings: one switch, "Ember Tune: tune every card for MH per watt out of the box (once after install, then weekly,
|
||||||
|
and after a driver or program change)". AMD needs no rights. NVIDIA needs the Power control switch (one administrator
|
||||||
|
prompt) for both knobs; off, it measures only.
|
||||||
|
|
||||||
|
## 2. The knobs, per vendor
|
||||||
|
|
||||||
|
| Vendor | Power limit | Core clock cap | Memory clock | How | Rights |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| NVIDIA | `nvidia-smi -pl <W>`, percent of the default, inside `power.min_limit` and `power.max_limit` | `nvidia-smi -lgc 0,<MHz>`, percent of `clocks.max.gr`; `-rgc` = unlocked | never touched (`-lmc` is not used); read back as `clocks.mem` | directly when the engine is elevated, else the one-prompt helper (`<seq> pl <W>`, `<seq> lgc <MHz>`, `<seq> rgc` in `sweep/cmd.txt`) | administrator, so only with Power control on |
|
||||||
|
| AMD | `igneum-gpu-telemetry --card N --set-plimit <offset>` (0 = default, -20 = 80%), inside the `tune` line's `plimit_range` (PC 1's 9070 XT: -30 to 10, so 70% is the floor) | `--set-gmax` only when the `tune` line's `gmax_range` is absolute MHz (floor 0 or above); on RDNA 4 the range is an offset from stock (-500 to 1000 on PC 1) and the clock knob stays closed until the stock clock is known; `--reset` for the default point | not settable through ADLX on RDNA 4; read back as `mclk_mhz`, and a step whose mean memory clock falls under 95% of the baseline's is marked and cannot win | the helper, one process per request, exit 0 and a `tune ... ok` line | none on Windows (ADLX manual tuning); root on Linux, so measure only there |
|
||||||
|
| Apple | none | none | none | measure only | none |
|
||||||
|
|
||||||
|
Vendor limits are never exceeded and the floor is never undercut: the plan clamps every point (`Limits::clamp_clock`,
|
||||||
|
`Limits::watts_for`), and a clock floor the vendor does not report is 60% of the maximum.
|
||||||
|
|
||||||
|
## 3. The plan and the choice
|
||||||
|
|
||||||
|
Full plan (a new model, or a prior that lost its confirm check): the power ladder 100, 90, 80, 70, 60, 50% at the
|
||||||
|
unlocked clock (duplicate watts dropped where the card's floor clamps them), then the clock ladder 90, 80, 70, 60%
|
||||||
|
of the maximum at the power point the power ladder chose. 60 s hold after 15 s settle per step; 9 steps on an
|
||||||
|
RTX 5090 (five power, four clock), about 12 minutes.
|
||||||
|
|
||||||
|
Confirm plan (the model's prior has 5 or more reports): the prior's point, then one neighbour (the next clock step up
|
||||||
|
when the prior caps the clock, else one power step down). If the neighbour beats the prior by over 1% MH/W, the full
|
||||||
|
plan is queued; else the prior stands. Two steps, about 3 minutes.
|
||||||
|
|
||||||
|
Baseline plan (measure only): one step at the card's current point. The "before" number for the row and the fleet.
|
||||||
|
|
||||||
|
The choice (`ember::choose`): among the usable steps whose rate is within the tolerance (1%, settable from the
|
||||||
|
manifest) of the fastest step, the best MH per watt; within 1% on efficiency the higher rate; within 1% on both the
|
||||||
|
lower draw. A card never gives up more than the tolerance in blocks for the saving. A step is unusable when it is
|
||||||
|
marked: `faulted` (a rejected or mismatched hash during the hold: the step is reverted and marked), `hot` (the GPU
|
||||||
|
reached 85 C; the run aborts at 90), `memory_clock_dropped`, `unapplied` (the readback disagreed with the request),
|
||||||
|
`no_readings` (under three draw samples or no STATUS line).
|
||||||
|
|
||||||
|
## 4. The data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
card mines 120 s ──> probe (limits, driver, how to set) ──> plan ──> steps ──> choice ──> point pinned
|
||||||
|
│
|
||||||
|
app log: TUNE start / TUNE card=.. step=.. / TUNE chosen / TUNE {json} (and stdout under --sweep)
|
||||||
|
│
|
||||||
|
log upload (every minute, the existing intake, site/api/log.mjs) ──> Neon miner_logs
|
||||||
|
│
|
||||||
|
relay/lib/ember.mjs aggregate: per (card model | driver major | program class)
|
||||||
|
median clock cap (10 MHz), median power %, median MH/W, MH/s, W, spread (MAD %), samples, machines
|
||||||
|
│ │
|
||||||
|
console: /r/<token>/c/tuning, `node tools/console.mjs tuning` site: tools/tuning.mjs --priors --site
|
||||||
|
│ -> site/miner-priors.json -> /miners#priors
|
||||||
|
tools/tuning.mjs --priors --write tuning.json (priors + ember settings beside the kernel-variant cards)
|
||||||
|
│
|
||||||
|
packaging/ota/publish-manifest.sh --tuning tuning.json --deploy (signed; carried over when not given)
|
||||||
|
│
|
||||||
|
every app: <app data>/tuning.json ──> ember::settings_of (kill switch, min samples, tolerance, period)
|
||||||
|
──> ember::prior_of(key) ──> a new card's confirm plan
|
||||||
|
```
|
||||||
|
|
||||||
|
The record (`ember::record_json`): `ts`, `machine` (the first 8 hex of SHA-256 over the install id; the id itself
|
||||||
|
is random per install and never sent), `app`, `os`, `card`, `vendor`, `driver`, `driver_major`, `class`, `key`,
|
||||||
|
`plan`, `steps` (the full table: clock, power %, limit, watts, MH/s, MH/W, core and memory clock, hottest reading,
|
||||||
|
faults, mark), `chosen`, `before` (the full plan's 100% step), `eff`, `mhs`, `watts`. The key: `<card model with
|
||||||
|
underscores>|<driver major>|<program class>`, the class from the worker's race line (`l128w16` today; `v2` before a
|
||||||
|
race has run).
|
||||||
|
|
||||||
|
## 5. Scheduling and safety
|
||||||
|
|
||||||
|
| Rule | Where |
|
||||||
|
|---|---|
|
||||||
|
| One card at a time; the card must have mined 120 s and have a STATUS line | `tick_sweep` |
|
||||||
|
| Never under a remote job hold, a pause, inside 600 s of the hour boundary, or while the app quits | `tick_sweep`, `sweep_drive` |
|
||||||
|
| Due once after install, every 7 days (manifest `ember.period_s`), and when the driver major or the program class changed since the last tune | `tick_sweep` (`CardPref.sweep_driver`, `sweep_class`) |
|
||||||
|
| A pinned card (the slider) is measured, never changed | `sweep_finish` |
|
||||||
|
| Kill switch: `tuning.ember.enabled = false` in the signed manifest stops every tune fleet-wide; the Settings line says so | `ember::settings_of`, `tick_sweep` |
|
||||||
|
| Faults: a rejected or mismatched hash marks the step; the card leaving `mining`, a worker error, a job, a pause or 90 C aborts the run and restores the point from before | `Run::sample_fault`, `sweep_drive`, `sweep_abort` |
|
||||||
|
| Memory clock held: never set; a step that drags it under 95% of the baseline's cannot win | `Row::from_samples` |
|
||||||
|
| Vendor limits: every point clamped to the reported range; the clock floor 60% when none is reported | `Limits` |
|
||||||
|
| A signed prior is only ever a starting point inside the card's OWN reported limits (`power.min_limit` to `power.max_limit`, the clock floor to `clocks.max.gr` or the ADLX `gmax_range`), never a memory clock, never a value the card did not report; the confirm step measures it and the full plan replaces it when a neighbour beats it, so a bad prior costs the fleet one confirm step per card, not a setting. The signing key (K1, docs/security/keys.md) therefore cannot push a card past its vendor ceiling or under its floor | `Plan::confirm` clamps through `Limits::clamp_clock` and `power_pct.clamp(50, 100)`; proven by `ember::tests::the_confirm_plan_checks_the_prior_and_its_neighbour` (a prior of 9,000 MHz at 30% becomes 3,090 MHz at 50%) and `limits_never_exceed_the_vendor_or_undercut_the_floor` |
|
||||||
|
| No prompt the user did not ask for: the NVIDIA helper starts only with Power control on; the `--sweep` job never counts as permission | `sweep_probe_known`, `sweep_helper_start` |
|
||||||
|
| The elevated helper restores the limit and resets the clocks by itself after 20 idle minutes | `sweep::helper_script_*` |
|
||||||
|
| A playbook that starts a second engine beside the installed app (the PC measurement jobs) gives it NO pipe (its output goes to a file the script tails: a pipe's write end is inherited by the engine's miners, and the installed app's jobs runner then waits forever for EOF after an abort; C35, PC 1 22:31 UTC, a 24-minute hang and orphaned miners), ends the engine's whole process tree at the end and on the budget (`taskkill /T /F`), and lets the installed app's miners come back only after that | `relay/playbooks/ember-tune-pc1.ps1`, `sweep-5090.ps1`; CI `tools/ci/second-engine-check.sh` fails any playbook without both |
|
||||||
|
| Every `quit:` line in the app log names its source (the window host's stdin, the host gone, `POST /api/quit`, the `--sweep` run's end) | `Cmd::Quit(&'static str)` (b671c8b) |
|
||||||
|
| A second engine never runs the updater: `IGNEUM_APP_NO_OTA=1` (implied by `--sweep`) skips the OTA tick and refuses Check now, whatever the manifest's `min_supported_version` says (the installer it would launch quits the installed app: PC 1, 22:31 UTC) | `Engine.no_ota`; the playbooks set the variable; `tools/ci/second-engine-check.sh` demands it |
|
||||||
|
|
||||||
|
## 6. Tests
|
||||||
|
|
||||||
|
| Test | What it fixes |
|
||||||
|
|---|---|
|
||||||
|
| `ember::tests::the_full_plan_is_the_power_ladder_then_the_clock_ladder_at_the_chosen_power` | 5 + 4 steps on the 5090's limits, the clamps, the dynamic second half, the 1% and 5% choices |
|
||||||
|
| `limits_never_exceed_the_vendor_or_undercut_the_floor` | clamps |
|
||||||
|
| `the_choice_keeps_the_best_mh_per_watt_within_the_rate_tolerance` | the rule, the ties, marked rows never win |
|
||||||
|
| `the_guards_mark_a_step_so_it_cannot_win` | faulted, hot, memory clock, unapplied, no readings, the line |
|
||||||
|
| `a_fault_during_a_step_reverts_it_and_the_run_goes_on` | the state machine with a fake clock: the faulted 70% step is marked and never chosen |
|
||||||
|
| `the_confirm_plan_checks_the_prior_and_its_neighbour` | the two steps, Keep against FullDue, a prior outside the range clamped |
|
||||||
|
| `a_baseline_plan_measures_the_card_as_it_runs` | no control, still a number and the Tuned line |
|
||||||
|
| `the_record_and_the_prior_round_trip_through_the_manifest_shape` | record fields (no address, no host), `priors` and `ember` beside `cards`, the sample floor, the kill switch |
|
||||||
|
| `control_reasons_per_vendor` | who measures only and why |
|
||||||
|
| `sweep::tests::helper_scripts_carry_the_protocol` | the helper's `pl`, `lgc`, `rgc` |
|
||||||
|
| `relay/test/ember.test.mjs` | five samples converge (2,470 MHz at 100%), an outlier (0.908 MH/W at 1,854 MHz) moves nothing, baseline records make no prior, de-duplication, the manifest merge keeps lever 2's cards, the canonical round trip, AMD keys |
|
||||||
|
| `app/igneum-app/ui/tune-line.test.mjs` | the row line per state |
|
||||||
|
|
||||||
|
Run: `cargo test -p igneum-app ember sweep` (on a PC through the build job, or on the Mac under the build lock),
|
||||||
|
`node --test relay/test/ember.test.mjs app/igneum-app/ui/tune-line.test.mjs`.
|
||||||
|
|
||||||
|
## 7. The tier consequences
|
||||||
|
|
||||||
|
| Tier | What Ember Tune does for it | What it costs |
|
||||||
|
|---|---|---|
|
||||||
|
| A laptop GPU (NVIDIA, 60 to 115 W) | the power ladder usually finds the vendor floor binding; the clock ladder is where a memory-bound program saves watts; the thermal mark keeps a hot chassis from winning a step it cannot hold | about 12 minutes once, then 3 minutes a week; under 1% of the hour during the tune (the worker never stops) |
|
||||||
|
| One 8 GB card | the same two knobs; the 8 GB card is identities-limited (2 by default), the tune does not change that | the same |
|
||||||
|
| One 12 or 16 GB card | the same | the same |
|
||||||
|
| One 24 or 32 GB card (the 5090) | the draw sits far under the cap (290 W under 460 W on PC 1), so the power ladder is flat and the clock ladder is the lever; expected saving from the 4 October stability line: tens of watts at under 1% rate, to be measured | the same |
|
||||||
|
| A rig (several cards) | one card at a time, so a six-card rig takes about 70 minutes to tune once; every card of one model after the first starts at the prior (3 minutes); the tune never touches a card a remote job holds | linear in cards once, then the confirm plan |
|
||||||
|
| A pool user | the same per card; a pool submits the same hashes, so the 1% rate tolerance is the same 1% of shares | the same |
|
||||||
|
| AMD on Linux | measure only (sysfs needs root); the row says so | 60 s a week |
|
||||||
|
| Apple silicon | measure only; the row says so | 60 s a week |
|
||||||
|
|
||||||
|
Privacy line: what is uploaded is the record in section 4 and nothing else: a hash of the random install id, the
|
||||||
|
card model, the driver version, the OS, the program class, the step table and the chosen point. No address, no
|
||||||
|
hostname, no raw machine id, no user name. The public priors table carries only the aggregate per model.
|
||||||
|
|
||||||
|
## 8. Measurements
|
||||||
|
|
||||||
|
### PC 1, 5 October 2026 (night)
|
||||||
|
|
||||||
|
Tonight's constraints, read from PC 1's own uploads: the installed app runs as `DESKTOP-KMCV30N\Admin` with
|
||||||
|
`elevated=False` (the account line at 19:02:33 UTC), the two in-app sweep attempts at 20:09 UTC aborted on the
|
||||||
|
cancelled administrator prompt (`SWEEP aborted ... the_elevated_helper_did_not_run_(the_administrator_prompt_was_cancelled)`),
|
||||||
|
so no stored sweep result exists from today, and the RX 9070 XT left the PCI bus at about 20:40 UTC (eGPU link,
|
||||||
|
not restarted tonight). NVIDIA's `-pl` and `-lgc` need administrator rights, Josh is asleep, and the app never raises
|
||||||
|
the prompt by itself, so tonight's run on PC 1 is the baseline plan on the 5090 through the whole pipeline (probe,
|
||||||
|
measure, TUNE record, upload, aggregation, prior shape in a test manifest). The two-knob tune on the 5090 and the
|
||||||
|
9070 XT run are owed: the 5090 the moment Power control is switched on (one prompt, then the tune runs by itself
|
||||||
|
within 2 minutes of steady mining), the 9070 XT when the card is back on the bus.
|
||||||
|
|
||||||
|
Run 1 (ember-tune-pc1-1, 22:30 UTC): aborted 46 s in by the installed app quitting, named the next morning: the
|
||||||
|
second engine's own updater (0.3.9 under min_supported_version = urgent) ran the per-user installer, whose
|
||||||
|
PrepareToInstall quit the installed app through its api/quit (C35 in the bench log); before any step; nothing set; the "before" snapshots are in the bench log (5090: 450 W of 575, 2,850 MHz core, 3,090 MHz
|
||||||
|
maximum, 14,001 MHz memory; 9070 XT present on bus 98 with OFFSET ranges `gmax_range -500 1000`, `plimit_range -30
|
||||||
|
10`). The offset finding changed the AMD mapping (054e041): an offset clock range closes the clock knob and the power
|
||||||
|
ladder runs on a percent scale bounded by `plimit_range`. The re-run follows the 0.3.11 rollout.
|
||||||
|
|
||||||
|
## 7a. One administrator approval, ever (0.3.13; Josh, 6 October 2026, 11:50 UTC)
|
||||||
|
|
||||||
|
What 0.3.12 does: Power control on raises one prompt and sets every cap in that step; every later cap (an app start, a
|
||||||
|
reboot, a slider move) and every tune's helper is another elevated launch, so another prompt. Not "once, ever".
|
||||||
|
|
||||||
|
What `src/powertask.rs` does: the first approval's elevated step also registers a per-user Windows scheduled task,
|
||||||
|
`Igneum Power Helper` (principal = the signed-in user, interactive logon, RunLevel Highest, no trigger, hidden, one
|
||||||
|
hour limit, new starts ignored while one runs), whose action is the app's own exe in the install folder with
|
||||||
|
`--power-helper`. A task the user owns is started by the user's unelevated engine with `Start-ScheduledTask`, no
|
||||||
|
prompt, and runs elevated. Every later cap and every tune's helper starts the task and writes the command file
|
||||||
|
`<app data>/app/sweep/cmd.txt` (`<seq> dev <n>`, `<seq> pl <W>`, `<seq> lgc <MHz>`, `<seq> rgc`, `quit`). The task
|
||||||
|
survives app restarts, updates (the per-user installer replaces the exe in place; the task's action path is the
|
||||||
|
install folder) and reboots. Power control off starts the task once and sends `remove`: the helper unregisters the
|
||||||
|
task (elevated) and exits; nothing is left behind. Linux keeps pkexec per step; macOS has no cap.
|
||||||
|
|
||||||
|
Threat note: the helper runs only fixed verbs with digit-only arguments through `Command::new(nvidia-smi).args`
|
||||||
|
(the driver's own path, never PATH, never a shell); a line that is anything else is ignored; the sequence must rise
|
||||||
|
(a stale file runs nothing); an attacker running as the user gains the power limit and clock cap of the user's own
|
||||||
|
NVIDIA cards inside the driver's ranges, which the same user could set with one approved prompt anyway; no file,
|
||||||
|
process, registry key or other binary is reachable through it. Tests: `powertask::tests` (the parser refuses every
|
||||||
|
non-digit or extra argument, the arguments reach nvidia-smi as a list, the registration is per-user, highest,
|
||||||
|
trigger-less and quote-safe, a stale command file runs nothing).
|
||||||
|
|
||||||
|
## 8a. Next-cut notes (for the 0.3.12 shipper)
|
||||||
|
|
||||||
|
| Commit | What | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| b671c8b | every `quit:` names its source; Power control alone decides; no cap at start under `--sweep` | main.rs, server.rs, engine.rs (separable) |
|
||||||
|
| e600e63 | a second engine never runs the updater (`IGNEUM_APP_NO_OTA`, implied by `--sweep`) | engine.rs (6 lines, separable) |
|
||||||
|
| 1e9550e | the elevated job path's output file is followed while the script runs, so the 5-minute progress reports carry its lines (a 35-minute run that never mined showed only "script running" on 6 October 2026); the tune playbook's watchdog fails a run that mines nothing within 120 s of its first status line, with the engine's last log line in the RESULT | jobrun.rs `follow_file`, relay/playbooks/ember-tune-pc1.ps1 |
|
||||||
|
|
||||||
|
## 9. Open
|
||||||
|
|
||||||
|
- The NVIDIA clock readback: `nvidia-smi -lgc` is confirmed only through the core clock during the hold (a mean over
|
||||||
|
the cap by 5% marks the step `unapplied`); the first run with Power control on tells whether the driver honours
|
||||||
|
the lock on the 5090 under this kernel.
|
||||||
|
- ADLX on RDNA 4 exposes no memory-clock setter; the memory-clock mark is the guard. The telemetry agent's 9070 XT
|
||||||
|
sweep tells whether a core cap drags the memory clock on that card.
|
||||||
|
- The confirm plan's neighbour is one step; a second neighbour (the other knob) would cost 75 s more and catch a
|
||||||
|
prior that is wrong on both knobs.
|
||||||
|
- Intel: no knob yet; the row says measure only.
|
||||||
111
docs/plans/morning-2026-10-06.md
Normal file
111
docs/plans/morning-2026-10-06.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Morning summary, 6 October 2026
|
||||||
|
|
||||||
|
Written for Josh at 08:40 UTC. Every number is in `docs/bench-log.md` or the named plan with the command that produced it. Failures are listed with the passes.
|
||||||
|
|
||||||
|
## The headline
|
||||||
|
|
||||||
|
Everything on the overnight list landed. The one thing that could have gone wrong, switching the live chain to program class v3, held.
|
||||||
|
|
||||||
|
| Piece | State | Number |
|
||||||
|
|---|---|---|
|
||||||
|
| 0.3.11 (class v3 + proving v1) | On master 630da6b, three master CI runs green, rolled out to the Mac, PC 2, the seed, both hand nodes; PC 1 took it at 07:00 | Live manifest 0.3.11, nine-field override, digest 0139ab9d |
|
||||||
|
| Counter ASIC 2.0 crossing | Crossed at 03:51:42 UTC, watcher verdict PASS | 58.7 blocks a minute before, 59.2 after; three hourly swaps since, no pause, 0 refusals |
|
||||||
|
| 12 GB proving | Floor broken on a patched SP1 server (`prover-floor` bcc6d68); real card lands today, test on PC 2 | Proves alone: 10.3 GB, 5.7 s a shard. Mines AND proves as a core-only prover handing its proof to a big-card aggregator: 8.1 GB beside the miner, 19.8 s a shard (measured 09:15 on the 5090's allocation). 16 GB mines and proves compressed (12.9 GB, 17.4 s). 8 GB stays out |
|
||||||
|
| On-die recompute chip | Emulated on the 5090's own L2 (`ledger-pc2` 564acab) | 0.256x honest, 5.1x worse per joule |
|
||||||
|
| Ledger | Rounds 2 and 3 closed (`fud-close` d16bc3b, `ledger-rebase` abb08a5) | 47 consequence rows, 42 closed or taken, 14 decisions |
|
||||||
|
| Branches ready for later cuts | pool-v0, rig-install, repro-bench b776199, ember-tune 9a6469f, ota-k2, asic-history, proving-methods | measured where PC 2 allowed |
|
||||||
|
|
||||||
|
## Since you got up (07:00 to 08:40 UTC)
|
||||||
|
|
||||||
|
| What | State |
|
||||||
|
|---|---|
|
||||||
|
| PC 1 | Back on 0.3.11 at 07:00, 5090 and 9070 XT mining; integrated card off. The update needed your click because the app's hourly rollout slot had not come; fixed as a catch-up rule (`update-catchup` 2207cd7, 0.3.12) |
|
||||||
|
| Mac | Mining paused through the app (persists); its node runs |
|
||||||
|
| Chain | 19 miner ids, 225 MH/s on the two PCs |
|
||||||
|
| Zero proven segments | Solved: the prover claims a whole segment and proves its 8 blocks in order (9 segments in 30 minutes on PC 2 beside the miner, 72 of 72 shard records paid, 11 percent hash cost). The segment record itself needs a consensus switch on the node fork (`proving_v1_fresh_rule_daa`), folded into 0.3.12 |
|
||||||
|
| Ember Tune re-run on PC 1 | Failed at 07:56 with no rows: the playbook wrote the test engine's settings with a byte-order mark, the engine parsed defaults, sat idle 35 minutes. Nothing was set on either card; the app restored its miners by itself. Fixed (8273494, watchdog 1e9550e, CI check). Re-run `ember-tune-pc1-3` needs one more click when you are back |
|
||||||
|
| GPU list order | Cards ordered by performance, integrated last (`card-order` ffb2bfa, 0.3.12) |
|
||||||
|
| Counter ASIC 3.0 | Running, all seven items plus a new item 8. See below |
|
||||||
|
| 0.3.12 | Being prepared: the app items plus the node fork with the segment switch; stops at the publish gate for your go |
|
||||||
|
|
||||||
|
## Counter ASIC 3.0 so far
|
||||||
|
|
||||||
|
The finding that matters: the chip that wins is not the clever recompute chip 2.0 priced. It is a stored-dataset chip, the whole dataset in DRAM, a 28 nm memory-controller die doing dependent reads.
|
||||||
|
|
||||||
|
| Attacker | Per chip vs 5090 | Per joule vs 5090 | Source |
|
||||||
|
|---|---|---|---|
|
||||||
|
| On-die recompute chip (2.0's model) | 0.92x with the 3x allowance | 1.86x | chip-model-v3 |
|
||||||
|
| Same, emulated on the 5090's L2 | 0.256x | 0.2x | M16 inline bench |
|
||||||
|
| Stored-dataset chip, GDDR7 | 1.22x | 5.1x | item 1 |
|
||||||
|
| Stored-dataset chip, HBM3 | about 1.2x | 7.5x to 9.2x | item 1 |
|
||||||
|
| Ethash precedent (E3, A10) | | 2.1x to 4.8x | history rows 3, 4 |
|
||||||
|
| Stored-dataset chip with the latency shadow filled (item 8, N = 100,000, parity cores) | | 2.7x vs 5090, 1.4x vs M5 Max | item 8 Mac rows |
|
||||||
|
|
||||||
|
Why: at the hash the 5090 spends about 55 W on memory and the rest keeping a GPU alive at 0.15 percent of its integer budget. The lever is RandomX's lever: make the hash use the rest of the chip. The 5090 can hide about 330,000 operations per hash behind its 128 reads; today it hides 512. Item 8 measures that fill: on the Mac it costs 1.5 percent of rate at 100,000 ops, the verifier barely notices, and the chip's edge drops from 5x to 2.7x. The 5090's rows are queued on PC 2. The deciding number is the chip core's energy per op against a GPU's ALU, which item 8 is pricing.
|
||||||
|
|
||||||
|
**Closed at 08:50.** One class v4 candidate, measured on the hash's own numbers and ready for its six-gate run on your word: mixer x8 plus 100,000 operations of program work per hash. The 5090 loses 0.2 percent of rate and the M5 Max 1.5 percent; the verifier adds 0.17 ms per warp; bit-exact on Metal, CUDA, Apple OpenCL and the CPU emulation. The chip must then carry a 14,000-lane ALU array, and its per-joule edge over the 5090 falls from 5.6x to 2.1x at a core as efficient as the GPU's, 1.5x at a realistic one. Your test as a number: the chip crosses 2x only if its datapath spends under half the energy per op that a GPU does. The cost per tier: a 5090 draws 431 W instead of 350 for the same blocks (a rig pays about 23 percent more electricity), the M5 Max 37 W instead of 21, a pool user sees nothing. The 9070 XT and 4060-class rows are owed, the AMD ones because PC 1 was left alone.
|
||||||
|
|
||||||
|
Other items: item 2 (per-day random derivation) works bit-exact at no hash cost and drops the recompute chip to 0.29x to 0.43x, but at full size its CPU verifier is over the 10 ms gate on an old core; it goes in as a reserve, the half-size draw passes, and the class v4 candidate is the pairing of derivation class and program length under one verifier gate. Item 3: cryptanalysis brief and budget line, USD 80,000 to 160,000, one firm plus one academic group, verdict GO to commission, nobody contacted. Items 4 and 5: share-pattern detector in the observer (fires on a fabricated fixed design, quiet on the devnet), issuance trigger at USD 20,000 a day, FPGA soft overlay 0.3x to 0.4x a 5090 per watt, layer 9 ranked above layer 7. Items 6 and 7 in flight.
|
||||||
|
|
||||||
|
## Decisions for you
|
||||||
|
|
||||||
|
The full list with recommendations is in `consequences-decisions.md` (14) and the 3.0 status file. The ones that bite first:
|
||||||
|
|
||||||
|
0. **Run the six gates on the class v4 candidate**, or wait for the 9070 XT and 4060-class rows first (3.0 status, decision 2).
|
||||||
|
1. **The public claim "under 2x".** True of the recompute chip per chip, false of the stored-dataset chip per joule. Two re-wordings are drafted in the 3.0 status file; nothing on the site changed. Pick one before any public push.
|
||||||
|
2. **The segment rule.** Answered at 08:50: it is a consensus rule (as shipped a fresh segment record is valid for an 8-second window). The fix is on the node fork behind a new switch `proving_v1_fresh_rule_daa`, so 0.3.12 carries the node and goes out as a two-manifest publish with the switch at tip + 14,400. Measured on PC 2 beside the miner: 9 whole segments in 30 minutes, 72 of 72 shard records paid, 11 percent of hash rate.
|
||||||
|
3. **0.3.12 go.** The state-reply fix, the update catch-up, the card order, Ember Tune and its guards, plus the node fork with the segment switch (two-manifest publish, switch at tip + 14,400). No prompt on any machine.
|
||||||
|
4. **The 12 GB card.** Into PC 2 when it lands (PC 1 has no prover toolchain); the playbooks are written. Core-only provers need a pool-protocol change (a core hand-off format only aggregators accept, the compressor's credit, the prover's signature over the proof hash); nothing on chain changes. Decide whether that goes into the pool spec now.
|
||||||
|
5. **Cryptanalysis budget** (D11 and item 3), **growth mapping (b)** (D4), **a release-tag convention**, **the bounty only once escrowed**.
|
||||||
|
|
||||||
|
## What went wrong, plainly
|
||||||
|
|
||||||
|
- Epoch-34 pack outage, 18:23 UTC, 56 minutes of both PCs down: a pack-attempt bug in the worker loader. Hot fix shipped by job; class fix in 0.3.10.
|
||||||
|
- GitHub Actions outage forced a PC-built 0.3.10. 0.3.11's first CI run then failed in the census crate nobody updated for class v3; fixed (2a62735), rerun green.
|
||||||
|
- Credits ran out at 19:58 UTC and killed three agents; resumed.
|
||||||
|
- The 9070 XT dropped off the bus four times; the reading was wrong, the relay's "PC1" is PC 2.
|
||||||
|
- A 2.2x verifier regression on the mixer branch, caught before publish.
|
||||||
|
- Two Mac-only v3 outages caught by the Metal gate before publish.
|
||||||
|
- "12 GB proves" was false on the shipped prover; the floor was SP1's server code; patched.
|
||||||
|
- A job quit PC 1's app at 22:31 UTC (a second engine ran the urgent updater, whose installer sent quit). Rule written, CI gate added; PC 1 stayed down all night. Fixed in e600e63.
|
||||||
|
- Three dark proving windows on PC 2, all the same state-reply bug (paid_wei above u64::MAX empties the reply); 0.3.12's first item.
|
||||||
|
- The Mac miner lost its node subscription for 11 minutes after a node restart (C43, 0.3.12).
|
||||||
|
- The 0.3.11 update on PC 1 waited on the hourly slot and needed your click; catch-up rule in 0.3.12.
|
||||||
|
- Ember's first real run burned 35 minutes and your prompt on a byte-order mark; fixed with a watchdog and a CI check.
|
||||||
|
- The 07:45 summary task never fired on its own and its manual run stalled on a tool prompt; this document was written by hand.
|
||||||
|
|
||||||
|
## Today's hands list
|
||||||
|
|
||||||
|
1. 0.3.12 go when the shipper reports it green.
|
||||||
|
2. One click for the Ember re-run on PC 1.
|
||||||
|
3. The 3060 into PC 2; the playbook runs the same fixture as the sweeps.
|
||||||
|
4. 16:00 UTC: the fee-switch check (every prover on 0.3.11 before H = 210,000 at about 19:15 UTC).
|
||||||
|
5. USB copies of the key backup (10:00 reminder).
|
||||||
|
6. The decisions above.
|
||||||
|
|
||||||
|
## Since noon (13:15 UTC)
|
||||||
|
|
||||||
|
| What | State |
|
||||||
|
|---|---|
|
||||||
|
| 0.3.12 | Shipped: merged to master 494c9c7, both master CI runs green, every node on the ten-field object, the fresh-record rule arms at about 15:55 UTC |
|
||||||
|
| The 12 GB card | Measured on the 4070 in PC 1: proves alone 7.6 GB in 7.7 s, mines and proves beside its own miner 9.0 GB in 24 s, every proof verified. The packaging row (our signed build of the patched server in the app) is built and verifying on PC 2; when it ships the public line moves to "12 GB mines and proves" |
|
||||||
|
| Rented fleet | 36 boxes on Vast and RunPod, about USD 7.5 an hour; live page https://dl.igneum.network/fleet-22adafa34bc2. Real-card rows so far: 24 GB stock server proves; 12 GB mines and proves at 10.1 GB on Linux; 10 GB and 8 GB prove alone at 2^26 (the floor is now "8 GB proves alone, slowly"); the miner costs 4x to 8x on proof time |
|
||||||
|
| Apple proving | SP1 on the M5 Max CPU proves the v1 shard in 72 s. RISC Zero's current release has no Metal prover (575 s on the CPU), so the Apple route is SP1 on the CPU; RISC Zero keeps the version slot for the 8 GB CUDA tier and its 5.7x smaller proof |
|
||||||
|
| Counter ASIC 3.0 | Closed: one class v4 candidate ready for its gate run on your word (see the 3.0 section above) |
|
||||||
|
| Ember on PC 1 | Three runs failed on tooling (byte-order mark, a locked scratch folder); fixed each time; the real run needs you at PC 1 for one click, since Windows cancels an unanswered prompt after two minutes. 0.3.13 carries the helper that makes it the last prompt ever. Rented containers refuse power caps, so tuned priors come only from PC 1 and PC 2 |
|
||||||
|
|
||||||
|
### Found by the fleet, both fixes in flight for 0.3.13
|
||||||
|
|
||||||
|
1. **A fresh node never executes the EVM.** A node syncing from the seed through the pruning proof has no blocks below the pruning point, and the execution follower, which walks from genesis, sits silent: zero wallet, empty explorer, no proving work, mining fine. Every 0.3.12 joiner today is in that state; only genesis-era nodes execute. Fix: a loud "not synced" status, and an execution state snapshot at the pruning point verified against the header's state root, fetched from a peer. Stop-gap for tonight's fleet: a full sync from genesis, or a copy of the observer's execution data.
|
||||||
|
2. **The seed drops every fresh peer every 30 seconds.** The per-checkpoint certificate burst fills the finality route and the inherited rule closes a full route's connection; the baseline shows 11,700 already-known certificates resent in five minutes. Fix: a sized route, no replay during sync, drop instead of disconnect, and a guard counter.
|
||||||
|
|
||||||
|
## Afternoon (16:00 UTC)
|
||||||
|
|
||||||
|
| What | State |
|
||||||
|
|---|---|
|
||||||
|
| 0.3.13 | Approved by Josh at 15:20 UTC with the one-time devnet state reset: execution restarts empty at chain block 27,276 (11:40 UTC today) and re-derives forward; everything earned since is back, the first days' balances are gone; the chain, finality and the hash untouched. Carries the execution persistence and snapshot path, the fresh-joiner fix, the finality route fix (an echo of old certificates, 13,354 re-locks in 7 minutes at the seed) and Ember's helper. Two publishes, three new override fields, protocol 15 to 16, no prompt anywhere |
|
||||||
|
| Rented fleet, phase 1 | Done on 11 real cards for USD 9: every NVIDIA card from 8 GB proves; 12 GB and up mine and prove at once; 8 GB proves alone or mines beside a core-only prover (docs/analysis/prover-tiers-real-cards.md). The 8x 4090 rig is measuring on RunPod; an 8x 5090 is refused by both providers so far; no provider rents consumer AMD |
|
||||||
|
| Ember | Root cause of every failed run found and measured: the engine's own folder lock stripped the permissions off files a job copied in. Dry run with no prompt passed (5090 127 MH/s at 316 W, 4070 28.7 at 103 W). The table run goes on 0.3.13 with Josh's one click, which registers the helper and is the last prompt ever |
|
||||||
|
| PC 1 | 5090, 4070 and the 9070 XT all attached (two enclosures). Queue: 0.3.13, the Ember run, then the owed AMD rows for the class v4 candidate |
|
||||||
|
| Packaged prover server | Built and verified on PC 2 (prover-floor bf7b174): the patched server shipped signed in the app, fail-fast where it hung, a per-shard timeout with threshold step-down, tiers from the real-card rows. Ships in 0.3.14 with the public line |
|
||||||
|
| Apple | SP1 on the M5 Max CPU proves a shard in 72 s; RISC Zero's release has no Metal prover (575 s on the CPU). The Mac prover path uses the CPU; RISC Zero keeps the version slot for the 8 GB CUDA tier |
|
||||||
|
|
@ -114,6 +114,8 @@ Reading. Nobody pays an aggregator as a separate role: Aztec's 30% goes to whoev
|
||||||
|
|
||||||
The resume path (5 October 2026, the 0.3.11 app): `POST /api/resume` on 0.3.9 re-armed only FAULTED cards (`stop_miners("paused")` clears every slot's `restart_at`), so a healthy paused card stayed "off" at 0 MH/s until the app was relaunched: PC 2 at 21:25:11Z (the aggregation-cost job's pause and resume; `[ok] mining resumed` then `0.00 MH/s, waiting` for 20 minutes), the Mac that afternoon. Now every slot without a live worker is re-armed and its pack exported again before the start, and 90 s later `resume_check` logs `resume: <card> is not mining 90 s after resume (state ..., pid ...)` for every enabled card without a hash rate (`engine.rs`, three unit tests: the state machine, the 21:25:11Z case against the old rule, the check).
|
The resume path (5 October 2026, the 0.3.11 app): `POST /api/resume` on 0.3.9 re-armed only FAULTED cards (`stop_miners("paused")` clears every slot's `restart_at`), so a healthy paused card stayed "off" at 0 MH/s until the app was relaunched: PC 2 at 21:25:11Z (the aggregation-cost job's pause and resume; `[ok] mining resumed` then `0.00 MH/s, waiting` for 20 minutes), the Mac that afternoon. Now every slot without a live worker is re-armed and its pack exported again before the start, and 90 s later `resume_check` logs `resume: <card> is not mining 90 s after resume (state ..., pid ...)` for every enabled card without a hash rate (`engine.rs`, three unit tests: the state machine, the 21:25:11Z case against the old rule, the check).
|
||||||
|
|
||||||
|
The prover-floor agent's first sweep (job `floor-sweep-1`, 22:34 to 22:38Z, PC 2's 5090, the miners stopped, this plan's per-point recipe, its patched `sp1-gpu-server` 5568108b built for sm_86, sm_89 and sm_120, every proof VERIFIED by the unpatched pv1 host): the control at upstream's sizes reproduces the curve above (empty shard 13,892 MiB and 2.2 s; the v1 shard 20,516 MiB and 4.2 s); with the core element threshold at 2^26 the v1 shard proves as four core shards in 5.3 s at **12,708 MiB** and the empty shard at 12,772 MiB; 2^25 gives 12,836 MiB at 8.5 s; 2^27 gives 15,396 MiB. The 12.7 GB left is the server's Setup (five recursion keys pre-built at a fixed 2^27 capacity plus the shrink and core keys: 9.7 GB before the first shard), which its patch v2 sizes to the need. Decided for the 12 GB profile: the split that lands under 11 GB wins (5.3 s a shard is inside the loop's own 25 to 30 s of carriage and 100x inside T); 2^27 is the second profile only if v2 leaves it under 11 GB with the miner's 1.8 GB beside it. The 12 GB row stays OPEN until the final pair (alone and beside the miner) lands and the on-order 3060 runs it.
|
||||||
|
|
||||||
### A self-built CUDA server (the 12 GB path), before 0.3.12 (consequences C26)
|
### A self-built CUDA server (the 12 GB path), before 0.3.12 (consequences C26)
|
||||||
|
|
||||||
If the prover-floor agent's rebuilt `sp1-gpu-server` (the Setup sizes cut, built on PC 2 under WSL2) proves a shard under 11 GB, it becomes a shipped artefact and needs its own row of rules before 0.3.12: it is built from a pinned SP1 source tag with `CUDA_ARCHS` covering sm_86, sm_89 and sm_120 (the 12 and 16 GB tiers are Ampere and Ada, not only the 5090's Blackwell; one card family per measured row), by the packaging path that builds the Windows payload (PC 1's build job for the Linux binary, the Mac signs the manifest as it does the DMG), lands in the DMG and the WSL2 package beside the host as `wsl2/bin/sp1-gpu-server` with its sha256 in `payload-inputs.json`, is named in `evidence.md` beside the prover rows ("prover built from SP1 <tag> at <sha>"), is rebuilt and re-measured at every SP1 upgrade, and ships only after `--mode verify-segment` and `--mode verify` on proofs it made show the pinned verifying keys unchanged (the server changes allocation, not the circuit; the ids `0x2b1a81cb...` and `0x474678f3...` must still verify them). The 12 GB claim itself waits for the on-order RTX 3060 to run that server on the same fixtures and recipe as the curve; until then the public line stays at 24 GB.
|
If the prover-floor agent's rebuilt `sp1-gpu-server` (the Setup sizes cut, built on PC 2 under WSL2) proves a shard under 11 GB, it becomes a shipped artefact and needs its own row of rules before 0.3.12: it is built from a pinned SP1 source tag with `CUDA_ARCHS` covering sm_86, sm_89 and sm_120 (the 12 and 16 GB tiers are Ampere and Ada, not only the 5090's Blackwell; one card family per measured row), by the packaging path that builds the Windows payload (PC 1's build job for the Linux binary, the Mac signs the manifest as it does the DMG), lands in the DMG and the WSL2 package beside the host as `wsl2/bin/sp1-gpu-server` with its sha256 in `payload-inputs.json`, is named in `evidence.md` beside the prover rows ("prover built from SP1 <tag> at <sha>"), is rebuilt and re-measured at every SP1 upgrade, and ships only after `--mode verify-segment` and `--mode verify` on proofs it made show the pinned verifying keys unchanged (the server changes allocation, not the circuit; the ids `0x2b1a81cb...` and `0x474678f3...` must still verify them). The 12 GB claim itself waits for the on-order RTX 3060 to run that server on the same fixtures and recipe as the curve; until then the public line stays at 24 GB.
|
||||||
|
|
@ -130,8 +132,8 @@ The GPU server of SP1 6.8.1 sets the memory, not the shard: a floor of 13.9 GB f
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 32 GB (RTX 5090) | the prototype shard, 28.3 GB, 10.8 s; the v1 shard 20.4 GB, 4.3 s | the prototype shard 30.1 GB, 33 s; the v1 shard 22.2 GB, 13.2 s | on, mine and prove, today |
|
| 32 GB (RTX 5090) | the prototype shard, 28.3 GB, 10.8 s; the v1 shard 20.4 GB, 4.3 s | the prototype shard 30.1 GB, 33 s; the v1 shard 22.2 GB, 13.2 s | on, mine and prove, today |
|
||||||
| 24 GB (RTX 4090, 3090) | the v1 shard 20.4 GB; the prototype shard does NOT fit (28.3 GB) | the v1 shard 22.2 GB measured on the 5090's allocation (2.3 GB spare on a 24 GB card; approximate for the card itself) | on, mine and prove, with the line "until the devnet's fee switch its shards are the prototype size, which needs 32 GB, so this card proves from the switch on" |
|
| 24 GB (RTX 4090, 3090) | the v1 shard 20.4 GB; the prototype shard does NOT fit (28.3 GB) | the v1 shard 22.2 GB measured on the 5090's allocation (2.3 GB spare on a 24 GB card; approximate for the card itself) | on, mine and prove, with the line "until the devnet's fee switch its shards are the prototype size, which needs 32 GB, so this card proves from the switch on" |
|
||||||
| 16 GB (RTX 5080, 4080) | an empty shard only (13.9 GB) | nothing (15.7 GB for an empty shard, no room for the display) | off, with the line |
|
| 16 GB (RTX 5080, 4080) | the shipped server: an empty shard only (13.9 GB); the patched server v3 b37defef at threshold 2^27: the v1 shard 12,915 MiB and 4.3 s, the prototype shard 13,459 MiB and 16.8 s (measured by the prover-floor agent on the 5090's allocation, job `floor-sweep-3`, 00:13 to 00:17Z 6 October; 2^27 + 2^26 gives 16,115 MiB, over the card) | the shipped server: nothing (15.7 GB for an empty shard); the patched server at 2^27 beside the miner (the 5090 mining at 95%, 338 W, same card; `floor-sweep-4`, 00:35 to 00:38Z): the v1 shard 14,786 MiB total with the miner's 3,833 MiB resident inside it, the server's own 10,953 MiB, 17.4 s a shard; on a 16 GB card that is 10.95 GB server + 1.7 GB miner = 12.7 GB plus the display, under the 15.0 GB line | off on the shipped server, with the line; on (mines and proves, 17 s a v1 shard, 4.3x the alone time) once the patched server ships (the packaging row below) |
|
||||||
| 12 GB (RTX 3060, 4070) | nothing: the floor is 13.9 GB, and the shipped server refuses the card outright | nothing | off; Josh's "make sure we can prove on 12 GB cards" is OPEN and in work: the prover-floor agent (branch prover-floor, 5 October night) read SP1 v6.8.1's GPU server source (`sp1-gpu/crates/prover_components/src/builder.rs` lines 35 to 39): it reads the card's memory, adds 4 and panics under 24 ("Unsupported GPU memory ... must be at least 24GB"), and builds its core (ELEMENT_THRESHOLD 2^28 + 2^27 elements + 2^21), recursion (2^27), shrink (2^25) and wrap (85 M element) provers at Setup whatever the mode, which is the 13.9 GB floor; no knob reaches them, so the fix is a server rebuilt from source on PC 2 (WSL2, nvcc 12.8, CUDA_ARCHS=120) with those sizes cut, measured on the same fixtures and recipe as the curve above (D2 carries the curve) |
|
| 12 GB (RTX 3060, 4070) | the shipped server: nothing (the floor is 13.9 GB, and the server refuses the card outright); the patched server v3 b37defef at threshold 2^26 (`SP1_GPU_ELEMENT_THRESHOLD=67108864`, the 12 GB profile): **the v1 shard 10,291 MiB and 5.7 s, an empty shard 9,971 MiB and 3.3 s**, the card's 2,089 MiB idle inside the peak and the server's own working set about 8.2 GB (6,535 MiB after Setup), so a 12 GB card proves alone with about 3 GB over it (measured by the prover-floor agent on the 5090's allocation, `floor-sweep-3`; the on-order RTX 3060 run is pending) | measured beside the miner (`floor-sweep-4`): at 2^26 the v1 shard 12,066 MiB total with the miner's 3,833 MiB inside, the server's own 8,233 MiB, 24.4 s; the empty shard 11,586 MiB, 13.0 s; 2^25 gains nothing (12,066 MiB, 43.5 s). On a 12 GB card that is 8.2 GB server + 1.7 GB miner = 9.9 GB before the display, over the 9.0 GB line Josh set, so mine-and-prove on 12 GB is NOT claimed | off on the shipped server; "proves alone" on the patched one once it ships (the packaging row below); mine-and-prove stays off on 12 GB (9.9 GB plus the display, over the 9.0 GB line). The public gate stays "12 GB proves; 16 GB mines and proves", both on the patched server, both pending a run on the card itself. Josh's "make sure we can prove on 12 GB cards" is answered on the 5090's allocation and OPEN on the card itself: the prover-floor agent (branch prover-floor, 5 October night) read SP1 v6.8.1's GPU server source (`sp1-gpu/crates/prover_components/src/builder.rs` lines 35 to 39): it reads the card's memory, adds 4 and panics under 24 ("Unsupported GPU memory ... must be at least 24GB"), and builds its core (ELEMENT_THRESHOLD 2^28 + 2^27 elements + 2^21), recursion (2^27), shrink (2^25) and wrap (85 M element) provers at Setup whatever the mode, which is the 13.9 GB floor; no knob reaches them, so the fix is a server rebuilt from source on PC 2 (WSL2, nvcc 12.8, CUDA_ARCHS=120) with those sizes cut, measured on the same fixtures and recipe as the curve above (D2 carries the curve) |
|
||||||
| under 12 GB | nothing | nothing | off, mine only |
|
| under 12 GB | nothing | nothing | off, mine only |
|
||||||
| AMD-only and Apple machines | nothing on the GPU: no zkVM proves on an AMD GPU today (`docs/analysis/amd-proving.md`, branch amd-prove); the CPU prover is about 5 minutes a shard at a 30 GB RSS whatever the shard size (PC 1, bench-log "the SP1 CPU prover on PC 1") | | off, "mines and does not prove"; the only non-NVIDIA path with a shipped backend is RISC Zero's Metal prover behind the `ProofSystem` seam (a second guest and pinned id, a verifier for both formats, no shared aggregation): an open item, not 0.3.11 |
|
| AMD-only and Apple machines | nothing on the GPU: no zkVM proves on an AMD GPU today (`docs/analysis/amd-proving.md`, branch amd-prove); the CPU prover is about 5 minutes a shard at a 30 GB RSS whatever the shard size (PC 1, bench-log "the SP1 CPU prover on PC 1") | | off, "mines and does not prove"; the only non-NVIDIA path with a shipped backend is RISC Zero's Metal prover behind the `ProofSystem` seam (a second guest and pinned id, a verifier for both formats, no shared aggregation): an open item, not 0.3.11 |
|
||||||
|
|
||||||
|
|
@ -141,3 +143,81 @@ The aggregation-cost agent's first rows (branch agg-cost, 5 October 2026 night,
|
||||||
|
|
||||||
The re-plans of block 344 at 2.25 M and 4.5 M pgas peak at 28.3 to 28.4 GB alone (the server's buffers step up between 4.7 M and 20 M cycles and are flat to 60 M), so no shard size between the v1 budget and the prototype one changes a tier; with the miner the adopted shard proves 3.1x slower (13.2 s against 4.2 s) and the chained aggregation 9.7 s against 2.5 s: a mining 24 GB card delivers one adopted-size shard plus one aggregation in about 23 s, inside T by 25x.
|
The re-plans of block 344 at 2.25 M and 4.5 M pgas peak at 28.3 to 28.4 GB alone (the server's buffers step up between 4.7 M and 20 M cycles and are flat to 60 M), so no shard size between the v1 budget and the prototype one changes a tier; with the miner the adopted shard proves 3.1x slower (13.2 s against 4.2 s) and the chained aggregation 9.7 s against 2.5 s: a mining 24 GB card delivers one adopted-size shard plus one aggregation in about 23 s, inside T by 25x.
|
||||||
|
|
||||||
|
## Segment-aligned proving (6 October 2026, Josh: "find a way to solve this")
|
||||||
|
|
||||||
|
**The fault was the work order, not the rules.** The shipped loop took the newest open shard each pass (`prover::choose`), so one prover scattered one block in about 45 across the segment grid and no segment ever had all its blocks proven: pending 56, proven 0, unproven 20 at 06:28Z. No consensus parameter moves.
|
||||||
|
|
||||||
|
**The change (app branch, commit ce8f34a, app and host):**
|
||||||
|
|
||||||
|
| Part | What it does | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| Whole-segment claiming | the work list (lookback 600, the record window) grouped into whole untouched segments: every block present, every shard open (past its 10-DAA exclusive window), unpaid, not in our pool | `app/igneum-app/src/segments.rs` `whole_segments` |
|
||||||
|
| The choice | candidates inside their deadline by a margin (240 DAA, or 1.5x the last segment's wall time), ranked by FNV-1a of (first block, this prover's key hash): deterministic per prover, different between provers, so several provers spread over the candidates with no coordinator; an attempted segment is not retried | `segments::candidates`, `rank`, `need_daa` |
|
||||||
|
| The statement check | for the best three: executed and pending; fresh only when the previous segment is not paid and no verified record of it waits in the pool (the chain rule would refuse a fresh record once that one pays); chained (`--prev`) when the previous is paid and its proof is in this node's pool | `prover.rs` `pick_segment` |
|
||||||
|
| The work | one export to the segment's last block, one fixture per block, one host run `--mode chain --save-shards [--prev]` that proves every shard and aggregates the segment in one process (one key setup), then every shard record signed and submitted (the shard payouts, 90% of the credit) and the segment record signed and submitted (the aggregator share, 10%) | `prover.rs` `prove_segment` |
|
||||||
|
| The fallback | when no whole segment qualifies, the per-block path as shipped | `prover::choose` |
|
||||||
|
| The host | `--mode chain` takes `--prev <aggregated.bin>` (chain_len continues; a previous proof that is not the parent block's is refused by number and parent hash) and with `--save-shards` writes per-shard records (statement, proof sha256, file, time) into the results | `proving/igneum-prove/host/src/main.rs` `run_chain` |
|
||||||
|
| The tile | "Segments: N proven whole, M paid (x IGN to the aggregator), the last in T s" and the segment path's last line; the state carries `segments_submitted`, `segments_paid`, `segment_paid_wei`, `segment_last_s` | `ui/app.js`, `state.rs` |
|
||||||
|
|
||||||
|
Tests: the grid, the grouping (missing block, paid shard, our shard in the pool, exclusive shard), the margin and the attempted set, the per-key order (deterministic, different between two keys), the margin from the last time: 6 unit tests in `segments.rs`; 120 app tests, 8 core and 9 host tests pass. The host flags were run on the Mac's CPU first (bench-log, 07:12Z to 07:17Z): per-shard records written for a chain of 2, then a chain of 1 continued from it (`base_chain_len` 2, final `chain_len` 3).
|
||||||
|
|
||||||
|
**Item 2, "own pool only", answered from the source:** a relayed proof record carries its proof bytes (`protocol/flows/src/v10/proving.rs`: `IgneumProofRecordMessage { record, proof }`, 8 MB bound, handed to the pool with `local = false`), and so does a segment record (message 75). So "this node's pool" is every record relayed to it, and any aggregator can already fold any 8 proven blocks it has received; no node or consensus change is needed for that. What does limit carriage: a block template carries only entries the node's own verifier marked verified (`template_segment_section`, `template_section`), so a node with the verifier `Off` (node 1) never carries a record and a node with `Trust` carries unverified ones; PC 2's node runs the host and verifies. With one prover the carrier is PC 2's own next block.
|
||||||
|
|
||||||
|
**What one 5090 completes (arithmetic from the 5 October rows, the measurement below replaces it):** a chain of 8 empty blocks took 135.6 s cold beside the miner; one export and one key setup per segment instead of eight; so about one segment every 150 to 200 s, 9 to 12 segments an hour out of 450 (2 to 3%), against none. The aggregator share of a segment is 8 x 0.088 IGN = 0.70 IGN plus the 8 shards' 90% share; the forfeited share of the other 97% stays in the escrow until the fleet grows (47 mining cards or 6 dedicated provers for 100% at 1 block/s).
|
||||||
|
|
||||||
|
**PC 2 measurement (job `segments-pc2-pv1c`, 07:52Z to 08:24Z, `tools/proving-v1/pc2-segments.ps1`; bench-log "the segment-aligned prover beside the miner"):**
|
||||||
|
|
||||||
|
| Figure | Value |
|
||||||
|
|---|---|
|
||||||
|
| Whole segments proven in 30 min, one 5090 beside its miner | 9, one every 210 s (export 1.5 s, cut 45 s, chain 160 s: 8 shards 63 s, 8 aggregations 80 s) |
|
||||||
|
| Shard records accepted and paid | 72 of 72, 0.91 to 2.72 IGN a shard (90% of the credit), carried 180 to 226 blocks after the block |
|
||||||
|
| Segment records accepted | 0 of 9: refused by the chain rule as shipped (below) |
|
||||||
|
| Miner's cost | 117.86 to 104.90 MH/s, 13.0 MH/s = 11.0% (the shipped prover: 5.0 MH/s, 4.0%, for 2.8x fewer shards) |
|
||||||
|
| GPU memory peak | 16.5 to 17.6 GB with the miner resident; 24 GB tier unchanged |
|
||||||
|
|
||||||
|
**The second fault, found by the measurement: the chain rule as shipped.** `check_segment_record` accepts a fresh record (chain_len = N) only when the previous segment is UNPROVEN at the carrier. A segment's own deadline is its last block's DAA plus 600, the previous segment's deadline is 8 DAA earlier, so a fresh record is valid for 8 DAA (8 s on devnet) and must be proven before and carried inside them. The refusal on the live node, 9 times: "segment 114470..114477 does not chain to segment 114462..114469 (chain_len 8), which is pending until DAA 169681". The fast-time harness passed on 5 October because its chain continued from proven segments (case 2) and its fresh case ran exactly in that window (case 3, 4 DAA at N = 4). No prover-side move escapes it: the record must be proven, submitted and carried inside the window, which the 210-s proof cannot meet.
|
||||||
|
|
||||||
|
**The fix (fork branch 0f0dda95, behind a switch, never by default):** `proving_v1_fresh_rule_daa`; from it a fresh record is valid whenever the previous segment is not proven (pending or unproven) at the carrier; after a proven one a record must still chain. Two records that do not chain each attest their own blocks against the native statement, so nothing is lost but the longer proof chain, which restarts. In the consensus digest only once set (the v1 pattern), so a 0.3.12 node on the live devnet keeps the 0.3.11 digest until the override file sets it; `igneum_getProvingStatus.v1.freshRuleDaa/freshRuleActive` and `igneum_getSegmentStatement.freshAdmissible` report it. Tests: the digest moves once set; fresh after a pending segment passes from the switch, is refused before it, never after a proven one; the harness `--fresh-rule 0` inverts case 3. This is a rule change in the execution layer, not a parameter tuning, and the code proved it unavoidable: Josh's "no consensus parameter change unless the code proves it is unavoidable" is met by the 8-DAA window above and the nine refusals. The 0.3.12 coordinator sets the height (the same tip + 14,400 rule) in the override object with the release.
|
||||||
|
|
||||||
|
**Until the switch:** the app (272b025) holds a refused segment record and offers it again every pass until the segment's deadline, so on the shipped rule it lands only if a carrier falls inside the 8-DAA window, and from the switch it lands on the first retry; the shard records (90% of the credit) land either way, 8 per segment.
|
||||||
|
|
||||||
|
**Per tier, with this change and the switch:**
|
||||||
|
|
||||||
|
| Card | What it does | Per 30 min, empty blocks (measured on the 5090, approximate elsewhere) |
|
||||||
|
|---|---|---|
|
||||||
|
| 32 GB mining and proving (5090) | 9 whole segments, 72 shards paid, 9 aggregator shares once the switch is set | miner 11.0% down; 72 x 0.9 IGN = 65 IGN of shard payouts measured, plus 9 x 0.70 IGN aggregator share from the switch |
|
||||||
|
| 24 GB mining and proving | the same path at the 16.5 to 17.6 GB peak measured; the fee-switch shard not yet measured on a 24 GB card | approximate: the 5090's numbers |
|
||||||
|
| 16 GB | mines and proves on the patched server only (prover-floor rows); segment path untested there | pending the prover-floor agent's build |
|
||||||
|
| 12 GB prove-only | proves alone on the patched server (10.3 GB); a dedicated prover takes 4 s a block alone (agg-cost rows), so about one segment every 40 s | approximate: 45 segments per 30 min, 6 such cards for 100% |
|
||||||
|
| A rig (several cards) | one prover loop per machine today; the segment path claims one segment at a time on the aggregation card | the per-card loop is the next item |
|
||||||
|
|
||||||
|
## v1 live on devnet (6 October 2026, C47)
|
||||||
|
|
||||||
|
v1 active at 154,800 (crossed at DAA 154,814, 03:51:42Z); first segment record: none, because on a one-prover devnet no segment can be proven. The app's aggregator (`aggregate_once`, 0.3.11) needs a shard proof of every shard of every block of the segment in its node's pool, and PC 2 alone proves 13 shards per 10 minutes of about 600 blocks (2.2% coverage), so a run of 8 consecutive proven blocks never occurs: node 1 at 04:16Z reads segmentsInWindow pending 55, proven 0, unproven 20, paidSegments 0, and PC 2's app log (run win-1ccfe586-20261005-235130, 04:11Z to 04:16Z) reads every 42 s "aggregator: segment N..N+7: waiting for shard proofs N/0 ... N+7/0 in this node's pool", all 8 missing, each segment then past its 600-DAA deadline unproven. No fault in the node, the app or the record path; the fast-time harness passed because its shards ran at 90% coverage. Meanwhile the aggregator share (a tenth of every block's pool credit) stays in the escrow; shard payouts continue; miners and block watchers see nothing. What ends it: coverage at 8 consecutive blocks, 47 mining 5090-class cards with the shard loop as shipped in 0.3.11 (13 shards per 10 minutes a card), 18 mining cards through the chain mode, or 6 (approximate) proving-only cards, at 1 block/s on empty blocks (the fleet table above), or the segment length lowered on a small devnet (`proving_v1_segment_blocks`, a consensus param, so a digest change). No PC 2 job and no 0.3.12 item follow from this; the open item is the fleet, not the code.
|
||||||
|
|
||||||
|
## The empty `/api/state` reply (6 October 2026)
|
||||||
|
|
||||||
|
The aggregation-cost agent's jobs read the two bytes `{}` from `/api/state` on PC 2 at 22:22Z, 22:41Z and 00:18Z (0.3.10 and 0.3.11); the 21:01Z reply was full. Cause, from the app source and node 1's RPC: `ProvingState.paid_wei` is a `u128`, and serde_json's `to_value` refuses a u128 over u64::MAX (18,446,744,073,709,551,615 wei, 18.45 IGN); `state_json()` turned that refusal into `json!({})` with no log line. A paid shard is 1.23 IGN on average (node 1, `igneum_getProvingStatus`: 814.64 IGN over 663 shards at 00:3xZ), so the fifteenth paid shard after an app start empties the reply. PC 2's prover was blind to the root-owned socket from 20:00:56Z to 22:01Z (paid_wei stayed 0, hence the full reply at 21:01Z), proved from 22:02:13Z, and crossed 18.45 IGN inside its first 15 paid shards, before 22:22Z. Every app restart resets the counter, so the reply comes back for about 15 shards and goes again.
|
||||||
|
|
||||||
|
What it means: the dashboard on a proving machine shows nothing within about 12 minutes of its prover's first payout; every PC playbook that reads a card from `/api/state` fails the same way (the agent's job 5 reads settings.json instead). Mining, proving and payouts are untouched; it is the status page only.
|
||||||
|
|
||||||
|
Fix on the app branch: `paid_wei` serialises as a decimal string (the dashboard already reads it with `Number()`), `state_json` logs `[error] state_json: ...` once instead of answering `{}`, and the reply on any future serialisation error carries `error` and `version` rather than nothing; unit test `a_paid_total_over_u64_max_still_serialises_the_whole_state`. Not in 0.3.11 (that tree closed at 22c2363, master 630da6b, published); 6714a45 heads 0.3.12, the morning's first cut, app only, before PC 1's relaunch (coordinator, counter-asic-2-rollout.md 8a); until then the workaround is settings.json for the card keys.
|
||||||
|
|
||||||
|
## Aggregation cost (5 October, night)
|
||||||
|
|
||||||
|
Josh, 5 October 2026: "fix everything else in the numbers tonight". Branch `agg-cost`; every number in `docs/bench-log.md`, "aggregation cost on the RTX 5090", with its job id. The proof statement and the pinned guests are unchanged: every existing fixture proof still verifies (`verify-segment` 0.027 s on the Mac, 0.036 to 0.041 s on PC 2).
|
||||||
|
|
||||||
|
| What | Before (5 October evening, `chain-pc2-pv1c`) | After (5 October night) |
|
||||||
|
|---|---|---|
|
||||||
|
| Chained aggregation, the card mining | 9.6 to 9.7 s a block | 9.6 to 9.8 s a block, the same (job `agg-cost-pc2-1`, phase A); the miner's presence is the whole cost: 2.1 to 2.2 s a block with the card to itself, 1.7 s unchained |
|
||||||
|
| Shard proof (empty shard), the card mining | 7.3 to 7.7 s | 7.4 to 7.8 s; 1.9 to 2.2 s with the card to itself |
|
||||||
|
| The miner's slowdown of the prover | 3 to 4x (against 4 October) | measured on the same fixtures 2 min apart: shards 3.6x, chained aggregation 4.5x, a block 4.2x |
|
||||||
|
| Where the time goes | not profiled | the host's share 0.000 s (the prove call is everything); the GPU server prints no timings; the second deferred proof (the chain rule) costs 0.4 to 0.5 s alone and 1.7 to 1.9 s under the miner; the prover alone keeps the card busy 15.8% of the time, the miner 93.9% |
|
||||||
|
| SP1 knobs (`SP1_WORKER_VERIFY_INTERMEDIATES=false`) | not tried | no gain: 7.8 s against 8.1 s over four aggregations, inside the spread; the shape knobs would change the recursion keys the pinned verifier accepts |
|
||||||
|
| Batch fold (K blocks in one aggregator call) | not estimated | estimate from the measured step costs: 0.9 s a block alone and 3.8 s mining at K = 4, 0.7 and 2.8 s at K = 8 (0.25 s per further deferred proof alone, 1.8 s mining); a tree fold gains nothing. A new pinned guest and program id either way, so not tonight |
|
||||||
|
| Two prover processes on one card | not tried | closed on SP1 6.8.1: both share one GPU server socket, run slower together (6.9 s a block against 4.1) and the second dies with the first (`early eof`) |
|
||||||
|
| The miner's kernel length (`--batch-log2` of the CUDA worker, 2^B nonces a launch; job `agg-cost-pc2-6`, the 5090 alone with the job's own miner) | not tried | 2^22 (the default) and 2^20: 18.1 and 18.0 s a block, 10.0 to 10.4 s a chained aggregation, 104 MH/s; 2^18: 15.6 s, 8.8 s, 99 MH/s (minus 4%); 2^16: 11.1 s, 6.1 to 6.2 s, 84 MH/s (minus 19%), reproduced |
|
||||||
|
| The GPU time-slice policy (`nvidia-smi compute-policy --set-timeslice`) | not tried | "Not Supported" on PC 2 (driver 13.3, Windows): closed |
|
||||||
|
| The chosen combination | the defaults | the defaults stay: batch-log2 22 and SP1's default knobs. The one knob that moves the prover (2^16) costs a fifth of the hash rate all the time for a prover that is busy a few seconds a minute on the devnet; it is Josh's trade, not a default (below) |
|
||||||
|
|
||||||
|
Reading. The per-block aggregation is 2.1 s and a block 4.1 s on a 5090 that only proves, 9.7 and 17.5 s on one that also mines; no knob, fold or stream on tonight's SP1 changes the first pair, and only the miner's kernel length changes the second, at 1 MH/s per 0.37 s of block time. So "under 3 s a block" and "under 1.5x" are met on a card that is not mining and are not reachable on one that is. What that means per tier: a 5090 that mines and proves delivers a proven empty block every 17.5 s (6 cards for 1 block/s), the same card proving only every 4.1 s (2 cards, plus the shard work of full blocks: the fleet table above), and a batch fold of the aggregator (a new pinned guest) would bring the proving-only card to about 2.7 s a block and the mining one to about 12 s. What is being done: the app and host defaults are left as measured; the plan's open decision for Josh is whether a card that holds a shard assignment should drop to 2^16 for the proof's minute (1.6x faster proof, 19% of its hash rate for that minute) or whether proving-only cards carry the aggregation (the clean 2.1 s), and the batch fold goes on the next pin's list. The state class found on the way (`/api/state` answering `{}` once `paid_wei` passes u64::MAX, fixed on the app branch at 6714a45) is in the bench log with the rest.
|
||||||
|
|
|
||||||
193
docs/plans/release-0.3.12.md
Normal file
193
docs/plans/release-0.3.12.md
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
# Igneum Miner 0.3.12: the fresh-record rule switch (proving v1) and the app cut, prepared to the publish gate, 6 October 2026
|
||||||
|
|
||||||
|
Release engineer, from 08:05 UTC, on the coordinator's instruction ("prepare 0.3.12, APP ONLY, up to the publish gate and STOP there; Josh
|
||||||
|
gives the go"), widened at 08:55Z on its clock: "no longer app only", the proving agent proved the segment record rule needs a consensus
|
||||||
|
switch, so the node fork `proving-v1` 0f0dda95 (`proving_v1_fresh_rule_daa`: never by default, in the digest only once set; from it a fresh
|
||||||
|
segment record is valid whenever the previous segment is not proven) and the app's segment-aligned prover (272b025, docs aea2f6a) ride in it.
|
||||||
|
Worktree `/Users/joshm/Projects/igneum-wt-ship0312`, branch `release-0.3.12` from master ddfcdac; `vendor/` symlinked to the main checkout's (46
|
||||||
|
entries); the fork worktree `vendor/igneum-node-0312`, branch `release-0.3.12-node` = 0f0dda95 cherry-picked onto 89dfcb95 (its parent ece42979
|
||||||
|
is inside 89dfcb95, so the rebase is the one commit: `params.rs`, `exec/proving.rs`, `exec/rpc.rs`, `daemon.rs`) = **83089544**. The 0.3.11 recipe (`release-0.3.11.md`) throughout; every Mac build under the main checkout's lock;
|
||||||
|
igneum-josh commits. Times are UTC.
|
||||||
|
|
||||||
|
## 1. What 0.3.12 carries
|
||||||
|
|
||||||
|
| Change | Where | State |
|
||||||
|
|---|---|---|
|
||||||
|
| `/api/state` never answers `{}` again: `paid_wei` (u128) is a decimal string, the error reply is logged; test | `proving-v1` app 6714a45 (the first item) | merged 9bcf4cd (the `docs/bench-log.md` conflict: both sides kept, the log is append-only) |
|
||||||
|
| An update published over an hour before the engine started skips the hourly rollout slot; `manifest::unix_from_rfc3339` + tests | `update-catchup` 2207cd7 | merged b984c17 |
|
||||||
|
| The GPU list ordered by performance (usable, discrete before integrated, rate in 5 MH/s buckets, memory); 3 UI tests | `card-order` ffb2bfa | merged c6608c1 |
|
||||||
|
| HiveOS local mode carries the override (`OVERRIDE=` in the Flight Sheet's extra config, written to `data/override-params.json` by `h-run.sh`, C41), rigs mine only until a Linux prover ships, `IDENTITIES=auto` by VRAM, the per-card README table | `hive-words` 98271ff (packaging/hive only) | merged b0a6231 |
|
||||||
|
| Ember Tune: two-knob plans + priors + the UI line; every quit names its source (b671c8b); a second engine never runs the updater (e600e63, C35); no pipe into a second engine (8ab9068); `jobrun.rs` elevated `follow_file` (1e9550e); the BOM fix + CI check (8273494); Power control switch (49bbe14 = 3562f26); `igneum-gpu-telemetry.exe` (ADLX) built by `build-windows.sh` and carried in the Windows inputs | `ember-tune` 9a6469f | NOT MERGED: conflicts in seven files against the 0.3.10/0.3.11 app (its base ca8d9f3 predates both): `ci.yml`, `config.rs`, `engine.rs` (the detect path, the power-cap plan, the test module), `ui/app.js` (four hunks against miner-ui-2's View), `ui/index.html` (the settings panel 0.3.10 removed), `proto-opencl/README.md`, `bench-log.md`. Its agent is rebasing it onto release-0.3.12 (section 2) |
|
||||||
|
| The hidden-console builder for every elevated launch, `windows-spawn-check.mjs`, the PC 1 console-watch scripts | `job-console` 13755b9 (+ 3562f26 Power control) | NOT MERGED: conflicts in six files (`ci.yml`, `config.rs`, `engine.rs`, `jobrun.rs`, `app.js`, `index.html`), base a93199a; carried by the ember-tune rebase (it already holds 3562f26) |
|
||||||
|
| The miner's gRPC resubscribe after a node restart (C43, the 0.3.11 finding) | no commit exists (the ledger entries b19fe5f, 0751dde, c8c831c only) | OWED, listed in section 10 |
|
||||||
|
| The fresh-record rule switch: `proving_v1_fresh_rule_daa` (Option, never by default; a node with the field set prints it and carries it in the digest; a fresh segment record is valid from it whenever the previous segment is not proven) | fork `proving-v1` 0f0dda95 on ece42979 | cherry-picked onto 89dfcb95 as 83089544 (`release-0.3.12-node`) |
|
||||||
|
| The segment-aligned prover: a segment record the chain rule refuses is held and offered again every pass until the segment closes; the fast-time harness on the fresh-record rule (both cases); the prover host and export; the WSL2 prover package script; `infra/fast-time/override-60x.json` (measured by its agent: 9 segments per 30 min on PC 2, 72 of 72 shards paid, 11% hash cost, 17.6 GB peak) | `proving-v1` app 272b025 + docs aea2f6a (on 6714a45) | merged 49e0e2c (clean) |
|
||||||
|
| The packaged line (C34): the ten-field object of section 4 | 7dd3ff7 | `packaged-config.sh --test` passes |
|
||||||
|
| The six version files | 81e4ecb (`--check`: 0.3.12 in all 6) | |
|
||||||
|
|
||||||
|
Left out on the coordinator's word: prover-floor's server (its packaging row is 0.3.13), explorer d7e797c, pool-v0, rig-install, ota-k2, the
|
||||||
|
ledger forks.
|
||||||
|
|
||||||
|
Changelog line (draft, for the manifest notes at the go): "Igneum Miner 0.3.12: the fresh-record rule for proving v1 from DAA 192,000 (a fresh
|
||||||
|
segment record is valid whenever the previous segment is not proven) and the segment-aligned prover; the GPU list in performance order; an
|
||||||
|
old update no longer waits for the hour; Ember Tune (every card tuned for MH per watt, Power control off by default, the app never asks for
|
||||||
|
administrator rights on its own); a second engine never installs over the app; /api/state always answers; HiveOS rigs carry the override.
|
||||||
|
Node 83089544."
|
||||||
|
|
||||||
|
## 2. The branch
|
||||||
|
|
||||||
|
| Commit | What |
|
||||||
|
|---|---|
|
||||||
|
| 9bcf4cd, b984c17, c6608c1, b0a6231 | the four merges above, in the coordinator's order (proving-v1 first) |
|
||||||
|
| 81e4ecb | `Igneum Miner 0.3.12: the six version files` |
|
||||||
|
| ebea8b6 | the ember-tune rebase tip 7f6c4e6 (with job-console 13755b9 inside), merged as one branch (section 3) |
|
||||||
|
| 11e8ca6, ab01f48, 01abcc2 | the plan |
|
||||||
|
| 062c3f8 | `node-source.pin` 83089544 with the second inputs push (the Windows-build commit of 0.3.12) |
|
||||||
|
| 37b6a7f | `tools/proving-v1/pc2-agg-cost.ps1`: `pkill -f sp1-gpu-server` (the CI root-socket check) |
|
||||||
|
| 88df58e | master d3b64cb merged (docs only): the release tip, CI green |
|
||||||
|
| 6532adf | `make-payload.sh`: on CI the AMD telemetry helper is taken from the unpacked inputs (the worker glob `igneum-worker-*.exe` missed `igneum-gpu-telemetry.exe`, so the first payload, run 37435975425, shipped without it: the inputs had it, the zip did not). The CI commit of 0.3.12 |
|
||||||
|
|
||||||
|
Checks on 81e4ecb before the rebase landed: the app `cargo test --release -p igneum-app` under the lock: ok 115 (lib) + 28 (ota-sign) + 8
|
||||||
|
(prove-verify), 0 failed (08:19:22 to 08:19:28Z, warm target cloned from the 0.3.11 worktree); the UI tests `notices`, `update-card`, `view`:
|
||||||
|
23 of 23.
|
||||||
|
|
||||||
|
## 3. Builds and artefacts
|
||||||
|
|
||||||
|
| What | Command | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| The HiveOS package (first build, app-only cut) | the 0.3.11 node and workers with hive-words' scripts | 08:19:45Z: b0a20917... (24,501,272); superseded below once the node changed |
|
||||||
|
| The merged tree | ember-tune 7f6c4e6 (release-0.3.12 b0a6231 merged INTO ember-tune as c5918c7, job-console 13755b9 cherry-picked on top; 0.3.11's six-section View and card order kept whole, Ember Tune's TuneLine block and the Power control switch added in 0.3.11's markup, `engine.rs` keeps the detect arm with the tune fields in `hotplug::apply_pref`, both test modules, `jobrun.rs` the hidden-console builder plus `follow_file`) merged as one branch | **ebea8b6**, 08:22Z (the CI commit is 062c3f8); the version files still 0.3.12 in all 6; packaged line, `node-source.pin` and `vendor/` untouched against master |
|
||||||
|
| The app | `cargo test --release -p igneum-app` under the lock, then `cargo build --release` | 08:22:56 to 08:23:06Z: ok 133 + 28 + 8, 0 failed; `igneum-app 0.3.12` (2,273,664) |
|
||||||
|
| The UI and relay tests | `node --test` notices, update-card, view, tune-line; `relay/test/*.test.mjs` | 26 of 26; 23 of 23 |
|
||||||
|
| The CI checks on the Mac | identity, no-conflict-markers, copied-sources, signer-pipe, prover-socket, second-engine, bash-body (self-test + tree), kit-path (self-test + tree), windows-spawn (self-test + tree), pinned-guests, no-secrets, check-workflow-shell | all ok; `link-check` passes after `node site/build.mjs` (as ci.yml runs it: the committed `litepaper.html` points at `/bench#counter-asic-2-0-the-numbers`, an id the site build creates from `bench-log.md`) |
|
||||||
|
| The Windows workers and the AMD telemetry helper | `proto-cuda/nvrtc/build-windows.sh` (mingw) under the lock, 08:23Z | the worker SOURCES are unchanged against master (`git diff master HEAD -- proto-cuda proto-opencl proto-metal igneum-pow`: only `build-windows.sh`, the new `gpu-telemetry.c` and its `.rc`), so the inputs carry the 0.3.11-verified workers that mined all night, igneum-worker-cuda.exe 2b3b8c92885442179f6bf2907c6f3eb453dc4a19908d90fd05981a09b7c2674c (1,536,512) and igneum-worker-opencl.exe edc4a75da3b93d814caa69fd635010780d63d5b622ec24c3741d433c584f91e3 (478,208), not this morning's rebuild of the same sources (12bfaa27..., e0fd7042...: mingw PE builds are not byte-reproducible); NEW igneum-gpu-telemetry.exe 8d679b52b19af3cbd6bf4fd6f77d337b2fb78af13d02627e7aa7e92507993459 (387,584; ADLX, SetupAPI, PDH; the Igneum resources, version 0.3.0 as the workers carry) |
|
||||||
|
| The Windows inputs | `IGNEUM_WIN_RELEASE=<fork>/target-integration/x86_64-pc-windows-gnu/release IGNEUM_NODE_SRC=vendor/igneum-node-0312 packaging/windows/push-inputs.sh`, 08:24:29Z (a deploy of the downloads folder only; the manifest untouched) | node 89dfcb95 (igneumd.exe be8e83c0..., igneum-miner.exe 1ba1a249..., PC 2's 0.3.11 build), the two workers above, the telemetry helper, the mingw DLLs and nvrtc; signed, verified, live (HTTP 200); `node-source.pin` unchanged 89dfcb95 |
|
||||||
|
| The DMG (first build, app-only cut) | the 0.3.11 node | 08:24:33Z: ff630e9d... (41,630,620); superseded below once the node changed |
|
||||||
|
| The fork's Mac node, 83089544 | `CARGO_TARGET_DIR=vendor/igneum-node/target-0312 cargo build --release -j 4 -p kaspad -p igneum-miner --features kaspad/igneum-pow` from `vendor/igneum-node-0312`, under the lock; the target dir cloned by APFS from `target-0311`; `target-integration` in the fork worktree links to it | 08:38:13 to 08:41:31Z: igneumd **746a931fde9b840ca444a03cd757854e2e2ce7ebc4782ddd00ed161644d705f9** (41,386,160), igneum-miner 5381683e5717d91416c5a97456e0d050dd9645b1e27bce7d55808ce621cc1a26 (8,763,936); `igneumd/2.1.0-83089544` |
|
||||||
|
| The seed's Linux node (glibc 2.36 target, zig) | `NODE_SRC=<abs fork> TARGET_DIR=vendor/igneum-node/target-0312-linux OUT_DIR=<scratch>/cross infra/cross/build-linux.sh` under the lock | 08:38:21 to 08:41:18Z (175 s): igneumd **4f142d5148f218f1286e24e7c4a167aa2f54262336f96e7cf281f520c714fc6f** (47,919,144), igneum-miner 38397ae66c265b63db8e5458b46e7feb942121a7dc5625919df0a8d35e7a1ba1 (9,861,072); version.txt names 83089544 |
|
||||||
|
| The prover host and export (the pinned guests unchanged) | `cargo build --release -j 4 -p igneum-prove-export -p igneum-prove-host` in `proving/igneum-prove` under the lock (the target cloned from the 0.3.11 worktree) | 08:39:07Z: igneum-prove-host b90d58d0529ce29f0e7ca8ae780a6442f92edcf1c71752fc60fbc72bc5c11fd8 (58,626,560), igneum-prove-export b60056127d32bda363c0e305e73e9f699a5774c0988aee5bfd28a0fc61a56b9a (2,808,160); `--mode id`: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a, the pin of 0.3.9 to 0.3.11; `pinned-guests-check` ok, `proving/igneum-prove/elf/` untouched |
|
||||||
|
| The HiveOS package | `NODE_OUT=<scratch>/cross WORKERS_OUT=<the 0.3.11 Linux workers> VERSION=0.3.12 OUT=<scratch>/hive packaging/hive/make-hive-package.sh` (the Linux workers 4aaff27f.../82d90890... unchanged: their sources are) | 08:41:54Z: `igneum-hive-0.3.12.tar.gz` **7972af92e7cd9a032303eca4d95b533f53e0e68d1b9cae5bfe406a5b7c30a454** (24,506,282); `h-run.sh` writes `data/override-params.json` from `OVERRIDE` and starts the node with `--override-params-file` (the 0.3.11 open item closed); the node inside is 83089544 |
|
||||||
|
| The DMG | `NODE=<fork igneumd> MINER=<fork igneum-miner> PROVE_HOST/PROVE_EXPORT=<this tree's build> packaging/mac/build-dmg.sh` under the lock | 08:42:49Z: `Igneum-Miner-0.3.12.dmg` **7a4a5f5f772956e983127280a5ec62a4fcfaf903b3afa38fe3a89a37cee23520** (41,702,535), engine 0.3.12, node 83089544 (igneumd 41,163,744 inside, stripped by the DMG build), the new prover host and export, `igneum-bench` from `proto-metal/main.swift` (unchanged, 66ec0e78...), `packaged-config` carries the ten-field object, hdiutil checksum valid |
|
||||||
|
| The node suites with the igneum-pow feature (the coordinator's ask; the PC runner's test units carry no features field, so this is the Mac's run; the PC 2 run is owed to the Counter ASIC 3.0 coordinator's window, section 3a) | `CARGO_TARGET_DIR=vendor/igneum-node/target-0312 cargo test --release -j 4 -p kaspa-consensus -p kaspa-consensus-core -p igneum-exec -p kaspa-pow -p igneum-miner -p kaspa-p2p-flows --features kaspa-consensus/igneum-pow,kaspa-pow/igneum-pow` from the fork, under the lock, 08:39:31 to 08:45:01Z | igneum-exec 17 of 17, igneum-miner 18 of 18, kaspa-consensus 97 passed, 2 failed, 4 ignored. The two: (1) `pruning_proof::igneum_m20_tests::witnesses_are_checked_in_epoch_order_under_their_own_seeds` (`igneum_m20_tests.rs:122`: the expected `EpochSeeds.era` is all zeros, the code draws `515e...`: the test predates the era draw of class v3) FAILS THE SAME on 89dfcb95 (run 08:45:48Z on the 0.3.11 fork): the known M20 era fail, NOT fixed by 0f0dda95, still owed; (2) `finality::tests::ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list` (`finality.rs:1883`) failed inside the full crate run and PASSES alone on both 83089544 and 89dfcb95: order-dependent, not a regression of this cut, owed as flaky. kaspa-consensus-core 107 passed, 1 failed (`config::params::tests::fast_time_60x_file_is_the_devnet_at_60x`: `infra/fast-time/override-60x.json` does not parse into `OverrideParams`, "duplicate field `proving_v1_activation_daa`" at line 64: the file has carried a second proving-v1 block since c2544be on 5 October, so the test fails on master's file and on 89dfcb95 alike; not a 0.3.12 regression, the file is owed a dedupe), `db_compat` 7 of 7, kaspa-pow 14 of 14, kaspa-p2p-flows 33 of 33 (08:47:13Z, no fail-fast). Net: 3 failures, each present on 89dfcb95, none from 0f0dda95 |
|
||||||
|
| PC 2 build-and-suite job | `IGNEUM_WIN_RELEASE=<scratch>/pc2-out node tools/build-job.mjs run --node vendor/igneum-node-0312 --target 1ccfe586 --targets linux,windows --node-tests "kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows" --app-tests igneum-app` from this worktree, published 08:54:56Z on the prover-floor agent's "PC 2 is yours" (its floor-core-alone and floor-core-miner closed 08:48:37Z and 08:53:13Z, after the Counter ASIC 3.0 coordinator's release at 08:43:24Z); CPU only, the prover on, nothing else touched. The coordinator's later order (after the prover-floor agent's SECOND pair) arrived once the job had run; the app's queue serialised them anyway: this job ended 09:02:04Z and floor-build-6 started 09:02:05Z, then floor-core2-miner closed 09:13:22Z with the prover on and the miners never stopped, so nothing ran beside a GPU row | `build-20261006-085456`: started 08:55:24Z, done 09:02:04Z (400 s), every stage ok: Linux node 135 s (igneumd 34877b86..., igneum-miner c779777f...), Windows node 165 s (igneumd.exe 5bbcbd59f592fa31bf31c18516cef81cc0e7e537d398382fc8063e3402d80917, igneum-miner.exe af973318...; PC-built, NOT shipped: the inputs carry the Mac cross-build f580b4aa..., placed under the scratchpad), the app both targets; `RESULT test node [the six crates] exit 0 44 s` (without the igneum-pow feature, the runner's shape: the M20 era test and the fast-time file test are outside its reach there) and `RESULT test app/igneum-app exit 0 6 s` |
|
||||||
|
| The Windows node exes | `CARGO_TARGET_DIR=vendor/igneum-node/target-0312-win proto-cuda/windows-node/cross-build.sh <fork> 4` on the Mac (mingw, the 0.3.5/0.3.6/0.3.9 path; PC 2 is the Counter ASIC 3.0 coordinator's this morning), the target cloned from `target-release-win`, under the lock | 08:40:43 to 08:48:05Z (6 min 42 s): igneumd.exe **f580b4aad1e19a47742d0d836a56dad36b9380d3890ca115b1babced4d83a8db** (52,177,920), igneum-miner.exe 06c17d4c23c8b1327793bebcd9b2cba115045ea91b68baea8cb92b232a94678b (11,040,768); static (KERNEL32, advapi32, api-ms-win-core only) |
|
||||||
|
| The Windows inputs, second push | `IGNEUM_WIN_RELEASE=<target-0312-win>/x86_64-pc-windows-gnu/release IGNEUM_NODE_SRC=vendor/igneum-node-0312 packaging/windows/push-inputs.sh`, 08:48:28Z | node 83089544 (the two exes above), the 0.3.11-verified workers 2b3b8c92.../edc4a75d..., the telemetry helper 8d679b52..., the mingw DLLs and nvrtc; `payload-inputs.zip` f8e567bd164b382d32a33fb488df658fa68555092ac6bdac85387d0a8bd5d547 (65,259,161), signed and verified, live (HTTP 200); `node-source.pin` 83089544 committed as **062c3f8**, the CI commit of 0.3.12 |
|
||||||
|
|
||||||
|
| The Windows installer and zip, first runs | runs 37435975425 (ebea8b6, no telemetry helper) and 37436904041 (6532adf, the 0.3.11 node) | superseded |
|
||||||
|
| The Windows installer and zip | `windows.yml` run 37438673235 on 062c3f8 (dispatched 08:49:12Z after the second inputs push) | `Igneum-Miner-Setup-0.3.12.exe` **f11a296acf1ea3efa8a6151efa357cc5c222e3b2ffec5701ea4bcefe29307810** (45,270,093); `igneum-windows-app.zip` **a6f33ef21bb1d1682f48ca22332d50c0302f4b4f552a99157581f3249c42ea3b** (65,507,597): igneumd.exe f580b4aa... (the cross-build, 52,177,920), igneum-miner.exe, igneum-app.exe 0.3.12 (3,700,736), the two workers, `igneum-gpu-telemetry.exe` (387,584) this time, the mingw DLLs, nvrtc64_120_0.dll and nvrtc-builtins64_128.dll |
|
||||||
|
|
||||||
|
## 4. The override objects and the digests (the 0.3.12 Mac node 746a931f..., ports 60975/60976, 22 s each, under `run`)
|
||||||
|
|
||||||
|
| Override file | Lines | Digest |
|
||||||
|
|---|---|---|
|
||||||
|
| none | `igneumd/2.1.0-83089544`, no activation line | c562d70e1428c9789823cc40067623b4767f7c555ce7ff4ea11c1498f013ef6c, EQUAL to 0.3.11's no-file digest: the new field is never by default and leaves the digest alone until set |
|
||||||
|
| the fleet's live nine-field object | the six activation lines of 0.3.11, no fresh-rule line | **0139ab9dc2992d449ec787d8f021974933631eb55740ab4b6ce9d5c226e72888**, EQUAL to the fleet's digest today: publish 1 (the binary) changes no handshake, a 0.3.12 node and a 0.3.11 node on the nine-field file accept each other |
|
||||||
|
| the ten-field object at the FIRST pin (`proving_v1_fresh_rule_daa` 192000, void: the floor failed at the go) | the six lines plus the fresh-rule line at 192000 | bd786a4b521e87c05bce3da4c46b4f4696deb16dfbdc913f181c980a8eb51688 (never published) |
|
||||||
|
| the ten-field object as SHIPPED (`proving_v1_fresh_rule_daa` 198000) | the six lines plus `Proving v1 fresh-record rule from the override file: from DAA score 198000 a fresh segment record is valid whenever the previous segment is not proven` | **7bd98cc4118616455709d5e32a30b799e6e67caa42d2b5d09875cd49848a7ed7** (read 11:22:01Z on 746a931f...) |
|
||||||
|
|
||||||
|
The ten-field object (the packaged line 7dd3ff7, the manifest of publish 2, the hand nodes' and the seed's files at step 2):
|
||||||
|
|
||||||
|
```
|
||||||
|
{"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":84100,"fees_v1_activation_daa":210000,"finality_v3_activation_daa":135200,"program_class_v3_activation_daa":154800,"proving_v1_activation_daa":154800,"proving_v1_segment_blocks":8,"proving_v1_unproven_daa":600,"proving_v1_aggregator_share_bps":1000,"proving_v1_fresh_rule_daa":198000}
|
||||||
|
```
|
||||||
|
|
||||||
|
N was first pinned at 192,000 (tip + 14,400 for a publish near 10:15Z; the floor would hold while the tip was at or under 181,200, about
|
||||||
|
11:15Z). Josh's go came at 11:20Z with the tip at 181,582: the floor read 10,418, under 10,800, so N was RE-PINNED to 198,000 (tip + 14,400
|
||||||
|
for publish 2 near 11:55Z; the floor holds until tip 187,200, about 12:55Z): the packaged line 8a6b133, the DMG rebuilt 11:22:08Z
|
||||||
|
(7bcbb8a94038ea7a87ebfab514b6771f93b8fce2d991340bd5610713dc9548e5, 41,702,608), the installer rebuilt on CI (windows-ci 37455874734 on 8a6b133,
|
||||||
|
green 11:27:33Z: `Igneum-Miner-Setup-0.3.12.exe` a6b3ea275373411e9f988ecd4ecc79f2cda5f68d54d681662dcbf7590689aef0, 45,275,988; `igneum-windows-app.zip`
|
||||||
|
7ab28e772670dc58428cda4c9ff584b35f70507057d525a85d04391ee145dc53, 65,507,595), the digest re-read, publish 1 delayed by 8 minutes. The lesson for
|
||||||
|
the next cut: pin N at the go, not at the forecast, or pin with a 3,600 margin over tip + 14,400 when the go is more than an hour out. A 0.3.11 node given the ten-field file dies on the unknown field
|
||||||
|
(`deny_unknown_fields`), which is why publish 2 comes only after every node runs the 0.3.12 binary (the reviewer's C39, the 0.3.11 order).
|
||||||
|
|
||||||
|
## 5. The publish gate: what runs at Josh's go, in which order (the 0.3.11 two-publish shape)
|
||||||
|
|
||||||
|
This was the plan at the gate; sections 6 and 7 record what ran. Runbook: the session scratchpad's `r0312/rollout-0312.sh` (every step a function; `step_floor` before each publish).
|
||||||
|
|
||||||
|
| Step | What | Gate |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 the floor | `step_floor`: 192,000 minus the tip's DAA at least 10,800 | read before publish 1 and again before publish 2 |
|
||||||
|
| 1a the hand nodes, the seed | the observer and node 1 on the 0.3.12 binary with the NINE-field file (`IGNEUMD=<fork>/target-integration/release/igneumd IGNEUMD_COMMIT=83089544 infra/devnet/restart-hand-nodes.sh '<nine>'`), then the seed (`IGNEUMD_LINUX=<scratch>/cross/igneumd IGNEUMD_LINUX_SHA256=4f142d51... infra/devnet/restart-seed.sh '<nine>'`); every one prints 0139ab9d..., nobody is refused; then `step_mac_miners` (the Mac's miner does not reconnect to a restarted node 1 by itself, C43) | Josh's go |
|
||||||
|
| 1b publish 1 | `node tools/ship-app.mjs 0.3.12 --node vendor/igneum-node-0312 --branch release-0.3.12 --public --activation-height 154800 --deadline-note "program class v3 + proving v1" --notes '<section 1>' --from ci`: ci "already" (the green Windows run), fetch, dmg "already", copy, manifest with `consensus` CARRIED OVER (the nine-field object; the digest stays 0139ab9d...), deploy, verify (`--from console` after the public index settles at the edge), the console item; `--public` carries the HiveOS package 7972af92... | after 1a |
|
||||||
|
| 1c update-now | the Mac (d937c69d) first; the laptop (37ba0461) with it if it is on the air; PC 2 (1ccfe586) on the Counter ASIC 3.0 coordinator's word (PC 2 is its this morning; the proving agent's constraints: the app's prover stays on, no quit or restart of anything but the update's own); PC 1 (ae432dc7) last, once Josh has relaunched its app (down since 22:31:06Z yesterday, on 0.3.10: it takes the nine-field object and 0.3.12 at its relaunch through the manifest; Power control is off by default so nothing asks for administrator rights) | each machine's STATUS line back on 0.3.12 with 0139ab9d... |
|
||||||
|
| 2a the floor again | `step_floor` | >= 10,800 or re-pin |
|
||||||
|
| 2b the hand nodes, the seed | the same two scripts with the TEN-field file; each prints bd786a4b... and refuses the nine-field side until it switches; `step_mac_miners` again | every app node on the 0.3.12 binary (1c) |
|
||||||
|
| 2b publish 2 | `publish-manifest.sh --version 0.3.12 --override '<ten>' --activation-height 192000 --deadline-note "proving v1 fresh-record rule" --notes '<section 1>' --public --deploy` | after the hand nodes |
|
||||||
|
| 2b update-now (switch) | the Mac and the laptop, then PC 2 on the 3.0 coordinator's word, then PC 1: each app writes the ten-field file at the manifest take and restarts its node at a safe moment (the Mac's node is node 1, already switched: nothing to restart) | |
|
||||||
|
| the sweep | every node prints bd786a4b521e87c05bce3da4c46b4f4696deb16dfbdc913f181c980a8eb51688; the fresh-record rule arms at DAA 192,000 | |
|
||||||
|
|
||||||
|
One line for Josh, per machine, when he says go: the Mac and PC 2 each restart their engine once for 0.3.12 (under a minute, the miner back on
|
||||||
|
the next template) and their node once more for the fresh-record switch (a few seconds, mining resumes on the same chain); PC 1 does the
|
||||||
|
same at its relaunch and, with Power control off by default, never asks for administrator rights again (its 5090 runs uncapped until he
|
||||||
|
switches Power control on in Settings); the observer, node 1 and the seed are restarted by hand twice; from DAA 198,000 (about 15:55Z) a
|
||||||
|
prover may file a fresh segment record whenever the previous segment is not proven, so paid segments stop stalling behind an unproven one;
|
||||||
|
until the switch nothing changes in consensus (digest 0139ab9d... through publish 1).
|
||||||
|
|
||||||
|
## 6. The rollout (Josh's go 11:20Z through the coordinator; two publishes)
|
||||||
|
|
||||||
|
Baseline 11:20:22Z: tip 181,582; the observer, node 1 and the seed on 89dfcb95 at 0139ab9d; the Mac app 0.3.11 (its miner PAUSED since
|
||||||
|
07:10Z on Josh's order "stop mining on the Mac", not the C42 class: I resumed it once at 11:26:11Z before the order reached me and the
|
||||||
|
coordinator re-paused it; it stays paused, no restart-miners after the hand restarts); PC 2 0.3.11 at 113 MH/s; PC 1 0.3.11, relaunched by
|
||||||
|
Josh at 11:17Z with the 5090 and a 4070 in the enclosure; the laptop and Sam's Mac off the air.
|
||||||
|
|
||||||
|
| Step | Time | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 the floor | 11:20:22Z | 10,418 < 10,800: FAILED at 192,000; re-pinned to 198,000 (section 4), publish 1 delayed to the installer rebuild |
|
||||||
|
| 1a the observer, node 1 | 11:22:12Z (pid 92464), 11:22:24Z (pid 92624) | `igneumd/2.1.0-83089544` on the nine-field file, digest 0139ab9d... (unchanged, nobody refused) |
|
||||||
|
| 1a the seed | 11:22:45Z (MainPID 136418) | the same binary 4f142d51..., the same digest |
|
||||||
|
| 1a restart-miners, the Mac | 11:22:56Z | ran; nothing to restart, the miner is paused on Josh's order (above) |
|
||||||
|
| 1b publish 1 | the ship 11:28:30 to 11:31:55Z from cf1ad2b (master f11b02e merged first: the preflight refuses a tree behind origin/master) | ci "already" (37455874734), fetch "already" (the re-pinned installer), dmg "already", copy ok, manifest 0.3.12 with `consensus` CARRIED OVER (the nine-field object, activation 154800), deploy ok, verify refused the public index at the edge (every cut); `--from console` 11:44:41Z: item #368 |
|
||||||
|
| 1c update-now, the Mac | 11:32:18Z | engine restart 11:32:56Z (run `mac-d937c69d-20261006-113256`), "updated to Igneum Miner 0.3.12 from 0.3.11", STATUS "0.00 MH/s, paused, node 5 peers, synced" (node 1) |
|
||||||
|
| 1c update-now, PC 2 | 11:32:46Z (the 3.0 coordinator's mkdir lock `/tmp/igneum-devnet/pc2-ca3.lock` absent; `pc2-ca3.clear` is a note, not a lock) | engine restart 11:33:41Z (run `win-1ccfe586-20261006-113341`), igneumd 83089544 started 11:33:45Z on the nine-field file (0139ab9d), worker ready 11:34:39Z, mining 11:34:42Z, 0 faults |
|
||||||
|
| 1c update-now, PC 1 | 11:33:28Z (on the prover-floor agent's "PC 1 build closed" 11:31:34Z and the coordinator's "PC 1 back") | the installer downloaded and verified 11:34:06Z, "per-user install, no administrator prompt", engine restart 11:34:12Z (run `win-ae432dc7-20261006-113412`), cards "RTX 5090, RTX 4070 [discrete], AMD integrated [off]", STATUS mining 11:35:13Z, the 5090's race base 140.2 MH/s, digest 0139ab9d |
|
||||||
|
| 2a the floor | 11:36:53Z | tip 182,570; 15,430 >= 10,800 at 198,000 |
|
||||||
|
| 2b the observer, node 1 | 11:36:55Z (pid 13642), 11:37:07Z (pid 13777) | the ten-field file, digest **7bd98cc4118616455709d5e32a30b799e6e67caa42d2b5d09875cd49848a7ed7** |
|
||||||
|
| 2b the seed | 11:37:25Z (MainPID 136590) | 7bd98cc4... |
|
||||||
|
| 2b publish 2 | 11:37:36Z | `publish-manifest.sh --version 0.3.12 --override '<ten>' --activation-height 198000 --deadline-note "proving v1 fresh-record rule" --public --deploy`; the HiveOS package 7972af92... served at `/public/igneum-miner-hive.tar.gz` and `dl/public/igneum-hive-0.3.12.tar.gz` (HTTP 200, 24,506,282), the 0.3.11 package removed |
|
||||||
|
| 2b switch, the Mac | 11:40:30Z | ran 11:40:58Z: "consensus override changed; the node restarts with it at a safe moment"; its node is node 1 (external), already on 7bd98cc4, nothing to restart |
|
||||||
|
| 2b switch, PC 2 | 11:40:56Z | ran 11:41:23Z, "restarting the node with the new consensus parameters", igneumd started 11:41:25Z (pid 18732) on 7bd98cc4..., mining again 11:43:42Z, 113.0 MH/s at 11:44:42Z |
|
||||||
|
| 2b switch, PC 1 (last) | 11:42:54Z | ran 11:43:28Z, node restarted 11:43:29Z (pid 5556) on 7bd98cc4..., "waiting" 11:43:44 to 11:44:14Z, mining 11:44:44Z, 100.95 MH/s ramping at 11:45:14Z: its miners read 0 MH/s for about a minute after the node restart before coming back (the C43 class: the miner waits out the restarted node instead of resubscribing at once; the coordinator's note); the Ember Tune run 3 on PC 1 (ember-tune-pc1-3, 11:44:58Z) then took the box, after this restart, not under it |
|
||||||
|
| the laptop, Sam's Mac | off the air | they take 0.3.12 and the ten-field object through the manifest when they return; no 0.3.11 app was on the air to take the ten-field file before its binary (C39) |
|
||||||
|
|
||||||
|
## 7. The digest sweep (closed 11:45:20Z)
|
||||||
|
|
||||||
|
| Node | Binary | Digest | Since |
|
||||||
|
|---|---|---|---|
|
||||||
|
| the observer | `igneumd/2.1.0-83089544` (746a931f...) | 7bd98cc4... | 11:36:55Z |
|
||||||
|
| node 1 | the same | 7bd98cc4... | 11:37:07Z |
|
||||||
|
| the seed | 83089544 (4f142d51..., glibc 2.36 target) | 7bd98cc4... | 11:37:25Z |
|
||||||
|
| PC 2 | the installer's igneumd.exe f580b4aa... (the Mac cross-build) | 7bd98cc4... | 11:41:25Z |
|
||||||
|
| PC 1 | the same | 7bd98cc4... | 11:43:29Z |
|
||||||
|
| the Mac | attached to node 1 | node 1's | 11:37:07Z |
|
||||||
|
| the laptop, Sam's Mac | 0.3.10 / 0.3.9 | pending | off the air |
|
||||||
|
|
||||||
|
Tip 183,154 at 11:45:20Z, no refusals on the hand nodes after the switch; the fresh-record rule arms at DAA 198,000 (about 15:55Z at 0.98 DAA/s).
|
||||||
|
The fleet during the window: PC 2 and PC 1 mined on 0139ab9d while the hand nodes and the seed were on 7bd98cc4 (11:37 to 11:41Z); each
|
||||||
|
rejoined at its switch; the Mac's miner paused throughout on Josh's order.
|
||||||
|
|
||||||
|
## 8. CI
|
||||||
|
|
||||||
|
| Run | On | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| `ci` 37435705568 | ebea8b6 (the branch push, 08:22:40Z) | green (pow tests and census build, simulators, site build + link check + identity, the PowerShell 5.1 parse job) |
|
||||||
|
| `windows-ci` 37435975425 | ebea8b6 | green 08:30:58Z (the parse job 08:25:16 to 08:25:56Z; engine, window host, payload, installer, smoke run 08:26:00 to 08:30:58Z); superseded by the run below (no telemetry helper in its payload) |
|
||||||
|
| `ci` 37436904569 | 6532adf (the branch push, 08:33Z) | (pending) |
|
||||||
|
| `windows-ci` 37436904041 | 6532adf | green 08:39:20Z; superseded by the run below (the node changed) |
|
||||||
|
| `ci` 37436904569 | 6532adf | green |
|
||||||
|
| `windows-ci` 37438673235 | 062c3f8 (`gh workflow run windows.yml --ref release-0.3.12`, 08:49:12Z, after the second inputs push) | green 08:54:27Z (the parse job 08:49:18 to 08:49:59Z; engine, window host, payload, installer, smoke run 08:50:02 to 08:54:27Z against the 83089544 inputs); fetched 09:03:17Z with `OTA_SKIP=1 CONSOLE_SKIP=1 packaging/windows/fetch-ci-artifacts.sh 37438673235` into the downloads folder, NOT deployed |
|
||||||
|
| `ci` 37438674529 | 062c3f8 | FAILED in one step, `prover-socket-check.sh`: `tools/proving-v1/pc2-agg-cost.ps1` (in through the proving-v1 merge) ends its root prover with `pkill -x sp1-gpu-server`, and the check wants `pkill -f`; the line now reads `pkill -f ... ; rm -f /tmp/sp1-cuda-*.sock` (one playbook line, nothing the app or the packaging reads; `git diff 062c3f8 <fix> -- app packaging proto-cuda proto-opencl proto-metal vendor` is empty, so the Windows artefacts of 37438673235 stand, as 0.3.11's did across 3b0262f and 2a62735); the rerun is the row below |
|
||||||
|
| `ci` 37440456687 | 37b6a7f (the one-line playbook fix) | green 09:07Z |
|
||||||
|
| `ci` 37440559793 | 88df58e (master d3b64cb merged in: the morning summary, docs only; the release tip) | green 09:08:08Z. The 0.3.12 CI verdict is therefore run 37440559793 on 88df58e; the Windows build is run 37438673235 on 062c3f8, the same app, packaging and node sources (`git diff 062c3f8 88df58e -- app packaging proto-cuda proto-opencl proto-metal vendor igneum-pow` is empty) |
|
||||||
|
|
||||||
|
The ship state file `~/.cache/igneum/ship/0.3.12.json` carries `sha` = 062c3f8 (the Windows-build commit, which the ci step looks up by commit; the tree is 88df58e, docs and one playbook line later, as 0.3.11's was two docs commits past its build commit),
|
||||||
|
`forkCommit` 89dfcb95, `bumpedAt` before the DMG's mtime (so the dmg step reads "already"), and `runId` once the Windows run is green.
|
||||||
|
|
||||||
|
## 9. Owed to the next cut (0.3.13)
|
||||||
|
|
||||||
|
| Item | What |
|
||||||
|
|---|---|
|
||||||
|
| C43, the miner's dead gRPC channel | no commit exists; `igneum-miner mine grpc://` must re-subscribe after its node restarts (the 0.3.11 finding: 11 min of 26 MH/s burned on the Mac); until then every hand restart of node 1 is followed by `restart --what miners` |
|
||||||
|
| prover-floor's server | its packaging row is 0.3.13 (the coordinator's word) |
|
||||||
|
| explorer d7e797c, pool-v0, rig-install, ota-k2, the ledger forks | left out on the coordinator's word |
|
||||||
|
| the Windows workers' reproducibility | mingw PE builds differ byte-for-byte between builds of the same sources (12bfaa27 vs 2b3b8c92 today); a `-Wl,--no-insert-timestamp` (or `SOURCE_DATE_EPOCH`) in `build-windows.sh` would make G5's one-commit rule checkable by hash |
|
||||||
|
| the site build's non-idempotence and the pre-push flip | unchanged from 0.3.10 and 0.3.11 |
|
||||||
150
docs/plans/release-0.3.13.md
Normal file
150
docs/plans/release-0.3.13.md
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# Igneum Miner 0.3.13: node-only, the execution layer follows again and the finality route; prepared to the publish gate, 6 October 2026
|
||||||
|
|
||||||
|
Release engineer, from 13:05 UTC, on the coordinator's instruction: "prepare, so it ships the moment the fix lands: a release-0.3.13 branch,
|
||||||
|
NODE-ONLY". Worktree `/Users/joshm/Projects/igneum-wt-ship0313`, branch `release-0.3.13` from master 19edae0; the fork worktree
|
||||||
|
`vendor/igneum-node-0313`, branch `release-0.3.13-node`, at 83089544 (the 0.3.12 node) until the two node fixes land on it; `vendor/`
|
||||||
|
symlinked to the main checkout's. The 0.3.12 recipe throughout; igneum-josh commits; times UTC.
|
||||||
|
|
||||||
|
## 1. Why, and what it carries
|
||||||
|
|
||||||
|
Since the publish-2 restarts of 0.3.12 (about 11:37Z) the execution layer is dead on every node: `eth_blockNumber` answers `0x0` and
|
||||||
|
`igneum_getProvingStatus` reads `active: false, paidShards 0, paidWei 0x0` (the observer at 13:05Z). The devnet's pruning point left genesis
|
||||||
|
today, and the follower, which walks from genesis in memory, can no longer start; `--archival` does not help an existing datadir.
|
||||||
|
|
||||||
|
| Change | Where | State |
|
||||||
|
|---|---|---|
|
||||||
|
| The execution layer follows again: the exec state persisted to the data dir every 5 min and at stop, resumed at start (`--igneum-exec-snapshot=<path>[,<sha256>]`, `igneum_exportExecSnapshot`, the loud "exec not synced" status); the snapshot served and fetched over p2p (protocol 16, messages 76 and 77); the archival walk through the ghostdag store when the virtual-chain query refuses a tip below the retention root; `exec_restart_number`, `exec_restart_hash` and `exec_restart_trust_daa` in the override object (in the digest once set: without them a node on this build stays blocked, the bodies below 27,276 being gone on every hand) | the proving agent's `exec-sync-0313` 05e93f0e (7 commits on 83089544) | merged onto the route fix as release-0.3.13-node **a9dfe78e** (clean, 23 files). Measured by its agent on a copy of node 1's data dir: 27,276 header-only records, the EVM state restarted at chain block 27,276 and re-executed to the sink (130,272) in 93 s; paidShards 1,482, paidWei 1,825.70 IGN; the state persisted (114.8 MB) and resumed in 9 s |
|
||||||
|
| The finality route (the fleet's finding, 26 fresh nodes): a certificate for an index below this node's window is ignored (the seed re-locked index 2954 2,811 times in seven minutes and the echo filled every fresh peer's route); the IgneumFinality route takes a checkpoint burst (4,096); a full route drops the message and keeps the peer; votes are skipped during IBD | fork branch `fin-route-0313` 5a339733 (the bench-log entry `fin-route` 05d0944, merged e6c939e) | in: p2p 33, flows 19, finality 12 tests green; harness s7 PASS |
|
||||||
|
| The trust window off when `exec_restart_trust_daa` is never (05e93f0e read `carrier_daa < trust` with trust at u64::MAX, always true, so a node WITHOUT the field had the native-statement veto and the assignee check off and would have paid any carried shard record); `startedFrom` reads "genesis" when the follower executed genesis | the proving agent's 78c7f961 (fe6756da inside), found by the igneum-exec test at `proving.rs:1171` on the merged tree | merged as release-0.3.13-node **544fc30f**; igneum-exec 18 of 18 |
|
||||||
|
| The Power Helper (Josh, 11:50Z: one administrator approval, ever: the first Power control action registers a per-user elevated scheduled task, no prompt after), the engine folder lock's ACL (`user:(OI)(CI)F` without /T), the verbatim settings copy for a measurement engine, the watchdog, no firewall prompt for a sweep engine, the jobrun `follow_file`, `tools/ci/playbook-quit-check.sh` (the 5 October rule as a gate; the two agg-cost scripts allow-listed under a dated note, db97665 drops them in the next cut with the aggregation-cost agent's `--stop-miners` change) | `ember-tune` 32b2688 (07d5a72 + master 494c9c7 merged in + the dry-run-3 bench entry), on the coordinator's condition: the Ember agent's unelevated dry run 3 on PC 1 PASSED (ember-dryrun-pc1-3, 14:56:18 to 15:02:08Z, exit 0: the 5090 127.31 MH/s at 316.5 W, the 4070 28.68 MH/s at 102.7 W, nothing set, no prompt) | merged 71f08d5 (07d5a72) then **4deea56** (32b2688); app tests 146 + 28 + 8, UI 26, relay 23, every CI check green including playbook-quit |
|
||||||
|
| A fresh node (the proving agent's rented 4090, 14:04Z): on the branch as first merged it executed genesis BEFORE IBD, and after IBD its bodies started at the pruning point, so the follower never proceeded and said nothing: every new 0.3.13 install would end with an empty EVM, silently. ecdccef3: nothing executes before consensus is synced (the sink within 10 minutes of the clock), the exec restart is retried every pass until consensus knows chain block 27,276, a walk that meets missing bodies below the retention root sets `blocked` and asks a peer for the snapshot; 8fe28de9: a flag's snapshot file whose tip consensus does not know yet is retried for 10 minutes | the proving agent's ecdccef3 (8fe28de9 inside), the coordinator's "take it" 15:1xZ; its harness PASSED on it 15:09:48Z (12 checks, both halves) | merged as release-0.3.13-node **bb43e9a8** (2 files, no params or proto change); igneum-exec 18 of 18; the digests unchanged |
|
||||||
|
| The six version files | 7c9b00f (`--check`: 0.3.13 in all 6) | |
|
||||||
|
|
||||||
|
A consensus change after all: the three exec-restart fields enter the digest once set, so this is the 0.3.12 two-publish shape (section 2),
|
||||||
|
not the one carried-over publish first planned. The thirteen-field object (the packaged line 71cb8a4, publish 2, the hands' and the seed's
|
||||||
|
files at step 2; `exec_restart_trust_daa` 200,000 pending the coordinator's word):
|
||||||
|
|
||||||
|
```
|
||||||
|
<the ten-field object of 0.3.12> + "exec_restart_number":27276,"exec_restart_hash":"bb45cf0dd2d7cc97ebfa5a2701527c09a8ede5d32de74efead9caa293b15688a","exec_restart_trust_daa":200000
|
||||||
|
```
|
||||||
|
|
||||||
|
PROTOCOL_VERSION 15 to 16: the handshake takes the lower version, a 0.3.12 and a 0.3.13 node peer during the window. The cut is no longer
|
||||||
|
node-only: the Ember tip rides in the app (above), so the Windows app build reran with the node fix.
|
||||||
|
|
||||||
|
### 1a. The devnet's one-time state reset (the coordinator's question, answered plainly)
|
||||||
|
|
||||||
|
No verified snapshot at chain block 27,276 exists. The executor starts at chain block 27,276 (DAA 45,537, the pruning point of 11:40Z) from an
|
||||||
|
EMPTY EVM state (`daemon.rs`: "Exec restart from the override file: the EVM state restarts empty at chain block {}"; 3dd9b2c9: "with
|
||||||
|
header-only records below it") and executes forward from the stored bodies; `exec_restart_hash` bb45cf0d... is that chain block's hash (where,
|
||||||
|
not a state root), and nothing is verified against a header's state root because there is no prior state to verify. Gone: every balance,
|
||||||
|
contract and nonce from before chain block 27,276 (the coinbase credits of the chain's first 45,537 DAA, the txgen harness wallets' transfers
|
||||||
|
from the relay tests, any contract state). Back, re-derived: coinbase credits from 27,276 on, every shard payout record (proving v0 began at
|
||||||
|
DAA 84,100, above the restart, so the proving ledger re-derives whole) and the fee flows after it.
|
||||||
|
|
||||||
|
| Reading | Before 11:37Z (node 1) | After the restart (the copy, 13:43Z) |
|
||||||
|
|---|---|---|
|
||||||
|
| `igneum_getProvingStatus` paidShards / paidWei | 663 / 814.64 IGN at 00:3xZ; 1,261 / 1,573 IGN at 08:30Z | 1,482 / 1,825.70 IGN (higher: it grows with the chain, nothing of it is lost) |
|
||||||
|
| PC 2's payout address 0xcafc6e74...516a | not read (no balance reading before 11:37Z exists anywhere: the hub's intake carries status lines, not balances) | 267,648 IGN (the rewards of chain blocks 27,276 to 130,272) |
|
||||||
|
| node 1's, PC 1's payout addresses | not read | re-derived from 27,276 on, as PC 2's |
|
||||||
|
| the first 45,537 DAA (chain blocks 1 to 27,275) | about 124,600 IGN of producer rewards (the subsidy 3.17 IGN at genesis rising to 3.67 at DAA 45,537 on the launch ramp, one blue block a second, the producer share 80%) and about 31,100 IGN of pool escrow (the proving agent's computation from `consensus/core/src/igneum.rs`, approximate) | gone; not attributable to addresses (the coinbase payloads with the IGNA payout addresses are in the pruned bodies, and the header's vote-key fallback names another address) |
|
||||||
|
| chain block 1 to 27,275 on the explorer | bodies and state | header-only; history below 27,276 is honest only as headers |
|
||||||
|
|
||||||
|
The chain, the finality locks and the hash are untouched; the reset is of the execution layer's state, once. Josh approved the one-time reset
|
||||||
|
with the go (15:20Z).
|
||||||
|
|
||||||
|
## 2. The order at the go (the coordinator relays it; nothing below runs before)
|
||||||
|
|
||||||
|
Runbook: the session scratchpad's `r0313/rollout-0313.sh`. The 0.3.12 shape: publish 1 the binary with the TEN-field object (digest
|
||||||
|
7bd98cc4... unchanged, no window), publish 2 the THIRTEEN-field object (a new digest, one window per side).
|
||||||
|
|
||||||
|
| Step | What | Check |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 the baseline | `step_check_observer`: `eth_blockNumber` 0x0 and `igneum_getProvingStatus` inactive on the observer today; node 1 read 814.64 IGN over 663 shards at 00:3xZ and 1,573 over 1,261 at 08:30Z (it grows with the chain) | the numbers to beat after the switch: paidShards >= 1,482, paidWei >= 1,825 IGN |
|
||||||
|
| 1a the hand nodes, the seed | `step_1a_hand_nodes`, `step_1a_seed`: the 0.3.13 binary with the SAME ten-field file; `step_mac_miners` only if the Mac mines (paused on Josh's order since 07:10Z) | each prints 7bd98cc4...; `igneum_getExecStatus.blocked` says why the exec layer waits (the three fields are not set yet) |
|
||||||
|
| 1b publish 1 | `step_1b_ship` (`--from ci`): consensus CARRIED OVER (the ten-field object), the DMG, the installer and zip from the Windows run, HiveOS with `--public` | the live manifest 0.3.13, digest unchanged |
|
||||||
|
| 1c update-now | PC 1 FIRST (Josh at its screen for the Ember click; the coordinator's 15:19Z order), then the Mac (its node is node 1: the engine alone), then PC 2 | each app's STATUS on 0.3.13, its node on 7bd98cc4; the coordinator told the second PC 1 shows 0.3.13 (the Ember elevated table run then takes PC 1 for about 45 minutes) |
|
||||||
|
| 2a the hand nodes, the seed | `step_2b_hand_nodes`, `step_2b_seed`: the thirteen-field file | each prints the new digest; within about 2 minutes `eth_blockNumber` climbs to the sink, `igneum_getExecStatus` reads `startedFrom "restart at chain block 27276"` (then "snapshot" on every restart after), `blocked null`, and `igneum_getProvingStatus` reads active with paidShards >= 1,482 and paidWei >= 1,825 IGN |
|
||||||
|
| 2b publish 2 | `step_2b_manifest`: the thirteen-field object, `--activation-height 198000`, the note names the exec restart | the live manifest carries the three fields |
|
||||||
|
| 2c the switch jobs | `step_2b_update_now` Mac (nothing to restart: node 1), then PC 2; PC 1's switch ONLY on the coordinator's "Ember closed" (a node restart under a step aborts the run) | each PC's node restarts once (seconds), its `[proving]` lines resume within minutes, its digest the new one |
|
||||||
|
| 3 the sweep | every node on the new digest, exec climbing, proving active; the fleet's rented nodes take the thirteen-field object through their operator (the fleet agent) | the per-machine times in section 4 |
|
||||||
|
| 3a a FRESH install | the proving agent's rented 4090 joins from scratch on the branch build the minute I send "publish 1 done" (the snapshot path: a 0.3.13 hand serves it over protocol 16) and again after publish 2 (the restart path): the start time, IBD done, the minute `eth_blockNumber` reached the sink, `startedFrom` and `blocked` then | the row in section 4: time to the executed tip |
|
||||||
|
|
||||||
|
## 3. Builds and artefacts (to fill when the fork tip is set)
|
||||||
|
|
||||||
|
| What | Command | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| The fork's Mac node, a9dfe78e | `CARGO_TARGET_DIR=vendor/igneum-node/target-0313 cargo build --release -j 4 -p kaspad -p igneum-miner --features kaspad/igneum-pow` from `vendor/igneum-node-0313`, under the lock (the target cloned from the route fix's) | 14:51:39 to 14:55:3xZ: igneumd **ef76ff5c1371317d783330e460ad4f6a1a8b3f2cdc55b228362ee64a227b30fa** (41,686,528), igneum-miner b7926642... (8,763,888); `igneumd/2.1.0-a9dfe78e` |
|
||||||
|
| The seed's Linux node (glibc 2.36 target, zig) | `NODE_SRC=<abs fork> TARGET_DIR=vendor/igneum-node/target-0313-linux OUT_DIR=<scratch>/r0313/cross infra/cross/build-linux.sh` under the lock | 14:51:47 to 14:55:12Z (203 s): igneumd **c7c696c5fa915350993b29378d3fceb252b88a1af880f6051472aef82f796777** (48,246,888), igneum-miner 77ab2e08... (9,860,400); handed to the fleet agent with the thirteen-field file at 14:56Z |
|
||||||
|
| The Windows node exes | `CARGO_TARGET_DIR=vendor/igneum-node/target-0313-win proto-cuda/windows-node/cross-build.sh <fork> 4` on the Mac (mingw) under the lock | 14:51:54 to 14:55:1xZ: igneumd.exe **f4e9ef8a83464535aa314e390682acb0ee0e23ceffea09815d546f5fd1ad90ae** (52,518,912), igneum-miner.exe 574adb79... (11,039,232) |
|
||||||
|
| The inputs, the pin | `IGNEUM_WIN_RELEASE=<target-0313-win>/x86_64-pc-windows-gnu/release IGNEUM_NODE_SRC=vendor/igneum-node-0313 packaging/windows/push-inputs.sh`, 14:56:51Z; the 0.3.11-verified workers 2b3b8c92.../edc4a75d... and the telemetry helper 8d679b52... unchanged | `payload-inputs.zip` 2327e1da165afbc2194fc7cd1f1be67c509b6845f6d7c032f6878a00a878991d (65,393,804), signed, live; `node-source.pin` a9dfe78e committed as **0c4f90e** (the CI commit) |
|
||||||
|
| The digests on the Mac node ef76ff5c... (ports 60995/60996, 22 s each, under `run`) | the ten-field file: **7bd98cc4...** (unchanged: publish 1 changes no handshake); the thirteen-field file: **b18ed271f75dd46406d230f4156c37472127415a4c32c558bac662f6f840e61c** with `Exec restart from the override file: the EVM state restarts empty at chain block 27276 bb45cf0d...` | |
|
||||||
|
| The DMG | `NODE=<fork igneumd> MINER=<fork igneum-miner> PROVE_HOST/PROVE_EXPORT=<the 0.3.12 build, unchanged> packaging/mac/build-dmg.sh` under the lock | 14:58:18 to 14:58:5xZ: `Igneum-Miner-0.3.13.dmg` **90864092fb69a90c0bd90fbfba91f24f9663499252c86e453abe3fa5dedbe716** (41,879,478), engine 0.3.13, node a9dfe78e (41,462,352 inside), the prover host ce03ceb5..., `igneum-bench` from `proto-metal/main.swift` (unchanged), `packaged-config` carries the thirteen-field object, hdiutil checksum valid |
|
||||||
|
| The HiveOS package | `NODE_OUT=<scratch>/r0313/cross WORKERS_OUT=<the 0.3.11 Linux workers> VERSION=0.3.13 packaging/hive/make-hive-package.sh`, then `publish-public.sh --hive` into `dl/public` (the 0.3.12 package removed; the ship's deploy carries it) | 14:58:59Z: `igneum-hive-0.3.13.tar.gz` **41e0633b2677005fabd360ad80669221ef8f7ea4da0a4aa48e1b659df8718eec** (24,632,169); the node inside is a9dfe78e |
|
||||||
|
| The node suites with the igneum-pow feature (the Mac, no fail-fast) | `CARGO_TARGET_DIR=vendor/igneum-node/target-0313 cargo test --release -j 4 --no-fail-fast -p kaspa-consensus -p kaspa-consensus-core -p igneum-exec -p kaspa-pow -p igneum-miner -p kaspa-p2p-flows -p kaspa-p2p-lib --features kaspa-consensus/igneum-pow,kaspa-pow/igneum-pow` from the fork, under the lock, 14:58:26 to 15:00:1xZ | igneum-miner 18 of 18, p2p-flows 33 of 33 (the IBD vote skip inside), p2p-lib 19 of 19 (the overflow-policy test), kaspa-pow 14 of 14, `db_compat` 7 of 7; kaspa-consensus 99 passed, 1 failed (the known M20 era test; the finality ban test green this run); consensus-core 107 passed, 1 failed (the known fast-time file duplicate-key test); igneum-exec 17 passed, **1 failed, NEW**: `proving::tests::assignment_follows_the_window_and_records_check_against_native_execution` (`proving.rs:1171`: the test expects a record to be refused, the code after 05e93f0e checks it as `assigned: true`; the crate was 17 of 17 on 83089544): the proving agent's to resolve before the gate (a fix commit, or the test's expectation is the stale half) |
|
||||||
|
| The Windows run, first round | `windows.yml` run 37483331039 on 0c4f90e (dispatched 14:57:37Z); `ci` 37483332527 | both green by 15:06Z; its installer eab82086... and zip cef39414... carry the a9dfe78e node (the over-paying trust rule): SUPERSEDED, not shipped |
|
||||||
|
| The second round (node 544fc30f, app 4deea56) | the Mac node rebuilt 15:02 to 15:03Z: igneumd **dd5eeda5b92463e929d07479b2fc77b4ba75c7e9354ab48a96ed22a9637e0b4d** (41,703,168); the Linux node 15:04Z: igneumd **2449d5fa3b16531b33068b75c3e5045de580119c9824053a8eaf58dfe6d8c484** (48,246,632), handed to the fleet agent in place of c7c696c5; the Windows node 15:04Z: igneumd.exe **b06f08dac020f639d2dbeec20b60f48d7025c333e59eaa3fbbed037ea33b96b9** (52,518,912); the digests re-read on dd5eeda5: 7bd98cc4... (ten) and b18ed271... (thirteen), unchanged; the inputs pushed 15:05:16Z: `payload-inputs.zip` 3a99a86f6d4dcbe4c4cdd3cc11c13e8973b1af9f3c87647cd1a420885ea94819 (65,393,980), `node-source.pin` 544fc30f as **55ef102** (the CI commit) | |
|
||||||
|
| The Windows run, second round | `windows.yml` run 37484511273 on 55ef102; the DMG 3697306e... and HiveOS 6933c0c7... on 544fc30f | SUPERSEDED by the third round (the fresh-joiner fix); the 544fc30f artefacts kept aside under the scratchpad |
|
||||||
|
| The third round (node bb43e9a8, app 4deea56) | the Mac node 15:09 to 15:10Z: igneumd **487312aa31c85bde583b7cea220ba2dd76cd4c64913c224c58664ca12f4f4f23** (41,719,760), igneum-miner b7926642...; the Linux node 15:10Z: igneumd **d6350586fe837b1f71696546628ec071b6accce2531aef776cf2b1e5487a8cdc** (48,263,528), handed to the fleet agent in place of 2449d5fa (and c7c696c5 before it); the Windows node 15:1xZ: igneumd.exe **c4441a3abed26465f2bddef6a60b237444219d4dcb6470694e19430d56d9f830** (52,527,616), igneum-miner.exe 574adb79...; the digests re-read on 487312aa: 7bd98cc4... (ten) and b18ed271... (thirteen); the inputs pushed 15:11:30Z: `payload-inputs.zip` 561878b8dcd91803ad3ff8055190e1201ba48975936bcbcdac17827f0fe7bb34 (65,398,104), `node-source.pin` bb43e9a8 as **be344ff** (the CI commit) | |
|
||||||
|
| The Windows run | `windows.yml` run 37485442000 on be344ff (dispatched 15:12:16Z); `ci` 37485442462 | both GREEN 15:18:48Z: `Igneum-Miner-Setup-0.3.13.exe` **499a8ede6268426342ffd3ae0da2354f3a14522dda7ff7c41aa21ae3e169deab** (45,396,595); `igneum-windows-app.zip` **dc3116242fc55c22259e5eb0339a5cb9dfbd8a4e7deb918f01307f291e3d1b76** (65,664,639), its igneumd.exe c4441a3a... (the bb43e9a8 cross-build); fetched 15:18:5xZ, not deployed. The 0.3.13 CI verdict: ci 37485442462 on be344ff; the Windows build 37485442000 on be344ff |
|
||||||
|
| The DMG (third round) | `NODE=<fork igneumd 487312aa> MINER=... PROVE_HOST/PROVE_EXPORT=<the 0.3.12 build, unchanged> packaging/mac/build-dmg.sh` under the lock | 15:12:51 to 15:13:1xZ: `Igneum-Miner-0.3.13.dmg` **2d35a0160925ef5fcd6d85dc653deab328c20261bfac1e37a7c3e8f4c18baf48** (41,859,802), engine 0.3.13 (the Ember helper inside), node bb43e9a8, `packaged-config` with the thirteen-field object, hdiutil checksum valid |
|
||||||
|
| The HiveOS package (third round) | `make-hive-package.sh` from the d6350586 Linux node and the 0.3.11 Linux workers, then `publish-public.sh --hive` into `dl/public` (the ship's deploy carries it) | `igneum-hive-0.3.13.tar.gz` **65ea42600236c7024844d85fc401ef2c4650e671d92061c8db6415038bac9530** (24,634,543) |
|
||||||
|
|
||||||
|
## 4. The rollout (Josh's go 15:20Z through the coordinator; two publishes)
|
||||||
|
|
||||||
|
Baseline 15:19:45Z: tip DAA about 196,900; the observer and node 1 were DOWN since 14:56:28Z (both SIGTERMed by a hand that was not mine,
|
||||||
|
the same minute the Igneum Wallet app's own node started on this Mac, pid 81040 on 26620/26621/26800; the live stats stale 24 minutes);
|
||||||
|
the seed on 83089544 at 7bd98cc4; the Mac 0.3.12 (paused on Josh's order), PC 2 0.3.12 at 115 MH/s, PC 1 0.3.12 with Josh at its screen.
|
||||||
|
|
||||||
|
| Step | Time | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| 1a the observer, node 1 | 15:20:38Z (pid 46848), 15:20:51Z (pid 48213) | `igneumd/2.1.0-bb43e9a8` on the ten-field file, 7bd98cc4...; the exec layer reads `blocked: exec not synced: the executor is at chain block 0 and the bodies below this node's retention root are gone`, `startedFrom genesis`, as designed before the three fields |
|
||||||
|
| 1a the seed | 15:21:18Z (MainPID 140366) | the same binary (d6350586...), the same digest |
|
||||||
|
| the fleet's first word | 15:24Z | "hands on 0.3.13" to the fleet agent (22 boxes on d6350586 with the ten-field file) |
|
||||||
|
| 1b publish 1 | the ship 15:22:01 to 15:23:45Z from 5ca4913 | ci "already" (37485442000), fetch "already", dmg "already", copy ok, manifest 0.3.13 published 15:22:05Z with `consensus` CARRIED OVER (the ten-field object), deployed 15:22:40Z, HiveOS 65ea4260... served |
|
||||||
|
| 1c update-now, PC 1 FIRST | 15:23:56Z | ran 15:24:42Z; engine restart 15:24:52Z (run `win-ae432dc7-20261006-152452`), "updated to Igneum Miner 0.3.13 from 0.3.12", per-user install, no prompt; cards "RTX 5090, RTX 4070, AMD integrated, RX 9070 XT"; mining 15:25:53Z; digest 7bd98cc4; the coordinator told 15:27Z, the Ember elevated table run then took PC 1 |
|
||||||
|
| 1c update-now, the Mac | 15:24:39Z | ran 15:25:11Z; engine restart 15:25:19Z (run `mac-d937c69d-20261006-152519`), 0.3.13, paused as ordered, node 1 six peers |
|
||||||
|
| 1c update-now, PC 2 | 15:25:08Z | ran 15:25:54Z; engine restart 15:26:06Z (run `win-1ccfe586-20261006-152606`), 0.3.13, worker ready 15:27:06Z, mining 15:27:07Z, digest 7bd98cc4 |
|
||||||
|
| 2a the observer, node 1 | 15:29:15Z (pid 63960), 15:29:29Z (pid 64106) | the thirteen-field file: **b18ed271...**, `Exec restart from the override file: the EVM state restarts empty at chain block 27276 bb45cf0d...` |
|
||||||
|
| 2a the seed | 15:29:53Z (MainPID 140546) | b18ed271... |
|
||||||
|
| 2b publish 2 | 15:30:03Z | the manifest with the thirteen-field object, activation 198000, "proving v1 fresh-record rule; exec restart at chain block 27276"; signed, verified, deployed |
|
||||||
|
| the exec checks, the observer (134,556 is the selected-chain height: chain blocks, one per selected-parent step, under the DAG's 151,606 blocks and the DAA 197,219; the two flat minutes were the node's own consensus re-sync after its restart, not the follower; the proving agent's reading) | 15:33:23Z | `eth_blockNumber` 134,556 (from 0 at 15:21Z), `igneum_getExecStatus` startedFrom "restart at chain block 27276", blocked null, `igneum_getProvingStatus` active, paidShards **1,482**, paidWei **1825.699240038 IGN**: the copy's numbers to the wei; the state persisted at 134,556 (118.7 MB, sha 0xe5194123...) at 15:35Z; 134,884 at 15:37:55Z, 134,909 at 15:39:47Z (the follower's rate after the restart: 1.2 then 0.2 chain blocks a second, under watch) |
|
||||||
|
| the fleet's second word | 15:35Z | "hands on b18ed271" to the fleet agent (STEP=file on 22 boxes; its per-box seconds-to-climbing follow) |
|
||||||
|
| 2c switch, the Mac | 15:35:42Z | ran 15:36:12Z: "consensus override changed; the node restarts with it at a safe moment"; its node is node 1, already switched |
|
||||||
|
| 2c switch, PC 2 | 15:36:08Z | ran 15:37:02Z, node restarted 15:37:03Z (pid 27552) on b18ed271, "Exec restart ... shard records carried below DAA score 200000 are paid as carried"; mining again 15:38:38Z, 13.7 MH/s ramping at 15:39:08Z. Its prover then logged `block 35012 shard 0: exporter: Error: segment 0: port state root 0x7e37a9fb... differs from the node's` and the same for block 61972: the assignment window hands it blocks far below the tip whose carried records were made over the old state; the re-derived state at those heights has another root, so those shards cannot be proven (they pay as carried below the trust DAA, so nothing is lost but prover passes): the proving agent's ruling: REAL and not designed. The exporter (igneum-prove-export) rebuilds a fixture's pre-state by replaying the exported chain from genesis, crediting block rewards from the headers, so above the restart it holds 27,276 blocks of rewards the node's restarted state never had and its root differs from the node's record: every shard assigned above R fails on every prover (the fleet's boxes too) until the fix, so every segment and shard payout stops from publish 2 until 0.3.14 (the carried records below the trust DAA pay regardless). The fix (about an hour, its commits to follow): `igneum_exportSegments` carries the restart (number, hash) and the exporter starts its replay at R from the registry-only state; one RPC field on the node, the exporter side in the proving package; 0.3.14's first item |
|
||||||
|
| 2c switch, PC 1 (last) | 15:42:50Z, on the coordinator's "Ember closed" (its run ended 15:39:03Z) | ran 15:43:36Z, node restarted 15:43:37Z (pid 3632) on b18ed271 with the exec restart line; the miner waiting at 15:43:54Z and mining again within the minute (the C43 class: a minute at 0 MH/s after a node restart); three cards |
|
||||||
|
| node 1's refusals | 15:39Z | 12 in the last 400 lines: the peers still on 7bd98cc4 (PC 1, the unswitched fleet boxes, the Igneum Wallet 0.1.4's bundled node on this Mac, which nobody updates: a wallet cut owes the thirteen-field object) |
|
||||||
|
| a FRESH install (the proving agent's rented 4090) | "publish 1 done" sent 15:25Z, "publish 2 done" 15:36Z | (its numbers pending) |
|
||||||
|
|
||||||
|
|
||||||
|
### 4a. The incident after publish 2 (15:42 to 15:52Z): the activation inside the window, the deep reorg, the moved pruning point
|
||||||
|
|
||||||
|
| Time | What |
|
||||||
|
|---|---|
|
||||||
|
| 15:42:57Z | `proving_v1_fresh_rule_daa` 198,000 armed while PC 1 (its switch held for the Ember run) and the fleet's unswitched boxes still mined on the ten-field object: two sides for a minute, the losing side 229 blocks deep (the coordinator's reading); PC 1's switch job went 15:42:50Z, its node on b18ed271 at 15:43:37Z, 40 s after the arming |
|
||||||
|
| 15:44:47Z | the hands' exec layer: `selected-chain reorg: 274 chain blocks removed, unwinding to height 134884 ... reorg deeper than the snapshot ring; replaying from genesis`; genesis executed, then nothing; `eth_blockNumber` 0, `startedFrom genesis`, `blocked null`; the same on the seed and on every fleet box that had come back (seven of them had reached the tip through the restart path in 87 to 188 s each: hub 88 s, 3080 87 s, 4070-1 113 s, 4090-3 138 s, rig-4090x8 138 s, A5000 163 s, 3090-4 188 s) |
|
||||||
|
| 15:48:00Z | the observer and node 1 restarted by me (the hands script, the same file): the restart path REFUSED: `sink ... is chain block 0; pruning point eb2a5d70... is chain block None (DAA 88763); retention root eb2a5d70... (DAA 88763)`, `exec restart at chain block 27276 ... not yet possible (the selected-parent walk from the sink never met the queried block)`: the devnet's pruning point moved from bb45cf0d (DAA 45,537) to eb2a5d70 (DAA 88,763) since 11:40Z, the anchor is below it and off the walk, and the genesis replay had overwritten the good 119 MB snapshot (tip 135,138) with a 2,629-byte tip-0 one (`exec-snapshot.prev.bin` keeps the good one). The exec layer and proving are at 0 on every node until the fix |
|
||||||
|
| the fix | the proving agent's, as 0.3.14 (the tooling refuses "0.3.13.1"): never replay from genesis on a pruned node (unwind through the persisted generations, else a peer's snapshot over protocol 16); keep executing from the persisted state when the pruning point passes the anchor (the anchor only for a node with nothing); never overwrite a good snapshot with a tip-0 one |
|
||||||
|
| node 1's follower | found in the same hour: node 1's `chain follower stopped` at every start since 12:37Z because both hands bound the eth_ JSON-RPC on 26790 and node 1 lost ("Address already in use"), so the Mac app's node never executed; `restart-hand-nodes.sh` now gives node 1 26791 (this commit), live at the next hand restart |
|
||||||
|
|
||||||
|
Rules from it (the coordinator's four and two more): the switch jobs go out before the height, no miner stays on the old side across an
|
||||||
|
activation; an activation height is never set inside a rollout window (the floor of 10,800 exists for that); a deep reorg must not reset the
|
||||||
|
exec layer (unwind through the persisted generations, else a peer's snapshot; never a genesis replay on a pruned node); the pruning point must
|
||||||
|
not strand an anchor (a node keeps executing from its persisted state, the anchor is for a node with nothing); a hand node restart is
|
||||||
|
announced before it runs (the 14:56:28Z SIGTERM of both hands by an unnamed hand cost 24 minutes of stale stats); the two hands never share a port.
|
||||||
|
|
||||||
|
The exec checks of section 2 are therefore RED at the close of this cut (every node at `eth_blockNumber` 0 since 15:44:47Z, `startedFrom`
|
||||||
|
genesis or snapshot-at-0, `paidShards` 0; the chain, the finality locks and the hash untouched; the proving ledger below the trust DAA pays
|
||||||
|
as carried once the exec layer is back); the cause is the pruning point past the anchor and the genesis replay over the snapshot; the fix is
|
||||||
|
0.3.14 (node-only, the proving agent's branch; one publish with the object carried over and a hand-node swap first if it is code-only, the
|
||||||
|
exec check per node the gate; two publishes only if a field changes; no re-pin of the anchor). The coordinator's decision 15:53Z.
|
||||||
|
|
||||||
|
## 5. The one line for Josh
|
||||||
|
|
||||||
|
When he says go: the execution layer comes back on every node at its restart (the hands and the seed by hand, the Mac, PC 2 and PC 1
|
||||||
|
through two update-nows each, the node restarting once for the switch), with the state restarted empty at chain block 27,276 (6 October
|
||||||
|
11:40Z, DAA 45,537) and re-derived forward, so every IGN earned since then by mining and proving is back on the ledger (PC 2's address
|
||||||
|
267,648 IGN; 1,482 shards, 1,825.70 IGN of prover payouts) and the chain's first 45,537 DAA (about 124,600 IGN of mining rewards and 31,100
|
||||||
|
IGN of escrow, approximate) are gone from it and cannot be given back to addresses, once; the chain, the finality locks and the hash are untouched; a fresh node joining the devnet no longer loses the seed every
|
||||||
|
checkpoint once the seed runs this.
|
||||||
|
|
||||||
|
## 6. Owed
|
||||||
25
docs/posts/reddit-2026-10-06.md
Normal file
25
docs/posts/reddit-2026-10-06.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Reddit post, 6 October 2026
|
||||||
|
|
||||||
|
Saved on Josh's word (16:4x UTC). For r/CryptoCurrency or r/gpumining, posted by Josh only, after 0.3.14 restores proving and the fleet numbers land. Finalised against the pages deployed at 16:12 UTC (master e763604); sources per number in docs/review/round-4-reddit-2026-10-06.md section 5.
|
||||||
|
|
||||||
|
**Igneum: a GPU-mined L1 where the miners are the provers. Devnet live. One person, no premine, every criticism logged.**
|
||||||
|
|
||||||
|
I mined through the GPU years. The Merge in September 2022 ended that income and the miners' place in Ethereum. I am building the coin I wanted to exist: GPU-mined, the miners prove the blocks, no founder allocation. One person, AI systems doing the engineering.
|
||||||
|
|
||||||
|
What runs today (devnet since 3 October 2026):
|
||||||
|
- A random mining program every hour, compiled on the card. Swaps measured on Apple, NVIDIA and AMD, 0 rejected blocks (bench log, 4 Oct).
|
||||||
|
- Memory-hard: 128 dependent random reads per hash. An RTX 5090 does 136 MH/s at 17.5 G reads a second, 82% of its memory's random-read ceiling, integer units at 0.15%. Computing items instead of loading them runs 4.8x slower (M5 Max, 3 Oct).
|
||||||
|
- GHOSTDAG ordering forked from rusty-kaspa, about 1 block a second.
|
||||||
|
- EVM execution, native on every node.
|
||||||
|
- Shards proven with SP1 on miners' cards and paid from the block: 388 shards, 446.13 IGN on 5 Oct. A 3060 mines and proves at an 8.9 GB peak (rented cards, 6 Oct).
|
||||||
|
- Finality by miner weight: 30 days of blocks per key, locks at two thirds, no stake, no slashing. No lock in the first 30 days of mainnet.
|
||||||
|
|
||||||
|
Run the vectors yourself: `git clone https://github.com/igneum-network/spec && cd spec/igneum-pow && cargo test --release` (96 vectors per pack; three GPU vendors, one fingerprint over 16.7 million nonces). The hash-rate reproduction package follows on igneum.network/evidence; your card's result file gets a row.
|
||||||
|
|
||||||
|
Costs: the miner software takes 1% (1 block in 100, off with `--dev-fee 0`). The protocol takes nothing. No cryptographer is hired yet; the review brief is USD 80,000 to 160,000, unfunded. The installer is unsigned. The chip model is published: the strongest chip in it reaches 5x to 9x per joule against a 5090 before the lever we are gating, about 2x after, sources on the page.
|
||||||
|
|
||||||
|
Every criticism we expect is at igneum.network/ledger: 167 entries, 53 conceded, 7 open, dated. A new one goes in with your name. "Don't ASICs make a chain safer?" is answered with numbers at igneum.network/litepaper#don-t-asics-make-a-chain-safer.
|
||||||
|
|
||||||
|
We want help: cryptographers, node engineers, miners who will test. Pick an open row on the ledger and write to hello@igneum.network.
|
||||||
|
|
||||||
|
Litepaper: igneum.network/litepaper. Live: igneum.network/live. Nothing is for sale.
|
||||||
267
docs/review/round-4-reddit-2026-10-06.md
Normal file
267
docs/review/round-4-reddit-2026-10-06.md
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
# Igneum hostile review, round 4 (reddit): what a sceptic reads today, and what would make them believe
|
||||||
|
|
||||||
|
Date: 6 October 2026, afternoon (UTC). Branch `review-round-4`, worktree `igneum-wt-review-4`, from master `b79f2cb` (master equals `origin/master`, so the files read here are the files the live site serves). The goal set at 18:10 UTC: "We want redditors to go through everything we publicly have and think 'holy shit, this is the one'." This round is those redditors first.
|
||||||
|
|
||||||
|
What was read: `site/index.html`, `site/litepaper.html`, `site/miner.html`, `site/wallet.html`, `site/evidence.html`, `site/explorer.html`, `site/live.html`, `site/miners.html`, `site/faucet.html`, `site/metamask.html`, `site/bench.html`, `site/journey.json`; `docs/evidence.md`; `docs/benchmarks/repro.md` (branch `repro-bench` b776199); `docs/analysis/prover-tiers-real-cards.md` (`gpu-fleet` f5ccfb9); `docs/analysis/asic-resistance-history.md` (`asic-history`); `docs/plans/counter-asic-3-status.md` and `counter-asic-2-public.md` (`ca3-coord`); `docs/fud-ledger.md` and `docs/fud-fixes.md` (`fud-close` dc12d76, judged as the page they should become); the live site at igneum.network (home, /live, /explorer, /bench, /evidence, /miners, the APIs), the public repository github.com/igneum-network/spec and its README, the downloads host, and the installed Igneum Miner 0.3.13 dashboard (paused, read only). Rounds 3 and 4 (`docs/review/round-3-2026-10-03.md`, `round-4-2026-10-04.md`), the red-team run and the ledger sweep were read first so nothing there is repeated; where a finding of theirs is still live on the page it is cited by ledger id, not re-argued.
|
||||||
|
|
||||||
|
Rank meanings are round 3's: fatal stops the post as written; serious must be fixed before the post; minor when convenient. Every number names its source; "approximate" marks a figure from memory.
|
||||||
|
|
||||||
|
## 0. The one finding that decides the rest
|
||||||
|
|
||||||
|
The site, the evidence page and the ledger disagree with each other today, and a reader who checks will find it in ten minutes.
|
||||||
|
|
||||||
|
| What | Where | What the reader sees |
|
||||||
|
|---|---|---|
|
||||||
|
| The ledger's 5 October night close moved 45 entries to "Conceded, stated" and cites the sentences on the site | `docs/fud-ledger.md` (fud-close), 45 status lines dated "5 October 2026, night"; the sentences live on branch `ledger-text` (commit 2026-10-05 19:35, 0 merge conflicts with master) | Of 14 cited sentences checked by grep against master, 13 do not match the live page: 10 are missing ("Every payment route", "The schedule is a bet", "Wrapped for light clients", "0% anyone else in the protocol", "of emission to miners and provers", "burned under", "100,000-gas calls", "say nothing about the price of a chip", "Circle's decision", "Devnet: coins have no value") and 3 overclaims the ledger calls removed are still live ("so the people who show up early get the most" `site/litepaper.html:497`; "a phone can check it in milliseconds" `:449`; "with earnings shown in IGN and in your currency ... offers a hardware wallet for your earnings" `:567`) |
|
||||||
|
| The bounty was withdrawn by the owner (ledger M1, M22, 6 October) and struck from the litepaper | `ca2-coord` commits 6141e01 and 8c5b02f (15:45 UTC), not merged | `site/litepaper.html:301` "a bounty follows the external review", `:424` the same, `:684` "A standing bounty pays anyone who can show a chip design that beats a GPU by more than 2x", `:686` "a bounty is attached" |
|
||||||
|
| The evidence page is two days behind its source | `site/evidence.html:178` "30 claims · 5 labels · 4 Oct 2026"; `docs/evidence.md` has 31 rows (two numbered 22), rows 15, 16, 21 restated on 5 October | Row 21 on the page: "the payout from it against proof records is unwritten"; the bench log of 5 October: 388 shards paid, 446.13 IGN |
|
||||||
|
|
||||||
|
A public ledger page that says "stated" when the page says otherwise is worse than no ledger page. Fix first, one hour: merge `ledger-text` (clean), take the four bounty sentences from `ca2-coord` (or merge it), rebuild, deploy, then regenerate `site/ledger.html`. Nothing else in this document is worth doing before that.
|
||||||
|
|
||||||
|
## 1. The personas
|
||||||
|
|
||||||
|
Each reads everything above, in the order a redditor would: the homepage, the litepaper, /live, /miners, /evidence, the spec repository, the app. Quotes are exact and carry the file and line on master.
|
||||||
|
|
||||||
|
### 1.1 The r/CryptoCurrency cynic (200 launches)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/index.html:372` the live strip: "26 / miners active". The ledger's own X5 count on 5 October: 21 vote keys were 5 machines, 4.2 keys each. The live page calls the same number "vote keys active in 10 min, a card runs several" (`site/live.html:232`). The homepage calls them miners.
|
||||||
|
2. `site/index.html:343` "A custom chip gains under 2x, and the model is public." Then `site/litepaper.html:301` "a bounty follows the external review." I search for the bounty terms. There are none, because it was withdrawn on 6 October (ledger M1). A promise that does not exist on the page that promises it.
|
||||||
|
3. `site/litepaper.html:497` "Nearly a quarter of all supply is mined in the first year and half in the first two, so the people who show up early get the most." That is the sentence every rug has.
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `site/litepaper.html:263` to `267`: "Method: one founder with AI systems ... This is not an offer to sell anything." and `:688` "One founder, pseudonymous, working with AI systems." Nobody writes that unless it is true.
|
||||||
|
2. The light client card on the homepage, live: "Checkpoint 6499, certified by 17 miners. Verified here. 124.9 ms, 69.8% of weight." A browser verifying a BLS certificate from a devnet node is a thing I can watch happen.
|
||||||
|
3. `site/litepaper.html:664` "Dates slip. Gates do not. Phase two decides everything. If consumer GPUs cannot prove shards fast enough, Igneum says so and does not launch on promises."
|
||||||
|
|
||||||
|
Verdict: "Pseudonymous founder, AI-written, three days old, promising a bounty it cancelled. Pass, but the light client thing is odd enough that I bookmarked it."
|
||||||
|
|
||||||
|
Flip: a fourth party's run. One `/miners` row labelled "reproduced externally" from a machine the project does not own, with the command, and the bounty sentences gone.
|
||||||
|
|
||||||
|
### 1.2 The Monero contributor (RandomX audits, the X3, the X5)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/index.html:434` "Monero's idea, finished for GPUs". RandomX's point was the CPU everyone owns. You took the random-program half and changed the target. "Monero's technique, applied to GPUs" is the ledger's own replacement (C1, overclaim 68) and it is still not on the page.
|
||||||
|
2. `site/litepaper.html:424` "It claims the gain is small, the response takes a week, and both are measured." The project's own Counter ASIC 3.0 status (`docs/plans/counter-asic-3-status.md`, section 2 item 1 and section 6) prices the stored-dataset memory-controller chip at 5.1x per joule on GDDR7 and 7.5x to 9.2x on HBM3 in its model, and 2.1x to 4.8x by the Ethash precedent, and writes "NOT SAFE TO PUBLISH as worded". The hero says under 2x.
|
||||||
|
3. `site/litepaper.html:412` "Open: no analysis of the lottery properties exists yet." RandomX had four paid audits before launch (Trail of Bits, X41, Kudelski, Quarkslab, 2019; `docs/analysis/asic-resistance-history.md` section 2.4). This has none and a 3 October birthday.
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `site/litepaper.html:416` "The one-bit select inside the maths costs a chip nothing and is not a defence; the random reads are." That is a project correcting itself against its own interest (ledger M18).
|
||||||
|
2. `docs/plans/counter-asic-3-status.md` section 2 item 1: the time-memory curve drawn, the f = 1 chip priced, the conclusion that the partial-store chip is never built, and the lever named (program work in the latency shadow, 100,000 ops per hash, 0.2% of the 5090's rate). That is the analysis ProgPoW never did (Least Authority's light-evaluation note, 2019).
|
||||||
|
3. The bench log's hash numbers trace to commands: `igneum-pow` 96 of 96 vectors on five compilers and three vendors with one fingerprint `25f96e7dce90bd4e` over 16.7 million nonces (`docs/benchmarks/repro.md` section 2.2), and the spec repository is public with the vectors (`cargo test --release` in `igneum-pow`, I ran the README in my head and it is the right shape).
|
||||||
|
|
||||||
|
Verdict: "The hash work is real and honest about itself. The headline about chips is not, by the project's own numbers. Show me the audit engagement and the 'under 2x' sentence rewritten to what the model says, and I read the spec."
|
||||||
|
|
||||||
|
Flip: replace the "under 2x" sentence with draft (b) of `counter-asic-3-status.md` section 6 (the measured fact: 136 MH/s at 17.5 G dependent reads a second, 82% of the memory's random-read ceiling, integer units at 0.15%), name the cryptanalysis engagement (funding plan row: USD 80,000 to 160,000, reviewer shortlist written, no outreach yet), and publish the chip model as a page, not a status file.
|
||||||
|
|
||||||
|
### 1.3 The Kaspa miner (watched the KS0 arrive)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/index.html:343` "Built for graphics cards. A custom chip gains under 2x". I heard "GPU coin" in 2022. Seventeen months later IceRiver shipped (`asic-resistance-history.md` row 23: 250x per joule).
|
||||||
|
2. `site/litepaper.html:684` "The benchmark tool ships in January 2027". The devnet is live now; the benchmark that would let me compare my card is three months away and the `/miners` table has 6 rows, two cards, no watts (`site/miners.html:183`).
|
||||||
|
3. `site/litepaper.html:569` "HiveOS support on day one" against `site/miner.html:313` "Hive itself is untested: report what breaks."
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `docs/analysis/prover-tiers-real-cards.md`: eleven rented cards, 3060 to 5090, each with MH/s, watts and the prover's memory peak beside the miner. The 3060 at 23.78 MH/s and 103.7 W mines and proves at an 8.9 GB peak. That table is the thing I wanted and it is not on the site.
|
||||||
|
2. `site/litepaper.html:737` "A cryptography team. Not yet." and `:736` "The first month of mainnet is proof of work with a 12-hour depth". Someone wrote the bad news down.
|
||||||
|
3. The live page: 26 keys, 28 voters, checkpoints locking every 30 s at 66.8% to 89.1% of weight, difficulty moving 5% to 12% a minute as cards come and go. It is a real small chain behaving like one.
|
||||||
|
|
||||||
|
Verdict: "GPU-mined, no premine, one-click app that works on a Mac. Kaspa said nothing about chips and got them; this one says 'under 2x' and its own file says that is not safe. Keep the app, drop the sentence, and put the card table up."
|
||||||
|
|
||||||
|
Flip: the real-card table on `/miners` with MH/W per card, and the hero sentence replaced by the measured fact.
|
||||||
|
|
||||||
|
### 1.4 The zk researcher (SP1, RISC Zero, what a proof claim must show)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/litepaper.html:449` "A proof is right or it is not, and a phone can check it in milliseconds." No wrapper exists; the light verifier is a 58 MB native binary with 1.3 to 2.1 s of setup (ledger P3); the number measured on a laptop was 139 to 155 ms for the BLS certificate, not the proof.
|
||||||
|
2. `site/index.html:384` "~60 s to a proof at launch, the target" beside the chain scene labelled "LIVE DEVNET ... proven 8" (`site/index.html:400`, the counter `cp` in the script) while `/live` says "proven not active". The proving counter in the live-labelled scene is the simulation's.
|
||||||
|
3. `site/litepaper.html:451` "That proof lands on-chain within about a minute at launch." The devnet carried proofs for 2.4% of blocks over 30 minutes with one prover (evidence row 15), and nothing in consensus checks an SP1 proof (ledger P21: v0 verifies off the consensus path through the public testnet, decided 6 October).
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. The program ids are on the app's tile and in the table: shard guest `0x2b1a81cb...`, aggregator `0x474678f3...`, and the record names them (`docs/analysis/prover-tiers-real-cards.md` Method; the app's Prove tab). A pinned guest is how a proof claim should start.
|
||||||
|
2. `docs/benchmarks/repro.md` section 2.2: a fixture shard proved on PC 2 beside a live miner, VERIFIED by the SDK's verifier, two tampered witnesses REJECTED, with the host path and the pinned id. Tamper cases are the test I run first.
|
||||||
|
3. `site/litepaper.html:451` "Full nodes also execute every block natively and reject a proof record whose result differs from their own execution, so a forged proof is a light-client problem and never a chain split." That is the right rule for an unaudited prover and it says why.
|
||||||
|
|
||||||
|
Verdict: "The proving pipeline is honest about being v0 in the docs and not on the homepage. The homepage's 'proven' counter is theatre. Fix the two sentences and I will read spec 7."
|
||||||
|
|
||||||
|
Flip: one chain block whose aggregated segment proof I can download from the explorer and verify with a published command against the pinned id, and the phone sentence replaced by the measured certificate time with "no wrapper yet".
|
||||||
|
|
||||||
|
### 1.5 The HiveOS farm operator (200 cards, MH/W, uptime, payouts)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/index.html:343` "An NVIDIA card with 24 GB or more proves every block and gets paid for it". My fleet is 3060s and 3070s. The project's own rented-card run (6 October, 13:27 UTC) shows the 3060 mining and proving at a 8.9 GB peak and the 4060 Ti 8 GB proving alone. The site tells me I am out.
|
||||||
|
2. `site/miners.html:183`: six rows, two card models, every MH per watt "not measured". `docs/analysis/prover-tiers-real-cards.md` has eleven cards with watts, taken today.
|
||||||
|
3. `site/litepaper.html:565` "One block template in 100 is requested with the dev address instead of yours". Fine. The app's state shows `dev_fee.address: ""` on 0.3.13 (the installed Mac dashboard, read only): 1% of my blocks to an address the app does not show me.
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `site/miner.html:469` the Hive flight sheet with the real fields: `DEV_FEE=1 IDENTITIES=8 WORKER=auto VOTE=1`, the sha256 beside the archive, and "Hive itself is untested so far. Report what breaks." Nobody ships that line unless they expect me to.
|
||||||
|
2. `site/miner.html:369` to `381`: recoveries measured in seconds against a fake worker (0 hashes: restart at 91.0 s; process dead: 101.4 s; node dead: 160.0 s). Uptime as seconds.
|
||||||
|
3. `docs/plans/counter-asic-3-status.md` section 7 item 1: the 5090's cap never binds, 0.41 MH per W flat from 400 W to 575 W, 310 to 314 W drawn. That is the efficiency fact I set my rigs by.
|
||||||
|
|
||||||
|
Verdict: "No MH/W table, a 24 GB line that their own run disproves, and a dev fee to a blank address. Not yet. The Hive package existing at all is more than most launches."
|
||||||
|
|
||||||
|
Flip: the eleven-card table on `/miners` with MH/W and the prover peak per card, the 24 GB sentence rewritten to the measured tiers, and the dev fee address printed on the page and in the app.
|
||||||
|
|
||||||
|
### 1.6 The Ethereum core developer (reads finality claims)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/litepaper.html:474` "finality in about two minutes" and `:701` "A miner lock arrives in about two minutes and that is the finality your contract sees." A lock is two thirds of a 30-day block-weight table signed by BLS keys with no slashing (F6: "equivocation costs history, not coins"). It is a confirmation rule, and a good one; calling it finality beside Ethereum's economic finality (`:462`) invites the comparison the ledger conceded (C5).
|
||||||
|
2. `site/litepaper.html:738` "A miner holding a third of the last 30 days of blocks can split finality during a network partition". The ledger's C4 measurement of 5 October: a finality fork from a 96-s honest partition with no attacker on the fast-time network, because the shipped node held the other side's certificate "pending" and never reorganised. The fix (certificate-driven reorg) landed the same night; the sentence does not say a partition alone did it.
|
||||||
|
3. `site/litepaper.html:645` "There are no admin keys in consensus." The installed app's log, 16:36 local today: "consensus parameters from the signed manifest: {difficulty_v2_activation_daa, finality_v3_activation_daa, program_class_v3_activation_daa, exec_restart_number 27276, exec_restart_hash ...}" followed by a signed job that restarted every node's execution state. On the devnet the release key sets activation heights and reset state. That is an admin key, used today, and the sentence does not scope itself.
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `site/litepaper.html:464` "In the chain's first 30 days no checkpoint locks at all ... the chain runs on proof of work and its 12-hour finality depth the way every new proof-of-work chain does." Measured on test networks (bench log, "finality fixes F17 and F1"). The honest version of the hardest problem.
|
||||||
|
2. `site/litepaper.html:467` the finality-depth paragraph: "a node never switches to a chain forked more than 12 hours of median time back ... Kaspa's one-hour merge depth is a limit on which old blocks a new block may merge, not a reorganisation bound." Round 3's exchange-engineer finding (F15), fixed and stated.
|
||||||
|
3. `docs/review/redteam-2026-10-04.md`: 30 scenarios against the shipping build, 8 fails named with ledger ids, the memory growth and the coinbase-limit bug found by the project's own red team and fixed in 0.3.5. I trust a chain by its bug list.
|
||||||
|
|
||||||
|
Verdict: "A checkpoint overlay on GHOSTDAG with a month of history as weight is a defensible design and the review trail is unusual. The word 'finality' is doing more work than the rule does, and the devnet is run by a signed manifest. Say both plainly."
|
||||||
|
|
||||||
|
Flip: one paragraph that names the lock as a miner-weight confirmation rule with its three limits (no slashing, pauses under two thirds, the C4 partition case and its fix), and the manifest path disclosed and dated with the testnet promise that closes it.
|
||||||
|
|
||||||
|
### 1.7 The security auditor ("no admin keys", "no founder coins", what is behind them)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `site/index.html:599` the tile "0 admin keys in consensus" against the app log above: a signed manifest carrying activation heights and `exec_restart_hash`, applied by every 0.3.13 node. Devnet or not, the mechanism exists in the shipped client and the page does not say so.
|
||||||
|
2. `site/litepaper.html:586` "Signed remote jobs ... The project's own machines take jobs signed by the release key". The same key signs updates for every user (`:585`) and consensus parameters (above). One Ed25519 key, custody policy "open (O-8.1)" (`docs/evidence.md` row 23), one pseudonymous holder.
|
||||||
|
3. `site/index.html:387` "100% / to miners and provers" and `:592` "0% anyone else", with the 1% client fee disclosed two screens lower (`:603`). The ledger (E5, L9) calls both tiles fixed; they are not on master.
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `site/litepaper.html:545` "for as long as miners run Ember with the fee on, 1 block in 100 pays the project." Exact, auditable from the chain, with the control measured (9 fee blocks in 785, `site/miner.html:434`).
|
||||||
|
2. `site/index.html:522` "What the app sends home": the machine id, the OS, the hash rate and the logs that name the payout address, to a service the project runs, read by the maintainers. A telemetry disclosure written before anyone asked.
|
||||||
|
3. `site/litepaper.html:613` "They do not yet carry a Developer ID or Authenticode signature, so the first install asks you to allow the app." Unsigned, and said.
|
||||||
|
|
||||||
|
Verdict: "No premine is true and checkable. 'No admin keys' is true of the consensus rules and false of the running devnet, through a key with no published custody. Disclose the key's powers in one table and I stop worrying about the rest."
|
||||||
|
|
||||||
|
Flip: a key-powers table on the miner page (what the release key can do today: sign updates, sign jobs to named machines, carry devnet override parameters; what it cannot do at mainnet; who holds it, how it rotates, spec 8.2), and the fee tiles changed.
|
||||||
|
|
||||||
|
### 1.8 The journalist (do the numbers trace to commands?)
|
||||||
|
|
||||||
|
Close the tab:
|
||||||
|
1. `docs/fud-ledger.md` M29, "Conceded, stated (5 October 2026, night): `site/litepaper.html`, For miners, 'One click, for everyone else', rewritten to Ember 0.3.9", against `site/litepaper.html:567` on the live site, unchanged: "with earnings shown in IGN and in your currency ... offers a hardware wallet for your earnings". Thirteen of fourteen cited sentences do not match the page (section 0). The ledger says fixed; the site says not.
|
||||||
|
2. `site/evidence.html:178` "30 claims · 5 labels · 4 Oct 2026" and row 21 "the payout from it against proof records is unwritten", against the bench log of 5 October: 388 shards paid, 446.13 IGN. The evidence page, the page whose job is to be current, is two days behind.
|
||||||
|
3. `site/litepaper.html:441` "Every number above is measured and logged with the commands that produced it." The bench log entries carry machines and commands; `docs/benchmarks/repro.md` section 2.2 reports PC 2's 5090 "beside the live miner, about half of what the card does alone" and PC 1's run "owed". A reproduction package that has not reproduced its own numbers on its own machines yet.
|
||||||
|
|
||||||
|
Stay:
|
||||||
|
1. `docs/benchmarks/repro.md` exists, with tolerances per number (vectors exact, hash 3%, sweep 10%, proof 25%), a result format, an ingestion script that refuses the "reproduced externally" label when a number disagrees, and a failing run "as public as a passing one". That is the ladder, written down.
|
||||||
|
2. The spec repository is public with the vectors, the simulators and the census, MIT, `cargo test --release` as the first command (README, `igneum-network/spec`, pushed 4 October 08:20 UTC). I can run that today.
|
||||||
|
3. `site/litepaper.html:405` "Two caveats, stated here before anyone else states them ... Nothing here has been reproduced by anyone outside the project yet; the evidence page says so row by row."
|
||||||
|
|
||||||
|
Verdict: "The method is right: labels, commands, a ladder, a ledger. The execution is behind the method by two days and three branches. Right now I can prove the site contradicts its own ledger faster than I can reproduce a hash rate."
|
||||||
|
|
||||||
|
Flip: the three merges of section 0 deployed, the evidence page regenerated from its source with the date it was regenerated, and the repro package on the downloads host with one outside result row.
|
||||||
|
|
||||||
|
## 2. Ranked findings
|
||||||
|
|
||||||
|
Fatal stops the post as written. Serious must be fixed before it. Minor when convenient. Lines are master's. Hours are Claude-side hours (the 3 October rule: never weeks).
|
||||||
|
|
||||||
|
| # | Rank | Finding | File and line | Fix in a sentence | Owner | Hours |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | Fatal | The ledger's 5 October text close (45 entries "stated") never reached master: 13 of 14 cited sentences do not match the live page; a public ledger page built from it is refuted on sight | `docs/fud-ledger.md` (fud-close) status lines "5 October 2026, night"; branch `ledger-text` 2026-10-05 19:35, unmerged, 0 conflicts; `site/litepaper.html:449, 497, 567`, `site/index.html:387, 592` | Merge `ledger-text` into master, rebuild, deploy, regenerate `site/ledger.html`; add a CI check that greps every ledger "stated" quote against the built site | Josh (merge), Claude (check) | 1 |
|
||||||
|
| 2 | Fatal | The bounty is withdrawn (M1, M22, 6 October) and four litepaper sentences still promise it; the strike exists on `ca2-coord` 6141e01 and 8c5b02f, unmerged | `site/litepaper.html:301, 424, 684, 686` | Take the four sentences from `ca2-coord` (the abstract and `:424` read "the claim is tested by paid independent cryptanalysis and the public benchmark"; `:684` loses the bounty sentence; `:686` reads "the public benchmark carries the metrics they test against") | Josh (merge) | 0.5 |
|
||||||
|
| 3 | Serious | "A custom chip gains under 2x" stands on the hero and in the abstract while the project's own Counter ASIC 3.0 status prices the stored-dataset chip at 5.1x to 9.2x per joule in the model and 2.1x to 4.8x by the Ethash precedent, and marks the sentence "NOT SAFE TO PUBLISH as worded" | `site/index.html:343`; `site/litepaper.html:301, 424, 733`; `docs/plans/counter-asic-3-status.md` section 2 item 1, section 6 item 1 | Replace with draft (b) of that status file (the measured fact: 136 MH/s, 17.5 G dependent reads a second, 82% of the random-read ceiling, integer units at 0.15%, "a chip beats it only by reading per watt what a 512-bit GDDR7 board reads"), and name the shadow-work lever as being measured | Josh (decision), Claude (text) | 1 |
|
||||||
|
| 4 | Serious | The homepage live strip labels vote keys as miners ("26 miners active") while the project's own count is about 5 machines | `site/index.html:372` | Label changed on this branch to "vote keys active in 10 min (a card runs several)"; add a machines count from the fleet fingerprint column (ledger X5, `ledger-observer` branch) when it ships | Claude (done), observer owner | 0.2 done, 2 for the machine count |
|
||||||
|
| 5 | Serious | The homepage chain scene carries the "LIVE DEVNET" label while its "proven" counter is the simulation's; `/live` says "proven not active" at the same moment | `site/index.html:400` (`c-proven`), `:798` (the simulated shard sparks run in live mode), `:837` (only the glint is live) | In live mode, read `proven` from `d.proving` (paid shards in the window) and hide the shard sparks; show "proving: not active" when `d.proving.active` is false | Claude (site) | 1.5 |
|
||||||
|
| 6 | Serious | "An NVIDIA card with 24 GB or more proves every block" on the hero, the Mine section and the miner page, while the rented-card run of 6 October shows 8, 10, 12 and 16 GB cards proving on the patched prover (12 GB mines and proves at 8.9 to 10.2 GB peak) | `site/index.html:343, 484`; `site/miner.html:252, 308`; `site/litepaper.html:301, 440, 452, 562`; `docs/analysis/prover-tiers-real-cards.md` | "An NVIDIA card with 24 GB proves every block on today's build; 8 to 16 GB cards proved the same shard on 6 October on rented cards (the table) and that prover ships next"; lines 252, 308, 440, 452, 562 are on `ledger-text` or `ca2-coord`, so apply after the merges | Claude (after merge) | 1 |
|
||||||
|
| 7 | Serious | The real-card table (eleven cards, MH/s, W, prover peak) exists on `gpu-fleet` and the public `/miners` page has 6 rows, two models, no MH/W | `site/miners.html:183`; `site/miner-bench.json`; `docs/analysis/prover-tiers-real-cards.md` | Ingest the eleven rows into `miner-bench.json` with "measured by the team on rented cards", MH/W and the prover peak columns, source the fleet document | Claude (site), fleet owner | 2 |
|
||||||
|
| 8 | Serious | "0 admin keys in consensus" and "There are no admin keys in consensus" with no word that the devnet's activation heights and an execution-state restart arrive through the signed update manifest (the 0.3.13 app log, 16:36 local, 6 October) | `site/index.html:599`; `site/litepaper.html:645`, `:585` to `:586` | One scoped sentence added to the governance bullet on this branch; a key-powers table on the miner page (updates, jobs to named machines, devnet overrides; the mainnet limit; custody per spec 8.2) is still owed | Claude (sentence done), Josh (custody policy, O-8.1) | 0.3 done, 1 for the table |
|
||||||
|
| 9 | Serious | The evidence page is two days behind its source: "30 claims · 4 Oct 2026", row 21 "unwritten", row 16 "RTX 3060 on order" (the 3060 was measured today), row 17 quotes a homepage sentence that is gone; `docs/evidence.md` has two rows numbered 22 | `site/evidence.html:178`, rows 16, 17, 21; `docs/evidence.md:49, 90` | Renumber the card-lifetime row to 31; regenerate `evidence.html` from the markdown by the build (as bench is) instead of by hand, stamp the regeneration date; both files are touched by `ledger-text`, so after the merge | Claude | 2 |
|
||||||
|
| 10 | Serious | "a phone can check it in milliseconds" is live; the certificate half measured 139 to 155 ms on a laptop core and no proof wrapper exists | `site/litepaper.html:449` (on `ledger-text`) | The P3 replacement: "Wrapped for light clients, a phone checks it in milliseconds; the wrapping cost is a phase two measurement" plus the laptop figures | merge | 0 after 1 |
|
||||||
|
| 11 | Serious | The reproduction package is built but not on the downloads host (404 at `/public/igneum-repro.tar.gz`), and its own PC 1 run is owed and its PC 2 5090 rows were taken beside a live miner | `docs/benchmarks/repro.md` sections 2.3, 7; `packaging/ota/publish-public.sh --repro` not run | Run PC 1's slot and PC 2's 5090-only slot, publish the package, link it from `/evidence` under "What would move a row" | repro owner, Josh (publish) | 2 |
|
||||||
|
| 12 | Serious | The litepaper's app paragraph still describes earnings in currency, game pause and a hardware wallet the app does not have (M29 "stated" on `ledger-text`) | `site/litepaper.html:567` | The `ledger-text` paragraph (Ember 0.3.9 from the shipped UI; the rest as a roadmap sentence), then bump to 0.3.13 | merge | 0 after 1 |
|
||||||
|
| 13 | Serious | "so the people who show up early get the most" is live (L2, counsel engaged) | `site/litepaper.html:497` | Drop the "so" clause (overclaim 54), as `ledger-text` does | merge | 0 after 1 |
|
||||||
|
| 14 | Minor | The dev fee's address is empty in the 0.3.13 app state (`dev_fee.address: ""`) and the miner page says the miner prints the fee and the address | `site/miner.html:444`; the app's `api/state` on the Mac, 6 October | Print the dev address on `/miner#fee` and in Settings beside the switch; the chain-side `igneum-miner payouts` tag already exists | app owner | 1 |
|
||||||
|
| 15 | Minor | "Monero's idea, finished for GPUs" on the homepage and the litepaper heading; the ledger's own replacement (C1, overclaim 68) is "Monero's technique, applied to GPUs" | `site/index.html:434`; `site/litepaper.html:429` | Apply overclaim 68 | Claude | 0.2 |
|
||||||
|
| 16 | Minor | The live page's eyebrow says "DEVNET V0"; the chain is devnet v4 and the app says so | `site/live.html:219` | "devnet v4" | Claude | 0.1 |
|
||||||
|
| 17 | Minor | The MetaMask page says the Igneum Wallet "is in the roadmap; until it ships, MetaMask is the wallet" while the wallet page ships 0.1.4 | `site/metamask.html:231`; `site/wallet.html:370` | "Igneum Wallet 0.1.4 is on macOS; MetaMask works too" | Claude | 0.2 |
|
||||||
|
| 18 | Minor | "About one a second" in the devnet table against 0.65 blocks a second over the hour to 16:00 UTC today (2,325 blocks, one PC off) and a 23-minute zero in `/api/live`'s blocks-per-minute series during the 0.3.13 state reset | `site/litepaper.html:396` | Stated on this branch with the hour's count; the reset should appear as an event on `/live` with its reason | Claude (done), observer owner | 0.2 done, 1 for the event |
|
||||||
|
| 19 | Minor | Ember Tune is "owed" on the miner page while Counter ASIC 3.0 section 7 records the 5090 run (0.41 MH per W flat, the cap never binds) | `site/miner.html:356, 407` | Replace "the first measured tune is owed" with the 5090 row | Claude | 0.3 |
|
||||||
|
| 20 | Minor | The captions "the app is still labelled Igneum Miner until 0.3.6" at 0.3.13 | `site/index.html:492`; `site/miner.html:266` | Changed on this branch to "the app window still says Igneum Miner at 0.3.13"; rename the window when the Ember name is final | Claude (done), app owner | done |
|
||||||
|
| 21 | Minor | The public spec README says "open an issue once issues are enabled"; issues are enabled (API: has_issues true, 0 open) | `igneum-network/spec` README, "Submit a break" | "or open an issue" | Claude (spec repo) | 0.1 |
|
||||||
|
| 22 | Minor | `/ledger` redirects to the homepage (`site/vercel.json`); the page drafted here is unreachable until the redirect goes; `ledger-text` reformats that file, so the one-line change waits for the merge | `site/vercel.json:4` | Delete the redirect after the merge; add "Ledger" to the nav partial | Josh (nav decision), Claude | 0.2 |
|
||||||
|
|
||||||
|
Already answered, cited and not repeated: F6 (no slashing, stated), F1 (the first month, stated and measured), M13 (Macs at a fifth), P6 and C10 (market size, collateral), X8 (no listings), G2 (the method line), X1 (the spec repository is public; the node is not), C4 (the partition fork, fixed the same night), E16 (the 20% pool's two halves, stated on `ledger-text`), X23 to X28 (the relay, fixed on `ledger-relay`, internal).
|
||||||
|
|
||||||
|
## 3. What makes them believe: the ten artefacts, in the order a sceptic reads them
|
||||||
|
|
||||||
|
| # | Artefact | State today | Owner | Hours |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | One homepage sentence that is measured, not targeted, in place of "under 2x": the 136 MH/s, 17.5 G reads a second, 82% of the ceiling line (draft (b)) | Missing on the page; written in `counter-asic-3-status.md` section 6 | Josh (decision), Claude | 0.5 |
|
||||||
|
| 2 | The ledger as a page, with the count table and every entry, and every "stated" claim true of the live site | Being built: `site/ledger.html` on this branch (167 entries: 53 conceded, 54 fixed or built, 27 closed by rule or decided, 13 answered with evidence, 13 by design, 7 open), identity terms scrubbed, passes the private-string check; blocked by finding 1 | Claude (page, done); Josh (merges) | 1 |
|
||||||
|
| 3 | The spec repository with `cargo test --release` as the first command and the vectors a stranger can run | Exists: `igneum-network/spec`, public since 4 October 08:20 UTC, MIT, 96 vectors per pack | none | 0 |
|
||||||
|
| 4 | The reproduction package on the downloads host, with the three project machines' own rows in tolerance | Being built: `docs/benchmarks/repro.md`, package v0.1.0-repro built; PC 1 owed, PC 2's 5090 rows void (shared card), not published | repro owner, Josh | 2 |
|
||||||
|
| 5 | The real-card table on `/miners`: eleven cards, MH/s, W, MH/W, prover peak beside the miner, tier verdicts | Being built: `docs/analysis/prover-tiers-real-cards.md` on `gpu-fleet`, 11 rows complete at 13:27 UTC; not on the site | Claude (ingest) | 2 |
|
||||||
|
| 6 | An evidence page regenerated from its source with the regeneration date, 31 rows, "reproduced externally" moved by one outside run of artefact 4 | Being built: `docs/evidence.md` current to 5 October, the page to 4 October | Claude | 2 |
|
||||||
|
| 7 | One outside result row: a stranger's `igneum-repro` JSON ingested with "reproduced externally" | Missing; needs artefact 4 published and one reply to a post | Josh (ask), Claude (ingest) | 1 after the post |
|
||||||
|
| 8 | The key-powers table: what the release key can do today (updates, jobs, devnet overrides), what it cannot at mainnet, who holds it, how it rotates | Missing; the custody policy is O-8.1 | Josh (policy), Claude (table) | 1 |
|
||||||
|
| 9 | Ember with the Ember name, the dev fee address printed, and the proving tile reading the verifier state (the 0.3.6 plan's rows, now at 0.3.13) | Being built: 0.3.13 on the fleet; window still says Igneum Miner; dev address blank in the state | app owner | 2 |
|
||||||
|
| 10 | The cryptanalysis engagement named: firm, scope (the mixer, the chained cache, the acceptance rule, x8 shape), start date, the report published whole | Missing; the brief and the shortlist are in `docs/plans/funding.md` (USD 80,000 to 160,000), no outreach | Josh | the engagement; 0.5 to state it |
|
||||||
|
|
||||||
|
Total Claude-side hours for artefacts 1, 2, 5, 6, 8, 9: about 9. Nothing on the list takes a week.
|
||||||
|
|
||||||
|
## 4. The ledger page
|
||||||
|
|
||||||
|
`site/ledger.html` on this branch, generated by `tools/ledger-page.mjs` from `docs/fud-ledger.md` (this run used the `fud-close` copy at dc12d76, 167 entries; the page regenerates from master once that branch merges). Every entry: id, title, the criticism in the critic's words, the status word, what was done (the status line in full, its "Was:" history included), the date, and the first-written answer behind a disclosure. The count table on top, with filters and a search. The only rewrites are the identity and provider terms of `site/forbidden-strings.txt` (the owner's name, the registrar, the host, the cloud provider, the rules file, the downloads host, the log key, local-time stamps), replaced in place; no criticism is softened and none is dropped. The page passes the private-string check with zero hits. The `/ledger` redirect in `site/vercel.json` must go before it is reachable (finding 22).
|
||||||
|
|
||||||
|
The introduction, as it stands on the page:
|
||||||
|
|
||||||
|
> This is every criticism the project expects, in the critic's words, with what was done about it and the date. 167 entries since 3 October 2026. Entries are never deleted; a status that changes keeps its history on the line. Where the critic was right the entry says Conceded. Where nothing has been done it says Open and names what settles it. The founder mined through the GPU years. Ethereum's move to proof of stake in September 2022 ended that income and the miners' place in that chain. This is one person building, with AI systems doing the engineering, the coin he wanted to exist for miners: GPU-mined, the miners are the provers, no founder allocation, every cost stated. Help is welcome and a team is wanted: cryptographers, node engineers, miners who will test. This ledger is the application form: pick an open row and write to hello@igneum.network with its id.
|
||||||
|
|
||||||
|
## 5. The post
|
||||||
|
|
||||||
|
For r/CryptoCurrency or r/gpumining. Finalised 6 October 2026 against the pages deployed at 16:12 UTC (master e763604). No adjectives, numbers with commands, costs stated. Posted by nobody but Josh.
|
||||||
|
|
||||||
|
> **Igneum: a GPU-mined L1 where the miners are the provers. Devnet live. One person, no premine, every criticism logged.**
|
||||||
|
>
|
||||||
|
> I mined through the GPU years. The Merge in September 2022 ended that income and the miners' place in Ethereum. I am building the coin I wanted to exist: GPU-mined, the miners prove the blocks, no founder allocation. One person, AI systems doing the engineering.
|
||||||
|
>
|
||||||
|
> What runs today (devnet since 3 October 2026):
|
||||||
|
> - A random mining program every hour, compiled on the card. Swaps measured on Apple, NVIDIA and AMD, 0 rejected blocks (bench log, 4 Oct).
|
||||||
|
> - Memory-hard: 128 dependent random reads per hash. An RTX 5090 does 136 MH/s at 17.5 G reads a second, 82% of its memory's random-read ceiling, integer units at 0.15%. Computing items instead of loading them runs 4.8x slower (M5 Max, 3 Oct).
|
||||||
|
> - GHOSTDAG ordering forked from rusty-kaspa, about 1 block a second.
|
||||||
|
> - EVM execution, native on every node.
|
||||||
|
> - Shards proven with SP1 on miners' cards and paid from the block: 388 shards, 446.13 IGN on 5 Oct. A 3060 mines and proves at an 8.9 GB peak (rented cards, 6 Oct).
|
||||||
|
> - Finality by miner weight: 30 days of blocks per key, locks at two thirds, no stake, no slashing. No lock in the first 30 days of mainnet.
|
||||||
|
>
|
||||||
|
> Run the vectors yourself: `git clone https://github.com/igneum-network/spec && cd spec/igneum-pow && cargo test --release` (96 vectors per pack; three GPU vendors, one fingerprint over 16.7 million nonces). The hash-rate reproduction package follows on igneum.network/evidence; your card's result file gets a row.
|
||||||
|
>
|
||||||
|
> Costs: the miner software takes 1% (1 block in 100, off with `--dev-fee 0`). The protocol takes nothing. No cryptographer is hired yet; the review brief is USD 80,000 to 160,000, unfunded. The installer is unsigned. The chip model is published: the strongest chip in it reaches 5x to 9x per joule against a 5090 before the lever we are gating, about 2x after, sources on the page.
|
||||||
|
>
|
||||||
|
> Every criticism we expect is at igneum.network/ledger: 167 entries, 53 conceded, 7 open, dated. A new one goes in with your name. "Don't ASICs make a chain safer?" is answered with numbers at igneum.network/litepaper#don-t-asics-make-a-chain-safer.
|
||||||
|
>
|
||||||
|
> We want help: cryptographers, node engineers, miners who will test. Pick an open row on the ledger and write to hello@igneum.network.
|
||||||
|
>
|
||||||
|
> Litepaper: igneum.network/litepaper. Live: igneum.network/live. Nothing is for sale.
|
||||||
|
|
||||||
|
The numbers in the post and where each lives: hourly swaps (bench log "first hourly program swap", 4 October); 136 MH/s, 17.5 G reads, 82%, 0.15% (`counter-asic-3-status.md` section 6 draft (b), from `chip-model-v3.md` and the read-width entry); 4.8x (`proto-metal/MEMHARD.md`, bench log 3 October); 388 shards and 446.13 IGN (bench log 5 October, "the first shards proven, verified and paid"); the 3060 (`prover-tiers-real-cards.md`); 96 vectors and the fingerprint `25f96e7dce90bd4e` (`repro.md` 2.1 and 2.2); 1 in 100 (`site/miner.html:434`, 9 in 785 measured); USD 80,000 to 160,000 (`docs/plans/funding.md`, the cryptanalysis row); 167, 53, 7 (this page's count table). The chip sentence is draft (a) as deployed on the hero (5x to 9x per joule in the model; the latency-shadow lever measured at 2.1x on GDDR7 at a chip core equal to the GPU's, Counter ASIC 3.0 item 8). The ASIC entry's rental row cites the fleet's bench entry that lands tonight; the post does not quote that figure.
|
||||||
|
|
||||||
|
## 6. Fixes made on this branch
|
||||||
|
|
||||||
|
Exact before and after, nothing pushed, nothing deployed. Lines touched by `ledger-text` or `ca2-coord` were left alone so those merges stay clean.
|
||||||
|
|
||||||
|
| File | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| `site/index.html:372` | `<div class="k">miners active</div>` | `<div class="k">vote keys active in 10 min (a card runs several)</div>` |
|
||||||
|
| `site/index.html:492` | `Igneum Ember (the app is still labelled Igneum Miner until 0.3.6)` | `Igneum Ember (the app window still says Igneum Miner at 0.3.13)` |
|
||||||
|
| `site/miner.html:266` | the same caption | the same replacement |
|
||||||
|
| `site/litepaper.html:396` | `<td>About one a second</td>` | `<td>About one a second with the full fleet; 0.65 a second over the hour to 16:00 UTC on 6 Oct 2026 with one PC off (the live page's hour count, 2,325 blocks)</td>` |
|
||||||
|
| `site/litepaper.html:400` | `The one-click miner on the fleet, version 0.3.5; 0.3.6 staged` | `The one-click miner on the fleet, version 0.3.13 (6 Oct 2026); the app window still says Igneum Miner` |
|
||||||
|
| `site/litepaper.html:645` | `... the bridge's is the one to read. Designed, open item O-5.4.</li>` | appended: `On the devnet the activation heights and one execution-state restart (6 October 2026) reach every node through the signed update manifest, so on the devnet the release key acts as the operator; the sentence above holds for mainnet consensus only once that path is closed, and the public testnet terms will say which parameters still travel that way.` |
|
||||||
|
| `site/ledger.html` | did not exist (`/ledger` redirects home) | the page of section 4, 167 entries |
|
||||||
|
| `tools/ledger-page.mjs` | did not exist | the generator, with the scrub list |
|
||||||
|
|
||||||
|
Not changed here, by design: everything on `ledger-text` and `ca2-coord` (findings 1, 2, 6, 10, 12, 13), `site/vercel.json` (finding 22), `docs/evidence.md` and `site/evidence.html` (finding 9), the hero sentence (finding 3, the owner's decision).
|
||||||
|
|
||||||
|
## 7. What was checked and how
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| master against origin | equal (`git status -sb`); the worktree files are the live files |
|
||||||
|
| `ledger-text` against master | 30 files, 0 merge conflicts (`git merge-tree`); the 14-sentence grep in section 0 |
|
||||||
|
| `ca2-coord` bounty commits | 6141e01, 8c5b02f (15:45 and 15:46 UTC), litepaper lines 301, 424, 684, 686; not an ancestor of master |
|
||||||
|
| Live site | home, /live, /explorer, /bench, /evidence, /miners, /faucet return 200; /ledger 307 to /; `/api/live` fresh (age 0.4 s), `/api/checkpoint` serving index 6497 to 6499 |
|
||||||
|
| Downloads host | Windows and macOS installers served (45.4 MB and 41.9 MB, last modified 15:51 UTC today); `/public/igneum-repro.tar.gz` 404 |
|
||||||
|
| Spec repository | public, MIT, pushed 4 October 08:20 UTC, issues enabled, 0 open; the node repository 404 |
|
||||||
|
| The homepage in the browser | light client card LIVE, "Checkpoint 6499, certified by 17 miners", 124.9 ms, 69.8% of weight; strip 26 / 0.7 blocks a second / 181.3 MH/s / 110,044 blocks; download meta v0.3.12 |
|
||||||
|
| The live page in the browser | 26 identities, 28 voters, locks at 66.8% to 89.1% of weight, difficulty moves of 5% to 12%, "proven not active", 2,325 blocks in the hour (38.8 a minute) |
|
||||||
|
| The installed app (127.0.0.1, read only, paused) | Igneum Miner 0.3.13, 23,056 blocks found lifetime, the manifest-parameters log line and the exec-restart job of 16:36 local; `api/state`: dev fee on at 1% with an empty address, proving off, verifier off |
|
||||||
|
| `site/ledger.html` | 167 entries, 0 private-string hits (the forbidden list plus the founder's name and the other brands); the site build passes with the page present |
|
||||||
181
infra/build-server/lib.sh
Executable file
181
infra/build-server/lib.sh
Executable file
|
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared by infra/build-server/run-from-mac.sh, tools/build-remote.sh and tools/cross-remote.sh. Source it, do not run it.
|
||||||
|
# Everything that talks to igneum-build-1 from the Mac goes through here: the host line, the ssh options (the ops key
|
||||||
|
# ~/.ssh/igneum_ed25519, a shared control socket so one build is one ssh session), the mirror push, the remote checkout
|
||||||
|
# and the source overlay (rsync by checksum, changed files re-stamped: the copied-sources rule of 5 October 2026).
|
||||||
|
#
|
||||||
|
# Layout on the box (provision.sh): /srv/builds/<worktree> mirrors the Mac's igneum worktree ROOT (the directory that holds
|
||||||
|
# igneum-pow/, app/, proving/ and vendor/), so the fork's relative path dependency `../../../../igneum-pow`
|
||||||
|
# (vendor/igneum-node/consensus/pow/Cargo.toml) resolves on the box exactly as on the Mac:
|
||||||
|
# Mac /Users/joshm/Projects/igneum-wt-ship0311/vendor/igneum-node-0311 -> box /srv/builds/igneum-wt-ship0311/vendor/igneum-node-0311
|
||||||
|
# Mac /Users/joshm/Projects/igneum-wt-ship0311/igneum-pow -> box /srv/builds/igneum-wt-ship0311/igneum-pow
|
||||||
|
# Mac /Users/joshm/Projects/igneum/app/igneum-app -> box /srv/builds/igneum/app/igneum-app
|
||||||
|
# The fork's kaspa-build-info reads `git rev-parse HEAD` at build time and the release plans check the commit in the binary's
|
||||||
|
# strings, so the fork tree on the box is a real clone of the bare mirror /srv/igneum-node.git checked out at the Mac's HEAD,
|
||||||
|
# with the Mac's uncommitted changes rsynced on top. The igneum repo's crates get the same from /srv/igneum.git.
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034 # shared with the scripts that source lib.sh
|
||||||
|
BS_KEY="${IGNEUM_BUILD_KEY:-$HOME/.ssh/igneum_ed25519}"
|
||||||
|
BS_HOST_FILE="${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" # one line: build@<ip>
|
||||||
|
BS_ROOT_REMOTE=/srv/builds
|
||||||
|
BS_MIRROR_REPO=/srv/igneum.git
|
||||||
|
BS_MIRROR_NODE=/srv/igneum-node.git
|
||||||
|
|
||||||
|
bs_log() { printf '%s %s: %s\n' "$(date -u +%H:%M:%S)" "${BS_TOOL:-build-server}" "$*" >&2; }
|
||||||
|
bs_die() { bs_log "ERROR: $*"; exit 1; }
|
||||||
|
|
||||||
|
bs_host() {
|
||||||
|
BS_HOST="${BUILD_HOST:-}"
|
||||||
|
if [ -z "$BS_HOST" ]; then
|
||||||
|
[ -s "$BS_HOST_FILE" ] || bs_die "no build server: write build@<ip> to $BS_HOST_FILE (infra/build-server/run-from-mac.sh does) or set BUILD_HOST"
|
||||||
|
BS_HOST="$(head -1 "$BS_HOST_FILE" | tr -d '[:space:]')"
|
||||||
|
fi
|
||||||
|
case "$BS_HOST" in *@*) ;; *) bs_die "BUILD_HOST must be user@host, got '$BS_HOST'" ;; esac
|
||||||
|
[ -r "$BS_KEY" ] || bs_die "no ssh key at $BS_KEY"
|
||||||
|
mkdir -p "$HOME/.ssh/cm"
|
||||||
|
BS_SSH_OPTS=(-i "$BS_KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 -o ServerAliveCountMax=6
|
||||||
|
-o ControlMaster=auto -o ControlPath="$HOME/.ssh/cm/igneum-build-%r@%h:%p" -o ControlPersist=900)
|
||||||
|
BS_SSH_CMD="ssh"; local o; for o in "${BS_SSH_OPTS[@]}"; do BS_SSH_CMD="$BS_SSH_CMD $(printf '%q' "$o")"; done
|
||||||
|
}
|
||||||
|
|
||||||
|
bs_ssh() { ssh "${BS_SSH_OPTS[@]}" "$BS_HOST" "$@"; }
|
||||||
|
bs_rsync() { rsync -e "$BS_SSH_CMD" "$@"; }
|
||||||
|
|
||||||
|
# the two sides' rustc must agree (the box is pinned by provision.sh RUST_TOOLCHAIN; the Mac runs rustup's stable):
|
||||||
|
# a different compiler gives different bytes and, across a minor version, different lints and errors
|
||||||
|
bs_toolchain_check() {
|
||||||
|
local mac box
|
||||||
|
mac=$("${CARGO_HOME:-$HOME/.cargo}/bin/rustc" --version 2>/dev/null | awk '{ print $2 }')
|
||||||
|
box=$(bs_ssh '. /etc/profile.d/igneum-build.sh; rustc --version' 2>/dev/null | awk '{ print $2 }')
|
||||||
|
[ -n "$box" ] || bs_die "cannot read rustc on $BS_HOST (is it provisioned? infra/build-server/run-from-mac.sh)"
|
||||||
|
if [ "$mac" != "$box" ]; then
|
||||||
|
if [ "${IGNEUM_TOOLCHAIN_MISMATCH:-}" = ok ]; then bs_log "WARNING: rustc $mac on the Mac, $box on the box (IGNEUM_TOOLCHAIN_MISMATCH=ok)"
|
||||||
|
else bs_die "rustc $mac on the Mac, $box on the box; re-provision with RUST_TOOLCHAIN=$mac or set IGNEUM_TOOLCHAIN_MISMATCH=ok"; fi
|
||||||
|
else bs_log "rustc $box on both sides"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where am I? Sets BS_KIND (node = a worktree of the fork under vendor/; repo = a crate of the igneum repo), BS_TOP (the git
|
||||||
|
# top level of the crate's repo), BS_WT_ROOT (the igneum worktree root), BS_WT (its name = the directory on the box),
|
||||||
|
# BS_CRATE (the crate dir, = $PWD), BS_CRATE_REL (relative to BS_WT_ROOT), BS_MIRROR, BS_BRANCH, BS_SHA, BS_REMOTE_WT,
|
||||||
|
# BS_REMOTE_CRATE, and BS_LOCAL_DIRS (every directory of a path dependency, relative to BS_WT_ROOT, from cargo metadata).
|
||||||
|
bs_context() {
|
||||||
|
BS_CRATE="$PWD"
|
||||||
|
[ -f "$BS_CRATE/Cargo.toml" ] || bs_die "no Cargo.toml in $BS_CRATE: run from the crate directory (the fork worktree, igneum-pow, app/igneum-app, proving/igneum-prove)"
|
||||||
|
BS_TOP=$(git -C "$BS_CRATE" rev-parse --show-toplevel 2>/dev/null) || bs_die "$BS_CRATE is not inside a git worktree"
|
||||||
|
case "$BS_TOP" in
|
||||||
|
*/vendor/*)
|
||||||
|
BS_KIND=node; BS_MIRROR=$BS_MIRROR_NODE
|
||||||
|
BS_WT_ROOT=$(cd "$BS_TOP/../.." && pwd)
|
||||||
|
[ -f "$BS_WT_ROOT/igneum-pow/Cargo.toml" ] || bs_die "$BS_TOP looks like a fork worktree but $BS_WT_ROOT/igneum-pow is missing"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
BS_KIND=repo; BS_MIRROR=$BS_MIRROR_REPO; BS_WT_ROOT="$BS_TOP"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
BS_WT=$(basename "$BS_WT_ROOT")
|
||||||
|
BS_CRATE_REL=$(python3 -c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$BS_CRATE" "$BS_WT_ROOT")
|
||||||
|
BS_TOP_REL=$(python3 -c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$BS_TOP" "$BS_WT_ROOT")
|
||||||
|
case "$BS_CRATE_REL" in ..*) bs_die "$BS_CRATE is outside the worktree root $BS_WT_ROOT" ;; esac
|
||||||
|
BS_BRANCH=$(git -C "$BS_TOP" branch --show-current 2>/dev/null || true)
|
||||||
|
BS_SHA=$(git -C "$BS_TOP" rev-parse HEAD)
|
||||||
|
[ -n "$BS_BRANCH" ] || BS_BRANCH="detached-$(git -C "$BS_TOP" rev-parse --short HEAD)"
|
||||||
|
BS_REMOTE_WT="$BS_ROOT_REMOTE/$BS_WT"
|
||||||
|
BS_REMOTE_CRATE="$BS_REMOTE_WT/$BS_CRATE_REL"
|
||||||
|
# every local (path) package of the crate's dependency graph, as directories relative to the worktree root; the ones inside
|
||||||
|
# BS_TOP are covered by the git checkout plus the overlay of BS_TOP itself (node kind) or synced one by one (repo kind)
|
||||||
|
BS_LOCAL_DIRS=$(cd "$BS_CRATE" && "${CARGO_HOME:-$HOME/.cargo}/bin/cargo" metadata --format-version 1 2>/dev/null | python3 -c '
|
||||||
|
import json, os, sys
|
||||||
|
d = json.load(sys.stdin); root = sys.argv[1]; top = sys.argv[2]; kind = sys.argv[3]
|
||||||
|
dirs = set()
|
||||||
|
for p in d["packages"]:
|
||||||
|
if p["source"] is not None: continue
|
||||||
|
m = os.path.dirname(p["manifest_path"])
|
||||||
|
if kind == "node" and (m == top or m.startswith(top + "/")): dirs.add(top); continue
|
||||||
|
dirs.add(m)
|
||||||
|
out = []
|
||||||
|
for m in sorted(dirs):
|
||||||
|
r = os.path.relpath(m, root)
|
||||||
|
if r.startswith(".."): sys.exit("path dependency %s is outside the worktree root %s" % (m, root))
|
||||||
|
out.append(r)
|
||||||
|
print("\n".join(out))' "$BS_WT_ROOT" "$BS_TOP" "$BS_KIND") || bs_die "cargo metadata failed in $BS_CRATE"
|
||||||
|
[ -n "$BS_LOCAL_DIRS" ] || bs_die "cargo metadata listed no local packages in $BS_CRATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# push the crate repo's HEAD to its bare mirror on the box (fast after the first time), then check the remote tree out at that
|
||||||
|
# commit ON A BRANCH of that name: kaspa-build-info (build-info/build.rs try_git_head) embeds the commit only when .git is a
|
||||||
|
# directory AND HEAD is a symbolic ref to a loose branch file; a detached HEAD or a worktree's .git file gives an empty hash
|
||||||
|
# (which is why the Mac's worktree builds print "igneumd 2.1.0" with no commit, 6 Oct 2026). The mirror doubles as the CI
|
||||||
|
# runner's source later.
|
||||||
|
bs_push_and_checkout() {
|
||||||
|
local url="$BS_HOST:$BS_MIRROR" remote_top="$BS_REMOTE_WT/$BS_TOP_REL"
|
||||||
|
[ "$BS_TOP_REL" = . ] && remote_top="$BS_REMOTE_WT"
|
||||||
|
bs_log "push $BS_TOP HEAD $BS_SHA ($BS_BRANCH) -> $url"
|
||||||
|
GIT_SSH_COMMAND="$BS_SSH_CMD" git -C "$BS_TOP" push -q --force "$url" "HEAD:refs/heads/$BS_BRANCH" || bs_die "push to the mirror failed"
|
||||||
|
bs_ssh "set -e; mkdir -p '$BS_REMOTE_WT'
|
||||||
|
if [ ! -d '$remote_top/.git' ]; then rm -rf '$remote_top'; git clone -q --no-checkout '$BS_MIRROR' '$remote_top'; fi
|
||||||
|
cd '$remote_top'; git fetch -q origin '+refs/heads/*:refs/remotes/origin/*'; git checkout -q -B '$BS_BRANCH' '$BS_SHA'; git reset -q --hard '$BS_SHA'
|
||||||
|
git status --porcelain | head -3" || bs_die "remote checkout at $remote_top failed"
|
||||||
|
BS_REMOTE_TOP="$remote_top"
|
||||||
|
}
|
||||||
|
|
||||||
|
# rsync one directory of the worktree to the same place on the box. By checksum and WITHOUT preserving times, so a file whose
|
||||||
|
# content changed is written with the box's clock and nothing older than the last build slips past cargo's mtime check (the
|
||||||
|
# stale-build class, 4 and 5 October 2026); the files rsync wrote are listed and re-stamped with touch as well, so the rule is
|
||||||
|
# visible here and tools/ci/copied-sources-check.sh sees it. target dirs and .git never travel; --delete keeps the box equal to
|
||||||
|
# the Mac inside the directory (excluded paths are protected).
|
||||||
|
bs_overlay_dir() {
|
||||||
|
local rel="$1" src="$BS_WT_ROOT/$1" dst="$BS_REMOTE_WT/$1" list nfiles ndirs
|
||||||
|
[ -d "$src" ] || bs_die "no $src"
|
||||||
|
list=$(mktemp)
|
||||||
|
bs_ssh "mkdir -p '$dst'"
|
||||||
|
bs_rsync -rlpgoD --checksum --delete --out-format='%n' \
|
||||||
|
--exclude '/target' --exclude '/target-*' --exclude '/target/' --exclude 'target-*/' --exclude '.git' --exclude '.DS_Store' --exclude 'node_modules' \
|
||||||
|
"$src/" "$BS_HOST:$dst/" > "$list" || { rm -f "$list"; bs_die "rsync of $rel failed"; }
|
||||||
|
# files only: directories are listed whenever an attribute differs (a fresh clone's ownership), and a tree whose files
|
||||||
|
# were all identical lists ONLY directories (first run of 6 October 2026: an empty file list failed the pipeline)
|
||||||
|
nfiles=$(grep -vc '/$' "$list" || true); ndirs=$(grep -c '/$' "$list" || true)
|
||||||
|
if [ "${nfiles:-0}" -gt 0 ]; then
|
||||||
|
if ! grep -v '/$' "$list" | tr '\n' '\0' | bs_ssh "cd '$dst' && xargs -0 -r touch --no-create"; then rm -f "$list"; bs_die "re-stamp of $rel failed"; fi
|
||||||
|
fi
|
||||||
|
bs_log "overlay $rel -> $dst: ${nfiles:-0} file(s) written and re-stamped, ${ndirs:-0} dir(s)"
|
||||||
|
rm -f "$list"
|
||||||
|
}
|
||||||
|
|
||||||
|
bs_sync_sources() {
|
||||||
|
local d
|
||||||
|
bs_push_and_checkout
|
||||||
|
for d in $BS_LOCAL_DIRS; do bs_overlay_dir "$d"; done
|
||||||
|
}
|
||||||
|
|
||||||
|
bs_sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||||
|
bs_size() { stat -f %z "$1" 2>/dev/null || stat -c %s "$1"; }
|
||||||
|
bs_fmt_secs() { local s=$1; printf '%d min %02d s' $((s / 60)) $((s % 60)); }
|
||||||
|
|
||||||
|
# kind of a run for the box's JSONL log (main's rule of 6 October 2026): <tool> <first cargo word>
|
||||||
|
bs_kind() {
|
||||||
|
local tool="$1" word="$2"
|
||||||
|
case "$word" in test) echo suite; return ;; check|clippy) echo check; return ;; esac
|
||||||
|
if [ "$tool" = cross-remote ]; then
|
||||||
|
case "$BS_KIND:$BS_CRATE_REL" in node:*) echo node-windows ;; repo:app/igneum-app) echo app-windows ;; *) echo other ;; esac; return
|
||||||
|
fi
|
||||||
|
case "$BS_KIND:$BS_CRATE_REL" in node:*) echo node-linux ;; repo:app/igneum-app) echo app ;; repo:proving/igneum-prove) echo prove ;; *) echo other ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# the remote runner (infra/build-server/remote-run.sh, piped to `bash -s` on the box behind the BR_* exports): takes one of the
|
||||||
|
# box's build slots (never the Mac's), runs the command in the crate dir with sccache, prints the RESULT line and appends one
|
||||||
|
# JSON line to /srv/builds/_log/builds.jsonl for the worker dashboard.
|
||||||
|
# bs_remote_run <remote crate dir> <label> <shell command string>
|
||||||
|
# with BR_KIND, BR_COMMAND, BR_TARGET and BR_ARTEFACTS set by the caller; the agent name is IGNEUM_AGENT (default the worktree)
|
||||||
|
# and is appended to the label as "; agent=<name>".
|
||||||
|
bs_remote_run() {
|
||||||
|
local dir="$1" label="$2" cmd="$3" agent="${IGNEUM_AGENT:-$BS_WT}" v
|
||||||
|
label="$label; agent=$agent"
|
||||||
|
BR_DIR="$dir" BR_LABEL="$label" BR_CMD="$cmd" BR_TOOL="${BS_TOOL:-build-remote}" BR_WT="$BS_WT" BR_CRATE="$BS_CRATE_REL" \
|
||||||
|
BR_BRANCH="$BS_BRANCH" BR_SHA="$BS_SHA" BR_AGENT="$agent" BR_KIND="${BR_KIND:-other}" BR_COMMAND="${BR_COMMAND:-}" \
|
||||||
|
BR_TARGET="${BR_TARGET:-}" BR_ARTEFACTS="${BR_ARTEFACTS:-}" \
|
||||||
|
bash -c '
|
||||||
|
for v in BR_DIR BR_LABEL BR_CMD BR_TOOL BR_WT BR_CRATE BR_BRANCH BR_SHA BR_AGENT BR_KIND BR_COMMAND BR_TARGET BR_ARTEFACTS; do
|
||||||
|
printf "export %s=%q\n" "$v" "${!v}"
|
||||||
|
done
|
||||||
|
cat "$0"' "$(dirname "${BASH_SOURCE[0]}")/remote-run.sh" | bs_ssh 'bash -s'
|
||||||
|
}
|
||||||
434
infra/build-server/provision.sh
Executable file
434
infra/build-server/provision.sh
Executable file
|
|
@ -0,0 +1,434 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Provision igneum-build-1, the Hetzner dedicated build server (AX162-1-LTD: EPYC 9454P 48 cores / 96 threads, 128 GB,
|
||||||
|
# 2x 3.84 TB NVMe, Falkenstein; ordered 6 October 2026). Idempotent: every step checks before it changes anything and
|
||||||
|
# says "ok" (nothing to do) or "changed". Run it over ssh as root; nothing here reads a secret.
|
||||||
|
#
|
||||||
|
# infra/build-server/run-from-mac.sh <ip> the usual way (ships this file, fills WORKTREES, writes the host file)
|
||||||
|
# ssh root@<ip> 'bash -s' < infra/build-server/provision.sh the bare way
|
||||||
|
# ssh root@<ip> 'MODE=install bash -s' < infra/build-server/provision.sh force the rescue-system path
|
||||||
|
#
|
||||||
|
# Two modes, chosen by MODE (auto, install, provision; default auto):
|
||||||
|
# install the box booted into Hetzner's rescue system (installimage present, hostname rescue*): run installimage in
|
||||||
|
# batch mode for Ubuntu 24.04 with software RAID 1 over the two NVMe drives, no swap, the rescue system's
|
||||||
|
# authorized_keys taken over, the image signature checked, then reboot. Run the script again after the reboot.
|
||||||
|
# Ran on igneum-build-1 on 6 October 2026 at 17:16 to 17:20 UTC (16 steps, no prompt).
|
||||||
|
# provision a running Ubuntu 24.04: user `build` with root's key, the compiler and cross toolchains, rustup pinned to
|
||||||
|
# RUST_TOOLCHAIN with the x86_64-pc-windows-gnu target, sccache with a 100 GB disk cache, Node 22, git, tmux,
|
||||||
|
# a swap-free tuned sysctl, the two bare mirrors (/srv/igneum.git, /srv/igneum-node.git), /srv/builds with one
|
||||||
|
# directory per agent worktree, sshd key-only, ufw with 22 and the seed p2p ports.
|
||||||
|
#
|
||||||
|
# Settings (environment, all optional):
|
||||||
|
# RUST_TOOLCHAIN 1.99.0 the Mac's `rustc --version` on 6 October 2026. Neither the repo nor the fork carries a
|
||||||
|
# rust-toolchain file (checked 6 October 2026), so the pin lives here; the fork's Cargo.toml says
|
||||||
|
# rust-version 1.91.0. tools/build-remote.sh compares the two sides and refuses a mismatch.
|
||||||
|
# SCCACHE_GB 100 the local disk cache at /srv/sccache
|
||||||
|
# SCCACHE_VERSION (unset) a `cargo install sccache --version` pin; unset = the newest on crates.io
|
||||||
|
# NODE_MAJOR 22
|
||||||
|
# WORKTREES "" space-separated agent worktree names, one /srv/builds/<name> each (run-from-mac.sh fills it from
|
||||||
|
# `git worktree list` on the Mac; tools/build-remote.sh creates a missing one on first use)
|
||||||
|
# SLOTS 1 remote build slots (tools/build-remote.sh takes one; with 1 slot every build gets the box)
|
||||||
|
# P2P_PORTS "26611 26811" TCP ports ufw opens beside 22: the devnet seed's p2p (infra/seed-nodes/config.sh devnet
|
||||||
|
# P2P_PORT=26611) and the testnet seed's (26811). A suffixed devnet (Devnet 2, the fleet's staging
|
||||||
|
# chain) listens on the same 26611 (infra/cloud-devnet/config.sh P2P_PORT=26611). RPC ports
|
||||||
|
# (26610, 28610, 26790 and the 268xx set) stay on loopback as on every seed, so they are not opened.
|
||||||
|
# BOX_HOSTNAME igneum-build-1
|
||||||
|
# WORKERS_HOST build.igneum.network Caddy serves /srv/workers/{workers,headline}.json there (read-only, all else 404)
|
||||||
|
# SSH_PUBKEY (unset) a public key line for the build user when root has no authorized_keys (installimage installs it)
|
||||||
|
#
|
||||||
|
# Mirrors: the Mac pushes to them (never a clone from GitHub; the fork vendor/igneum-node exists only on the Mac):
|
||||||
|
# git -C /Users/joshm/Projects/igneum remote add build build@<ip>:/srv/igneum.git
|
||||||
|
# git -C /Users/joshm/Projects/igneum/vendor/igneum-node remote add build build@<ip>:/srv/igneum-node.git
|
||||||
|
# git push build --all (tools/build-remote.sh pushes the branch it builds before every build)
|
||||||
|
#
|
||||||
|
# What this script does NOT do: install zig or cargo-zigbuild (the Mac's glibc 2.36 Linux cross-build, infra/cross/build-linux.sh,
|
||||||
|
# stays on the Mac until the box is proven; a native build here links glibc 2.39, which Debian 13 seeds accept and HiveOS
|
||||||
|
# does not), start any node, or copy a secret. The installimage flags and the image name were read from the live rescue system
|
||||||
|
# on 6 October 2026 (`installimage -h`, /root/.oldroot/nfs/images); the script still reads the image list instead of hard-coding a name.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="${MODE:-auto}"
|
||||||
|
RUST_TOOLCHAIN="${RUST_TOOLCHAIN:-1.99.0}"
|
||||||
|
SCCACHE_GB="${SCCACHE_GB:-100}"
|
||||||
|
SCCACHE_VERSION="${SCCACHE_VERSION:-}"
|
||||||
|
NODE_MAJOR="${NODE_MAJOR:-22}"
|
||||||
|
WORKTREES="${WORKTREES:-}"
|
||||||
|
SLOTS="${SLOTS:-1}"
|
||||||
|
P2P_PORTS="${P2P_PORTS:-26611 26811}"
|
||||||
|
BOX_HOSTNAME="${BOX_HOSTNAME:-igneum-build-1}"
|
||||||
|
WORKERS_HOST="${WORKERS_HOST:-build.igneum.network}" # the dashboard feed's HTTPS name (A record in deSEC, 6 Oct 2026)
|
||||||
|
SSH_PUBKEY="${SSH_PUBKEY:-}"
|
||||||
|
BUILD_USER=build
|
||||||
|
BUILD_HOME=/home/$BUILD_USER
|
||||||
|
|
||||||
|
log() { printf '%s provision: %s\n' "$(date -u +%H:%M:%S)" "$*"; }
|
||||||
|
die() { log "ERROR: $*" >&2; exit 1; }
|
||||||
|
changed() { log "$1: changed${2:+ ($2)}"; }
|
||||||
|
ok() { log "$1: ok${2:+ ($2)}"; }
|
||||||
|
as_build() { su - "$BUILD_USER" -c "$*"; }
|
||||||
|
|
||||||
|
[ "$(id -u)" = 0 ] || die "run as root"
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------------------------------
|
||||||
|
# install mode: the rescue system
|
||||||
|
# ----------------------------------------------------------------------------------------------------------------------
|
||||||
|
INSTALLIMAGE=/root/.oldroot/nfs/install/installimage # not on PATH in a non-interactive ssh shell (read 6 Oct 2026)
|
||||||
|
|
||||||
|
in_rescue() {
|
||||||
|
[ -x "$INSTALLIMAGE" ] || return 1
|
||||||
|
case "$(hostname)" in rescue*) return 0 ;; esac
|
||||||
|
[ -d /root/.oldroot/nfs/images ]
|
||||||
|
}
|
||||||
|
|
||||||
|
do_install() {
|
||||||
|
local images drives image parts
|
||||||
|
images=/root/.oldroot/nfs/images
|
||||||
|
[ -d "$images" ] || die "no image directory at $images: not the Hetzner rescue system"
|
||||||
|
# the Ubuntu 24.04 (noble) amd64 base image the rescue system offers (read on 6 October 2026: Ubuntu-2404-noble-amd64-base.tar.zst
|
||||||
|
# with a detached .sig; read, not hard-coded, because Hetzner refreshes the names)
|
||||||
|
image=$(find "$images" -maxdepth 1 -type f -iregex '.*/ubuntu-2404.*amd64.*\.tar\.\(zst\|gz\|xz\)' -printf '%f\n' | sort | tail -1 || true)
|
||||||
|
[ -n "$image" ] || die "no Ubuntu 24.04 amd64 image under $images: $(find "$images" -maxdepth 1 -type f -printf '%f ' )"
|
||||||
|
mapfile -t drives < <(lsblk -dn -o NAME,TYPE | awk '$2 == "disk" && $1 ~ /^nvme/ { print $1 }' | sort)
|
||||||
|
[ "${#drives[@]}" = 2 ] || die "expected exactly two NVMe drives for RAID 1, found ${#drives[@]}: ${drives[*]:-none}"
|
||||||
|
[ -s /root/.ssh/authorized_keys ] || die "/root/.ssh/authorized_keys is empty in the rescue system; -t yes would carry nothing into the image"
|
||||||
|
[ -d /sys/firmware/efi ] || log "WARNING: no /sys/firmware/efi, the box booted in BIOS mode; the esp partition is harmless but grub goes to the MBR"
|
||||||
|
# no swap partition: 128 GB of RAM and a swap-free sysctl (the provision mode checks no swap is active)
|
||||||
|
parts="/boot/efi:esp:512M,/boot:ext4:1G,/:ext4:all"
|
||||||
|
log "installimage: image $image, drives ${drives[*]} as software RAID 1, partitions $parts, hostname $BOX_HOSTNAME, rescue ssh keys taken over (-t yes), image signature checked (-g)"
|
||||||
|
log "this WIPES ${drives[*]}"
|
||||||
|
# flag form, read from `installimage -h` on 6 October 2026: -a batch, -n hostname, -r raid, -l level, -i image, -g verify
|
||||||
|
# the detached signature, -p partitions mount:fs:size, -d drives, -t yes take over the rescue system's ssh keys (root's
|
||||||
|
# authorized_keys), -G yes new host keys. The -c config form forbids every other flag, so the keys could not travel with it.
|
||||||
|
TERM="${TERM:-xterm}" "$INSTALLIMAGE" -a -n "$BOX_HOSTNAME" -r yes -l 1 -i "$images/$image" -g -p "$parts" -d "$(IFS=,; echo "${drives[*]}")" -t yes -G yes
|
||||||
|
log "installimage finished; rebooting into Ubuntu. Run this script again (MODE=provision or auto) once ssh answers (the host key is new: -G yes)."
|
||||||
|
sync; reboot
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------------------------------
|
||||||
|
# provision mode: the installed Ubuntu
|
||||||
|
# ----------------------------------------------------------------------------------------------------------------------
|
||||||
|
step_hostname() {
|
||||||
|
if [ "$(hostnamectl --static 2>/dev/null || hostname)" = "$BOX_HOSTNAME" ]; then ok hostname "$BOX_HOSTNAME"; return; fi
|
||||||
|
hostnamectl set-hostname "$BOX_HOSTNAME"; grep -q "$BOX_HOSTNAME" /etc/hosts || printf '127.0.1.1 %s\n' "$BOX_HOSTNAME" >> /etc/hosts
|
||||||
|
changed hostname "$BOX_HOSTNAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_os_check() {
|
||||||
|
. /etc/os-release
|
||||||
|
[ "${ID:-}" = ubuntu ] && [ "${VERSION_ID:-}" = 24.04 ] || die "this is ${PRETTY_NAME:-unknown}, not Ubuntu 24.04"
|
||||||
|
ok os "$PRETTY_NAME, $(nproc) threads, $(awk '/MemTotal/ { printf "%d GB", $2 / 1024 / 1024 }' /proc/meminfo)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# the proven Ubuntu 24.04 set: the PC build job's APT lists (app/igneum-app/src/jobbuild.rs: mingw-w64 posix threads so
|
||||||
|
# libstdc++ has std::thread for rocksdb, clang for librocksdb-sys's bindgen, protoc for the node's proto crates) plus the
|
||||||
|
# task's list (build-essential, clang, lld, pkg-config, libssl-dev, cmake, git, tmux) and what the scripts here call
|
||||||
|
APT_PACKAGES=(
|
||||||
|
build-essential clang lld llvm libclang-dev pkg-config libssl-dev cmake protobuf-compiler
|
||||||
|
gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 binutils-mingw-w64-x86-64 mingw-w64-x86-64-dev mingw-w64-tools
|
||||||
|
git tmux curl ca-certificates xz-utils zstd unzip rsync jq python3 ufw htop file caddy
|
||||||
|
)
|
||||||
|
step_apt() {
|
||||||
|
local need=() p
|
||||||
|
for p in "${APT_PACKAGES[@]}"; do dpkg -s "$p" >/dev/null 2>&1 || need+=("$p"); done
|
||||||
|
if [ "${#need[@]}" = 0 ]; then ok apt "${#APT_PACKAGES[@]} packages present"; return; fi
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq --no-install-recommends "${need[@]}"
|
||||||
|
changed apt "installed ${need[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_mingw_alternatives() {
|
||||||
|
# Ubuntu ships -posix and -win32 variants behind update-alternatives; the Windows exes want posix threads (jobbuild.rs)
|
||||||
|
local tool want cur any=0
|
||||||
|
for tool in gcc g++; do
|
||||||
|
want="/usr/bin/x86_64-w64-mingw32-$tool-posix"
|
||||||
|
cur=$(readlink -f "/etc/alternatives/x86_64-w64-mingw32-$tool" 2>/dev/null || true)
|
||||||
|
[ -x "$want" ] || die "no $want after apt"
|
||||||
|
if [ "$cur" != "$want" ]; then update-alternatives --set "x86_64-w64-mingw32-$tool" "$want" >/dev/null; any=1; fi
|
||||||
|
done
|
||||||
|
[ "$any" = 1 ] && changed mingw-alternatives "posix threads" || ok mingw-alternatives "posix threads"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_no_swap() {
|
||||||
|
local any=0
|
||||||
|
if [ -n "$(swapon --noheadings --show 2>/dev/null)" ]; then swapoff -a; any=1; fi
|
||||||
|
if grep -qE '^[^#].*\sswap\s' /etc/fstab; then sed -i -E 's/^([^#].*\sswap\s.*)$/# \1 (disabled by infra\/build-server\/provision.sh)/' /etc/fstab; any=1; fi
|
||||||
|
[ "$any" = 1 ] && changed swap "off, fstab entry commented" || ok swap "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_sysctl() {
|
||||||
|
local f=/etc/sysctl.d/90-igneum-build.conf tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
cat > "$tmp" <<'EOF'
|
||||||
|
# igneum-build-1: a compile box with no swap (infra/build-server/provision.sh)
|
||||||
|
vm.swappiness = 0
|
||||||
|
vm.overcommit_memory = 0
|
||||||
|
vm.dirty_ratio = 20
|
||||||
|
vm.dirty_background_ratio = 5
|
||||||
|
vm.max_map_count = 1048576
|
||||||
|
fs.file-max = 4194304
|
||||||
|
fs.inotify.max_user_watches = 1048576
|
||||||
|
fs.inotify.max_user_instances = 8192
|
||||||
|
kernel.pid_max = 4194304
|
||||||
|
kernel.threads-max = 1048576
|
||||||
|
net.core.somaxconn = 4096
|
||||||
|
net.ipv4.tcp_fin_timeout = 15
|
||||||
|
EOF
|
||||||
|
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok sysctl "$f"; return; fi
|
||||||
|
install -m 644 "$tmp" "$f"; rm -f "$tmp"; sysctl --system >/dev/null
|
||||||
|
changed sysctl "$f applied"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_limits() {
|
||||||
|
local f=/etc/security/limits.d/90-igneum-build.conf
|
||||||
|
if [ -f "$f" ]; then ok limits; return; fi
|
||||||
|
printf '%s soft nofile 1048576\n%s hard nofile 1048576\n%s soft nproc unlimited\n' "$BUILD_USER" "$BUILD_USER" "$BUILD_USER" > "$f"
|
||||||
|
changed limits "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_user() {
|
||||||
|
local keys
|
||||||
|
if ! id -u "$BUILD_USER" >/dev/null 2>&1; then useradd -m -s /bin/bash -G users "$BUILD_USER"; changed user "$BUILD_USER created"; else ok user "$BUILD_USER"; fi
|
||||||
|
install -d -m 700 -o "$BUILD_USER" -g "$BUILD_USER" "$BUILD_HOME/.ssh"
|
||||||
|
if [ -n "$SSH_PUBKEY" ]; then keys="$SSH_PUBKEY"; elif [ -s /root/.ssh/authorized_keys ]; then keys=$(cat /root/.ssh/authorized_keys); else die "no key for $BUILD_USER: root has no authorized_keys and SSH_PUBKEY is unset"; fi
|
||||||
|
if [ -f "$BUILD_HOME/.ssh/authorized_keys" ] && [ "$(cat "$BUILD_HOME/.ssh/authorized_keys")" = "$keys" ]; then ok authorized_keys; else
|
||||||
|
printf '%s\n' "$keys" > "$BUILD_HOME/.ssh/authorized_keys"; chmod 600 "$BUILD_HOME/.ssh/authorized_keys"; chown "$BUILD_USER:$BUILD_USER" "$BUILD_HOME/.ssh/authorized_keys"
|
||||||
|
changed authorized_keys "$(printf '%s\n' "$keys" | grep -c .) key(s) from root"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
worktree_count() { find /srv/builds -mindepth 1 -maxdepth 1 -type d -not -name '_*' | wc -l | tr -d ' '; }
|
||||||
|
|
||||||
|
step_dirs() {
|
||||||
|
local d any=0
|
||||||
|
for d in /srv/builds /srv/builds/_locks /srv/sccache /srv/artefacts; do
|
||||||
|
if [ ! -d "$d" ]; then install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "$d"; any=1; fi
|
||||||
|
done
|
||||||
|
if [ ! -f /srv/builds/_locks/slots ] || [ "$(cat /srv/builds/_locks/slots)" != "$SLOTS" ]; then printf '%s\n' "$SLOTS" > /srv/builds/_locks/slots; chown "$BUILD_USER:$BUILD_USER" /srv/builds/_locks/slots; any=1; fi
|
||||||
|
for d in $WORKTREES; do
|
||||||
|
case "$d" in */*|.*|_*) die "worktree name '$d' is not a plain directory name" ;; esac
|
||||||
|
if [ ! -d "/srv/builds/$d" ]; then install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "/srv/builds/$d"; any=1; fi
|
||||||
|
done
|
||||||
|
[ "$any" = 1 ] && changed dirs "/srv/builds ($(worktree_count) worktree dirs), /srv/sccache, slots=$SLOTS" || ok dirs "$(worktree_count) worktree dirs, slots=$SLOTS"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_mirrors() {
|
||||||
|
local r any=0
|
||||||
|
for r in /srv/igneum.git /srv/igneum-node.git; do
|
||||||
|
if [ ! -d "$r" ]; then install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "$r"; as_build "git init -q --bare -b master $r"; any=1; fi
|
||||||
|
done
|
||||||
|
as_build "git config --global --get safe.directory >/dev/null 2>&1 || git config --global --add safe.directory '*'"
|
||||||
|
as_build "git config --global init.defaultBranch master; git config --global gc.auto 0"
|
||||||
|
[ "$any" = 1 ] && changed mirrors "bare /srv/igneum.git and /srv/igneum-node.git (push from the Mac, see the header)" || ok mirrors
|
||||||
|
}
|
||||||
|
|
||||||
|
step_rustup() {
|
||||||
|
local cargo="$BUILD_HOME/.cargo/bin/cargo" rustup="$BUILD_HOME/.cargo/bin/rustup" any=0 t
|
||||||
|
if [ ! -x "$rustup" ]; then
|
||||||
|
as_build "curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path --default-toolchain $RUST_TOOLCHAIN" >/dev/null
|
||||||
|
any=1
|
||||||
|
fi
|
||||||
|
if ! as_build "$rustup toolchain list" | grep -q "^$RUST_TOOLCHAIN-"; then as_build "$rustup toolchain install $RUST_TOOLCHAIN --profile minimal" >/dev/null; any=1; fi
|
||||||
|
if [ "$(as_build "$rustup default" | cut -d- -f1)" != "$RUST_TOOLCHAIN" ]; then as_build "$rustup default $RUST_TOOLCHAIN" >/dev/null; any=1; fi
|
||||||
|
for t in x86_64-pc-windows-gnu x86_64-unknown-linux-gnu; do
|
||||||
|
as_build "$rustup target list --installed --toolchain $RUST_TOOLCHAIN" | grep -qx "$t" || { as_build "$rustup target add $t --toolchain $RUST_TOOLCHAIN" >/dev/null; any=1; }
|
||||||
|
done
|
||||||
|
as_build "$rustup component list --installed --toolchain $RUST_TOOLCHAIN" | grep -q '^clippy' || { as_build "$rustup component add clippy rustfmt --toolchain $RUST_TOOLCHAIN" >/dev/null; any=1; }
|
||||||
|
[ "$any" = 1 ] && changed rustup "$(as_build "$cargo --version"), targets: $(as_build "$rustup target list --installed" | tr '\n' ' ')" || ok rustup "$(as_build "$cargo --version"), targets: $(as_build "$rustup target list --installed" | tr '\n' ' ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_sccache() {
|
||||||
|
local cargo="$BUILD_HOME/.cargo/bin/cargo" bin="$BUILD_HOME/.cargo/bin/sccache" cfgdir="$BUILD_HOME/.config/sccache" any=0 bytes tmp
|
||||||
|
if [ ! -x "$bin" ] || { [ -n "$SCCACHE_VERSION" ] && ! "$bin" --version | grep -q " $SCCACHE_VERSION\$"; }; then
|
||||||
|
as_build "$cargo install sccache --locked ${SCCACHE_VERSION:+--version $SCCACHE_VERSION}" >/dev/null 2>&1 || as_build "$cargo install sccache ${SCCACHE_VERSION:+--version $SCCACHE_VERSION}" >/dev/null
|
||||||
|
any=1
|
||||||
|
fi
|
||||||
|
bytes=$(( SCCACHE_GB * 1024 * 1024 * 1024 ))
|
||||||
|
install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" "$cfgdir"
|
||||||
|
tmp=$(mktemp)
|
||||||
|
printf '[cache.disk]\ndir = "/srv/sccache"\nsize = %s\n' "$bytes" > "$tmp"
|
||||||
|
if ! cmp -s "$tmp" "$cfgdir/config"; then install -m 644 -o "$BUILD_USER" -g "$BUILD_USER" "$tmp" "$cfgdir/config"; any=1; fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
[ "$any" = 1 ] && changed sccache "$(as_build "$bin --version"), disk cache /srv/sccache, $SCCACHE_GB GB" || ok sccache "$(as_build "$bin --version"), /srv/sccache $SCCACHE_GB GB"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_cargo_config() {
|
||||||
|
# the build user's cargo defaults: sccache in front of rustc, 90 jobs (96 threads, 6 left for ssh, rsync and the
|
||||||
|
# system), lld for the native target through clang. The Windows target's compilers and flags are NOT here: they are
|
||||||
|
# set per build by tools/cross-remote.sh, the same variables as the Mac's proto-cuda/windows-node/cross-build.sh and
|
||||||
|
# the PC's jobbuild.rs, so a build's flags are visible in the script that runs it.
|
||||||
|
local f="$BUILD_HOME/.cargo/config.toml" tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
cat > "$tmp" <<'EOF'
|
||||||
|
# igneum-build-1 (infra/build-server/provision.sh)
|
||||||
|
[build]
|
||||||
|
rustc-wrapper = "/home/build/.cargo/bin/sccache"
|
||||||
|
jobs = 90
|
||||||
|
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
|
||||||
|
|
||||||
|
[net]
|
||||||
|
git-fetch-with-cli = true
|
||||||
|
EOF
|
||||||
|
if cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok cargo-config "$f"; return; fi
|
||||||
|
install -m 644 -o "$BUILD_USER" -g "$BUILD_USER" "$tmp" "$f"; rm -f "$tmp"
|
||||||
|
changed cargo-config "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_profile() {
|
||||||
|
# sourced by tools/build-remote.sh's remote script (a non-login ssh shell reads no profile) and by login shells
|
||||||
|
local f=/etc/profile.d/igneum-build.sh tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
cat > "$tmp" <<EOF
|
||||||
|
# igneum-build-1 (infra/build-server/provision.sh)
|
||||||
|
export PATH="/home/build/.cargo/bin:/usr/local/bin:\$PATH"
|
||||||
|
export SCCACHE_DIR=/srv/sccache
|
||||||
|
export SCCACHE_CACHE_SIZE=${SCCACHE_GB}G
|
||||||
|
export CARGO_INCREMENTAL=0
|
||||||
|
export IGNEUM_BUILD_SLOTS_DIR=/srv/builds/_locks
|
||||||
|
export IGNEUM_BUILD_ROOT=/srv/builds
|
||||||
|
export IGNEUM_RUST_TOOLCHAIN=$RUST_TOOLCHAIN
|
||||||
|
EOF
|
||||||
|
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok profile "$f"; return; fi
|
||||||
|
install -m 644 "$tmp" "$f"; rm -f "$tmp"; changed profile "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_node() {
|
||||||
|
local want have shasums tarball ver dir
|
||||||
|
have=$(/usr/local/bin/node --version 2>/dev/null || true)
|
||||||
|
case "$have" in v$NODE_MAJOR.*) ok node "$have"; return ;; esac
|
||||||
|
# the newest $NODE_MAJOR release from nodejs.org, checked against its SHASUMS256.txt (https, the official host)
|
||||||
|
shasums=$(curl -fsSL "https://nodejs.org/dist/latest-v$NODE_MAJOR.x/SHASUMS256.txt")
|
||||||
|
tarball=$(printf '%s\n' "$shasums" | awk '$2 ~ /linux-x64\.tar\.xz$/ { print $2 }' | head -1)
|
||||||
|
[ -n "$tarball" ] || die "no linux-x64 tarball in the Node $NODE_MAJOR SHASUMS"
|
||||||
|
ver=${tarball#node-}; ver=${ver%-linux-x64.tar.xz}
|
||||||
|
dir=/usr/local/lib/nodejs
|
||||||
|
install -d "$dir"
|
||||||
|
( cd "$dir" && curl -fsSLO "https://nodejs.org/dist/latest-v$NODE_MAJOR.x/$tarball" && printf '%s\n' "$shasums" | grep " $tarball\$" | sha256sum -c --quiet - && tar -xJf "$tarball" && rm -f "$tarball" )
|
||||||
|
ln -sfn "$dir/node-$ver-linux-x64/bin/node" /usr/local/bin/node
|
||||||
|
ln -sfn "$dir/node-$ver-linux-x64/bin/npm" /usr/local/bin/npm
|
||||||
|
ln -sfn "$dir/node-$ver-linux-x64/bin/npx" /usr/local/bin/npx
|
||||||
|
changed node "$(/usr/local/bin/node --version) from nodejs.org (sha256 checked)"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_sshd() {
|
||||||
|
local f=/etc/ssh/sshd_config.d/10-igneum-build.conf tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
cat > "$tmp" <<'EOF'
|
||||||
|
# igneum-build-1 (infra/build-server/provision.sh): keys only
|
||||||
|
PasswordAuthentication no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
ChallengeResponseAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
PermitRootLogin prohibit-password
|
||||||
|
PermitEmptyPasswords no
|
||||||
|
X11Forwarding no
|
||||||
|
MaxAuthTries 4
|
||||||
|
ClientAliveInterval 60
|
||||||
|
ClientAliveCountMax 10
|
||||||
|
EOF
|
||||||
|
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; ok sshd "$f"; return; fi
|
||||||
|
install -m 644 "$tmp" "$f"; rm -f "$tmp"
|
||||||
|
# Hetzner's installimage may leave a cloud-init drop-in that sets PasswordAuthentication yes; the lowest-numbered file wins
|
||||||
|
if [ -f /etc/ssh/sshd_config.d/50-cloud-init.conf ] && grep -qi '^PasswordAuthentication yes' /etc/ssh/sshd_config.d/50-cloud-init.conf; then
|
||||||
|
sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config.d/50-cloud-init.conf
|
||||||
|
fi
|
||||||
|
sshd -t || die "sshd -t rejected the configuration; the drop-in $f was NOT activated"
|
||||||
|
systemctl reload ssh 2>/dev/null || systemctl reload sshd
|
||||||
|
changed sshd "key-only, root prohibit-password"
|
||||||
|
}
|
||||||
|
|
||||||
|
# the worker dashboard's feed (asked for on 6 October 2026, approved by main): Caddy serves exactly two files of /srv/workers
|
||||||
|
# over HTTPS at build.igneum.network (A record in deSEC, set by main), read-only, no directory listing, every other path 404,
|
||||||
|
# CORS for dl.igneum.network, no caching. Nothing under /srv/workers is a secret (workers.json and headline.json are written
|
||||||
|
# by the dashboard collector and remote-run.sh); the Caddyfile refuses every other file name anyway. Issuer pinned to Let's
|
||||||
|
# Encrypt: Ubuntu's Caddy 2.6.2 fails the ZeroSSL fallback (HTTP 422 caddy_legacy_user_removed, 6 Oct 2026) and would retry it for ever.
|
||||||
|
step_caddy() {
|
||||||
|
local f=/etc/caddy/Caddyfile tmp
|
||||||
|
command -v caddy >/dev/null 2>&1 || die "caddy is not installed (apt)"
|
||||||
|
install -d -m 755 -o "$BUILD_USER" -g "$BUILD_USER" /srv/workers
|
||||||
|
tmp=$(mktemp)
|
||||||
|
cat > "$tmp" <<EOF
|
||||||
|
# igneum-build-1 (infra/build-server/provision.sh): the worker dashboard's two JSON files, nothing else
|
||||||
|
$WORKERS_HOST {
|
||||||
|
tls {
|
||||||
|
issuer acme
|
||||||
|
}
|
||||||
|
root * /srv/workers
|
||||||
|
header Access-Control-Allow-Origin https://dl.igneum.network
|
||||||
|
header Cache-Control "no-store"
|
||||||
|
@notjson not path /workers.json /headline.json
|
||||||
|
respond @notjson 404
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; systemctl is-active --quiet caddy || systemctl start caddy; ok caddy "$WORKERS_HOST"; return; fi
|
||||||
|
caddy validate --config "$tmp" --adapter caddyfile >/dev/null 2>&1 || { rm -f "$tmp"; die "caddy validate rejected the Caddyfile"; }
|
||||||
|
install -m 644 "$tmp" "$f"; rm -f "$tmp"
|
||||||
|
systemctl enable --quiet caddy 2>/dev/null || true
|
||||||
|
systemctl reload caddy 2>/dev/null || systemctl restart caddy
|
||||||
|
changed caddy "$WORKERS_HOST serving /srv/workers/{workers,headline}.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_ufw() {
|
||||||
|
local p want=() any=0 status
|
||||||
|
status=$(ufw status verbose 2>/dev/null || true)
|
||||||
|
grep -q 'Default: deny (incoming), allow (outgoing)' <<<"$status" || { ufw --force default deny incoming >/dev/null; ufw --force default allow outgoing >/dev/null; any=1; }
|
||||||
|
want=(22 80 443)
|
||||||
|
for p in $P2P_PORTS; do want+=("$p"); done
|
||||||
|
for p in "${want[@]}"; do
|
||||||
|
grep -qE "^$p/tcp +ALLOW IN +Anywhere *$" <<<"$status" || { ufw allow "$p/tcp" >/dev/null; any=1; }
|
||||||
|
done
|
||||||
|
grep -q '^Status: active' <<<"$status" || { ufw --force enable >/dev/null; any=1; }
|
||||||
|
[ "$any" = 1 ] && changed ufw "22, 80, 443 and ${P2P_PORTS} open, everything else denied" || ok ufw "22, 80, 443 and ${P2P_PORTS}"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_summary() {
|
||||||
|
log "summary:"
|
||||||
|
{
|
||||||
|
printf 'host %s, %s threads, %s RAM, root fs %s free\n' "$(hostname)" "$(nproc)" "$(awk '/MemTotal/ { printf "%d GB", $2 / 1024 / 1024 }' /proc/meminfo)" "$(df -h / | awk 'NR == 2 { print $4 }')"
|
||||||
|
printf 'raid: %s\n' "$(grep -E '^md' /proc/mdstat 2>/dev/null | tr '\n' ';' || echo none)"
|
||||||
|
printf 'rust: %s | %s | targets %s\n' "$(as_build "$BUILD_HOME/.cargo/bin/rustc --version")" "$(as_build "$BUILD_HOME/.cargo/bin/cargo --version")" "$(as_build "$BUILD_HOME/.cargo/bin/rustup target list --installed" | tr '\n' ' ')"
|
||||||
|
printf 'sccache: %s, %s\n' "$(as_build "$BUILD_HOME/.cargo/bin/sccache --version")" "$(cat "$BUILD_HOME/.config/sccache/config" | tr '\n' ' ')"
|
||||||
|
printf 'mingw: %s\n' "$(x86_64-w64-mingw32-gcc-posix --version | head -1)"
|
||||||
|
printf 'clang: %s | lld: %s\n' "$(clang --version | head -1)" "$(ld.lld --version | head -1)"
|
||||||
|
printf 'node: %s | git: %s | tmux: %s\n' "$(/usr/local/bin/node --version)" "$(git --version)" "$(tmux -V)"
|
||||||
|
printf 'swap: %s\n' "$(swapon --noheadings --show 2>/dev/null | wc -l | awk '{ print ($1 == 0) ? "none" : $1 " device(s) ACTIVE" }')"
|
||||||
|
printf 'mirrors: /srv/igneum.git (%s) /srv/igneum-node.git (%s)\n' "$(as_build 'git -C /srv/igneum.git branch --list | wc -l') branches" "$(as_build 'git -C /srv/igneum-node.git branch --list | wc -l') branches"
|
||||||
|
printf 'builds: %s worktree dirs under /srv/builds, %s slot(s)\n' "$(worktree_count)" "$(cat /srv/builds/_locks/slots)"
|
||||||
|
printf 'ufw: %s\n' "$(ufw status | grep -E 'ALLOW' | awk '{ print $1 }' | tr '\n' ' ')"
|
||||||
|
printf 'caddy: %s, %s\n' "$(caddy version 2>/dev/null | cut -d' ' -f1)" "$(systemctl is-active caddy 2>/dev/null) at https://$WORKERS_HOST/headline.json"
|
||||||
|
printf 'ssh line: ssh -i ~/.ssh/igneum_ed25519 build@%s\n' "$(hostname -I 2>/dev/null | awk '{ print $1 }')"
|
||||||
|
} | sed 's/^/ /'
|
||||||
|
}
|
||||||
|
|
||||||
|
do_provision() {
|
||||||
|
step_os_check
|
||||||
|
step_hostname
|
||||||
|
step_apt
|
||||||
|
step_mingw_alternatives
|
||||||
|
step_no_swap
|
||||||
|
step_sysctl
|
||||||
|
step_limits
|
||||||
|
step_user
|
||||||
|
step_dirs
|
||||||
|
step_mirrors
|
||||||
|
step_rustup
|
||||||
|
step_sccache
|
||||||
|
step_cargo_config
|
||||||
|
step_profile
|
||||||
|
step_node
|
||||||
|
step_sshd
|
||||||
|
step_caddy
|
||||||
|
step_ufw
|
||||||
|
step_summary
|
||||||
|
log "done"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
install) do_install ;;
|
||||||
|
provision) do_provision ;;
|
||||||
|
auto) if in_rescue; then log "rescue system detected: install mode"; do_install; else do_provision; fi ;;
|
||||||
|
*) die "MODE must be auto, install or provision" ;;
|
||||||
|
esac
|
||||||
117
infra/build-server/remote-run.sh
Executable file
117
infra/build-server/remote-run.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# The remote half of tools/build-remote.sh and tools/cross-remote.sh. It runs ON igneum-build-1, fed by lib.sh bs_remote_run
|
||||||
|
# over `ssh build@<box> bash -s` with these exports prepended (never run it by hand on the Mac):
|
||||||
|
# BR_DIR the crate directory on the box BR_CMD the shell string to run there (cargo ...)
|
||||||
|
# BR_LABEL the slot-file label (ends with "; agent=<name>")
|
||||||
|
# BR_TOOL build-remote | cross-remote BR_KIND node-linux | node-windows | app | app-windows | prove | suite | check | other
|
||||||
|
# BR_WT the worktree name BR_CRATE the crate path relative to the worktree root
|
||||||
|
# BR_COMMAND the cargo command as typed BR_TARGET the rust target triple
|
||||||
|
# BR_BRANCH BR_SHA BR_AGENT the agent name (IGNEUM_AGENT on the Mac, else the worktree)
|
||||||
|
# BR_ARTEFACTS space-separated paths (relative to BR_DIR) the Mac will fetch; empty for test, check, clippy
|
||||||
|
#
|
||||||
|
# 1. Takes a build slot: flock on $IGNEUM_BUILD_SLOTS_DIR/build-<k> for k below the count in .../slots (the box's own slot
|
||||||
|
# files, never the Mac's); when every slot is busy it waits up to 2 h on build-0 and exits 75 if it gives up. The holder
|
||||||
|
# line is `pid N since HH:MM:SSZ waited S s: <label>`, the format tools/lock/with-lock.sh writes on the Mac.
|
||||||
|
# 2. Runs BR_CMD in BR_DIR with sccache, prints one `build-remote: RESULT rc= secs= compiles= sccache_hits_total= ...` line.
|
||||||
|
# 3. Appends one JSON line to /srv/builds/_log/builds.jsonl (the worker dashboard reads it; asked for by main on 6 October
|
||||||
|
# 2026): on success, on failure and on the slot give-up. UTC ISO 8601 Z times, numbers unquoted, unknown fields omitted,
|
||||||
|
# artefacts with bytes and sha256 only when the run succeeded (a failed build would list the previous build's files),
|
||||||
|
# the line kept under 4 KB.
|
||||||
|
set -uo pipefail
|
||||||
|
. /etc/profile.d/igneum-build.sh
|
||||||
|
: "${BR_DIR:?}" "${BR_CMD:?}" "${BR_LABEL:?}" "${BR_TOOL:?}" "${BR_KIND:?}"
|
||||||
|
BR_HOST=$(hostname); BR_PID=$$; BR_T0=$(date +%s)
|
||||||
|
export BR_HOST BR_PID BR_T0
|
||||||
|
LOG_DIR=/srv/builds/_log; mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
|
# jsonlog <exit> <slot> <wait_s> <start> <end> <secs> <compiles> <hits> <misses> <hits_total> <misses_total>
|
||||||
|
jsonlog() {
|
||||||
|
BR_EXIT="$1" BR_SLOT="$2" BR_WAIT="$3" BR_START="$4" BR_END="$5" BR_SECS="$6" BR_COMPILES="$7" \
|
||||||
|
BR_HITS="$8" BR_MISSES="$9" BR_HITS_T="${10}" BR_MISSES_T="${11}" BR_LOG="$LOG_DIR/builds.jsonl" python3 - <<'PY' || echo "build-remote: WARNING the JSONL log line was not written" >&2
|
||||||
|
import hashlib, json, os, time
|
||||||
|
e = os.environ
|
||||||
|
def iso(t):
|
||||||
|
return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(int(t))) if t else None
|
||||||
|
def num(v):
|
||||||
|
try: return int(v)
|
||||||
|
except (TypeError, ValueError): return None
|
||||||
|
d = {
|
||||||
|
"v": 1, "id": f"{e['BR_HOST']}-{e['BR_T0']}-{e['BR_PID']}", "host": e['BR_HOST'], "tool": e['BR_TOOL'],
|
||||||
|
"worktree": e.get('BR_WT'), "crate": e.get('BR_CRATE'), "kind": e['BR_KIND'], "command": e.get('BR_COMMAND'),
|
||||||
|
"target": e.get('BR_TARGET'), "branch": e.get('BR_BRANCH'), "sha": e.get('BR_SHA'), "label": e['BR_LABEL'],
|
||||||
|
"agent": e.get('BR_AGENT'), "slot": num(e['BR_SLOT']), "wait_s": num(e['BR_WAIT']), "queued_at": iso(e['BR_T0']),
|
||||||
|
"start": iso(e['BR_START']), "end": iso(e['BR_END']), "secs": num(e['BR_SECS']), "exit": num(e['BR_EXIT']),
|
||||||
|
"compiles": num(e['BR_COMPILES']),
|
||||||
|
}
|
||||||
|
sc = {k: num(e[v]) for k, v in (("hits", "BR_HITS"), ("misses", "BR_MISSES"), ("hits_total", "BR_HITS_T"), ("misses_total", "BR_MISSES_T"))}
|
||||||
|
sc = {k: v for k, v in sc.items() if v is not None}
|
||||||
|
if sc: d["sccache"] = sc
|
||||||
|
try:
|
||||||
|
d["load_end"] = [float(x) for x in open('/proc/loadavg').read().split()[:3]]
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
arts = []
|
||||||
|
if d["exit"] == 0:
|
||||||
|
for p in e.get('BR_ARTEFACTS', '').split():
|
||||||
|
fp = os.path.join(e['BR_DIR'], p)
|
||||||
|
if os.path.isfile(fp):
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(fp, 'rb') as f:
|
||||||
|
for chunk in iter(lambda: f.read(1 << 20), b''):
|
||||||
|
h.update(chunk)
|
||||||
|
arts.append({"path": p, "bytes": os.path.getsize(fp), "sha256": h.hexdigest()})
|
||||||
|
d["artefacts"] = arts
|
||||||
|
d = {k: v for k, v in d.items() if v is not None and v != ""}
|
||||||
|
line = json.dumps(d, separators=(',', ':'))
|
||||||
|
if len(line) > 4000:
|
||||||
|
for k in ("command", "label"):
|
||||||
|
if k in d: d[k] = d[k][:200]
|
||||||
|
line = json.dumps(d, separators=(',', ':'))
|
||||||
|
with open(e['BR_LOG'], 'a') as f:
|
||||||
|
f.write(line + "\n")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
slots=$(cat "$IGNEUM_BUILD_SLOTS_DIR/slots" 2>/dev/null || echo 1); [ "$slots" -ge 1 ] 2>/dev/null || slots=1
|
||||||
|
got=""
|
||||||
|
for k in $(seq 0 $((slots - 1))); do
|
||||||
|
exec {fd}>"$IGNEUM_BUILD_SLOTS_DIR/build-$k"
|
||||||
|
if flock -n "$fd"; then got=$k; break; fi
|
||||||
|
exec {fd}>&-
|
||||||
|
done
|
||||||
|
if [ -z "$got" ]; then
|
||||||
|
echo "build-remote: all $slots slot(s) busy, waiting (up to 2 h) for build-0: $(head -c 160 "$IGNEUM_BUILD_SLOTS_DIR/build-0" 2>/dev/null)" >&2
|
||||||
|
# the queue is visible while it waits (the worker dashboard reads wait-* files; asked for on 6 October 2026): the same line
|
||||||
|
# format as a slot file, removed the moment the slot is taken or the wait is given up
|
||||||
|
waitfile="$IGNEUM_BUILD_SLOTS_DIR/wait-$BR_PID"
|
||||||
|
printf 'pid %s since %sZ waited 0 s: %s\n' "$BR_PID" "$(date -u +%H:%M:%S)" "$BR_LABEL" > "$waitfile"
|
||||||
|
trap 'rm -f "$waitfile"' EXIT
|
||||||
|
exec {fd}>"$IGNEUM_BUILD_SLOTS_DIR/build-0"
|
||||||
|
if ! flock -w 7200 "$fd"; then
|
||||||
|
echo "build-remote: gave up waiting for a slot after 2 h" >&2
|
||||||
|
jsonlog 75 0 $(( $(date +%s) - BR_T0 )) "" "$(date +%s)" "" "" "" "" "" ""
|
||||||
|
rm -f "$waitfile"
|
||||||
|
exit 75
|
||||||
|
fi
|
||||||
|
rm -f "$waitfile"; trap - EXIT
|
||||||
|
got=0
|
||||||
|
fi
|
||||||
|
waited=$(( $(date +%s) - BR_T0 ))
|
||||||
|
printf 'pid %s since %sZ waited %s s: %s\n' "$BR_PID" "$(date -u +%H:%M:%S)" "$waited" "$BR_LABEL" > "$IGNEUM_BUILD_SLOTS_DIR/build-$got"
|
||||||
|
echo "build-remote: holding build-$got on $BR_HOST (waited $waited s)" >&2
|
||||||
|
|
||||||
|
cd "$BR_DIR" || { jsonlog 2 "$got" "$waited" "" "$(date +%s)" "" "" "" "" "" ""; exit 2; }
|
||||||
|
sccache --start-server >/dev/null 2>&1 || true
|
||||||
|
stat_field() { sccache --show-stats 2>/dev/null | awk -v key="$1" 'index($0, key) == 1 { print $NF; exit }'; }
|
||||||
|
exec_before=$(stat_field "Compile requests executed"); hits_before=$(stat_field "Cache hits "); misses_before=$(stat_field "Cache misses ")
|
||||||
|
t1=$(date +%s)
|
||||||
|
eval "$BR_CMD"
|
||||||
|
rc=$?
|
||||||
|
t2=$(date +%s); secs=$(( t2 - t1 ))
|
||||||
|
exec_after=$(stat_field "Compile requests executed"); hits_after=$(stat_field "Cache hits "); misses_after=$(stat_field "Cache misses ")
|
||||||
|
compiles=$(( ${exec_after:-0} - ${exec_before:-0} )); hits=$(( ${hits_after:-0} - ${hits_before:-0} )); misses=$(( ${misses_after:-0} - ${misses_before:-0} ))
|
||||||
|
printf 'build-remote: RESULT rc=%s secs=%s compiles=%s sccache_hits=%s sccache_misses=%s sccache_hits_total=%s sccache_misses_total=%s load=%s\n' \
|
||||||
|
"$rc" "$secs" "$compiles" "$hits" "$misses" "${hits_after:-?}" "${misses_after:-?}" "$(cut -d' ' -f1-3 /proc/loadavg)"
|
||||||
|
jsonlog "$rc" "$got" "$waited" "$t1" "$t2" "$secs" "$compiles" "$hits" "$misses" "${hits_after:-}" "${misses_after:-}"
|
||||||
|
: > "$IGNEUM_BUILD_SLOTS_DIR/build-$got"
|
||||||
|
exit "$rc"
|
||||||
62
infra/build-server/run-from-mac.sh
Executable file
62
infra/build-server/run-from-mac.sh
Executable file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Provision igneum-build-1 from this Mac and wire the Mac to it. Idempotent; run it again after any change to provision.sh.
|
||||||
|
#
|
||||||
|
# infra/build-server/run-from-mac.sh <ip> install (if in rescue) or provision, then wire the Mac
|
||||||
|
# infra/build-server/run-from-mac.sh <ip> --wire-only skip provision.sh: only the host file, the remotes and the mirror push
|
||||||
|
# RUST_TOOLCHAIN=1.99.0 SLOTS=2 infra/build-server/run-from-mac.sh <ip> settings pass through to provision.sh
|
||||||
|
#
|
||||||
|
# What it does: 1. ssh root@<ip> with provision.sh on stdin, WORKTREES filled from `git worktree list` of the igneum repo
|
||||||
|
# (one /srv/builds/<name> per agent worktree); 2. writes build@<ip> to ~/.config/igneum/build-server (what tools/build-remote.sh
|
||||||
|
# and tools/cross-remote.sh read); 3. adds the `build` remote to the igneum repo and to the fork vendor/igneum-node and pushes
|
||||||
|
# every branch to the bare mirrors on the box (the fork exists only on this Mac; the mirror is its first copy elsewhere);
|
||||||
|
# 4. prints the ssh line. If the box is still in the rescue system, provision.sh installs Ubuntu and reboots; run this again
|
||||||
|
# when ssh answers (the host key changes: the old entry is removed here).
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO="$(cd "$HERE/../.." && pwd)"
|
||||||
|
MAIN_REPO="${IGNEUM_MAIN_REPO:-/Users/joshm/Projects/igneum}" # the shared checkout: the fork lives under its vendor/
|
||||||
|
# shellcheck disable=SC2034 # shared with the scripts that source lib.sh
|
||||||
|
BS_TOOL=run-from-mac
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
. "$HERE/lib.sh"
|
||||||
|
|
||||||
|
IP="${1:-}"; shift || true
|
||||||
|
[ -n "$IP" ] || bs_die "usage: run-from-mac.sh <ip> [--wire-only]"
|
||||||
|
WIRE_ONLY=0; [ "${1:-}" = --wire-only ] && WIRE_ONLY=1
|
||||||
|
PASS="" # a string, not an array: bash 3.2 (the Mac) treats an empty array as unbound under set -u
|
||||||
|
for v in MODE RUST_TOOLCHAIN SCCACHE_GB SCCACHE_VERSION NODE_MAJOR SLOTS P2P_PORTS BOX_HOSTNAME SSH_PUBKEY; do
|
||||||
|
[ -n "${!v:-}" ] && PASS="$PASS $v=$(printf '%q' "${!v}")"
|
||||||
|
done
|
||||||
|
|
||||||
|
ROOT_SSH=(ssh -i "$BS_KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=15 "root@$IP")
|
||||||
|
|
||||||
|
if [ "$WIRE_ONLY" = 0 ]; then
|
||||||
|
WORKTREES=$(git -C "$MAIN_REPO" worktree list --porcelain | awk '/^worktree /{ print $2 }' | xargs -n1 basename | tr '\n' ' ')
|
||||||
|
bs_log "provisioning root@$IP with $(printf '%s\n' "$WORKTREES" | wc -w | tr -d ' ') worktree names${PASS:+ and$PASS}"
|
||||||
|
if ! "${ROOT_SSH[@]}" "WORKTREES='$WORKTREES'$PASS bash -s" < "$HERE/provision.sh"; then
|
||||||
|
bs_log "provision.sh did not finish (an install-mode run ends with a reboot and a lost connection: wait for ssh, then run this again)"
|
||||||
|
ssh-keygen -R "$IP" >/dev/null 2>&1 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if "${ROOT_SSH[@]}" 'hostname' 2>/dev/null | grep -q '^rescue'; then bs_die "still in the rescue system after provision.sh"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$BS_HOST_FILE")"
|
||||||
|
printf 'build@%s\n' "$IP" > "$BS_HOST_FILE"
|
||||||
|
bs_log "wrote $BS_HOST_FILE"
|
||||||
|
bs_host
|
||||||
|
bs_ssh 'hostname; nproc' >/dev/null || bs_die "build@$IP does not answer with $BS_KEY"
|
||||||
|
|
||||||
|
wire_remote() { # repo-dir mirror label
|
||||||
|
local dir="$1" mirror="$2" label="$3" url="$BS_HOST:$2" cur
|
||||||
|
cur=$(git -C "$dir" remote get-url build 2>/dev/null || true)
|
||||||
|
if [ -z "$cur" ]; then git -C "$dir" remote add build "$url"; bs_log "$label: remote build = $url"
|
||||||
|
elif [ "$cur" != "$url" ]; then git -C "$dir" remote set-url build "$url"; bs_log "$label: remote build -> $url"
|
||||||
|
else bs_log "$label: remote build ok"; fi
|
||||||
|
GIT_SSH_COMMAND="$BS_SSH_CMD" git -C "$dir" push -q --force build --all && bs_log "$label: every branch pushed to $mirror ($(git -C "$dir" branch --list | wc -l | tr -d ' ') branches)" || bs_die "$label: push to $mirror failed"
|
||||||
|
}
|
||||||
|
wire_remote "$MAIN_REPO" "$BS_MIRROR_REPO" "igneum"
|
||||||
|
wire_remote "$MAIN_REPO/vendor/igneum-node" "$BS_MIRROR_NODE" "igneum-node (the fork)"
|
||||||
|
|
||||||
|
bs_log "ssh line: ssh -i $BS_KEY build@$IP"
|
||||||
|
bs_log "next: cd <crate> && $REPO/tools/build-remote.sh (cross: tools/cross-remote.sh)"
|
||||||
21
infra/build-server/workers/igneum-workers.service
Normal file
21
infra/build-server/workers/igneum-workers.service
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# The worker dashboard's collector on igneum-build-1: one pass, written by igneum-workers.timer every 30 s.
|
||||||
|
# Reads /proc, /sys, /srv/builds/_locks, the process table and /srv/builds/_log/builds.jsonl; writes /srv/workers/workers.json.
|
||||||
|
# Installed by infra/build-server/workers/install.sh (copies tools/workers/{lib,collect}.mjs to /srv/workers/bin).
|
||||||
|
[Unit]
|
||||||
|
Description=Igneum worker dashboard collector (one pass)
|
||||||
|
After=local-fs.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=build
|
||||||
|
Group=build
|
||||||
|
Environment=HOME=/home/build
|
||||||
|
Environment=PATH=/home/build/.cargo/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=SCCACHE_DIR=/srv/sccache
|
||||||
|
Environment=IGNEUM_WORKERS_DIR=/srv/workers
|
||||||
|
Environment=IGNEUM_BUILD_SLOTS_DIR=/srv/builds/_locks
|
||||||
|
Environment=IGNEUM_BUILD_ROOT=/srv/builds
|
||||||
|
WorkingDirectory=/srv/workers/bin
|
||||||
|
ExecStart=/usr/local/bin/node /srv/workers/bin/collect.mjs
|
||||||
|
TimeoutStartSec=25
|
||||||
|
Nice=10
|
||||||
13
infra/build-server/workers/igneum-workers.timer
Normal file
13
infra/build-server/workers/igneum-workers.timer
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Fires the collector every 30 s from boot. `systemctl list-timers igneum-workers.timer` shows the next pass;
|
||||||
|
# `journalctl -u igneum-workers.service -n 20` the last errors.
|
||||||
|
[Unit]
|
||||||
|
Description=Igneum worker dashboard collector, every 30 s
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=20s
|
||||||
|
OnUnitActiveSec=30s
|
||||||
|
AccuracySec=1s
|
||||||
|
Unit=igneum-workers.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
14
infra/build-server/workers/install.sh
Executable file
14
infra/build-server/workers/install.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install or refresh the worker dashboard collector on igneum-build-1 from this Mac.
|
||||||
|
# infra/build-server/workers/install.sh copies tools/workers/{lib,collect}.mjs and the two units, enables the timer
|
||||||
|
# Needs root over ssh (root@<ip> with ~/.ssh/igneum_ed25519); the host ip comes from ~/.config/igneum/build-server (build@<ip>).
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; ROOT="$(cd "$HERE/../../.." && pwd)"
|
||||||
|
KEY="${IGNEUM_BUILD_KEY:-$HOME/.ssh/igneum_ed25519}"
|
||||||
|
HOST_LINE="$(head -1 "${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" | tr -d '[:space:]')"
|
||||||
|
IP="${HOST_LINE#*@}"; [ -n "$IP" ] || { echo "no build server in ~/.config/igneum/build-server" >&2; exit 1; }
|
||||||
|
SSH=(ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10)
|
||||||
|
"${SSH[@]}" "root@$IP" 'install -d -o build -g build /srv/workers /srv/workers/bin /srv/workers/sources /srv/builds/_log; chown build:build /srv/builds/_log'
|
||||||
|
scp -q -i "$KEY" "$ROOT/tools/workers/lib.mjs" "$ROOT/tools/workers/collect.mjs" "build@$IP:/srv/workers/bin/"
|
||||||
|
scp -q -i "$KEY" "$HERE/igneum-workers.service" "$HERE/igneum-workers.timer" "root@$IP:/etc/systemd/system/"
|
||||||
|
"${SSH[@]}" "root@$IP" 'systemctl daemon-reload && systemctl enable --now igneum-workers.timer >/dev/null 2>&1; systemctl start igneum-workers.service; systemctl is-active igneum-workers.timer; systemctl list-timers igneum-workers.timer --no-pager | sed -n 2p; ls -la /srv/workers/workers.json'
|
||||||
|
|
@ -26,7 +26,7 @@ nohup "$BIN" --devnet --nodnsseed --disable-upnp --appdir=/tmp/igneum-devnet/obs
|
||||||
lines /tmp/igneum-devnet/observer-v4.out
|
lines /tmp/igneum-devnet/observer-v4.out
|
||||||
# node 1, under caffeinate as it runs today
|
# node 1, under caffeinate as it runs today
|
||||||
stop "igneumd --devnet.*appdir=/tmp/igneum-devnet/node1 "
|
stop "igneumd --devnet.*appdir=/tmp/igneum-devnet/node1 "
|
||||||
nohup caffeinate -dims "$BIN" --devnet --nodnsseed --disable-upnp --enable-unsynced-mining --appdir=/tmp/igneum-devnet/node1 --rpclisten=0.0.0.0:26610 --listen=0.0.0.0:26611 \
|
nohup caffeinate -dims "$BIN" --devnet --nodnsseed --disable-upnp --enable-unsynced-mining --appdir=/tmp/igneum-devnet/node1 --rpclisten=0.0.0.0:26610 --listen=0.0.0.0:26611 --evm-rpclisten=127.0.0.1:26791 \
|
||||||
--addpeer=188.245.5.161:26611 --addpeer=192.168.68.67:26611 --override-params-file="$OV" --nologfiles --yes >> /tmp/igneum-devnet/node1.out 2>&1 &
|
--addpeer=188.245.5.161:26611 --addpeer=192.168.68.67:26611 --override-params-file="$OV" --nologfiles --yes >> /tmp/igneum-devnet/node1.out 2>&1 &
|
||||||
lines /tmp/igneum-devnet/node1.out
|
lines /tmp/igneum-devnet/node1.out
|
||||||
echo "running now:"; ps -o pid=,lstart=,command= -p "$(pgrep -f 'igneumd --devnet' | tr '\n' ',' | sed 's/,$//')" | cut -c1-160
|
echo "running now:"; ps -o pid=,lstart=,command= -p "$(pgrep -f 'igneumd --devnet' | tr '\n' ',' | sed 's/,$//')" | cut -c1-160
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,23 @@ the two GPU workers (`igneum-worker-cuda` for NVIDIA, `igneum-worker-opencl` for
|
||||||
scripts were self-tested with stub binaries (`selftest.sh`) and the binaries were cross-compiled on a Mac; the first real
|
scripts were self-tested with stub binaries (`selftest.sh`) and the binaries were cross-compiled on a Mac; the first real
|
||||||
run on a Hive rig is still to come. Report what breaks.
|
run on a Hive rig is still to come. Report what breaks.
|
||||||
|
|
||||||
|
**A HiveOS rig mines only.** The package (`make-hive-package.sh`) carries `igneumd`, `igneum-miner` and the two
|
||||||
|
workers; no `igneum-prove-host`, no `igneum-prove-export`, no SP1 GPU server. So a rig on this package earns from the
|
||||||
|
80% lottery share and nothing from the 20% proving share until a Linux prover build is published. The Ubuntu rig
|
||||||
|
installer (`packaging/linux/README.md`, branch `rig-install`, commit dd632c1) carries a prover unit that idles in state
|
||||||
|
`setup` for the same reason; its per-card table is the fuller version of the one below. What each card could do once
|
||||||
|
the prover ships, from the app's default (`app/igneum-app/src/provedefault.rs` at 440fd59 on `proving-v1`: proving on
|
||||||
|
at 24 GB or more, off below) and from the proving agent's S_p curve of 5 October 2026 (`docs/bench-log.md`, "proving
|
||||||
|
v1": jobs `memsweep-pc2-pv1`, `memminer-pc2-pv1` and the S_p curve, one RTX 5090, SP1 6.8.1's GPU server):
|
||||||
|
|
||||||
|
| Card | Once a Linux prover ships | Measured on 5 October 2026 |
|
||||||
|
|---|---|---|
|
||||||
|
| 8 GB | mines only | the prover's floor is 13.9 GB for an empty shard |
|
||||||
|
| 12 GB | mines only; proves nothing on this SP1 build | the same 13.9 GB floor |
|
||||||
|
| 16 GB | in practice mines only: proves empty shards alone, nothing beside the miner | 13.9 GB alone; 15.7 GB beside the miner leaves nothing for the display |
|
||||||
|
| 24 GB | proves the adopted v1 shard (30,000 pgas) from the fee switch at DAA 210,000, nothing before it | 20.4 GB alone, about 22 GB beside the miner (approximate, not measured on a 24 GB card); the prototype shard at 28.3 GB does not fit |
|
||||||
|
| 32 GB | mines and proves, prototype shard included | 28.3 GB alone, 30.0 GB beside the miner, 2.5 GB spare |
|
||||||
|
|
||||||
## Flight Sheet
|
## Flight Sheet
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|
|
@ -16,7 +33,9 @@ run on a Hive rig is still to come. Report what breaks.
|
||||||
| Wallet and worker template | `0x<40 hex>.%WORKER_NAME%`: the payout address is an EVM address you hold the key for; the part after the dot labels this rig's keys |
|
| Wallet and worker template | `0x<40 hex>.%WORKER_NAME%`: the payout address is an EVM address you hold the key for; the part after the dot labels this rig's keys |
|
||||||
| Pool URL | `grpc://<your node>:26610` (your own igneumd, solo mining), or `local` to run the bundled node on the rig |
|
| Pool URL | `grpc://<your node>:26610` (your own igneumd, solo mining), or `local` to run the bundled node on the rig |
|
||||||
| Pass | empty |
|
| Pass | empty |
|
||||||
| Extra config arguments | `DEV_FEE=1 IDENTITIES=8 WORKER=auto VOTE=1` (one per line also works); `PEERS=a:26611,b:26611` for the bundled node; `EXTRA="..."` for more miner flags |
|
| OVERRIDE | the devnet's consensus override as one JSON object on its own line, needed with `local`: `OVERRIDE={"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":84100,"fees_v1_activation_daa":210000,"finality_v3_activation_daa":135200}` is the four-field object 0.3.10 shipped; after the 0.3.11 switch it has nine fields, and the downloads page carries the live one. Set OVERRIDE from the downloads page when it changes. **A rig without it is refused**: its node runs on genesis parameters, prints another digest, and every devnet peer drops it (`h-run.sh` warns in the main log) |
|
||||||
|
| Extra config arguments | `DEV_FEE=1 IDENTITIES=auto WORKER=auto VOTE=1` (one per line also works); `PEERS=a:26611,b:26611` for the bundled node; `EXTRA="..."` for more miner flags |
|
||||||
|
| IDENTITIES | vote keys per card: 8 for a card with 8 GB or more, else 2 (`IDENTITIES=auto` applies that rule, per card, from `nvidia-smi` or the amdgpu sysfs; 8 when neither answers; a number overrides it for every card). The rule is the app's (`app/igneum-app/src/detect.rs`) |
|
||||||
|
|
||||||
Solo mining: there is no pool. Each card mines block templates from the node and the block reward pays the wallet in
|
Solo mining: there is no pool. Each card mines block templates from the node and the block reward pays the wallet in
|
||||||
the template (80% of each block to its finder, 20% to the proving pool; the protocol takes no fee for anyone).
|
the template (80% of each block to its finder, 20% to the proving pool; the protocol takes no fee for anyone).
|
||||||
|
|
@ -37,8 +56,8 @@ The protocol carries no fee: this is the software's, and any other miner client
|
||||||
|
|
||||||
| Hook | What |
|
| Hook | What |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `h-config.sh` | writes `igneum.conf` from the Flight Sheet (node URL, wallet, label, DEV_FEE, IDENTITIES, WORKER, VOTE, PEERS, EXTRA); refuses a wallet that is not 0x + 40 hex |
|
| `h-config.sh` | writes `igneum.conf` from the Flight Sheet (node URL, wallet, label, DEV_FEE, IDENTITIES, WORKER, VOTE, PEERS, EXTRA, OVERRIDE); refuses an OVERRIDE that is not `{...}`; resolves `IDENTITIES=auto` to `IDENTITIES_GPU<N>` keys by VRAM; refuses a wallet that is not 0x + 40 hex |
|
||||||
| `h-run.sh` | starts the bundled node when the URL is `local`, waits for the node, exports the hourly program pack (`igneum-miner export-pack`), then one `igneum-miner` per GPU with its worker; restarts a miner that exits (exit 42 = program change without prepare support: the pack is re-exported first); per-GPU logs `<log>.gpu<N>.log`, merged into the main log |
|
| `h-run.sh` | starts the bundled node when the URL is `local` (writes `data/override-params.json` from OVERRIDE and passes `--override-params-file`; warns when OVERRIDE is empty; copies the node's switch lines and its `Consensus params digest` line into the main log), waits for the node, exports the hourly program pack (`igneum-miner export-pack`), then one `igneum-miner` per GPU with its worker (`--identities` from `IDENTITIES_GPU<N>`, else `IDENTITIES`); restarts a miner that exits (exit 42 = program change without prepare support: the pack is re-exported first); per-GPU logs `<log>.gpu<N>.log`, merged into the main log |
|
||||||
| `h-stats.sh` | per-GPU hash rate from each miner's last `STATUS` line (`now=<MH/s>`), accepted and rejected totals, dev-fee block count, temperatures and fans from Hive's `gpu-stats` (else `nvidia-smi`), uptime, version |
|
| `h-stats.sh` | per-GPU hash rate from each miner's last `STATUS` line (`now=<MH/s>`), accepted and rejected totals, dev-fee block count, temperatures and fans from Hive's `gpu-stats` (else `nvidia-smi`), uptime, version |
|
||||||
|
|
||||||
Stats JSON (what Hive reads from `$stats`): `hs` (kH/s per GPU), `hs_units` (`khs`), `temp`, `fan`, `uptime` (s),
|
Stats JSON (what Hive reads from `$stats`): `hs` (kH/s per GPU), `hs_units` (`khs`), `temp`, `fan`, `uptime` (s),
|
||||||
|
|
@ -50,7 +69,9 @@ Stats JSON (what Hive reads from `$stats`): `hs` (kH/s per GPU), `hs_units` (`kh
|
||||||
on the library path). The worker compiles the hourly program at run time with NVRTC; without the library it says so
|
on the library path). The worker compiles the hourly program at run time with NVRTC; without the library it says so
|
||||||
and the miner retries. Hive images ship the driver; whether `libnvrtc.so.12` is present depends on the image, untested.
|
and the miner retries. Hive images ship the driver; whether `libnvrtc.so.12` is present depends on the image, untested.
|
||||||
- AMD: an OpenCL ICD (`libOpenCL.so.1` from ROCm or amdgpu-pro). The OpenCL worker compiles the program through the ICD.
|
- AMD: an OpenCL ICD (`libOpenCL.so.1` from ROCm or amdgpu-pro). The OpenCL worker compiles the program through the ICD.
|
||||||
- The bundled node (`local`) keeps its chain data under the miner folder (`data/`); a devnet chain is small today.
|
- The bundled node (`local`) keeps its chain data under the miner folder (`data/`); a devnet chain is small today. It
|
||||||
|
needs OVERRIDE (the Flight Sheet table): compare the `node: Consensus params digest:` line in the main log with the
|
||||||
|
digest on the downloads page; a different one means the override is stale and the node is refused.
|
||||||
- Ports: the bundled node listens on 26611 (p2p) and answers RPC on 127.0.0.1:26610 only.
|
- Ports: the bundled node listens on 26611 (p2p) and answers RPC on 127.0.0.1:26610 only.
|
||||||
|
|
||||||
## Building the package
|
## Building the package
|
||||||
|
|
@ -67,3 +88,7 @@ Stats JSON (what Hive reads from `$stats`): `hs` (kH/s per GPU), `hs_units` (`kh
|
||||||
- GPU order: the CUDA device index is assumed to follow `nvidia-smi` order and Hive's `gpu-stats` arrays (NVIDIA first);
|
- GPU order: the CUDA device index is assumed to follow `nvidia-smi` order and Hive's `gpu-stats` arrays (NVIDIA first);
|
||||||
a mixed NVIDIA and AMD rig may show temperatures against the wrong card.
|
a mixed NVIDIA and AMD rig may show temperatures against the wrong card.
|
||||||
- Each card runs its own `igneum-miner` and node connection; the node's template RPC serves them all.
|
- Each card runs its own `igneum-miner` and node connection; the node's template RPC serves them all.
|
||||||
|
- Before this change no package carried the override: `h-run.sh` started the node without `--override-params-file`, so
|
||||||
|
a `local` rig was refused by every devnet peer (release 0.3.11, section 5, C41). Still untested on a rig.
|
||||||
|
- No prover in the package (the second paragraph above): the rig earns nothing from the proving share until a Linux
|
||||||
|
prover build ships.
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,19 @@
|
||||||
# CUSTOM_USER_CONFIG extra lines, KEY=VALUE, one per line or separated by spaces:
|
# CUSTOM_USER_CONFIG extra lines, KEY=VALUE, one per line or separated by spaces:
|
||||||
# DEV_FEE=1 the miner software's dev fee in whole percent (1 block template in 100 to the dev
|
# DEV_FEE=1 the miner software's dev fee in whole percent (1 block template in 100 to the dev
|
||||||
# address); DEV_FEE=0 turns it off. The protocol itself takes no fee.
|
# address); DEV_FEE=0 turns it off. The protocol itself takes no fee.
|
||||||
# IDENTITIES=8 vote keys per card (8 for a big card, 2 for a small one)
|
# IDENTITIES=auto vote keys per card: 8 for a card with 8 GB or more, else 2 (IDENTITIES=auto
|
||||||
|
# applies that rule per card from nvidia-smi or the amdgpu sysfs, 8 when neither
|
||||||
|
# answers; the app's rule, app/igneum-app/src/detect.rs). A number overrides it
|
||||||
|
# for every card.
|
||||||
# WORKER=auto auto | cuda | opencl (the GPU worker; auto = cuda on NVIDIA, opencl on AMD)
|
# WORKER=auto auto | cuda | opencl (the GPU worker; auto = cuda on NVIDIA, opencl on AMD)
|
||||||
# VOTE=1 sign finality checkpoints (0 = mine without voting)
|
# VOTE=1 sign finality checkpoints (0 = mine without voting)
|
||||||
# PEERS=a:26611,b:26611 peers for the bundled node when CUSTOM_URL=local
|
# PEERS=a:26611,b:26611 peers for the bundled node when CUSTOM_URL=local
|
||||||
# EXTRA="..." appended to every igneum-miner command line
|
# EXTRA="..." appended to every igneum-miner command line
|
||||||
|
# OVERRIDE={...} the devnet's consensus override, one JSON object on its own line (single or
|
||||||
|
# double quotes around it are fine in the Hive UI); the bundled node (CUSTOM_URL=local)
|
||||||
|
# starts with --override-params-file from it. Without it the node runs on genesis
|
||||||
|
# parameters and every devnet peer refuses it. Copy the live object from the
|
||||||
|
# downloads page whenever it changes.
|
||||||
# Hive sources h-manifest.conf before this hook; the fallback is for a run outside Hive (selftest.sh)
|
# Hive sources h-manifest.conf before this hook; the fallback is for a run outside Hive (selftest.sh)
|
||||||
[[ -z "$CUSTOM_CONFIG_FILENAME" ]] && . "$(dirname "${BASH_SOURCE[0]}")/h-manifest.conf"
|
[[ -z "$CUSTOM_CONFIG_FILENAME" ]] && . "$(dirname "${BASH_SOURCE[0]}")/h-manifest.conf"
|
||||||
|
|
||||||
|
|
@ -29,22 +37,67 @@ if ! [[ "$wallet" =~ ^0x[0-9a-fA-F]{40}$ ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# defaults, then the user's KEY=VALUE lines
|
# defaults, then the user's KEY=VALUE lines
|
||||||
DEV_FEE=1; IDENTITIES=8; WORKER=auto; VOTE=1; PEERS=""; EXTRA=""
|
DEV_FEE=1; IDENTITIES=auto; WORKER=auto; VOTE=1; PEERS=""; EXTRA=""; OVERRIDE=""
|
||||||
while read -r kv; do
|
while IFS= read -r line; do
|
||||||
[[ -z "$kv" || "$kv" == \#* ]] && continue
|
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||||
key="${kv%%=*}"; val="${kv#*=}"
|
if [[ "$line" == OVERRIDE=* ]]; then # the whole line: JSON may carry spaces; quotes around it are stripped
|
||||||
case "$key" in
|
val="${line#OVERRIDE=}"; val="${val#\'}"; val="${val%\'}"; val="${val#\"}"; val="${val%\"}"
|
||||||
DEV_FEE|IDENTITIES|WORKER|VOTE|PEERS|EXTRA) printf -v "$key" '%s' "$val" ;;
|
OVERRIDE="$val"; continue
|
||||||
*) echo "Igneum: unknown setting '$key' ignored" ;;
|
fi
|
||||||
esac
|
while read -r kv; do
|
||||||
done < <(printf '%s\n' "$CUSTOM_USER_CONFIG" | tr ' ' '\n' | sed 's/^"//; s/"$//')
|
[[ -z "$kv" ]] && continue
|
||||||
|
key="${kv%%=*}"; val="${kv#*=}"
|
||||||
|
case "$key" in
|
||||||
|
DEV_FEE|IDENTITIES|WORKER|VOTE|PEERS|EXTRA) printf -v "$key" '%s' "$val" ;;
|
||||||
|
*) echo "Igneum: unknown setting '$key' ignored" ;;
|
||||||
|
esac
|
||||||
|
done < <(printf '%s\n' "$line" | tr ' ' '\n' | sed 's/^"//; s/"$//')
|
||||||
|
done < <(printf '%s\n' "$CUSTOM_USER_CONFIG")
|
||||||
|
if [[ -n "$OVERRIDE" && ! "$OVERRIDE" =~ ^\{.*\}$ ]]; then
|
||||||
|
echo -e "${YELLOW:-}Igneum: OVERRIDE must be one JSON object, {\"...\":N,...} from the downloads page; got '${OVERRIDE:0:40}'${NOCOLOR:-}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
[[ "$DEV_FEE" =~ ^[0-9]+$ ]] || DEV_FEE=1
|
[[ "$DEV_FEE" =~ ^[0-9]+$ ]] || DEV_FEE=1
|
||||||
[[ "$IDENTITIES" =~ ^[0-9]+$ ]] || IDENTITIES=8
|
[[ "$IDENTITIES" =~ ^[0-9]+$ || "$IDENTITIES" == "auto" ]] || IDENTITIES=auto
|
||||||
|
|
||||||
|
# IDENTITIES=auto: 8 for a card with 8 GiB (8192 MiB) or more, else 2, per card, in the order h-run.sh numbers them
|
||||||
|
# (NVIDIA first unless WORKER=opencl, then AMD unless WORKER=cuda). VRAM from nvidia-smi (MiB per line) and from
|
||||||
|
# /sys/class/drm/card<N>/device/mem_info_vram_total (bytes, amdgpu). A card whose VRAM cannot be read gets 8.
|
||||||
|
ident_block="" # IDENTITIES_GPU<N>=... lines, one per card, newline-terminated
|
||||||
|
ident_summary=""
|
||||||
|
if [[ "$IDENTITIES" == "auto" ]]; then
|
||||||
|
vrams=()
|
||||||
|
if [[ "$WORKER" != "opencl" ]] && command -v nvidia-smi >/dev/null 2>&1; then
|
||||||
|
nvq=(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits)
|
||||||
|
command -v timeout >/dev/null 2>&1 && nvq=(timeout 20 "${nvq[@]}")
|
||||||
|
while read -r mib; do
|
||||||
|
mib="${mib//[[:space:]]/}"
|
||||||
|
[[ "$mib" =~ ^[0-9]+$ ]] && vrams+=("$mib") || vrams+=("")
|
||||||
|
done < <("${nvq[@]}" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if [[ "$WORKER" != "cuda" ]]; then
|
||||||
|
for d in "${IGNEUM_DRM_ROOT:-/sys/class/drm}"/card*; do
|
||||||
|
[[ "$(basename "$d")" =~ ^card[0-9]+$ && -r "$d/device/mem_info_vram_total" ]] || continue
|
||||||
|
bytes="$(cat "$d/device/mem_info_vram_total" 2>/dev/null)"
|
||||||
|
[[ "$bytes" =~ ^[0-9]+$ ]] && vrams+=("$((bytes / 1048576))") || vrams+=("")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
n=0
|
||||||
|
for mib in ${vrams[@]+"${vrams[@]}"}; do
|
||||||
|
if [[ -z "$mib" || "$mib" -ge 8192 ]]; then ident=8; else ident=2; fi
|
||||||
|
ident_block+="IDENTITIES_GPU$n=$ident"$'\n'
|
||||||
|
ident_summary+="gpu$n ${mib:-?}MiB:$ident "
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
IDENTITIES=8 # the fallback h-run.sh uses for a card h-config.sh could not see
|
||||||
|
[[ $n == 0 ]] && ident_summary="no VRAM readable, 8 per card"
|
||||||
|
fi
|
||||||
|
|
||||||
url="$CUSTOM_URL"
|
url="$CUSTOM_URL"
|
||||||
[[ "$url" == "local" ]] && url="local"
|
[[ "$url" == "local" ]] && url="local"
|
||||||
[[ "$url" != "local" && "$url" != grpc://* ]] && url="grpc://$url"
|
[[ "$url" != "local" && "$url" != grpc://* ]] && url="grpc://$url"
|
||||||
|
|
||||||
|
sq() { local v="${1//\'/\'\\\'\'}"; printf "'%s'" "$v"; } # single-quoted for sourcing: JSON and flags carry shell characters
|
||||||
mkdir -p "$(dirname "$CUSTOM_CONFIG_FILENAME")"
|
mkdir -p "$(dirname "$CUSTOM_CONFIG_FILENAME")"
|
||||||
cat > "$CUSTOM_CONFIG_FILENAME" <<CONF
|
cat > "$CUSTOM_CONFIG_FILENAME" <<CONF
|
||||||
# written by h-config.sh from the Flight Sheet; edit the Flight Sheet, not this file
|
# written by h-config.sh from the Flight Sheet; edit the Flight Sheet, not this file
|
||||||
|
|
@ -53,9 +106,10 @@ WALLET=$(printf '%s' "$wallet" | tr 'A-F' 'a-f')
|
||||||
LABEL=$label
|
LABEL=$label
|
||||||
DEV_FEE=$DEV_FEE
|
DEV_FEE=$DEV_FEE
|
||||||
IDENTITIES=$IDENTITIES
|
IDENTITIES=$IDENTITIES
|
||||||
WORKER=$WORKER
|
${ident_block}WORKER=$WORKER
|
||||||
VOTE=$VOTE
|
VOTE=$VOTE
|
||||||
PEERS=$PEERS
|
PEERS=$PEERS
|
||||||
EXTRA=$EXTRA
|
EXTRA=$(sq "$EXTRA")
|
||||||
|
OVERRIDE=$(sq "$OVERRIDE")
|
||||||
CONF
|
CONF
|
||||||
echo "Igneum: config written to $CUSTOM_CONFIG_FILENAME (node $url, wallet ${wallet:0:8}..., dev fee ${DEV_FEE}%, $IDENTITIES identities per card)"
|
echo "Igneum: config written to $CUSTOM_CONFIG_FILENAME (node $url, wallet ${wallet:0:8}..., dev fee ${DEV_FEE}%, identities ${ident_summary:-$IDENTITIES per card}, override ${OVERRIDE:+set}${OVERRIDE:-NONE: a local node will be refused by devnet peers})"
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,20 @@ if [[ "$NODE_URL" == "local" ]]; then
|
||||||
peers=()
|
peers=()
|
||||||
if [[ -n "$PEERS" ]]; then IFS=',' read -r -a plist <<< "$PEERS"; for p in "${plist[@]}"; do peers+=("--addpeer=$p"); done
|
if [[ -n "$PEERS" ]]; then IFS=',' read -r -a plist <<< "$PEERS"; for p in "${plist[@]}"; do peers+=("--addpeer=$p"); done
|
||||||
else peers+=("--addpeer=188.245.5.161:26611"); fi # the public devnet seed (app/igneum-app/src/config.rs)
|
else peers+=("--addpeer=188.245.5.161:26611"); fi # the public devnet seed (app/igneum-app/src/config.rs)
|
||||||
say "starting the bundled node (devnet v4, data $HERE/data, peers ${peers[*]#--addpeer=})"
|
# the consensus override (OVERRIDE in the Flight Sheet's extra config, written to igneum.conf by h-config.sh): the
|
||||||
|
# devnet's activation heights. A node without it runs on genesis parameters, prints another digest in the p2p
|
||||||
|
# handshake and is refused by every devnet peer (release 0.3.11, section 5, C41).
|
||||||
|
override=()
|
||||||
|
if [[ -n "${OVERRIDE:-}" ]]; then
|
||||||
|
printf '%s\n' "$OVERRIDE" > "$HERE/data/override-params.json"
|
||||||
|
override=("--override-params-file=$HERE/data/override-params.json")
|
||||||
|
say "consensus override from the Flight Sheet: $OVERRIDE"
|
||||||
|
else
|
||||||
|
say "WARNING: no OVERRIDE in the Flight Sheet; the node runs on genesis parameters and every devnet peer will refuse it. Set OVERRIDE from the downloads page."
|
||||||
|
fi
|
||||||
|
say "starting the bundled node (devnet, data $HERE/data, peers ${peers[*]#--addpeer=})"
|
||||||
"$BIN/igneumd" --devnet "--appdir=$HERE/data" --rpclisten=127.0.0.1:26610 --listen=0.0.0.0:26611 "${peers[@]}" \
|
"$BIN/igneumd" --devnet "--appdir=$HERE/data" --rpclisten=127.0.0.1:26610 --listen=0.0.0.0:26611 "${peers[@]}" \
|
||||||
--nodnsseed --disable-upnp --nologfiles --yes >> "$CUSTOM_LOG_BASENAME.node.log" 2>&1 &
|
${override[@]+"${override[@]}"} --nodnsseed --disable-upnp --nologfiles --yes >> "$CUSTOM_LOG_BASENAME.node.log" 2>&1 &
|
||||||
pids+=($!)
|
pids+=($!)
|
||||||
fi
|
fi
|
||||||
say "node $NODE_URL; waiting for it to answer"
|
say "node $NODE_URL; waiting for it to answer"
|
||||||
|
|
@ -37,6 +48,13 @@ for _ in $(seq 1 60); do
|
||||||
"$BIN/igneum-miner" watch 1 "$NODE_URL" >/dev/null 2>&1 && break
|
"$BIN/igneum-miner" watch 1 "$NODE_URL" >/dev/null 2>&1 && break
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
if [[ -f "$CUSTOM_LOG_BASENAME.node.log" ]]; then
|
||||||
|
# the switch lines and the digest the node printed at start, so the operator can compare the digest with the one
|
||||||
|
# on the downloads page (another digest = refused by every peer)
|
||||||
|
n=0
|
||||||
|
while IFS= read -r l; do say "node: $l"; n=$((n + 1)); done < <(grep -E 'override params file|from the override file|Consensus params digest' "$CUSTOM_LOG_BASENAME.node.log" | head -12)
|
||||||
|
[[ $n == 0 ]] && say "node: no digest line yet in $CUSTOM_LOG_BASENAME.node.log (an older node, or not started); check it by hand"
|
||||||
|
fi
|
||||||
|
|
||||||
# 2. the GPUs: Hive's gpu-detect when present, else the driver tools
|
# 2. the GPUs: Hive's gpu-detect when present, else the driver tools
|
||||||
nv=0; amd=0
|
nv=0; amd=0
|
||||||
|
|
@ -59,7 +77,8 @@ run_gpu() {
|
||||||
local log="$CUSTOM_LOG_BASENAME.gpu$idx.log"
|
local log="$CUSTOM_LOG_BASENAME.gpu$idx.log"
|
||||||
local args=(mine "$NODE_URL" 1 100000000 "$LABEL-gpu$idx" --worker "$BIN/$worker" --worker-args "--device $dev --pack packs/devnet"
|
local args=(mine "$NODE_URL" 1 100000000 "$LABEL-gpu$idx" --worker "$BIN/$worker" --worker-args "--device $dev --pack packs/devnet"
|
||||||
--prepare-packs packs/prepare --exit-on-seed-change --evm-address "$WALLET" --payout-label "$LABEL-gpu$idx" --status-secs 30)
|
--prepare-packs packs/prepare --exit-on-seed-change --evm-address "$WALLET" --payout-label "$LABEL-gpu$idx" --status-secs 30)
|
||||||
[[ "$IDENTITIES" -gt 1 ]] && args+=(--identities "$IDENTITIES")
|
local identv="IDENTITIES_GPU$idx" ident="$IDENTITIES"; [[ -n "${!identv:-}" ]] && ident="${!identv}" # per-card from h-config.sh, else the fallback
|
||||||
|
[[ "$ident" -gt 1 ]] && args+=(--identities "$ident")
|
||||||
[[ "$VOTE" == "0" ]] && args+=(--no-vote)
|
[[ "$VOTE" == "0" ]] && args+=(--no-vote)
|
||||||
[[ "$DEV_FEE" != "1" ]] && args+=(--dev-fee "$DEV_FEE")
|
[[ "$DEV_FEE" != "1" ]] && args+=(--dev-fee "$DEV_FEE")
|
||||||
[[ -n "$EXTRA" ]] && args+=($EXTRA)
|
[[ -n "$EXTRA" ]] && args+=($EXTRA)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ case "$1" in
|
||||||
esac
|
esac
|
||||||
FAKE
|
FAKE
|
||||||
chmod +x "$M/bin/igneum-miner"
|
chmod +x "$M/bin/igneum-miner"
|
||||||
|
cat > "$M/bin/igneumd" <<'FAKE'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "fake igneumd $*"
|
||||||
|
for a in "$@"; do case "$a" in --override-params-file=*) echo "Finality rule v3 from the override file: active from checkpoint DAA score 135200" ;; esac; done
|
||||||
|
echo "Consensus params digest: 0139ab9dc2992d449ec787d8f021974933631eb55740ab4b6ce9d5c226e72888 (exchanged in the p2p handshake; a peer with another digest is refused)"
|
||||||
|
sleep 600
|
||||||
|
FAKE
|
||||||
|
chmod +x "$M/bin/igneumd"
|
||||||
mkdir -p "$T/bin"
|
mkdir -p "$T/bin"
|
||||||
printf '#!/bin/sh\n[ "$1" = NVIDIA ] && echo 2 || echo 0\n' > "$T/bin/gpu-detect"
|
printf '#!/bin/sh\n[ "$1" = NVIDIA ] && echo 2 || echo 0\n' > "$T/bin/gpu-detect"
|
||||||
cat > "$T/bin/gpu-stats" <<'GS'
|
cat > "$T/bin/gpu-stats" <<'GS'
|
||||||
|
|
@ -49,6 +57,27 @@ export CUSTOM_CONFIG_FILENAME="$M/igneum.conf" CUSTOM_LOG_BASENAME="$T/log/igneu
|
||||||
( . "$M/h-config.sh" ) || { echo "h-config.sh failed"; exit 1; }
|
( . "$M/h-config.sh" ) || { echo "h-config.sh failed"; exit 1; }
|
||||||
grep -q '^WALLET=0xabcd000000000000000000000000000000000001$' "$M/igneum.conf" && grep -q '^DEV_FEE=0$' "$M/igneum.conf" && grep -q '^LABEL=rig7$' "$M/igneum.conf" && grep -q '^IDENTITIES=4$' "$M/igneum.conf" && echo " conf ok: $(tr '\n' ' ' < "$M/igneum.conf" | cut -c1-160)"
|
grep -q '^WALLET=0xabcd000000000000000000000000000000000001$' "$M/igneum.conf" && grep -q '^DEV_FEE=0$' "$M/igneum.conf" && grep -q '^LABEL=rig7$' "$M/igneum.conf" && grep -q '^IDENTITIES=4$' "$M/igneum.conf" && echo " conf ok: $(tr '\n' ' ' < "$M/igneum.conf" | cut -c1-160)"
|
||||||
( CUSTOM_TEMPLATE="notanaddress" . "$M/h-config.sh" >/dev/null 2>&1 ) && { echo "h-config.sh accepted a bad wallet"; exit 1; } || echo " bad wallet refused ok"
|
( CUSTOM_TEMPLATE="notanaddress" . "$M/h-config.sh" >/dev/null 2>&1 ) && { echo "h-config.sh accepted a bad wallet"; exit 1; } || echo " bad wallet refused ok"
|
||||||
|
echo "== h-config.sh IDENTITIES=auto"
|
||||||
|
# no GPU tool on the PATH (gpu-detect is not a VRAM source) and no amdgpu sysfs: every card falls back to 8
|
||||||
|
( CUSTOM_USER_CONFIG="IDENTITIES=auto" IGNEUM_DRM_ROOT="$T/no-drm" CUSTOM_CONFIG_FILENAME="$T/auto-none.conf" . "$M/h-config.sh" >/dev/null ) || { echo "h-config.sh failed on IDENTITIES=auto"; exit 1; }
|
||||||
|
grep -q '^IDENTITIES=8$' "$T/auto-none.conf" && ! grep -q '^IDENTITIES_GPU' "$T/auto-none.conf" && echo " auto with no GPU tools: IDENTITIES=8, no per-card keys ok" || { echo "FAIL: auto without tools"; cat "$T/auto-none.conf"; exit 1; }
|
||||||
|
# the default is auto: no IDENTITIES line at all gives the same
|
||||||
|
( CUSTOM_USER_CONFIG="" IGNEUM_DRM_ROOT="$T/no-drm" CUSTOM_CONFIG_FILENAME="$T/auto-default.conf" . "$M/h-config.sh" >/dev/null ) && grep -q '^IDENTITIES=8$' "$T/auto-default.conf" && echo " default (no IDENTITIES line) is auto ok" || { echo "FAIL: default not auto"; exit 1; }
|
||||||
|
# a stubbed nvidia-smi: 6 GB and 24 GB cards resolve to 2 and 8, in nvidia-smi order
|
||||||
|
printf '#!/bin/sh\nprintf "6144\\n24576\\n"\n' > "$T/bin/nvidia-smi"; chmod +x "$T/bin/nvidia-smi"
|
||||||
|
( CUSTOM_USER_CONFIG="IDENTITIES=auto" IGNEUM_DRM_ROOT="$T/no-drm" CUSTOM_CONFIG_FILENAME="$T/auto-nv.conf" . "$M/h-config.sh" >/dev/null ) || { echo "h-config.sh failed on stubbed nvidia-smi"; exit 1; }
|
||||||
|
grep -q '^IDENTITIES_GPU0=2$' "$T/auto-nv.conf" && grep -q '^IDENTITIES_GPU1=8$' "$T/auto-nv.conf" && grep -q '^IDENTITIES=8$' "$T/auto-nv.conf" && grep -q '^WORKER=auto$' "$T/auto-nv.conf" && echo " auto with nvidia-smi 6144/24576: gpu0=2, gpu1=8 ok" || { echo "FAIL: auto by VRAM"; cat "$T/auto-nv.conf"; exit 1; }
|
||||||
|
# a number still overrides every card
|
||||||
|
( CUSTOM_USER_CONFIG="IDENTITIES=4" CUSTOM_CONFIG_FILENAME="$T/auto-num.conf" . "$M/h-config.sh" >/dev/null ) && grep -q '^IDENTITIES=4$' "$T/auto-num.conf" && ! grep -q '^IDENTITIES_GPU' "$T/auto-num.conf" && echo " numeric override keeps IDENTITIES=4 ok" || { echo "FAIL: numeric override"; exit 1; }
|
||||||
|
rm -f "$T/bin/nvidia-smi"
|
||||||
|
echo "== h-config.sh OVERRIDE"
|
||||||
|
OV='{"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":84100,"fees_v1_activation_daa":210000,"finality_v3_activation_daa":135200}'
|
||||||
|
( CUSTOM_USER_CONFIG="DEV_FEE=0 VOTE=1"$'\n'"OVERRIDE='$OV'"$'\n'"IDENTITIES=4" CUSTOM_CONFIG_FILENAME="$T/ov.conf" . "$M/h-config.sh" >/dev/null ) || { echo "h-config.sh failed on OVERRIDE"; exit 1; }
|
||||||
|
( . "$T/ov.conf"; [[ "$OVERRIDE" == "$OV" && "$DEV_FEE" == 0 && "$IDENTITIES" == 4 ]] ) && echo " OVERRIDE (single-quoted, own line) sourced back intact beside the other keys ok" || { echo "FAIL: OVERRIDE round trip"; cat "$T/ov.conf"; exit 1; }
|
||||||
|
( CUSTOM_USER_CONFIG="OVERRIDE=notjson" CUSTOM_CONFIG_FILENAME="$T/ov-bad.conf" . "$M/h-config.sh" >/dev/null 2>&1 ) && { echo "FAIL: OVERRIDE=notjson accepted"; exit 1; } || echo " OVERRIDE that is not {...} refused ok"
|
||||||
|
( CUSTOM_USER_CONFIG="" CUSTOM_CONFIG_FILENAME="$T/ov-none.conf" . "$M/h-config.sh" | grep -q "override NONE" ) && grep -q "^OVERRIDE=''$" "$T/ov-none.conf" && echo " no OVERRIDE: empty in the conf and named in the summary ok" || { echo "FAIL: empty OVERRIDE"; exit 1; }
|
||||||
|
# h-run.sh reads IDENTITIES_GPU<N> first: write a conf with gpu1=3 and check the second miner's argv
|
||||||
|
( CUSTOM_USER_CONFIG=$'DEV_FEE=0\nIDENTITIES=auto\n'"OVERRIDE=$OV" IGNEUM_DRM_ROOT="$T/no-drm" CUSTOM_CONFIG_FILENAME="$M/igneum.conf" . "$M/h-config.sh" >/dev/null ) && printf 'IDENTITIES_GPU1=3\n' >> "$M/igneum.conf"
|
||||||
echo "== h-run.sh (fake GPUs: 2 NVIDIA, fake node, fake miner)"
|
echo "== h-run.sh (fake GPUs: 2 NVIDIA, fake node, fake miner)"
|
||||||
( cd "$M" && CUSTOM_CONFIG_FILENAME="$M/igneum.conf" CUSTOM_LOG_BASENAME="$T/log/igneum" ./h-run.sh > "$T/log/run.out" 2>&1 ) &
|
( cd "$M" && CUSTOM_CONFIG_FILENAME="$M/igneum.conf" CUSTOM_LOG_BASENAME="$T/log/igneum" ./h-run.sh > "$T/log/run.out" 2>&1 ) &
|
||||||
disown
|
disown
|
||||||
|
|
@ -56,9 +85,21 @@ sleep 8
|
||||||
ls "$T/log" | sed 's/^/ /'
|
ls "$T/log" | sed 's/^/ /'
|
||||||
_lines=$(grep -c "dev fee off (--dev-fee 0)" "$T/log/igneum.log" || true); echo " dev fee lines in the main log: $_lines"; [[ "$_lines" -ge 2 ]] || { echo "FAIL: expected the two miners' dev fee lines in the main log"; cat "$T/log/run.out"; exit 1; }
|
_lines=$(grep -c "dev fee off (--dev-fee 0)" "$T/log/igneum.log" || true); echo " dev fee lines in the main log: $_lines"; [[ "$_lines" -ge 2 ]] || { echo "FAIL: expected the two miners' dev fee lines in the main log"; cat "$T/log/run.out"; exit 1; }
|
||||||
grep -q -- "--dev-fee 0" "$M/argv.log" && echo " DEV_FEE=0 reached the miner as --dev-fee 0 ok" || { echo "FAIL: --dev-fee 0 missing"; exit 1; }
|
grep -q -- "--dev-fee 0" "$M/argv.log" && echo " DEV_FEE=0 reached the miner as --dev-fee 0 ok" || { echo "FAIL: --dev-fee 0 missing"; exit 1; }
|
||||||
grep -q -- "--identities 4" "$M/argv.log" && echo " IDENTITIES=4 reached the miner ok" || { echo "FAIL: --identities 4 missing"; exit 1; }
|
grep -q -- "rig7-gpu0 .*--identities 8" "$M/argv.log" && echo " gpu0 took the IDENTITIES=8 fallback ok" || { echo "FAIL: --identities 8 missing on gpu0"; cat "$M/argv.log"; exit 1; }
|
||||||
|
grep -q -- "rig7-gpu1 .*--identities 3" "$M/argv.log" && echo " gpu1 took IDENTITIES_GPU1=3 over the fallback ok" || { echo "FAIL: --identities 3 missing on gpu1"; cat "$M/argv.log"; exit 1; }
|
||||||
|
[[ "$(cat "$M/data/override-params.json")" == "$OV" ]] && echo " data/override-params.json written from OVERRIDE ok" || { echo "FAIL: override file"; exit 1; }
|
||||||
|
grep -q -- "--override-params-file=$M/data/override-params.json" "$T/log/igneum.node.log" && echo " the node got --override-params-file ok" || { echo "FAIL: node flag"; cat "$T/log/igneum.node.log"; exit 1; }
|
||||||
|
grep -q "node: Consensus params digest: 0139ab9d" "$T/log/igneum.log" && grep -q "node: Finality rule v3 from the override file" "$T/log/igneum.log" && echo " the node's digest and switch lines reached the main log ok" || { echo "FAIL: digest relay"; cat "$T/log/igneum.log"; exit 1; }
|
||||||
grep -q "igneum-worker-cuda" "$M/argv.log" && echo " NVIDIA cards got the cuda worker ok" || { echo "FAIL: cuda worker missing"; exit 1; }
|
grep -q "igneum-worker-cuda" "$M/argv.log" && echo " NVIDIA cards got the cuda worker ok" || { echo "FAIL: cuda worker missing"; exit 1; }
|
||||||
[[ -f "$M/exited42" && $(grep -c "^mine" "$M/argv.log") -ge 3 ]] && echo " exit 42 restarted the miner and re-exported the pack ok" || { echo "FAIL: no restart after exit 42"; exit 1; }
|
[[ -f "$M/exited42" && $(grep -c "^mine" "$M/argv.log") -ge 3 ]] && echo " exit 42 restarted the miner and re-exported the pack ok" || { echo "FAIL: no restart after exit 42"; exit 1; }
|
||||||
echo "== h-stats.sh (sourced)"
|
echo "== h-stats.sh (sourced)"
|
||||||
( . "$M/h-stats.sh"; echo " khs=$khs"; echo " stats=$stats"; python3 -c "import json,sys; s=json.loads(sys.argv[1]); assert s['hs_units']=='khs' and len(s['hs'])==2 and s['ar'][0]>0 and s['algo']=='igneum' and len(s['temp'])==2, s; print(' stats JSON ok: hs', s['hs'], 'temp', s['temp'], 'ar', s['ar'], 'bus', s['bus_numbers'])" "$stats" )
|
( . "$M/h-stats.sh"; echo " khs=$khs"; echo " stats=$stats"; python3 -c "import json,sys; s=json.loads(sys.argv[1]); assert s['hs_units']=='khs' and len(s['hs'])==2 and s['ar'][0]>0 and s['algo']=='igneum' and len(s['temp'])==2, s; print(' stats JSON ok: hs', s['hs'], 'temp', s['temp'], 'ar', s['ar'], 'bus', s['bus_numbers'])" "$stats" )
|
||||||
|
echo "== h-run.sh without OVERRIDE (the warning)"
|
||||||
|
( CUSTOM_USER_CONFIG=$'DEV_FEE=0' IGNEUM_DRM_ROOT="$T/no-drm" CUSTOM_CONFIG_FILENAME="$T/noov.conf" . "$M/h-config.sh" >/dev/null )
|
||||||
|
mkdir -p "$T/log2"
|
||||||
|
( cd "$M" && CUSTOM_CONFIG_FILENAME="$T/noov.conf" CUSTOM_LOG_BASENAME="$T/log2/igneum" ./h-run.sh > "$T/log2/run.out" 2>&1 ) &
|
||||||
|
disown
|
||||||
|
sleep 4
|
||||||
|
grep -q "WARNING: no OVERRIDE in the Flight Sheet" "$T/log2/igneum.log" && ! grep -q -- "--override-params-file" "$T/log2/igneum.node.log" && echo " no OVERRIDE: warning in the main log, no flag on the node ok" || { echo "FAIL: missing warning"; cat "$T/log2/igneum.log"; exit 1; }
|
||||||
|
_p2="$(cat "$T/log2/igneum.pid" 2>/dev/null)"; [[ -n "$_p2" ]] && kill -TERM "$_p2" 2>/dev/null; sleep 1
|
||||||
echo "== self-test passed (scripts and stats shape; Hive itself is untested)"
|
echo "== self-test passed (scripts and stats shape; Hive itself is untested)"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>VERSION_STAMP</string>
|
<string>VERSION_STAMP</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.11</string>
|
<string>0.3.13</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ DL_HOST="https://dl.igneum.network"
|
||||||
# carry the devnet's activation height here, the same N as every other devnet node, before it is cut (Mac and CI alike:
|
# carry the devnet's activation height here, the same N as every other devnet node, before it is cut (Mac and CI alike:
|
||||||
# make-payload.sh sources this file). Rule and order: docs/plans/difficulty-v2-rollout-devnet.md.
|
# make-payload.sh sources this file). Rule and order: docs/plans/difficulty-v2-rollout-devnet.md.
|
||||||
# Example: NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa": 123456}'
|
# Example: NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa": 123456}'
|
||||||
NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":84100,"fees_v1_activation_daa":210000,"finality_v3_activation_daa":135200,"program_class_v3_activation_daa":154800,"proving_v1_activation_daa":154800,"proving_v1_segment_blocks":8,"proving_v1_unproven_daa":600,"proving_v1_aggregator_share_bps":1000}'
|
NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":84100,"fees_v1_activation_daa":210000,"finality_v3_activation_daa":135200,"program_class_v3_activation_daa":154800,"proving_v1_activation_daa":154800,"proving_v1_segment_blocks":8,"proving_v1_unproven_daa":600,"proving_v1_aggregator_share_bps":1000,"proving_v1_fresh_rule_daa":198000,"exec_restart_number":27276,"exec_restart_hash":"bb45cf0dd2d7cc97ebfa5a2701527c09a8ede5d32de74efead9caa293b15688a","exec_restart_trust_daa":200000}'
|
||||||
|
|
||||||
# igneum_secret_file <env var name> <base name> -> the file to read: the variable when set, else <base>.next when it
|
# igneum_secret_file <env var name> <base name> -> the file to read: the variable when set, else <base>.next when it
|
||||||
# exists, else <base>; IGNEUM_CONFIG_DIR (tests) replaces ~/.config/igneum
|
# exists, else <base>; IGNEUM_CONFIG_DIR (tests) replaces ~/.config/igneum
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
#define ArtDir "..\..\brand\icons"
|
#define ArtDir "..\..\brand\icons"
|
||||||
#endif
|
#endif
|
||||||
#ifndef AppVersion
|
#ifndef AppVersion
|
||||||
#define AppVersion "0.3.11"
|
#define AppVersion "0.3.13"
|
||||||
#endif
|
#endif
|
||||||
#define AppName "Igneum Miner"
|
#define AppName "Igneum Miner"
|
||||||
#define Publisher "Igneum"
|
#define Publisher "Igneum"
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,8 @@ done
|
||||||
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
|
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
|
||||||
found_workers=0
|
found_workers=0
|
||||||
if [ -n "${IGNEUM_WORKERS_DIR:-}" ] && [ -d "$IGNEUM_WORKERS_DIR" ]; then
|
if [ -n "${IGNEUM_WORKERS_DIR:-}" ] && [ -d "$IGNEUM_WORKERS_DIR" ]; then
|
||||||
for f in "$IGNEUM_WORKERS_DIR"/igneum-worker-*.exe "$IGNEUM_WORKERS_DIR"/nvrtc*.dll "$IGNEUM_WORKERS_DIR"/LICENSE-*.txt "$IGNEUM_WORKERS_DIR"/THIRD-PARTY.md; do
|
# igneum-gpu-telemetry.exe rides in the inputs too (push-inputs.sh); the worker glob does not match its name (6 October 2026, 0.3.12)
|
||||||
|
for f in "$IGNEUM_WORKERS_DIR"/igneum-worker-*.exe "$IGNEUM_WORKERS_DIR"/igneum-gpu-telemetry.exe "$IGNEUM_WORKERS_DIR"/nvrtc*.dll "$IGNEUM_WORKERS_DIR"/LICENSE-*.txt "$IGNEUM_WORKERS_DIR"/THIRD-PARTY.md; do
|
||||||
[ -f "$f" ] && { cp "$f" "$STAGE/"; found_workers=1; }
|
[ -f "$f" ] && { cp "$f" "$STAGE/"; found_workers=1; }
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
|
|
@ -72,6 +73,7 @@ else
|
||||||
ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh); the engine will not use it"
|
ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh); the engine will not use it"
|
||||||
fi
|
fi
|
||||||
if [ -f "$ROOT/proto-opencl/igneum-worker-opencl.exe" ]; then cp "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$STAGE/"; found_workers=1; fi
|
if [ -f "$ROOT/proto-opencl/igneum-worker-opencl.exe" ]; then cp "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$STAGE/"; found_workers=1; fi
|
||||||
|
if [ -f "$ROOT/proto-opencl/igneum-gpu-telemetry.exe" ]; then cp "$ROOT/proto-opencl/igneum-gpu-telemetry.exe" "$STAGE/"; fi # AMD power, heat, fans, clocks (5 October 2026)
|
||||||
fi
|
fi
|
||||||
if [ "$found_workers" = 1 ]; then echo "workers: $(cd "$STAGE" && ls igneum-worker-*.exe nvrtc*.dll 2>/dev/null | tr '\n' ' ')"
|
if [ "$found_workers" = 1 ]; then echo "workers: $(cd "$STAGE" && ls igneum-worker-*.exe nvrtc*.dll 2>/dev/null | tr '\n' ' ')"
|
||||||
else echo "note: no prebuilt igneum-worker-cuda.exe / igneum-worker-opencl.exe found; the engine builds the CUDA worker from proto-cuda\\ on the PC (CUDA Toolkit and MSVC needed)"; fi
|
else echo "note: no prebuilt igneum-worker-cuda.exe / igneum-worker-opencl.exe found; the engine builds the CUDA worker from proto-cuda\\ on the PC (CUDA Toolkit and MSVC needed)"; fi
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
89dfcb95be5ace1a2b7a4fb18d88aeb22023cab4
|
bb43e9a85f2683786fccc98d5533e85828b24138
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ fi
|
||||||
|
|
||||||
NODE_BRANCH="$(git -C "$NODE_SRC" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
NODE_BRANCH="$(git -C "$NODE_SRC" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
||||||
NODE_COMMIT="$(git -C "$NODE_SRC" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
NODE_COMMIT="$(git -C "$NODE_SRC" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
NODE_COMMIT_FULL="$(git -C "$NODE_SRC" rev-parse HEAD 2>/dev/null || echo unknown)" # the PC job writes it into a .git for kaspa-build-info (6 Oct 2026)
|
||||||
NODE_DIRTY=false; [ -z "$(git -C "$NODE_SRC" status --porcelain 2>/dev/null)" ] || NODE_DIRTY=true
|
NODE_DIRTY=false; [ -z "$(git -C "$NODE_SRC" status --porcelain 2>/dev/null)" ] || NODE_DIRTY=true
|
||||||
REPO_BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
REPO_BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
||||||
REPO_COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
REPO_COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
|
@ -108,9 +109,9 @@ rsync -a --exclude 'target' --exclude 'target-*' --exclude '.DS_Store' "$ROOT/ig
|
||||||
echo "packing proto-cuda (without nvrtc/redist)"
|
echo "packing proto-cuda (without nvrtc/redist)"
|
||||||
rsync -a --exclude 'nvrtc/redist' --exclude '.DS_Store' --exclude '*.exe' --exclude '*.dll' "$ROOT/proto-cuda/" "$STAGE/proto-cuda/"
|
rsync -a --exclude 'nvrtc/redist' --exclude '.DS_Store' --exclude '*.exe' --exclude '*.dll' "$ROOT/proto-cuda/" "$STAGE/proto-cuda/"
|
||||||
|
|
||||||
python3 - "$STAGE/manifest.json" "$NODE_BRANCH" "$NODE_COMMIT" "$NODE_DIRTY" "${NODE_SRC#"$ROOT"/}" "$REPO_BRANCH" "$REPO_COMMIT" "$REPO_DIRTY" "$APP_VERSION" "$WITH_APP" "$NODE_TESTS" "$APP_TESTS" "${WITH_NODE:-1}" <<'PY'
|
python3 - "$STAGE/manifest.json" "$NODE_BRANCH" "$NODE_COMMIT" "$NODE_DIRTY" "${NODE_SRC#"$ROOT"/}" "$REPO_BRANCH" "$REPO_COMMIT" "$REPO_DIRTY" "$APP_VERSION" "$WITH_APP" "$NODE_TESTS" "$APP_TESTS" "${WITH_NODE:-1}" "$NODE_COMMIT_FULL" <<'PY'
|
||||||
import json, sys, datetime
|
import json, sys, datetime
|
||||||
out, nb, nc, nd, ns, rb, rc, rd, av, with_app, node_tests, app_tests, with_node = sys.argv[1:14]
|
out, nb, nc, nd, ns, rb, rc, rd, av, with_app, node_tests, app_tests, with_node, ncf = sys.argv[1:15]
|
||||||
builds = [{"dir": "node", "packages": ["kaspad", "igneum-miner"], "features": ["kaspad/igneum-pow"], "bins": ["igneumd", "igneum-miner"], "targets": ["linux", "windows"]}] if with_node == "1" else []
|
builds = [{"dir": "node", "packages": ["kaspad", "igneum-miner"], "features": ["kaspad/igneum-pow"], "bins": ["igneumd", "igneum-miner"], "targets": ["linux", "windows"]}] if with_node == "1" else []
|
||||||
tests = []
|
tests = []
|
||||||
if node_tests.strip() and with_node == "1":
|
if node_tests.strip() and with_node == "1":
|
||||||
|
|
@ -121,7 +122,7 @@ if with_app == "1":
|
||||||
tests.append({"dir": "app/igneum-app", "packages": app_tests.split()})
|
tests.append({"dir": "app/igneum-app", "packages": app_tests.split()})
|
||||||
m = {
|
m = {
|
||||||
"created_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
"created_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
"node": {"branch": nb, "commit": nc, "dirty": nd == "true", "source": ns},
|
"node": {"branch": nb, "commit": nc, "commit_full": ncf, "dirty": nd == "true", "source": ns},
|
||||||
"repo": {"branch": rb, "commit": rc, "dirty": rd == "true"},
|
"repo": {"branch": rb, "commit": rc, "dirty": rd == "true"},
|
||||||
"app_version": av if with_app == "1" else "",
|
"app_version": av if with_app == "1" else "",
|
||||||
"builds": builds,
|
"builds": builds,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc
|
||||||
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
|
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
|
||||||
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
|
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
|
||||||
CL_WORKER="$ROOT/proto-opencl/igneum-worker-opencl.exe"
|
CL_WORKER="$ROOT/proto-opencl/igneum-worker-opencl.exe"
|
||||||
|
TELEMETRY="$ROOT/proto-opencl/igneum-gpu-telemetry.exe" # AMD power, heat, fans, clocks (5 October 2026)
|
||||||
TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
||||||
DLSITE="${IGNEUM_DLSITE:-}"
|
DLSITE="${IGNEUM_DLSITE:-}"
|
||||||
[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true
|
[ -n "$DLSITE" ] || { [ -f "$HOME/.config/igneum/dlsite-dir" ] && DLSITE="$(tr -d '[:space:]' < "$HOME/.config/igneum/dlsite-dir")"; } || true
|
||||||
|
|
@ -59,6 +60,7 @@ if [ -f "$NVRTC_DIR/igneum-worker-cuda.exe" ]; then
|
||||||
ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh)"
|
ls "$STAGE"/nvrtc64_*_0.dll >/dev/null 2>&1 || echo "warning: igneum-worker-cuda.exe without nvrtc64_*_0.dll (run $NVRTC_DIR/fetch-redist.sh)"
|
||||||
else echo "warning: no $NVRTC_DIR/igneum-worker-cuda.exe (run $NVRTC_DIR/build-windows.sh); the app will build the CUDA worker on the PC"; fi
|
else echo "warning: no $NVRTC_DIR/igneum-worker-cuda.exe (run $NVRTC_DIR/build-windows.sh); the app will build the CUDA worker on the PC"; fi
|
||||||
[ -f "$CL_WORKER" ] && cp "$CL_WORKER" "$STAGE/" || echo "warning: no $CL_WORKER"
|
[ -f "$CL_WORKER" ] && cp "$CL_WORKER" "$STAGE/" || echo "warning: no $CL_WORKER"
|
||||||
|
[ -f "$TELEMETRY" ] && cp "$TELEMETRY" "$STAGE/" || echo "warning: no $TELEMETRY (AMD cards show no draw or temperature)"
|
||||||
|
|
||||||
# the signer, built from the app crate (it includes src/manifest.rs and src/inputs.rs, so it signs what the runner verifies)
|
# the signer, built from the app crate (it includes src/manifest.rs and src/inputs.rs, so it signs what the runner verifies)
|
||||||
KEY="$HOME/.config/igneum/ota-signing-key"
|
KEY="$HOME/.config/igneum/ota-signing-key"
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ VERIFY="$ROOT/packaging/windows/resources/verify-exe.py"
|
||||||
# The coin icon and the version blocks, as COFF objects the linker takes like any other input
|
# The coin icon and the version blocks, as COFF objects the linker takes like any other input
|
||||||
[ -f "$ICONS/igneum.ico" ] || { echo "== no $ICONS/igneum.ico, making the icons"; python3 "$ICONS/make-icons.py"; }
|
[ -f "$ICONS/igneum.ico" ] || { echo "== no $ICONS/igneum.ico, making the icons"; python3 "$ICONS/make-icons.py"; }
|
||||||
RES="$(mktemp -d)"
|
RES="$(mktemp -d)"
|
||||||
for w in cuda opencl; do
|
for w in cuda opencl gpu-telemetry; do
|
||||||
"$WINDRES" -I "$ICONS" -i "$HERE/igneum-worker-$w.rc" -O coff -o "$RES/igneum-worker-$w.res.o"
|
"$WINDRES" -I "$ICONS" -i "$HERE/igneum-worker-$w.rc" -O coff -o "$RES/igneum-worker-$w.res.o"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
@ -37,11 +37,16 @@ echo "== igneum-worker-opencl.exe"
|
||||||
-I "$RED/include" -I "$PLACEHOLDER" -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' \
|
-I "$RED/include" -I "$PLACEHOLDER" -DIGNEUM_KERNEL_PATH='"kernel_bound.cl"' \
|
||||||
-o "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$ROOT/proto-opencl/host.c" "$RES/igneum-worker-opencl.res.o"
|
-o "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$ROOT/proto-opencl/host.c" "$RES/igneum-worker-opencl.res.o"
|
||||||
"$STRIP" "$ROOT/proto-opencl/igneum-worker-opencl.exe"
|
"$STRIP" "$ROOT/proto-opencl/igneum-worker-opencl.exe"
|
||||||
|
echo "== igneum-gpu-telemetry.exe (ADLX, SetupAPI, PDH; vendor/adlx is the SDK clone)"
|
||||||
|
[ -f "$ROOT/vendor/adlx/SDK/Include/ADLX.h" ] || { echo "no vendor/adlx: git clone --depth 1 https://github.com/GPUOpen-LibrariesAndSDKs/ADLX.git $ROOT/vendor/adlx" >&2; exit 1; }
|
||||||
|
"$CC" -std=gnu99 -O2 -Wall -Wno-unused-parameter -Wno-unused-function -static -I "$ROOT/vendor/adlx/SDK/Include" \
|
||||||
|
-o "$ROOT/proto-opencl/igneum-gpu-telemetry.exe" "$ROOT/proto-opencl/gpu-telemetry.c" "$ROOT/vendor/adlx/SDK/ADLXHelper/Windows/C/ADLXHelper.c" "$RES/igneum-worker-gpu-telemetry.res.o" -lsetupapi -lpdh
|
||||||
|
"$STRIP" "$ROOT/proto-opencl/igneum-gpu-telemetry.exe"
|
||||||
rm -rf "$RES"
|
rm -rf "$RES"
|
||||||
for exe in "$HERE/igneum-worker-cuda.exe" "$ROOT/proto-opencl/igneum-worker-opencl.exe"; do
|
for exe in "$HERE/igneum-worker-cuda.exe" "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$ROOT/proto-opencl/igneum-gpu-telemetry.exe"; do
|
||||||
printf '%s: %d bytes, imports:' "$(basename "$exe")" "$(stat -f %z "$exe")"
|
printf '%s: %d bytes, imports:' "$(basename "$exe")" "$(stat -f %z "$exe")"
|
||||||
x86_64-w64-mingw32-objdump -p "$exe" | sed -n 's/^[[:space:]]*DLL Name: //p' | tr '\n' ' '
|
x86_64-w64-mingw32-objdump -p "$exe" | sed -n 's/^[[:space:]]*DLL Name: //p' | tr '\n' ' '
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
# the icon and version block survived the strip (strip keeps .rsrc; this proves it)
|
# the icon and version block survived the strip (strip keeps .rsrc; this proves it)
|
||||||
python3 "$VERIFY" --version 0.3.0 "$HERE/igneum-worker-cuda.exe" "$ROOT/proto-opencl/igneum-worker-opencl.exe"
|
python3 "$VERIFY" --version 0.3.0 "$HERE/igneum-worker-cuda.exe" "$ROOT/proto-opencl/igneum-worker-opencl.exe" "$ROOT/proto-opencl/igneum-gpu-telemetry.exe"
|
||||||
|
|
|
||||||
35
proto-cuda/nvrtc/igneum-worker-gpu-telemetry.rc
Normal file
35
proto-cuda/nvrtc/igneum-worker-gpu-telemetry.rc
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Windows resources for igneum-gpu-telemetry.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).
|
||||||
|
// Josh'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,0,0
|
||||||
|
PRODUCTVERSION 0,3,0,0
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904B0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "Igneum"
|
||||||
|
VALUE "FileDescription", "Igneum Miner GPU telemetry (AMD power, heat, fans, clocks)"
|
||||||
|
VALUE "FileVersion", "0.3.0"
|
||||||
|
VALUE "InternalName", "igneum-gpu-telemetry"
|
||||||
|
VALUE "LegalCopyright", "Igneum contributors"
|
||||||
|
VALUE "OriginalFilename", "igneum-gpu-telemetry.exe"
|
||||||
|
VALUE "ProductName", "Igneum Miner"
|
||||||
|
VALUE "ProductVersion", "0.3.0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
@ -36,10 +36,22 @@ export BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw
|
||||||
# after that commit imports no mingw DLL and the pairing rule is moot for it; the gate and the DLL copy stay for any
|
# after that commit imports no mingw DLL and the pairing rule is moot for it; the gate and the DLL copy stay for any
|
||||||
# older fork. The PC-built exe (GCC 13) died at its first rocksdb call with the dynamic runtime; see the
|
# older fork. The PC-built exe (GCC 13) died at its first rocksdb call with the dynamic runtime; see the
|
||||||
# release-0.3.6 plan, section 10.
|
# release-0.3.6 plan, section 10.
|
||||||
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-static-libstdc++"
|
# 6 October 2026 (main's decision): -Wl,--no-insert-timestamp makes the exe reproducible (the PE header timestamp was the one
|
||||||
|
# byte-level difference between two builds of one tree on igneum-build-1); the box (tools/cross-remote.sh) and the PC job carry it too
|
||||||
|
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-static-libstdc++ -C link-arg=-Wl,--no-insert-timestamp"
|
||||||
cd "$NODE"
|
cd "$NODE"
|
||||||
|
# The commit hash (6 October 2026, found on igneum-build-1): kaspa-build-info's build.rs embeds the commit only when .git is
|
||||||
|
# a DIRECTORY and HEAD is a symbolic ref to a branch file, and once it has found nothing it emits no rerun-if-changed, so
|
||||||
|
# cargo never runs it again in this target dir. Two steps: clean that one crate when the commit changed since the last build
|
||||||
|
# here, and refuse a worktree (.git is a file there: the hash would be empty and tools/ci/commit-string-check.sh would fail
|
||||||
|
# the release). Build the Windows exes from the fork's main checkout or through tools/cross-remote.sh (the box checks out a branch).
|
||||||
|
sha=$(git rev-parse HEAD)
|
||||||
|
[ -d .git ] || { echo "$NODE/.git is not a directory (a worktree): kaspa-build-info would embed no commit; use tools/cross-remote.sh or the fork's main checkout" >&2; exit 1; }
|
||||||
|
[ "$(cat ".cross-build-sha-$CARGO_TARGET_DIR" 2>/dev/null)" = "$sha" ] || cargo clean -q --release -p kaspa-build-info --target x86_64-pc-windows-gnu 2>/dev/null || true
|
||||||
nice -n 19 cargo build --release -j "$JOBS" -p kaspad -p igneum-miner --features igneum-pow --target x86_64-pc-windows-gnu
|
nice -n 19 cargo build --release -j "$JOBS" -p kaspad -p igneum-miner --features igneum-pow --target x86_64-pc-windows-gnu
|
||||||
|
echo "$sha" > ".cross-build-sha-$CARGO_TARGET_DIR"
|
||||||
for exe in igneumd igneum-miner; do
|
for exe in igneumd igneum-miner; do
|
||||||
f="$CARGO_TARGET_DIR/x86_64-pc-windows-gnu/release/$exe.exe"
|
f="$CARGO_TARGET_DIR/x86_64-pc-windows-gnu/release/$exe.exe"
|
||||||
echo "$f: $(stat -f %z "$f") bytes; DLLs: $(x86_64-w64-mingw32-objdump -p "$f" | grep 'DLL Name' | awk '{print $3}' | sort -u | tr '\n' ' ')"
|
echo "$f: $(stat -f %z "$f") bytes; DLLs: $(x86_64-w64-mingw32-objdump -p "$f" | grep 'DLL Name' | awk '{print $3}' | sort -u | tr '\n' ' ')"
|
||||||
|
[ "$exe" = igneumd ] && "$ROOT/tools/ci/commit-string-check.sh" "$f" "$sha" # only kaspad depends on kaspa-build-info
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ proto-opencl/
|
||||||
cl_dynamic.h Windows one-click build: OpenCL.dll loaded at run time (IGNEUM_CL_DYNAMIC)
|
cl_dynamic.h Windows one-click build: OpenCL.dll loaded at run time (IGNEUM_CL_DYNAMIC)
|
||||||
test-generic.sh the --pack mode checked here through Apple OpenCL (needs proto-cuda/nvrtc/emu/test.sh's packs)
|
test-generic.sh the --pack mode checked here through Apple OpenCL (needs proto-cuda/nvrtc/emu/test.sh's packs)
|
||||||
test_host.c device-free unit tests of host.c's rules (the duplicate-platform fold); run with test-host.sh
|
test_host.c device-free unit tests of host.c's rules (the duplicate-platform fold); run with test-host.sh
|
||||||
|
gpu-telemetry.c igneum-gpu-telemetry: AMD power, temperature, fan, clocks and busy per card (ADLX on Windows, amdgpu sysfs on Linux),
|
||||||
|
one line per card per sample; the app's AMD card row reads it (engine.rs amd_telemetry_line)
|
||||||
build.sh macOS (-framework OpenCL, or the Khronos ICD loader) and Linux (-lOpenCL)
|
build.sh macOS (-framework OpenCL, or the Khronos ICD loader) and Linux (-lOpenCL)
|
||||||
build.bat Windows (MSVC cl.exe + OpenCL.lib)
|
build.bat Windows (MSVC cl.exe + OpenCL.lib)
|
||||||
WAVEFRONT.md wave32 vs wave64 on AMD, and why the kernel cannot tell the difference
|
WAVEFRONT.md wave32 vs wave64 on AMD, and why the kernel cannot tell the difference
|
||||||
|
|
|
||||||
274
proto-opencl/gpu-telemetry.c
Normal file
274
proto-opencl/gpu-telemetry.c
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
// igneum-gpu-telemetry: power, temperature, fan and clocks of every AMD GPU, one line per card per sample.
|
||||||
|
// 5 October 2026, after Josh watched a 9070 XT at 90% usage with its fans barely turning and the app could not say
|
||||||
|
// what it drew (the app's draw, temperature and MH per watt line came from nvidia-smi only).
|
||||||
|
//
|
||||||
|
// igneum-gpu-telemetry [-l SECONDS] one sample (default), or one every SECONDS until stdin closes or SIGTERM
|
||||||
|
//
|
||||||
|
// Windows: ADLX (the AMD Device Library eXtra, amdadlx64.dll, shipped with Adrenalin; vendor/adlx is the SDK clone,
|
||||||
|
// MIT) for the metrics, keyed by the card's PCI bus from SetupAPI (the display class, matched by the same name ADLX
|
||||||
|
// reports). Without ADLX (no AMD driver, an old one, or the DLL missing) only the utilisation is read, from the
|
||||||
|
// GPU Engine performance counters through PDH, keyed by the adapter LUID that Windows uses there.
|
||||||
|
// Linux: the amdgpu sysfs (/sys/class/drm/card*/device: hwmon power1_average, temp1_input, fan1_input, pwm1,
|
||||||
|
// pp_dpm_mclk, gpu_busy_percent), keyed by the PCI address of the device link.
|
||||||
|
//
|
||||||
|
// Line format (space separated, every field present, a value the source cannot give prints as -):
|
||||||
|
// amd <ordinal> bus <pci bus or address> kind integrated|discrete name "<name>" watts <W> temp_c <C> fan_rpm <rpm>
|
||||||
|
// fan_pct <%> mclk_mhz <MHz> gclk_mhz <MHz> util_pct <%> source adlx|sysfs|perfcounter
|
||||||
|
// then one `end <ms>` line per sample. The app (engine.rs amd_telemetry_line) parses it; parsers are unit-tested
|
||||||
|
// against lines captured on PC 1.
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
static volatile int gStop = 0;
|
||||||
|
static void onSignal(int s) { (void)s; gStop = 1; }
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char bus[64];
|
||||||
|
char kind[16];
|
||||||
|
char name[128];
|
||||||
|
double watts, tempC, fanRpm, fanPct, mclk, gclk, util; /* -1 = not available */
|
||||||
|
const char* source;
|
||||||
|
} Sample;
|
||||||
|
|
||||||
|
static void sampleInit(Sample* s) { memset(s, 0, sizeof(*s)); strcpy(s->bus, "-"); strcpy(s->kind, "-"); strcpy(s->name, "-"); s->watts = s->tempC = s->fanRpm = s->fanPct = s->mclk = s->gclk = s->util = -1.0; s->source = "-"; }
|
||||||
|
static void printNum(double v, const char* fmt) { if (v < 0) printf(" -"); else printf(fmt, v); }
|
||||||
|
static void printSample(int ordinal, const Sample* s) {
|
||||||
|
printf("amd %d bus %s kind %s name \"%s\" watts", ordinal, s->bus, s->kind, s->name);
|
||||||
|
printNum(s->watts, " %.1f"); printf(" temp_c"); printNum(s->tempC, " %.1f"); printf(" fan_rpm"); printNum(s->fanRpm, " %.0f");
|
||||||
|
printf(" fan_pct"); printNum(s->fanPct, " %.0f"); printf(" mclk_mhz"); printNum(s->mclk, " %.0f"); printf(" gclk_mhz"); printNum(s->gclk, " %.0f");
|
||||||
|
printf(" util_pct"); printNum(s->util, " %.0f"); printf(" source %s\n", s->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
#include <setupapi.h>
|
||||||
|
#include <pdh.h>
|
||||||
|
#include "../vendor/adlx/SDK/ADLXHelper/Windows/C/ADLXHelper.h"
|
||||||
|
#include "../vendor/adlx/SDK/Include/IPerformanceMonitoring.h"
|
||||||
|
|
||||||
|
/* The SDK declares these three and leaves them to the platform file of each sample. */
|
||||||
|
adlx_handle ADLX_CDECL_CALL adlx_load_library(const TCHAR* filename) { return (adlx_handle)LoadLibrary(filename); }
|
||||||
|
int ADLX_CDECL_CALL adlx_free_library(adlx_handle module) { return FreeLibrary((HMODULE)module) ? 1 : 0; }
|
||||||
|
void* ADLX_CDECL_CALL adlx_get_proc_address(adlx_handle module, const char* procName) { return (void*)GetProcAddress((HMODULE)module, procName); }
|
||||||
|
|
||||||
|
static double nowMs(void) { LARGE_INTEGER f, c; QueryPerformanceFrequency(&f); QueryPerformanceCounter(&c); return (double)c.QuadPart * 1000.0 / (double)f.QuadPart; }
|
||||||
|
|
||||||
|
/* The PCI bus of every display-class device, by its name (SetupAPI; the names are the ones ADLX reports). */
|
||||||
|
typedef struct { char name[128]; int bus; } BusEntry;
|
||||||
|
static int listBuses(BusEntry* out, int cap) {
|
||||||
|
static const GUID DISPLAY = { 0x4d36e968, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } };
|
||||||
|
HDEVINFO set = SetupDiGetClassDevsA(&DISPLAY, NULL, NULL, DIGCF_PRESENT);
|
||||||
|
SP_DEVINFO_DATA d;
|
||||||
|
DWORD i;
|
||||||
|
int n = 0;
|
||||||
|
if (set == INVALID_HANDLE_VALUE) return 0;
|
||||||
|
d.cbSize = sizeof(d);
|
||||||
|
for (i = 0; SetupDiEnumDeviceInfo(set, i, &d) && n < cap; ++i) {
|
||||||
|
char name[128] = { 0 };
|
||||||
|
DWORD bus = 0, type = 0, got = 0;
|
||||||
|
if (!SetupDiGetDeviceRegistryPropertyA(set, &d, SPDRP_DEVICEDESC, &type, (BYTE*)name, sizeof(name) - 1, &got)) continue;
|
||||||
|
if (!SetupDiGetDeviceRegistryPropertyA(set, &d, SPDRP_BUSNUMBER, &type, (BYTE*)&bus, sizeof(bus), &got)) continue;
|
||||||
|
snprintf(out[n].name, sizeof(out[n].name), "%s", name); out[n].bus = (int)bus; ++n;
|
||||||
|
}
|
||||||
|
SetupDiDestroyDeviceInfoList(set);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
static int busOf(const BusEntry* b, int n, const char* name, int* taken) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < n; ++i) if (!taken[i] && strcmp(b[i].name, name) == 0) { taken[i] = 1; return b[i].bus; }
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ADLX: one sample of every GPU. Returns the number of lines printed, -1 when ADLX is not usable (reason printed). */
|
||||||
|
static IADLXSystem* gSys = NULL;
|
||||||
|
static IADLXPerformanceMonitoringServices* gPerf = NULL;
|
||||||
|
static int adlxOpen(void) {
|
||||||
|
ADLX_RESULT r = ADLXHelper_Initialize();
|
||||||
|
if (!ADLX_SUCCEEDED(r)) { printf("info adlx: ADLXHelper_Initialize returned %d (no AMD driver with ADLX; amdadlx64.dll missing or too old)\n", (int)r); return 0; }
|
||||||
|
gSys = ADLXHelper_GetSystemServices();
|
||||||
|
if (!gSys) { printf("info adlx: no system services\n"); return 0; }
|
||||||
|
r = gSys->pVtbl->GetPerformanceMonitoringServices(gSys, &gPerf);
|
||||||
|
if (!ADLX_SUCCEEDED(r) || !gPerf) { printf("info adlx: GetPerformanceMonitoringServices returned %d\n", (int)r); return 0; }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
static int adlxSample(const BusEntry* buses, int nBuses) {
|
||||||
|
IADLXGPUList* gpus = NULL;
|
||||||
|
adlx_uint it;
|
||||||
|
int ordinal = 0;
|
||||||
|
int taken[32] = { 0 };
|
||||||
|
ADLX_RESULT r = gSys->pVtbl->GetGPUs(gSys, &gpus);
|
||||||
|
if (!ADLX_SUCCEEDED(r) || !gpus) { printf("info adlx: GetGPUs returned %d\n", (int)r); return 0; }
|
||||||
|
for (it = gpus->pVtbl->Begin(gpus); it != gpus->pVtbl->End(gpus); ++it) {
|
||||||
|
IADLXGPU* gpu = NULL;
|
||||||
|
IADLXGPUMetrics* m = NULL;
|
||||||
|
Sample s;
|
||||||
|
const char* name = NULL;
|
||||||
|
ADLX_GPU_TYPE type = GPUTYPE_UNDEFINED;
|
||||||
|
adlx_double dv = 0; adlx_int iv = 0;
|
||||||
|
if (!ADLX_SUCCEEDED(gpus->pVtbl->At_GPUList(gpus, it, &gpu)) || !gpu) continue;
|
||||||
|
sampleInit(&s);
|
||||||
|
s.source = "adlx";
|
||||||
|
if (ADLX_SUCCEEDED(gpu->pVtbl->Name(gpu, &name)) && name) snprintf(s.name, sizeof(s.name), "%s", name);
|
||||||
|
if (ADLX_SUCCEEDED(gpu->pVtbl->Type(gpu, &type))) strcpy(s.kind, type == GPUTYPE_INTEGRATED ? "integrated" : type == GPUTYPE_DISCRETE ? "discrete" : "-");
|
||||||
|
{ int b = busOf(buses, nBuses, s.name, taken); if (b >= 0) snprintf(s.bus, sizeof(s.bus), "%d", b); }
|
||||||
|
r = gPerf->pVtbl->GetCurrentGPUMetrics(gPerf, gpu, &m);
|
||||||
|
if (ADLX_SUCCEEDED(r) && m) {
|
||||||
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUPower(m, &dv))) s.watts = dv;
|
||||||
|
if (s.watts < 0 && ADLX_SUCCEEDED(m->pVtbl->GPUTotalBoardPower(m, &dv))) s.watts = dv;
|
||||||
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUTemperature(m, &dv))) s.tempC = dv;
|
||||||
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUFanSpeed(m, &iv))) s.fanRpm = iv;
|
||||||
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUVRAMClockSpeed(m, &iv))) s.mclk = iv;
|
||||||
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUClockSpeed(m, &iv))) s.gclk = iv;
|
||||||
|
if (ADLX_SUCCEEDED(m->pVtbl->GPUUsage(m, &dv))) s.util = dv;
|
||||||
|
m->pVtbl->Release(m);
|
||||||
|
} else {
|
||||||
|
printf("info adlx: GetCurrentGPUMetrics for \"%s\" returned %d\n", s.name, (int)r);
|
||||||
|
}
|
||||||
|
/* fan percent: ADLX gives rpm only here; the tuning interface has the range, the app shows rpm when pct is - */
|
||||||
|
printSample(ordinal++, &s);
|
||||||
|
gpu->pVtbl->Release(gpu);
|
||||||
|
}
|
||||||
|
gpus->pVtbl->Release(gpus);
|
||||||
|
return ordinal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PDH fallback: GPU engine utilisation per adapter LUID, summed over the engines (no power, no temperature). */
|
||||||
|
static int pdhSample(void) {
|
||||||
|
PDH_HQUERY q = NULL;
|
||||||
|
PDH_HCOUNTER c = NULL;
|
||||||
|
DWORD size = 0, count = 0, i;
|
||||||
|
PDH_FMT_COUNTERVALUE_ITEM_A* items;
|
||||||
|
int ordinal = 0;
|
||||||
|
if (PdhOpenQueryA(NULL, 0, &q) != ERROR_SUCCESS) { printf("info perfcounter: PdhOpenQuery failed\n"); return 0; }
|
||||||
|
if (PdhAddEnglishCounterA(q, "\\GPU Engine(*)\\Utilization Percentage", 0, &c) != ERROR_SUCCESS) { printf("info perfcounter: no GPU Engine counters\n"); PdhCloseQuery(q); return 0; }
|
||||||
|
PdhCollectQueryData(q); Sleep(1000); PdhCollectQueryData(q);
|
||||||
|
PdhGetFormattedCounterArrayA(c, PDH_FMT_DOUBLE, &size, &count, NULL);
|
||||||
|
items = (PDH_FMT_COUNTERVALUE_ITEM_A*)malloc(size ? size : 1);
|
||||||
|
if (PdhGetFormattedCounterArrayA(c, PDH_FMT_DOUBLE, &size, &count, items) == ERROR_SUCCESS) {
|
||||||
|
/* instance names: pid_1234_luid_0x00000000_0x0000D4E3_phys_0_eng_0_engtype_3D; sum per luid */
|
||||||
|
char luids[16][40]; double sums[16]; int n = 0, k;
|
||||||
|
for (i = 0; i < count; ++i) {
|
||||||
|
const char* p = strstr(items[i].szName, "luid_");
|
||||||
|
char luid[40];
|
||||||
|
if (!p) continue;
|
||||||
|
snprintf(luid, sizeof(luid), "%.39s", p); { char* e = strstr(luid, "_phys"); if (e) *e = 0; }
|
||||||
|
for (k = 0; k < n; ++k) if (strcmp(luids[k], luid) == 0) break;
|
||||||
|
if (k == n && n < 16) { strcpy(luids[n], luid); sums[n] = 0; ++n; }
|
||||||
|
if (k < 16) sums[k] += items[i].FmtValue.doubleValue;
|
||||||
|
}
|
||||||
|
for (k = 0; k < n; ++k) {
|
||||||
|
Sample s; sampleInit(&s); s.source = "perfcounter";
|
||||||
|
snprintf(s.bus, sizeof(s.bus), "%s", luids[k]);
|
||||||
|
s.util = sums[k] > 100.0 ? 100.0 : sums[k];
|
||||||
|
printSample(ordinal++, &s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(items);
|
||||||
|
PdhCloseQuery(q);
|
||||||
|
return ordinal;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
int every = 0, i, haveAdlx;
|
||||||
|
BusEntry buses[32];
|
||||||
|
int nBuses;
|
||||||
|
for (i = 1; i < argc; ++i) if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) every = atoi(argv[++i]);
|
||||||
|
signal(SIGINT, onSignal); signal(SIGTERM, onSignal);
|
||||||
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||||
|
nBuses = listBuses(buses, 32);
|
||||||
|
for (i = 0; i < nBuses; ++i) printf("info display device \"%s\" bus %d\n", buses[i].name, buses[i].bus);
|
||||||
|
haveAdlx = adlxOpen();
|
||||||
|
do {
|
||||||
|
double t0 = nowMs();
|
||||||
|
int n = haveAdlx ? adlxSample(buses, nBuses) : pdhSample();
|
||||||
|
printf("end %.1f ms %d card(s)\n", nowMs() - t0, n);
|
||||||
|
fflush(stdout); /* a redirected stdout is fully buffered on the Windows CRT whatever setvbuf asks (PC 1 lost 60 s of samples at the kill) */
|
||||||
|
if (every > 0) Sleep((DWORD)every * 1000);
|
||||||
|
} while (every > 0 && !gStop);
|
||||||
|
if (haveAdlx) { if (gPerf) gPerf->pVtbl->Release(gPerf); ADLXHelper_Terminate(); }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
static double nowMs(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; }
|
||||||
|
static int readText(const char* path, char* out, size_t cap) { FILE* f = fopen(path, "r"); size_t n; if (!f) return 0; n = fread(out, 1, cap - 1, f); fclose(f); out[n] = 0; return 1; }
|
||||||
|
static double readNumber(const char* path) { char b[64]; if (!readText(path, b, sizeof(b))) return -1.0; return atof(b); }
|
||||||
|
/* pp_dpm_mclk: lines "0: 96Mhz", "3: 1258Mhz *"; the starred line is the current state */
|
||||||
|
static double dpmCurrent(const char* text) {
|
||||||
|
const char* p = text;
|
||||||
|
while (p && *p) {
|
||||||
|
const char* nl = strchr(p, '\n');
|
||||||
|
size_t len = nl ? (size_t)(nl - p) : strlen(p);
|
||||||
|
const char* star = memchr(p, '*', len);
|
||||||
|
if (star) { const char* colon = memchr(p, ':', len); if (colon) return atof(colon + 1); }
|
||||||
|
p = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
static int sysfsSample(const char* root) {
|
||||||
|
DIR* d = opendir(root);
|
||||||
|
struct dirent* e;
|
||||||
|
int ordinal = 0;
|
||||||
|
if (!d) { printf("info sysfs: no %s\n", root); return 0; }
|
||||||
|
while ((e = readdir(d)) != NULL) {
|
||||||
|
char dev[512], path[640], text[4096], link[512];
|
||||||
|
ssize_t ln;
|
||||||
|
Sample s;
|
||||||
|
DIR* hw; struct dirent* he;
|
||||||
|
if (strncmp(e->d_name, "card", 4) != 0 || strchr(e->d_name + 4, '-')) continue;
|
||||||
|
snprintf(dev, sizeof(dev), "%s/%s/device", root, e->d_name);
|
||||||
|
snprintf(path, sizeof(path), "%s/vendor", dev);
|
||||||
|
if (!readText(path, text, sizeof(text)) || strtol(text, NULL, 16) != 0x1002) continue;
|
||||||
|
sampleInit(&s);
|
||||||
|
s.source = "sysfs";
|
||||||
|
ln = readlink(dev, link, sizeof(link) - 1);
|
||||||
|
if (ln > 0) { link[ln] = 0; { const char* base = strrchr(link, '/'); snprintf(s.bus, sizeof(s.bus), "%.63s", base ? base + 1 : link); } }
|
||||||
|
snprintf(path, sizeof(path), "%s/product_name", dev);
|
||||||
|
if (readText(path, text, sizeof(text))) { text[strcspn(text, "\n")] = 0; snprintf(s.name, sizeof(s.name), "%s", text); }
|
||||||
|
else { snprintf(path, sizeof(path), "%s/device", dev); if (readText(path, text, sizeof(text))) { text[strcspn(text, "\n")] = 0; snprintf(s.name, sizeof(s.name), "amdgpu %s", text); } }
|
||||||
|
snprintf(path, sizeof(path), "%s/boot_vga", dev);
|
||||||
|
strcpy(s.kind, "discrete");
|
||||||
|
snprintf(path, sizeof(path), "%s/hwmon", dev);
|
||||||
|
hw = opendir(path);
|
||||||
|
if (hw) {
|
||||||
|
while ((he = readdir(hw)) != NULL) {
|
||||||
|
char hp[900];
|
||||||
|
if (strncmp(he->d_name, "hwmon", 5) != 0) continue;
|
||||||
|
snprintf(hp, sizeof(hp), "%s/%s/power1_average", path, he->d_name); s.watts = readNumber(hp); if (s.watts < 0) { snprintf(hp, sizeof(hp), "%s/%s/power1_input", path, he->d_name); s.watts = readNumber(hp); } if (s.watts >= 0) s.watts /= 1e6;
|
||||||
|
snprintf(hp, sizeof(hp), "%s/%s/temp1_input", path, he->d_name); s.tempC = readNumber(hp); if (s.tempC >= 0) s.tempC /= 1000.0;
|
||||||
|
snprintf(hp, sizeof(hp), "%s/%s/fan1_input", path, he->d_name); s.fanRpm = readNumber(hp);
|
||||||
|
{ double pwm, pwmMax; snprintf(hp, sizeof(hp), "%s/%s/pwm1", path, he->d_name); pwm = readNumber(hp); snprintf(hp, sizeof(hp), "%s/%s/pwm1_max", path, he->d_name); pwmMax = readNumber(hp); if (pwm >= 0 && pwmMax > 0) s.fanPct = 100.0 * pwm / pwmMax; else if (pwm >= 0) s.fanPct = 100.0 * pwm / 255.0; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
closedir(hw);
|
||||||
|
}
|
||||||
|
snprintf(path, sizeof(path), "%s/pp_dpm_mclk", dev); if (readText(path, text, sizeof(text))) s.mclk = dpmCurrent(text);
|
||||||
|
snprintf(path, sizeof(path), "%s/pp_dpm_sclk", dev); if (readText(path, text, sizeof(text))) s.gclk = dpmCurrent(text);
|
||||||
|
snprintf(path, sizeof(path), "%s/gpu_busy_percent", dev); s.util = readNumber(path);
|
||||||
|
printSample(ordinal++, &s);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return ordinal;
|
||||||
|
}
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
int every = 0, i;
|
||||||
|
const char* root = getenv("IGNEUM_DRM_ROOT") ? getenv("IGNEUM_DRM_ROOT") : "/sys/class/drm"; /* a fixture tree for tests */
|
||||||
|
for (i = 1; i < argc; ++i) if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) every = atoi(argv[++i]);
|
||||||
|
signal(SIGINT, onSignal); signal(SIGTERM, onSignal);
|
||||||
|
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||||
|
do {
|
||||||
|
double t0 = nowMs();
|
||||||
|
int n = sysfsSample(root);
|
||||||
|
printf("end %.1f ms %d card(s)\n", nowMs() - t0, n);
|
||||||
|
fflush(stdout); /* a redirected stdout is fully buffered on the Windows CRT whatever setvbuf asks (PC 1 lost 60 s of samples at the kill) */
|
||||||
|
if (every > 0) sleep((unsigned)every);
|
||||||
|
} while (every > 0 && !gStop);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -93,9 +93,16 @@ fn run() -> Result<()> {
|
||||||
// proof (the chain rule of design 5.3), the measurement of docs/plans/proving-v1.md step 2
|
// proof (the chain rule of design 5.3), the measurement of docs/plans/proving-v1.md step 2
|
||||||
let list = arg("--chain").or_else(|| args.get(1).filter(|a| !a.starts_with("--")).cloned()).context("--chain <f1.json,f2.json,...> (consecutive fixtures)")?;
|
let list = arg("--chain").or_else(|| args.get(1).filter(|a| !a.starts_with("--")).cloned()).context("--chain <f1.json,f2.json,...> (consecutive fixtures)")?;
|
||||||
let fixtures: Vec<String> = list.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
let fixtures: Vec<String> = list.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||||
return run_chain(&pinned, &fixtures, prover, out_path.as_deref());
|
// --save-shards writes every shard's compressed proof next to the results (block-N-shard-i-compressed.bin),
|
||||||
|
// so `--mode aggregate` can re-run the aggregation of the same proofs under other settings
|
||||||
|
let save_shards = args.iter().any(|a| a == "--save-shards");
|
||||||
|
// --prev <file>: the previous segment's aggregated proof; the chain continues from it (chain_len grows past
|
||||||
|
// the segment length, the chain rule of spec 7.8) instead of starting fresh. The app's segment path (6 October
|
||||||
|
// 2026) passes it when the node reports the previous segment paid and its proof in the pool.
|
||||||
|
let prev = arg("--prev");
|
||||||
|
return run_chain(&pinned, &fixtures, prover, out_path.as_deref(), save_shards, prev.as_deref());
|
||||||
}
|
}
|
||||||
let path = args.get(1).filter(|a| !a.starts_with("--")).context("usage: igneum-prove-host <fixture.json> [--mode native|execute|shard|compressed|block|all] [--shard N] [--budget <test pgas>] [--prover 0x..] [--out results.json]; --mode chain --chain <f1,f2,...> [--prover 0x..] [--out results.json]; --mode aggregate --proofs <a.bin,...> --parent 0x.. [--prev prev.bin] [--out results.json]; --mode verify --proof <file> --statement 0x..; --mode verify-segment --proof <file> --statement 0x..; --mode id")?;
|
let path = args.get(1).filter(|a| !a.starts_with("--")).context("usage: igneum-prove-host <fixture.json> [--mode native|execute|shard|compressed|block|all] [--shard N] [--budget <test pgas>] [--prover 0x..] [--out results.json]; --mode chain --chain <f1,f2,...> [--prover 0x..] [--out results.json] [--save-shards] [--prev prev.bin]; --mode aggregate --proofs <a.bin,...> --parent 0x.. [--prev prev.bin] [--out results.json]; --mode verify --proof <file> --statement 0x..; --mode verify-segment --proof <file> --statement 0x..; --mode id")?;
|
||||||
let shard_index: usize = arg("--shard").map(|s| s.parse()).transpose()?.unwrap_or(0);
|
let shard_index: usize = arg("--shard").map(|s| s.parse()).transpose()?.unwrap_or(0);
|
||||||
// `--budget <pgas>`: re-plan the fixture's block at a TEST budget (the S_p curve of 5 October 2026); the fixture's
|
// `--budget <pgas>`: re-plan the fixture's block at a TEST budget (the S_p curve of 5 October 2026); the fixture's
|
||||||
// own per-shard plan is then not compared (the chain and the sums still are), and `--out` records the cut
|
// own per-shard plan is then not compared (the chain and the sums still are), and `--out` records the cut
|
||||||
|
|
@ -577,6 +584,8 @@ fn setup_sp1(pinned: &pinned::Pinned, results: &mut serde_json::Map<String, serd
|
||||||
let sp1 = Sp1ProofSystem::from_env(pinned.shard_elf(), pinned.agg_elf())?;
|
let sp1 = Sp1ProofSystem::from_env(pinned.shard_elf(), pinned.agg_elf())?;
|
||||||
let setup_s = t.elapsed().as_secs_f64();
|
let setup_s = t.elapsed().as_secs_f64();
|
||||||
println!("RESULT setup: {:.2} s, ProofSystem v{} shard program id {} aggregator id {} at {}", setup_s, Sp1ProofSystem::VERSION, sp1.program_id(), sp1.aggregator_id(), now());
|
println!("RESULT setup: {:.2} s, ProofSystem v{} shard program id {} aggregator id {} at {}", setup_s, Sp1ProofSystem::VERSION, sp1.program_id(), sp1.aggregator_id(), now());
|
||||||
|
println!("RESULT sp1 knobs: {}", Sp1ProofSystem::env_knobs());
|
||||||
|
results.insert("sp1_env_knobs".into(), Sp1ProofSystem::env_knobs().into());
|
||||||
if sp1.program_id() != pinned.shard_id || sp1.aggregator_id() != pinned.agg_id {
|
if sp1.program_id() != pinned.shard_id || sp1.aggregator_id() != pinned.agg_id {
|
||||||
bail!("SP1's key setup derived shard program id {} and aggregator id {} from the embedded guests, the pinned manifest says {} and {}: this host would make proofs no other node accepts (re-pin with proving/igneum-prove/pin-guests.sh)", sp1.program_id(), sp1.aggregator_id(), pinned.shard_id, pinned.agg_id);
|
bail!("SP1's key setup derived shard program id {} and aggregator id {} from the embedded guests, the pinned manifest says {} and {}: this host would make proofs no other node accepts (re-pin with proving/igneum-prove/pin-guests.sh)", sp1.program_id(), sp1.aggregator_id(), pinned.shard_id, pinned.agg_id);
|
||||||
}
|
}
|
||||||
|
|
@ -616,7 +625,7 @@ fn out_dir_of(out_path: Option<&str>) -> std::path::PathBuf {
|
||||||
/// `--mode chain`: every fixture in order, consecutive on the chain (number and parent hash), each block's shards
|
/// `--mode chain`: every fixture in order, consecutive on the chain (number and parent hash), each block's shards
|
||||||
/// proven compressed and aggregated with the previous block's aggregated proof (`AggInput.prev`, the chain rule),
|
/// proven compressed and aggregated with the previous block's aggregated proof (`AggInput.prev`, the chain rule),
|
||||||
/// every proof verified. One RESULT line per shard, per block (with the running totals) and for the chain.
|
/// every proof verified. One RESULT line per shard, per block (with the running totals) and for the chain.
|
||||||
fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_path: Option<&str>) -> Result<()> {
|
fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_path: Option<&str>, save_shards: bool, prev_path: Option<&str>) -> Result<()> {
|
||||||
if fixtures.is_empty() {
|
if fixtures.is_empty() {
|
||||||
bail!("--chain needs at least one fixture");
|
bail!("--chain needs at least one fixture");
|
||||||
}
|
}
|
||||||
|
|
@ -654,16 +663,34 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
|
||||||
println!("RESULT chain native block {}: {} shard(s), pgas {}, gas {}, pre {} post {}", f.block.env.number, shards.len(), outcome.pgas_used, outcome.gas_used, pre_root, outcome.state_root);
|
println!("RESULT chain native block {}: {} shard(s), pgas {}, gas {}, pre {} post {}", f.block.env.number, shards.len(), outcome.pgas_used, outcome.gas_used, pre_root, outcome.state_root);
|
||||||
built.push((f.block.env.number, f.block.env.hash, shards, pre_root));
|
built.push((f.block.env.number, f.block.env.hash, shards, pre_root));
|
||||||
}
|
}
|
||||||
|
// the previous segment's proof: the chain continues from it (its block must be the parent of the first fixture)
|
||||||
|
let mut prev: Option<proof_system::Sp1SegmentProof> = match prev_path {
|
||||||
|
None => None,
|
||||||
|
Some(p) => {
|
||||||
|
let bytes = std::fs::read(p).with_context(|| format!("read {p}"))?;
|
||||||
|
let proof: sp1_sdk::SP1ProofWithPublicValues = bincode::deserialize(&bytes).with_context(|| format!("{p} is not a bincode SP1 proof"))?;
|
||||||
|
let output = BlockOutput::from_bytes(proof.public_values.as_slice()).with_context(|| format!("{p}: public values are not a block statement"))?;
|
||||||
|
if output.number + 1 != first || output.block_hash != loaded[0].1.block.env.parent_hash {
|
||||||
|
bail!("--prev attests block {} ({}), the chain starts at block {first} with parent {}: the previous proof must be the parent block's", output.number, output.block_hash, loaded[0].1.block.env.parent_hash);
|
||||||
|
}
|
||||||
|
println!("RESULT chain prev: block {} chain_len {} (the chain continues from it)", output.number, output.chain_len);
|
||||||
|
Some(proof_system::Sp1SegmentProof { proof, output })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let base_len = prev.as_ref().map(|p| p.output.chain_len).unwrap_or(0);
|
||||||
let sp1 = setup_sp1(pinned, &mut results)?;
|
let sp1 = setup_sp1(pinned, &mut results)?;
|
||||||
let chain_t = Instant::now();
|
let chain_t = Instant::now();
|
||||||
let mut prev: Option<proof_system::Sp1SegmentProof> = None;
|
|
||||||
let mut blocks_json = Vec::with_capacity(built.len());
|
let mut blocks_json = Vec::with_capacity(built.len());
|
||||||
let (mut shard_total, mut agg_total, mut shards_total) = (0.0f64, 0.0f64, 0usize);
|
let (mut shard_total, mut agg_total, mut shards_total) = (0.0f64, 0.0f64, 0usize);
|
||||||
let out_dir = out_dir_of(out_path);
|
let out_dir = out_dir_of(out_path);
|
||||||
|
let mut shard_files: Vec<String> = Vec::new();
|
||||||
for (number, _hash, shards, _) in &built {
|
for (number, _hash, shards, _) in &built {
|
||||||
let block_t = Instant::now();
|
let block_t = Instant::now();
|
||||||
let mut proofs: Vec<Sp1ShardProof> = Vec::with_capacity(shards.len());
|
let mut proofs: Vec<Sp1ShardProof> = Vec::with_capacity(shards.len());
|
||||||
let mut shard_secs = Vec::new();
|
let mut shard_secs = Vec::new();
|
||||||
|
// with --save-shards: what a shard's proof record carries (the statement, the proof's sha256, the file), so
|
||||||
|
// the app's segment path signs and submits every shard of the chain from one run
|
||||||
|
let mut shard_records: Vec<serde_json::Value> = Vec::new();
|
||||||
for s in shards {
|
for s in shards {
|
||||||
let i = s.output.shard_index;
|
let i = s.output.shard_index;
|
||||||
stage(&format!("chain block {number} compressed shard {i}"));
|
stage(&format!("chain block {number} compressed shard {i}"));
|
||||||
|
|
@ -676,12 +703,24 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
|
||||||
}
|
}
|
||||||
shard_secs.push(dt);
|
shard_secs.push(dt);
|
||||||
shard_total += dt;
|
shard_total += dt;
|
||||||
|
if save_shards {
|
||||||
|
let file = out_dir.join(format!("block-{number}-shard-{i}-compressed.bin"));
|
||||||
|
let bytes = bincode::serialize(&p.proof)?;
|
||||||
|
std::fs::write(&file, &bytes).with_context(|| format!("write {}", file.display()))?;
|
||||||
|
let proof_hash: [u8; 32] = sha2::Sha256::digest(&bytes).into();
|
||||||
|
shard_records.push(serde_json::json!({
|
||||||
|
"number": number, "block_hash": s.input.env.hash.to_string(), "shard": i, "statement": alloy_primitives::keccak256(s.output.to_bytes()).to_string(),
|
||||||
|
"proof_sha256": format!("0x{}", hex::encode(proof_hash)), "proof_bytes": bytes.len(), "proof_file": file.display().to_string(), "prove_seconds": dt,
|
||||||
|
}));
|
||||||
|
shard_files.push(file.display().to_string());
|
||||||
|
}
|
||||||
proofs.push(p);
|
proofs.push(p);
|
||||||
}
|
}
|
||||||
shards_total += proofs.len();
|
shards_total += proofs.len();
|
||||||
stage(&format!("chain block {number} aggregate {} shards{}", proofs.len(), if prev.is_some() { " with the previous block proof" } else { "" }));
|
stage(&format!("chain block {number} aggregate {} shards{}", proofs.len(), if prev.is_some() { " with the previous block proof" } else { "" }));
|
||||||
let seg = sp1.aggregate(prev.as_ref(), &proofs)?;
|
let seg = sp1.aggregate(prev.as_ref(), &proofs)?;
|
||||||
let adt = sp1.last_timing("aggregate").unwrap_or_default().as_secs_f64();
|
let adt = sp1.last_timing("aggregate").unwrap_or_default().as_secs_f64();
|
||||||
|
let sdt = sp1.last_timing("aggregate-stdin").unwrap_or_default().as_secs_f64();
|
||||||
agg_total += adt;
|
agg_total += adt;
|
||||||
let claim = SegmentClaim::from_block(&seg.output);
|
let claim = SegmentClaim::from_block(&seg.output);
|
||||||
let ok = sp1.verify_segment(&seg, &claim);
|
let ok = sp1.verify_segment(&seg, &claim);
|
||||||
|
|
@ -690,9 +729,10 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
|
||||||
let block_s = block_t.elapsed().as_secs_f64();
|
let block_s = block_t.elapsed().as_secs_f64();
|
||||||
let cumulative = chain_t.elapsed().as_secs_f64();
|
let cumulative = chain_t.elapsed().as_secs_f64();
|
||||||
println!(
|
println!(
|
||||||
"RESULT chain block {number}: {} shards ({:.1} s of shard proofs), aggregate prove {adt:.1} s, proof {bytes} bytes, verify {vdt:.3} s, {}; chain_len {}, agg_vk {}; this block {block_s:.1} s, cumulative {cumulative:.1} s over {} block(s) at {}",
|
"RESULT chain block {number}: {} shards ({:.1} s of shard proofs), aggregate prove {adt:.1} s (stdin {sdt:.3} s, {} deferred proofs), proof {bytes} bytes, verify {vdt:.3} s, {}; chain_len {}, agg_vk {}; this block {block_s:.1} s, cumulative {cumulative:.1} s over {} block(s) at {}",
|
||||||
seg.output.shard_count,
|
seg.output.shard_count,
|
||||||
shard_secs.iter().sum::<f64>(),
|
shard_secs.iter().sum::<f64>(),
|
||||||
|
proofs.len() + usize::from(prev.is_some()),
|
||||||
if ok { "VERIFIED (shard program id, aggregator id and claim checked)" } else { "VERIFY FAILED" },
|
if ok { "VERIFIED (shard program id, aggregator id and claim checked)" } else { "VERIFY FAILED" },
|
||||||
seg.output.chain_len,
|
seg.output.chain_len,
|
||||||
seg.output.agg_vk,
|
seg.output.agg_vk,
|
||||||
|
|
@ -702,19 +742,21 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
|
||||||
if !ok {
|
if !ok {
|
||||||
bail!("the aggregated proof of block {number} did not verify");
|
bail!("the aggregated proof of block {number} did not verify");
|
||||||
}
|
}
|
||||||
let expected_len = blocks_json.len() as u64 + 1;
|
let expected_len = base_len + blocks_json.len() as u64 + 1;
|
||||||
if seg.output.chain_len != expected_len {
|
if seg.output.chain_len != expected_len {
|
||||||
bail!("block {number}: chain_len {} is not {expected_len}", seg.output.chain_len);
|
bail!("block {number}: chain_len {} is not {expected_len}", seg.output.chain_len);
|
||||||
}
|
}
|
||||||
blocks_json.push(serde_json::json!({
|
blocks_json.push(serde_json::json!({
|
||||||
"number": number, "shards": seg.output.shard_count, "shard_prove_seconds": shard_secs, "aggregate_prove_seconds": adt,
|
"number": number, "shards": seg.output.shard_count, "shard_prove_seconds": shard_secs, "aggregate_prove_seconds": adt, "aggregate_stdin_seconds": sdt,
|
||||||
"aggregate_verify_seconds": vdt, "proof_bytes": bytes, "chain_len": seg.output.chain_len, "block_seconds": block_s, "cumulative_seconds": cumulative,
|
"aggregate_verify_seconds": vdt, "proof_bytes": bytes, "chain_len": seg.output.chain_len, "block_seconds": block_s, "cumulative_seconds": cumulative,
|
||||||
"post_root": seg.output.post_root.to_string(), "statement": alloy_primitives::keccak256(seg.output.to_bytes()).to_string(),
|
"post_root": seg.output.post_root.to_string(), "statement": alloy_primitives::keccak256(seg.output.to_bytes()).to_string(),
|
||||||
|
"shard_records": shard_records,
|
||||||
}));
|
}));
|
||||||
prev = Some(seg);
|
prev = Some(seg);
|
||||||
}
|
}
|
||||||
let seg = prev.unwrap();
|
let seg = prev.unwrap();
|
||||||
let total = chain_t.elapsed().as_secs_f64();
|
let total = chain_t.elapsed().as_secs_f64();
|
||||||
|
results.insert("base_chain_len".into(), base_len.into());
|
||||||
let (statement, bytes) = segment_results(&seg, &out_dir, &mut results)?;
|
let (statement, bytes) = segment_results(&seg, &out_dir, &mut results)?;
|
||||||
println!(
|
println!(
|
||||||
"RESULT chain: {} blocks {first}..={last}, {shards_total} shards, shard proofs {shard_total:.1} s, aggregation {agg_total:.1} s, end to end {total:.1} s; final proof {bytes} bytes attests chain_len {} (statement {statement}), pre {} post {} provers {} at {}",
|
"RESULT chain: {} blocks {first}..={last}, {shards_total} shards, shard proofs {shard_total:.1} s, aggregation {agg_total:.1} s, end to end {total:.1} s; final proof {bytes} bytes attests chain_len {} (statement {statement}), pre {} post {} provers {} at {}",
|
||||||
|
|
@ -732,6 +774,7 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
|
||||||
results.insert("shard_prove_seconds_total".into(), shard_total.into());
|
results.insert("shard_prove_seconds_total".into(), shard_total.into());
|
||||||
results.insert("aggregate_prove_seconds_total".into(), agg_total.into());
|
results.insert("aggregate_prove_seconds_total".into(), agg_total.into());
|
||||||
results.insert("chain_seconds".into(), total.into());
|
results.insert("chain_seconds".into(), total.into());
|
||||||
|
results.insert("shard_proof_files".into(), shard_files.into_iter().map(serde_json::Value::from).collect::<Vec<_>>().into());
|
||||||
drop(sp1);
|
drop(sp1);
|
||||||
finish(results, out_path.map(|s| s.to_string()))
|
finish(results, out_path.map(|s| s.to_string()))
|
||||||
}
|
}
|
||||||
|
|
@ -794,16 +837,18 @@ fn run_aggregate(pinned: &pinned::Pinned, proofs: &str, parent: &str, prev_path:
|
||||||
for shards in &blocks {
|
for shards in &blocks {
|
||||||
let number = shards[0].output.number;
|
let number = shards[0].output.number;
|
||||||
stage(&format!("aggregate block {number}, {} shards", shards.len()));
|
stage(&format!("aggregate block {number}, {} shards", shards.len()));
|
||||||
|
let deferred = shards.len() + usize::from(prev.is_some());
|
||||||
let seg = sp1.aggregate(prev.as_ref(), shards)?;
|
let seg = sp1.aggregate(prev.as_ref(), shards)?;
|
||||||
let adt = sp1.last_timing("aggregate").unwrap_or_default().as_secs_f64();
|
let adt = sp1.last_timing("aggregate").unwrap_or_default().as_secs_f64();
|
||||||
|
let sdt = sp1.last_timing("aggregate-stdin").unwrap_or_default().as_secs_f64();
|
||||||
let claim = SegmentClaim::from_block(&seg.output);
|
let claim = SegmentClaim::from_block(&seg.output);
|
||||||
let ok = sp1.verify_segment(&seg, &claim);
|
let ok = sp1.verify_segment(&seg, &claim);
|
||||||
let vdt = sp1.last_timing("verify-block").unwrap_or_default().as_secs_f64();
|
let vdt = sp1.last_timing("verify-block").unwrap_or_default().as_secs_f64();
|
||||||
println!("RESULT aggregate block {number}: {} shards, prove {adt:.1} s, proof {} bytes, verify {vdt:.3} s, {}; chain_len {}, post {} at {}", seg.output.shard_count, bincode::serialize(&seg.proof)?.len(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, seg.output.chain_len, seg.output.post_root, now());
|
println!("RESULT aggregate block {number}: {} shards, prove {adt:.1} s (stdin {sdt:.3} s, {deferred} deferred proofs), proof {} bytes, verify {vdt:.3} s, {}; chain_len {}, post {} at {}", seg.output.shard_count, bincode::serialize(&seg.proof)?.len(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, seg.output.chain_len, seg.output.post_root, now());
|
||||||
if !ok {
|
if !ok {
|
||||||
bail!("the aggregated proof of block {number} did not verify");
|
bail!("the aggregated proof of block {number} did not verify");
|
||||||
}
|
}
|
||||||
per_block.push(serde_json::json!({ "number": number, "shards": seg.output.shard_count, "aggregate_prove_seconds": adt, "aggregate_verify_seconds": vdt, "chain_len": seg.output.chain_len }));
|
per_block.push(serde_json::json!({ "number": number, "shards": seg.output.shard_count, "aggregate_prove_seconds": adt, "aggregate_stdin_seconds": sdt, "deferred_proofs": deferred, "aggregate_verify_seconds": vdt, "chain_len": seg.output.chain_len }));
|
||||||
prev = Some(seg);
|
prev = Some(seg);
|
||||||
}
|
}
|
||||||
let seg = prev.unwrap();
|
let seg = prev.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,16 @@ impl Sp1ProofSystem {
|
||||||
self.timings.lock().unwrap().push((what.to_string(), dt));
|
self.timings.lock().unwrap().push((what.to_string(), dt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The SP1 prover knobs set in this process's environment (the GPU server inherits them; the names from
|
||||||
|
/// sp1-core-executor 6.8.1 `opts.rs` and sp1-prover 6.8.1 `worker/config.rs`), for the RESULT lines, so a
|
||||||
|
/// measurement names the settings it ran under. "none" when the defaults apply.
|
||||||
|
pub fn env_knobs() -> String {
|
||||||
|
let fixed = ["SHARD_SIZE", "ELEMENT_THRESHOLD", "HEIGHT_THRESHOLD", "FULL_SIZE_SHARDS", "MINIMAL_TRACE_CHUNK_THRESHOLD", "TRACE_CHUNK_SLOTS", "MEMORY_LIMIT", "WITHOUT_VK_VERIFICATION", "RUST_LOG"];
|
||||||
|
let mut out: Vec<String> = std::env::vars().filter(|(k, _)| k.starts_with("SP1_WORKER_") || fixed.contains(&k.as_str())).map(|(k, v)| format!("{k}={v}")).collect();
|
||||||
|
out.sort();
|
||||||
|
if out.is_empty() { "none".into() } else { out.join(" ") }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn last_timing(&self, what: &str) -> Option<Duration> {
|
pub fn last_timing(&self, what: &str) -> Option<Duration> {
|
||||||
self.timings.lock().unwrap().iter().rev().find(|(k, _)| k == what).map(|(_, d)| *d)
|
self.timings.lock().unwrap().iter().rev().find(|(k, _)| k == what).map(|(_, d)| *d)
|
||||||
}
|
}
|
||||||
|
|
@ -286,6 +296,9 @@ impl ProofSystem for Sp1ProofSystem {
|
||||||
/// The aggregator guest over the shard proofs (and the previous segment's proof when given), by recursion.
|
/// The aggregator guest over the shard proofs (and the previous segment's proof when given), by recursion.
|
||||||
fn aggregate(&self, prev: Option<&Sp1SegmentProof>, shards: &[Sp1ShardProof]) -> Result<Sp1SegmentProof> {
|
fn aggregate(&self, prev: Option<&Sp1SegmentProof>, shards: &[Sp1ShardProof]) -> Result<Sp1SegmentProof> {
|
||||||
let first = shards.first().ok_or_else(|| anyhow!("no shards"))?;
|
let first = shards.first().ok_or_else(|| anyhow!("no shards"))?;
|
||||||
|
// 5 October 2026 (aggregation cost): the stdin build (the proof clones into the request) is timed apart
|
||||||
|
// from the prove call, so the host's own share of an aggregation is visible next to the GPU's.
|
||||||
|
let t_stdin = Instant::now();
|
||||||
let mut stdin = SP1Stdin::new();
|
let mut stdin = SP1Stdin::new();
|
||||||
let input = AggInput {
|
let input = AggInput {
|
||||||
shard_vk: self.shard_vk_hash(),
|
shard_vk: self.shard_vk_hash(),
|
||||||
|
|
@ -302,6 +315,7 @@ impl ProofSystem for Sp1ProofSystem {
|
||||||
let SP1Proof::Compressed(proof) = p.proof.proof.clone() else { return Err(anyhow!("the previous block proof is not a compressed proof")) };
|
let SP1Proof::Compressed(proof) = p.proof.proof.clone() else { return Err(anyhow!("the previous block proof is not a compressed proof")) };
|
||||||
stdin.write_proof(*proof, self.agg_vk.vk.clone());
|
stdin.write_proof(*proof, self.agg_vk.vk.clone());
|
||||||
}
|
}
|
||||||
|
self.record("aggregate-stdin", t_stdin.elapsed());
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let proof = self.client.prove(&self.agg_pk, stdin).compressed().run()?;
|
let proof = self.client.prove(&self.agg_pk, stdin).compressed().run()?;
|
||||||
self.record("aggregate", t.elapsed());
|
self.record("aggregate", t.elapsed());
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,13 @@ if [ "${SKIP_GATE:-0}" != "1" ]; then
|
||||||
fi
|
fi
|
||||||
H="$ROOT/proving/igneum-prove/target/release/igneum-prove-host"
|
H="$ROOT/proving/igneum-prove/target/release/igneum-prove-host"
|
||||||
for f in "$ROOT"/proving/fixtures/block-*.json; do
|
for f in "$ROOT"/proving/fixtures/block-*.json; do
|
||||||
|
# the exporter's side files (block-N.json.node-plan.json, 5 October 2026) are not fixtures
|
||||||
|
case "$f" in *.node-plan.json) continue ;; esac
|
||||||
if ! "$H" "$f" --mode native >>"$GATE_LOG" 2>&1; then echo "GATE FAILED: native run of $(basename "$f"); see $GATE_LOG"; exit 1; fi
|
if ! "$H" "$f" --mode native >>"$GATE_LOG" 2>&1; then echo "GATE FAILED: native run of $(basename "$f"); see $GATE_LOG"; exit 1; fi
|
||||||
done
|
done
|
||||||
if ! "$ROOT/tools/lock/with-lock.sh" measure "$H" "$ROOT/proving/fixtures/block-338-shard1.json" --mode execute --shard 0 >>"$GATE_LOG" 2>&1; then
|
# the execute step reports a cycle count, not a time: the `run` lock (tools/lock/with-lock.sh: counts, not ms), so the
|
||||||
|
# gate does not queue behind every build and measurement on the Mac (5 October 2026: 25 min behind a packbench run)
|
||||||
|
if ! "$ROOT/tools/lock/with-lock.sh" run "$H" "$ROOT/proving/fixtures/block-338-shard1.json" --mode execute --shard 0 >>"$GATE_LOG" 2>&1; then
|
||||||
echo "GATE FAILED: the guest did not execute the shard fixture (the exact failure the PC hit on 4 October); see $GATE_LOG"; exit 1
|
echo "GATE FAILED: the guest did not execute the shard fixture (the exact failure the PC hit on 4 October); see $GATE_LOG"; exit 1
|
||||||
fi
|
fi
|
||||||
"$H" --mode id | tee -a "$GATE_LOG" # the pinned program ids this package carries (the PC's build embeds the same elf/ files)
|
"$H" --mode id | tee -a "$GATE_LOG" # the pinned program ids this package carries (the PC's build embeds the same elf/ files)
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@
|
||||||
// GET chain igneum.network/api/live trimmed + the Hetzner results item
|
// GET chain igneum.network/api/live trimmed + the Hetzner results item
|
||||||
// GET log?limit=&since= work-log items (kinds log, build, note), newest first
|
// GET log?limit=&since= work-log items (kinds log, build, note), newest first
|
||||||
// GET results bench entries (synced from docs/bench-log.md) + the FUD ledger counts
|
// GET results bench entries (synced from docs/bench-log.md) + the FUD ledger counts
|
||||||
|
// GET tuning?days=30&min=5 Ember Tune: the fleet priors per (card model, driver major, program class) from the
|
||||||
|
// TUNE records in miner_logs (relay/lib/ember.mjs), with the sample counts and MH/W
|
||||||
// POST post {kind,title,body,who,key?,meta?} one item; with key it upserts
|
// POST post {kind,title,body,who,key?,meta?} one item; with key it upserts
|
||||||
// POST sync {items:[...]} bulk upsert by key
|
// POST sync {items:[...]} bulk upsert by key
|
||||||
import { neon, authed, readJson, str, iso } from '../lib/relay.mjs';
|
import { neon, authed, readJson, str, iso } from '../lib/relay.mjs';
|
||||||
import { kv, kvNum, lastMatch, FAULT, parseLabel, parseMinerTail, parseHeader, parseAppTail, STALE_S, markStale } from '../lib/parse.mjs';
|
import { kv, kvNum, lastMatch, FAULT, parseLabel, parseMinerTail, parseHeader, parseAppTail, STALE_S, markStale } from '../lib/parse.mjs';
|
||||||
|
import { parseRecords, aggregate } from '../lib/ember.mjs';
|
||||||
|
|
||||||
const json = (res, status, obj) => { res.status(status).setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(JSON.stringify(obj)); };
|
const json = (res, status, obj) => { res.status(status).setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(JSON.stringify(obj)); };
|
||||||
const CACHE_MS = 10_000;
|
const CACHE_MS = 10_000;
|
||||||
|
|
@ -213,6 +216,13 @@ async function chain(sql) {
|
||||||
hetzner: het.length ? itemOut(het[0]) : null,
|
hetzner: het.length ? itemOut(het[0]) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/// Ember Tune: every TUNE record of the window, folded into priors (the same aggregation the publisher uses).
|
||||||
|
async function tuning(sql, days, min) {
|
||||||
|
const rows = await sql(`SELECT lines FROM miner_logs WHERE received_at > now() - ($1 || ' days')::interval AND lines LIKE '%TUNE {%' ORDER BY received_at DESC LIMIT 2000`, [String(days)]);
|
||||||
|
const records = rows.flatMap(r => parseRecords(r.lines));
|
||||||
|
const { priors, table } = aggregate(records, { minSamples: min });
|
||||||
|
return { days, min_samples: min, records: records.length, priors, table };
|
||||||
|
}
|
||||||
async function results(sql) {
|
async function results(sql) {
|
||||||
const [bench, ledger] = await Promise.all([
|
const [bench, ledger] = await Promise.all([
|
||||||
sql(`SELECT * FROM console_items WHERE kind = 'bench' ORDER BY (meta->>'date') DESC NULLS LAST, (meta->>'pos')::int DESC LIMIT 60`),
|
sql(`SELECT * FROM console_items WHERE kind = 'bench' ORDER BY (meta->>'date') DESC NULLS LAST, (meta->>'pos')::int DESC LIMIT 60`),
|
||||||
|
|
@ -253,6 +263,11 @@ export default async function handler(req, res) {
|
||||||
if (fn === 'builds') return json(res, 200, { ok: true, now: new Date().toISOString(), ...(await cached('builds', () => builds(sql))) });
|
if (fn === 'builds') return json(res, 200, { ok: true, now: new Date().toISOString(), ...(await cached('builds', () => builds(sql))) });
|
||||||
if (fn === 'chain') return json(res, 200, { ok: true, ...(await cached('chain', () => chain(sql))) });
|
if (fn === 'chain') return json(res, 200, { ok: true, ...(await cached('chain', () => chain(sql))) });
|
||||||
if (fn === 'results') return json(res, 200, { ok: true, ...(await cached('results', () => results(sql))) });
|
if (fn === 'results') return json(res, 200, { ok: true, ...(await cached('results', () => results(sql))) });
|
||||||
|
if (fn === 'tuning') {
|
||||||
|
const days = Math.min(365, Math.max(1, Number(q.days) || 30));
|
||||||
|
const min = Math.min(100, Math.max(1, Number(q.min) || 5));
|
||||||
|
return json(res, 200, { ok: true, now: new Date().toISOString(), ...(await cached(`tuning-${days}-${min}`, () => tuning(sql, days, min))) });
|
||||||
|
}
|
||||||
if (fn === 'log') {
|
if (fn === 'log') {
|
||||||
const limit = Math.min(300, Math.max(1, Number(q.limit) || 100));
|
const limit = Math.min(300, Math.max(1, Number(q.limit) || 100));
|
||||||
const params = []; let where = `kind IN ('log','build','note')`;
|
const params = []; let where = `kind IN ('log','build','note')`;
|
||||||
|
|
|
||||||
131
relay/lib/ember.mjs
Normal file
131
relay/lib/ember.mjs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
// Ember Tune, the fleet side (docs/plans/ember-tune.md): the TUNE records every app uploads with its log are folded
|
||||||
|
// into one prior per (card model, driver major, program class): the median chosen point, its spread and the sample
|
||||||
|
// count. The publisher writes the priors into the signed manifest's `tuning` section beside the kernel-variant
|
||||||
|
// cards (tools/tuning.mjs --write), the console shows them (api/console.mjs fn=tuning, tools/console.mjs tuning),
|
||||||
|
// and the public bench table lists them per model (site/miner-priors.json). No dependencies; the tests in
|
||||||
|
// relay/test/ember.test.mjs drive these functions with a fixture of captured records.
|
||||||
|
//
|
||||||
|
// A record (app/igneum-app/src/ember.rs record_json): {ts, machine (a hash of the install id), app, os, card, vendor,
|
||||||
|
// driver, driver_major, class, key, plan: full|confirm|baseline, steps: [{clock_mhz, power_pct, limit_w, watts, mhs,
|
||||||
|
// eff, gclk, mclk, tmax, faults, mark}], chosen: {...}, before: {...}|null, eff, mhs, watts}. Nothing identifies the
|
||||||
|
// owner: no address, no hostname, no raw machine id.
|
||||||
|
|
||||||
|
/// The TUNE records inside uploaded log text, de-duplicated on (machine, card, ts) because the log is re-sent every
|
||||||
|
/// minute. Baseline records (measure only) are kept apart: they say what a card does untuned, never what to set.
|
||||||
|
export function parseRecords(text) {
|
||||||
|
const out = [];
|
||||||
|
for (const line of String(text || '').split('\n')) {
|
||||||
|
const i = line.indexOf('TUNE {');
|
||||||
|
if (i < 0) continue;
|
||||||
|
let rec;
|
||||||
|
try { rec = JSON.parse(line.slice(i + 5)); } catch { continue; }
|
||||||
|
if (!rec || !rec.card || !rec.key || !rec.plan) continue;
|
||||||
|
out.push(rec);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dedupe(records) {
|
||||||
|
const seen = new Set();
|
||||||
|
const out = [];
|
||||||
|
for (const r of records) {
|
||||||
|
const k = `${r.machine}|${r.card}|${r.ts}`;
|
||||||
|
if (seen.has(k)) continue;
|
||||||
|
seen.add(k);
|
||||||
|
out.push(r);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const median = xs => { const s = xs.filter(x => Number.isFinite(x)).sort((a, b) => a - b); return s.length ? (s.length % 2 ? s[(s.length - 1) / 2] : (s[s.length / 2 - 1] + s[s.length / 2]) / 2) : 0; };
|
||||||
|
|
||||||
|
/// The median absolute deviation as a percent of the median (0 for one sample or a zero median).
|
||||||
|
export function spreadPct(xs) {
|
||||||
|
const m = median(xs);
|
||||||
|
if (!m || xs.length < 2) return 0;
|
||||||
|
return Number((median(xs.map(x => Math.abs(x - m))) / m * 100).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
const usable = r => r && r.chosen && r.chosen.mark === 'ok' && r.chosen.eff > 0 && (r.plan === 'full' || r.plan === 'confirm');
|
||||||
|
|
||||||
|
/// Folds records into priors: one per key, from the full and confirm records with a usable chosen point. The point
|
||||||
|
/// is the median clock cap and the median power percent (each rounded to the step the apps use: 10 MHz, 1%), the
|
||||||
|
/// efficiency, rate and draw are medians, the spread is the MAD of the efficiency in percent, `samples` counts the
|
||||||
|
/// records and `machines` the distinct install hashes. An outlier (one bad card, one hot room) moves the median by
|
||||||
|
/// at most one rank, never by its size. Baseline records are summarised beside the prior as `baseline` (median
|
||||||
|
/// MH/W untuned) so the console can show the gain.
|
||||||
|
export function aggregate(records, { minSamples = 1 } = {}) {
|
||||||
|
const byKey = new Map();
|
||||||
|
for (const r of dedupe(records)) {
|
||||||
|
const g = byKey.get(r.key) || { key: r.key, card: r.card, vendor: r.vendor || '', driver_major: r.driver_major || '', class: r.class || 'v2', tuned: [], baseline: [], machines: new Set() };
|
||||||
|
byKey.set(r.key, g);
|
||||||
|
g.machines.add(r.machine);
|
||||||
|
if (usable(r)) g.tuned.push(r);
|
||||||
|
else if (r.plan === 'baseline' && r.chosen && r.chosen.eff > 0) g.baseline.push(r);
|
||||||
|
}
|
||||||
|
const priors = {};
|
||||||
|
const table = [];
|
||||||
|
for (const g of byKey.values()) {
|
||||||
|
const t = g.tuned;
|
||||||
|
const row = {
|
||||||
|
key: g.key, card: g.card, vendor: g.vendor, driver_major: g.driver_major, class: g.class,
|
||||||
|
samples: t.length, machines: g.machines.size,
|
||||||
|
baseline_samples: g.baseline.length,
|
||||||
|
baseline_eff: g.baseline.length ? Number(median(g.baseline.map(r => r.chosen.eff)).toFixed(4)) : null,
|
||||||
|
baseline_mhs: g.baseline.length ? Number(median(g.baseline.map(r => r.chosen.mhs)).toFixed(2)) : null,
|
||||||
|
baseline_watts: g.baseline.length ? Number(median(g.baseline.map(r => r.chosen.watts)).toFixed(1)) : null,
|
||||||
|
};
|
||||||
|
if (t.length) {
|
||||||
|
const effs = t.map(r => r.chosen.eff);
|
||||||
|
const prior = {
|
||||||
|
clock_mhz: Math.round(median(t.map(r => r.chosen.clock_mhz)) / 10) * 10,
|
||||||
|
power_pct: Math.round(median(t.map(r => r.chosen.power_pct))),
|
||||||
|
eff: Number(median(effs).toFixed(4)),
|
||||||
|
mhs: Number(median(t.map(r => r.chosen.mhs)).toFixed(2)),
|
||||||
|
watts: Number(median(t.map(r => r.chosen.watts)).toFixed(1)),
|
||||||
|
spread_pct: spreadPct(effs),
|
||||||
|
samples: t.length,
|
||||||
|
machines: g.machines.size,
|
||||||
|
card: g.card,
|
||||||
|
vendor: g.vendor,
|
||||||
|
driver_major: g.driver_major,
|
||||||
|
class: g.class,
|
||||||
|
updated: new Date(Math.max(...t.map(r => Number(r.ts) || 0)) * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
||||||
|
};
|
||||||
|
// the untuned reference: the full plan's first step (the power ladder's 100%), else the baseline records
|
||||||
|
const befores = t.map(r => r.before && r.before.eff > 0 ? r.before.eff : null).filter(x => x !== null);
|
||||||
|
if (befores.length) prior.before_eff = Number(median(befores).toFixed(4));
|
||||||
|
else if (row.baseline_eff) prior.before_eff = row.baseline_eff;
|
||||||
|
if (prior.before_eff) prior.gain_pct = Number(((prior.eff / prior.before_eff - 1) * 100).toFixed(1));
|
||||||
|
Object.assign(row, prior);
|
||||||
|
if (t.length >= minSamples) priors[g.key] = prior;
|
||||||
|
}
|
||||||
|
table.push(row);
|
||||||
|
}
|
||||||
|
table.sort((a, b) => (b.samples - a.samples) || (a.key < b.key ? -1 : 1));
|
||||||
|
return { priors, table };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The manifest's tuning section with the priors folded in: the kernel-variant `cards` object is kept as is,
|
||||||
|
/// `priors` replaces the previous priors (a key that lost its samples drops out), `ember` carries the settings.
|
||||||
|
export function mergeTuning(existing, priors, ember = {}) {
|
||||||
|
const base = existing && typeof existing === 'object' ? existing : {};
|
||||||
|
const cards = base.cards && typeof base.cards === 'object' && !Array.isArray(base.cards) ? base.cards : {};
|
||||||
|
const settings = { enabled: true, min_samples: 5, rate_tolerance_pct: 1, ...(base.ember && typeof base.ember === 'object' ? base.ember : {}), ...ember };
|
||||||
|
return { ...base, updated: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'), cards, ember: settings, priors: priors || {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A prior as a card starts from it (app/igneum-app/src/ember.rs prior_of): None under the sample floor.
|
||||||
|
export function priorFor(tuning, key, minSamples) {
|
||||||
|
const p = tuning && tuning.priors && tuning.priors[key];
|
||||||
|
const floor = Number.isFinite(minSamples) ? minSamples : (tuning && tuning.ember && tuning.ember.min_samples) || 5;
|
||||||
|
if (!p || !(p.samples >= floor)) return null;
|
||||||
|
return { clock_mhz: p.clock_mhz || 0, power_pct: Math.min(100, Math.max(50, p.power_pct || 100)), eff: p.eff, samples: p.samples };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One text line per prior for the console and the CLI.
|
||||||
|
export function priorLine(p) {
|
||||||
|
const point = p.clock_mhz ? `${p.clock_mhz} MHz at ${p.power_pct}%` : `${p.power_pct}% (clock unlocked)`;
|
||||||
|
const gain = p.gain_pct != null ? ` (${p.gain_pct >= 0 ? '+' : ''}${p.gain_pct}% over untuned ${p.before_eff} MH/W)` : '';
|
||||||
|
return `${p.card.replace(/_/g, ' ')} | driver ${p.driver_major} | ${p.class}: ${point}, ${p.eff} MH/W${gain}, ${p.mhs} MH/s at ${p.watts} W, spread ${p.spread_pct}%, ${p.samples} sample(s) from ${p.machines} machine(s)`;
|
||||||
|
}
|
||||||
204
relay/playbooks/ember-tune-pc1.ps1
Normal file
204
relay/playbooks/ember-tune-pc1.ps1
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
# Igneum run job: Ember Tune end to end on PC 1 (machine ae432dc7), unattended. 5 October 2026.
|
||||||
|
# Published as a `run` job with --stop-miners (docs/plans/ember-tune.md): the installed app stops its miners and
|
||||||
|
# holds them; this script takes the engine that carries src/ember.rs (the one the fetch job put in
|
||||||
|
# <app dir>\jobs\ember-kit-1\igneum-app-ember.exe, else the installed one; the AMD helper with the --tune and --set
|
||||||
|
# commands from the telemetry agent's fetch job, <app dir>\jobs\amd-kit-1\kit\igneum-gpu-telemetry.exe), copies the install folder to a scratch
|
||||||
|
# folder beside it, swaps the engine in, and starts that SECOND engine with `--sweep` in a scratch data folder (the
|
||||||
|
# real settings.json, machine-id and wallet.json copied in; remote jobs, auto-update and proving switched off
|
||||||
|
# there). That engine finds the installed app's node on 127.0.0.1:26610, mines on every card with the live program,
|
||||||
|
# tunes them one after the other (the full two-knob plan where the card can be controlled, the baseline measurement
|
||||||
|
# where it cannot: NVIDIA without administrator rights, Apple), prints every TUNE line on stdout, uploads its log
|
||||||
|
# (the TUNE {json} record reaches the intake) and quits. Every TUNE line is re-emitted as a RESULT line, so
|
||||||
|
# `node tools/jobs.mjs <job id>` shows the table. Before and after, nvidia-smi's limits and clocks and the AMD
|
||||||
|
# helper's `--tune` lines are printed, so the restore can be read. The installed app's miners restart when the job
|
||||||
|
# ends. Not elevated: nothing asks for administrator rights (Josh asleep, 5 October 2026); the NVIDIA card is
|
||||||
|
# therefore measure only tonight unless the engine finds itself elevated.
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$resultTag = 'TUNE'
|
||||||
|
$budgetMinutes = 45
|
||||||
|
if (-not ($budgetMinutes -is [int]) -or $budgetMinutes -lt 5) { $budgetMinutes = 35 } # a budget under 5 minutes is a bug, not a budget (C35)
|
||||||
|
$started = Get-Date
|
||||||
|
$deadline = $started.AddMinutes($budgetMinutes)
|
||||||
|
function Say([string] $m) { Write-Host ("[" + (Get-Date -Format 'HH:mm:ss') + "] " + $m) }
|
||||||
|
|
||||||
|
# the installed engine: the per-user install (0.3.3+), else Program Files
|
||||||
|
$installDir = $null
|
||||||
|
foreach ($d in @((Join-Path $env:LOCALAPPDATA 'Programs\Igneum Miner'), (Join-Path $env:ProgramFiles 'Igneum Miner'))) {
|
||||||
|
if (Test-Path (Join-Path $d 'igneum-app.exe')) { $installDir = $d; break }
|
||||||
|
}
|
||||||
|
if (-not $installDir) { Write-Output 'RESULT TUNE error=no_engine reason=igneum-app.exe_not_found'; exit 2 }
|
||||||
|
$appData = $env:IGNEUM_APP_DATA
|
||||||
|
if (-not $appData) { $appData = Join-Path $env:LOCALAPPDATA 'igneum' }
|
||||||
|
$appDir = $env:IGNEUM_APP_DIR
|
||||||
|
if (-not $appDir) { $appDir = Join-Path $appData 'app' }
|
||||||
|
|
||||||
|
# the scratch install: the whole folder (workers, node, helper, DLLs) with the Ember engine swapped in
|
||||||
|
# a FRESH scratch root per run (run 3, 6 October 2026, 11:45Z: the folder an earlier elevated engine had locked to itself
|
||||||
|
# refused the settings copy, the engine started on stale files and exited in 6 s); old roots are small and left alone
|
||||||
|
$root = Join-Path $env:LOCALAPPDATA ('igneum-tune-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||||
|
$bin = Join-Path $root 'bin'
|
||||||
|
$sApp = Join-Path $root 'app'
|
||||||
|
$sLogs = Join-Path $root 'logs'
|
||||||
|
New-Item -ItemType Directory -Force -Path $root, $sApp, $sLogs | Out-Null
|
||||||
|
if (Test-Path $bin) { Remove-Item -LiteralPath $bin -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
Copy-Item -LiteralPath $installDir -Destination $bin -Recurse -Force
|
||||||
|
# the installed engine carries Ember Tune from 0.3.12 on: prefer it; the kit is for a PC still on an older app
|
||||||
|
$installedVer = (& (Join-Path $installDir 'igneum-app.exe') --version 2>&1 | Out-String).Trim()
|
||||||
|
$installedHasEmber = $false
|
||||||
|
if ($installedVer -match 'igneum-app (\d+)\.(\d+)\.(\d+)') { $installedHasEmber = ([int]$Matches[1] -gt 0) -or ([int]$Matches[2] -gt 3) -or (([int]$Matches[2] -eq 3) -and ([int]$Matches[3] -ge 12)) }
|
||||||
|
$ember = $null
|
||||||
|
# ember-kit-3 (the engine with Settings::for_measurement) is preferred when present, whatever the installed version;
|
||||||
|
# older kits only when the installed app predates Ember Tune
|
||||||
|
$k3 = Join-Path $appDir 'jobs\ember-kit-5\igneum-app-ember.exe'
|
||||||
|
if (Test-Path $k3) { $ember = $k3 }
|
||||||
|
elseif (-not $installedHasEmber) { foreach ($cand in @((Join-Path $appDir 'jobs\ember-kit-2\igneum-app-ember.exe'), (Join-Path $appDir 'jobs\ember-kit-1\igneum-app-ember.exe'))) { if (Test-Path $cand) { $ember = $cand; break } } }
|
||||||
|
if ($ember) {
|
||||||
|
Copy-Item -LiteralPath $ember -Destination (Join-Path $bin 'igneum-app.exe') -Force
|
||||||
|
Say ("engine: the Ember build from " + $ember)
|
||||||
|
} else {
|
||||||
|
Say ('engine: the installed one (' + $installedVer + $(if ($installedHasEmber) { ', carries Ember Tune' } else { '; no kit found: an older engine ignores the tune and reports no_rows' }) + ')')
|
||||||
|
}
|
||||||
|
$helper = $null
|
||||||
|
$found = Get-ChildItem -Path (Join-Path $appDir 'jobs') -Recurse -Filter 'igneum-gpu-telemetry.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match 'amd-kit' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||||
|
if ($found) { $helper = $found.FullName }
|
||||||
|
if ($helper) {
|
||||||
|
Copy-Item -LiteralPath $helper -Destination (Join-Path $bin 'igneum-gpu-telemetry.exe') -Force
|
||||||
|
Say ("helper: the Ember build of igneum-gpu-telemetry from " + $helper + " sha256=" + (Get-FileHash -LiteralPath $helper -Algorithm SHA256).Hash.ToLower())
|
||||||
|
} else { Say 'helper: the installed igneum-gpu-telemetry (no jobs\amd-kit-1\kit\igneum-gpu-telemetry.exe); without --tune the AMD card measures only' }
|
||||||
|
$exe = Join-Path $bin 'igneum-app.exe'
|
||||||
|
$ver = (& $exe --version 2>&1 | Out-String).Trim()
|
||||||
|
Say ("engine: " + $exe + " (" + $ver + ")")
|
||||||
|
Write-Output ("RESULT TUNE engine " + $ver + " sha256=" + (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash.ToLower())
|
||||||
|
if ($ver -notmatch 'igneum-app (\d+)\.(\d+)\.(\d+)') { Write-Output 'RESULT TUNE error=version_unknown'; exit 2 }
|
||||||
|
|
||||||
|
foreach ($f in @('settings.json', 'machine-id', 'wallet.json', 'tuning.json', 'firewall-rule.json')) { # the firewall flag too: an older kit then asks nothing
|
||||||
|
$src = Join-Path $appDir $f
|
||||||
|
if (Test-Path $src) { Copy-Item -LiteralPath $src -Destination (Join-Path $sApp $f) -Force }
|
||||||
|
}
|
||||||
|
# the copies are VERBATIM (run 4, 6 October 2026: a PowerShell ConvertFrom-Json | ConvertTo-Json round trip rewrote big
|
||||||
|
# integers as doubles, the engine read the file as defaults, no payout address, every card off, 96 old jobs run in
|
||||||
|
# the scratch root); the engine itself switches remote jobs, updates, proving and Power control off under --sweep
|
||||||
|
# (Settings::for_measurement) and makes every card due. Only a report line is read here.
|
||||||
|
$sj = Join-Path $sApp 'settings.json'
|
||||||
|
if (-not (Test-Path -LiteralPath $sj)) { Write-Output 'RESULT TUNE error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 }
|
||||||
|
$installedPowerControl = 'unknown'; $addr = ''; $ncards = 0
|
||||||
|
try { $back = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json; $addr = [string]$back.address; if ($back.cards) { $ncards = @($back.cards.PSObject.Properties).Count }; if ($back.PSObject.Properties.Name -contains 'power_control') { $installedPowerControl = ([bool]$back.power_control).ToString().ToLower() } } catch { }
|
||||||
|
$bom = (Get-Content -LiteralPath $sj -Encoding Byte -TotalCount 3 -ErrorAction SilentlyContinue) -join ','
|
||||||
|
Write-Output ('RESULT TUNE installed_power_control=' + $installedPowerControl + ' (the tune engine runs with it off: no prompt unless the job itself is elevated)')
|
||||||
|
Write-Output ('RESULT TUNE scratch settings (verbatim copy): address ' + $(if ($addr) { $addr.Substring(0, [Math]::Min(10, $addr.Length)) + '...' } else { 'EMPTY' }) + ', cards ' + $ncards + ', first bytes ' + $bom + ', ' + (Get-Item -LiteralPath $sj).Length + ' bytes')
|
||||||
|
if (-not $addr -and -not (Test-Path (Join-Path $sApp 'wallet.json'))) { Write-Output 'RESULT TUNE error=no_address reason=the_copied_settings_carry_no_payout_address_and_no_wallet.json'; exit 2 }
|
||||||
|
Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# the state before, for the report
|
||||||
|
$smi = Join-Path $env:ProgramFiles 'NVIDIA Corporation\NVSMI\nvidia-smi.exe'
|
||||||
|
if (-not (Test-Path $smi)) { $smi = Join-Path $env:SystemRoot 'System32\nvidia-smi.exe' }
|
||||||
|
$tele = Join-Path $bin 'igneum-gpu-telemetry.exe'
|
||||||
|
function Snapshot([string] $tag) {
|
||||||
|
if (Test-Path $smi) {
|
||||||
|
$q = (& $smi --query-gpu=index,name,driver_version,power.draw,power.limit,power.default_limit,power.min_limit,power.max_limit,clocks.gr,clocks.max.gr,clocks.mem --format=csv,noheader 2>&1 | Out-String).Trim()
|
||||||
|
Write-Output ("RESULT TUNE " + $tag + " nvidia " + ($q -replace "`r?`n", ' | '))
|
||||||
|
}
|
||||||
|
if (Test-Path $tele) {
|
||||||
|
$t = (& $tele --tune 2>&1 | Out-String).Trim()
|
||||||
|
Write-Output ("RESULT TUNE " + $tag + " amd " + ($t -replace "`r?`n", ' | '))
|
||||||
|
} else { Write-Output ("RESULT TUNE " + $tag + " amd no_helper") }
|
||||||
|
}
|
||||||
|
Snapshot 'before'
|
||||||
|
|
||||||
|
# the tune engine: status every 10 s (6 rate samples per 60 s hold)
|
||||||
|
$env:IGNEUM_APP_DATA = $root
|
||||||
|
$env:IGNEUM_APP_LOGS = $sLogs
|
||||||
|
$env:IGNEUM_APP_STATUS_SECS = '10'
|
||||||
|
$env:IGNEUM_APP_NO_OTA = '1' # C35: a second engine never runs the updater (the installer would quit the installed app)
|
||||||
|
# C35 (5 October 2026): the engine's output goes to a FILE, never a pipe. A pipe's write end is inherited by every
|
||||||
|
# process the engine starts (its miners and workers), so after an abort the installed app's jobs runner waits for an
|
||||||
|
# EOF that never comes and hangs in its own quit; and the engine's whole tree is killed at the end (nothing orphaned).
|
||||||
|
$outFile = Join-Path $root 'engine-stdout.log'
|
||||||
|
$errFile = Join-Path $root 'engine-stderr.log'
|
||||||
|
Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue
|
||||||
|
$p = Start-Process -FilePath $exe -ArgumentList '--sweep' -WorkingDirectory (Split-Path $exe) -WindowStyle Hidden -PassThru -RedirectStandardOutput $outFile -RedirectStandardError $errFile
|
||||||
|
Say ("engine started, pid " + $p.Id + ", data " + $root + ", stdout " + $outFile)
|
||||||
|
function EndTree([int] $procId, [string] $why) {
|
||||||
|
$before = @(Get-Process -Name 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-worker-metal' -ErrorAction SilentlyContinue).Count
|
||||||
|
& taskkill /T /F /PID $procId 2>&1 | Out-Null
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
$after = @(Get-Process -Name 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-worker-metal' -ErrorAction SilentlyContinue).Count
|
||||||
|
Write-Output ('RESULT ' + $resultTag + ' tree ended (' + $why + '): igneum processes ' + $before + ' -> ' + $after + ' (the installed app''s own miners are stopped and held by the job)')
|
||||||
|
}
|
||||||
|
$seen = 0
|
||||||
|
$rows = 0
|
||||||
|
# the watchdog (coordinator, 6 October 2026): a tune engine that mines nothing for 120 s after its first status line
|
||||||
|
# (every card "waiting" or 0.00 MH/s: no payout address, no worker, no node) fails the job at once with the engine's
|
||||||
|
# last log line in the RESULT, its tree ended, mining restored by the job runner; a job that cannot mine never burns
|
||||||
|
# its budget silently again
|
||||||
|
$firstStatusAt = $null
|
||||||
|
$lastMining = $null
|
||||||
|
$lastEngineLine = ''
|
||||||
|
function EngineLogDump([string] $why) {
|
||||||
|
Write-Output ('===== engine log tail (' + $why + ')')
|
||||||
|
$t = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||||
|
if ($t) { Get-Content -LiteralPath $t.FullName -ErrorAction SilentlyContinue | Where-Object { $_ -notmatch 'status: accepted 0 blocks' } | Select-Object -Last 80 | ForEach-Object { Write-Output (' ' + $_) } } else { Write-Output ' (no app-*.log in the scratch logs folder)' }
|
||||||
|
if (Test-Path -LiteralPath $errFile) { Write-Output '===== engine stderr'; Get-Content -LiteralPath $errFile -ErrorAction SilentlyContinue | Select-Object -Last 20 | ForEach-Object { Write-Output (' ' + $_) } }
|
||||||
|
}
|
||||||
|
function EngineTail() { $t = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($t) { $l = Get-Content -LiteralPath $t.FullName -Tail 1 -ErrorAction SilentlyContinue; if ($l) { return [string]$l } }; return '' }
|
||||||
|
while (-not $p.HasExited) {
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
$tailLine = EngineTail
|
||||||
|
if ($tailLine) { $lastEngineLine = $tailLine }
|
||||||
|
if ($lastEngineLine -match ' status: ') {
|
||||||
|
if (-not $firstStatusAt) { $firstStatusAt = Get-Date }
|
||||||
|
if ($lastEngineLine -match ', mining \|' -or ($lastEngineLine -match '(\d+\.\d+) MH/s' -and [double]$Matches[1] -gt 0)) { $lastMining = Get-Date }
|
||||||
|
}
|
||||||
|
if ($firstStatusAt -and -not $lastMining -and ((Get-Date) - $firstStatusAt).TotalSeconds -gt 120) {
|
||||||
|
Write-Output ('RESULT TUNE error=not_mining reason=no_card_mined_within_120_s_of_the_first_status_line last_log_line=' + ($lastEngineLine -replace '\s+', '_'))
|
||||||
|
EngineLogDump 'watchdog: not mining'
|
||||||
|
EndTree $p.Id 'watchdog: not mining'
|
||||||
|
Write-Output 'RESULT TUNE error=no_rows'
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
if ($lastMining -and ((Get-Date) - $lastMining).TotalSeconds -gt 300) {
|
||||||
|
Write-Output ('RESULT TUNE error=stopped_mining reason=every_card_idle_for_300_s last_log_line=' + ($lastEngineLine -replace '\s+', '_'))
|
||||||
|
EngineLogDump 'watchdog: stopped mining'
|
||||||
|
EndTree $p.Id 'watchdog: stopped mining'
|
||||||
|
Write-Output 'RESULT TUNE error=no_rows'
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||||
|
while ($seen -lt $all.Count) {
|
||||||
|
$l = [string]$all[$seen]; $seen++
|
||||||
|
if ($l -match '^TUNE ') { Write-Output ('RESULT ' + $l); if ($l -match '^TUNE card=') { $rows++ } }
|
||||||
|
elseif ($l -match '^SWEEP ') { Write-Output ('RESULT ' + $l) }
|
||||||
|
elseif ($l -match '^(URL|STATE) ') { }
|
||||||
|
else { Say $l }
|
||||||
|
}
|
||||||
|
if ((Get-Date) -gt $deadline) {
|
||||||
|
Say ("budget of " + $budgetMinutes + " min spent; asking the tune engine to quit")
|
||||||
|
# C35 (5 October 2026): the only quit this script may send goes to the TUNE engine's own URL file in the scratch
|
||||||
|
# root, never to a file under the installed app's folder; the RESULT line names the file it used
|
||||||
|
$u = Join-Path $sApp 'app.url'
|
||||||
|
# the only URL file this script may quit is its own scratch root's; the installed app's folder is refused by name
|
||||||
|
if ((Resolve-Path -LiteralPath $u -ErrorAction SilentlyContinue).Path -like (Join-Path $appDir '*') -or $u -like '*\igneum\app\*') {
|
||||||
|
Write-Output ('RESULT TUNE quit refused: ' + $u + ' is the installed app''s URL file')
|
||||||
|
} elseif (Test-Path -LiteralPath $u) {
|
||||||
|
Write-Output ('RESULT TUNE quit asked of the tune engine through ' + $u + ' (pid ' + $p.Id + ')')
|
||||||
|
try { Invoke-WebRequest -Uri ((Get-Content -LiteralPath $u -Raw).Trim() + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null } catch { }
|
||||||
|
} else { Write-Output ('RESULT TUNE quit not sent: no URL file at ' + $u + '; killing pid ' + $p.Id) }
|
||||||
|
Start-Sleep -Seconds 20
|
||||||
|
if (-not $p.HasExited) { EndTree $p.Id 'budget' }
|
||||||
|
Write-Output 'RESULT TUNE error=budget_exceeded'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||||
|
while ($seen -lt $all.Count) { $l = [string]$all[$seen]; $seen++; if ($l -match '^TUNE ') { Write-Output ('RESULT ' + $l); if ($l -match '^TUNE card=') { $rows++ } } }
|
||||||
|
Say ("tune engine exited " + $p.ExitCode + " after " + [int]((Get-Date) - $started).TotalSeconds + " s, " + $rows + " table rows")
|
||||||
|
EndTree $p.Id 'end of run'
|
||||||
|
# Josh, 6 October 2026, 07:25Z: both cards stay on their best MH/W points (the tune pins them); no factory reset here.
|
||||||
|
Snapshot 'after'
|
||||||
|
# the tune engine's own log: the TUNE lines and what happened around them
|
||||||
|
$log = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||||
|
if ($log) {
|
||||||
|
Say ("engine log " + $log.FullName + ":")
|
||||||
|
Get-Content -LiteralPath $log.FullName | Where-Object { $_ -match 'TUNE|tune|power cap|GPUs:|worker ready|STATUS|exited|upload' } | Select-Object -Last 100 | ForEach-Object { Say (' ' + $_) }
|
||||||
|
}
|
||||||
|
if ($rows -eq 0) { Write-Output 'RESULT TUNE error=no_rows'; exit 1 }
|
||||||
|
exit 0
|
||||||
|
|
@ -52,13 +52,8 @@ function Stop-App {
|
||||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stop 2>&1 | ForEach-Object { Say (" " + (Strip "$_")) }
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stop 2>&1 | ForEach-Object { Say (" " + (Strip "$_")) }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url'
|
# (5 October 2026 rule: a job never quits the installed app it did not start; the api/quit that stood here is gone.
|
||||||
if (Test-Path $urlFile) {
|
# Stopping the app is the signed `restart` job kind's work; without the stop script this waits for the app to stop.)
|
||||||
$url = (Get-Content $urlFile -Raw).Trim()
|
|
||||||
if ($url) {
|
|
||||||
try { Invoke-WebRequest -Uri ($url + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null; Say 'asked the engine to quit over its local API' } catch { Say ('local API did not answer: ' + $_.Exception.Message) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$until = (Get-Date).AddSeconds(50)
|
$until = (Get-Date).AddSeconds(50)
|
||||||
while ((Get-Date) -lt $until) {
|
while ((Get-Date) -lt $until) {
|
||||||
if (@(Get-Process -Name 'igneum-app', 'igneumd' -ErrorAction SilentlyContinue).Count -eq 0) { break }
|
if (@(Get-Process -Name 'igneum-app', 'igneumd' -ErrorAction SilentlyContinue).Count -eq 0) { break }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
# miners restart when the job ends. Elevated, so nvidia-smi -pl needs no prompt (the engine detects that: mode=direct).
|
# miners restart when the job ends. Elevated, so nvidia-smi -pl needs no prompt (the engine detects that: mode=direct).
|
||||||
# UNTESTED on a PC as of 4 Oct 2026 (parse-checked only).
|
# UNTESTED on a PC as of 4 Oct 2026 (parse-checked only).
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$resultTag = 'SWEEP'
|
||||||
$budgetMinutes = 40
|
$budgetMinutes = 40
|
||||||
$started = Get-Date
|
$started = Get-Date
|
||||||
$deadline = $started.AddMinutes($budgetMinutes)
|
$deadline = $started.AddMinutes($budgetMinutes)
|
||||||
|
|
@ -36,15 +37,8 @@ foreach ($f in @('settings.json', 'machine-id', 'wallet.json')) {
|
||||||
$src = Join-Path $appDir $f
|
$src = Join-Path $appDir $f
|
||||||
if (Test-Path $src) { Copy-Item -LiteralPath $src -Destination (Join-Path $sApp $f) -Force }
|
if (Test-Path $src) { Copy-Item -LiteralPath $src -Destination (Join-Path $sApp $f) -Force }
|
||||||
}
|
}
|
||||||
# the second engine must not poll jobs (it would see this one), update itself, or prove
|
# the copies are verbatim: the engine switches jobs, updates and proving off itself under --sweep (Settings::for_measurement, 0.3.13)
|
||||||
$sj = Join-Path $sApp 'settings.json'
|
if (-not (Test-Path (Join-Path $sApp 'settings.json'))) { Write-Output 'RESULT SWEEP error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 }
|
||||||
if (Test-Path $sj) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json
|
|
||||||
$j.remote_jobs = $false; $j.auto_update = $false; $j.prove = $false; $j.paused = $false; $j.setup_done = $true
|
|
||||||
$j | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $sj -Encoding utf8
|
|
||||||
} catch { Say ("settings.json: " + $_.Exception.Message) }
|
|
||||||
} else { Write-Output 'RESULT SWEEP error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 }
|
|
||||||
Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# the cap state before, for the report
|
# the cap state before, for the report
|
||||||
|
|
@ -59,29 +53,29 @@ if (Test-Path $smi) {
|
||||||
$env:IGNEUM_APP_DATA = $root
|
$env:IGNEUM_APP_DATA = $root
|
||||||
$env:IGNEUM_APP_LOGS = $sLogs
|
$env:IGNEUM_APP_LOGS = $sLogs
|
||||||
$env:IGNEUM_APP_STATUS_SECS = '10'
|
$env:IGNEUM_APP_STATUS_SECS = '10'
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
$env:IGNEUM_APP_NO_OTA = '1' # C35: a second engine never runs the updater (the installer would quit the installed app)
|
||||||
$psi.FileName = $exe
|
# C35 (5 October 2026): the engine's output goes to a FILE, never a pipe. A pipe's write end is inherited by every
|
||||||
$psi.Arguments = '--sweep'
|
# process the engine starts (its miners and workers), so after an abort the installed app's jobs runner waits for an
|
||||||
$psi.WorkingDirectory = Split-Path $exe
|
# EOF that never comes and hangs in its own quit; and the engine's whole tree is killed at the end (nothing orphaned).
|
||||||
$psi.UseShellExecute = $false
|
$outFile = Join-Path $root 'engine-stdout.log'
|
||||||
$psi.RedirectStandardOutput = $true
|
$errFile = Join-Path $root 'engine-stderr.log'
|
||||||
$psi.RedirectStandardError = $true
|
Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue
|
||||||
$psi.CreateNoWindow = $true
|
$p = Start-Process -FilePath $exe -ArgumentList '--sweep' -WorkingDirectory (Split-Path $exe) -WindowStyle Hidden -PassThru -RedirectStandardOutput $outFile -RedirectStandardError $errFile
|
||||||
$p = New-Object System.Diagnostics.Process
|
Say ("engine started, pid " + $p.Id + ", data " + $root + ", stdout " + $outFile)
|
||||||
$p.StartInfo = $psi
|
function EndTree([int] $procId, [string] $why) {
|
||||||
$lines = New-Object System.Collections.ArrayList
|
$before = @(Get-Process -Name 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-worker-metal' -ErrorAction SilentlyContinue).Count
|
||||||
$h = { if ($EventArgs.Data) { [void]$Event.MessageData.Add($EventArgs.Data) } }
|
& taskkill /T /F /PID $procId 2>&1 | Out-Null
|
||||||
Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $h -MessageData $lines | Out-Null
|
Start-Sleep -Seconds 2
|
||||||
Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $h -MessageData $lines | Out-Null
|
$after = @(Get-Process -Name 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-worker-metal' -ErrorAction SilentlyContinue).Count
|
||||||
[void]$p.Start()
|
Write-Output ('RESULT ' + $resultTag + ' tree ended (' + $why + '): igneum processes ' + $before + ' -> ' + $after + ' (the installed app''s own miners are stopped and held by the job)')
|
||||||
$p.BeginOutputReadLine(); $p.BeginErrorReadLine()
|
}
|
||||||
Say ("sweep engine started, pid " + $p.Id + ", data " + $root)
|
|
||||||
$seen = 0
|
$seen = 0
|
||||||
$rows = 0
|
$rows = 0
|
||||||
while (-not $p.HasExited) {
|
while (-not $p.HasExited) {
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
while ($seen -lt $lines.Count) {
|
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||||
$l = [string]$lines[$seen]; $seen++
|
while ($seen -lt $all.Count) {
|
||||||
|
$l = [string]$all[$seen]; $seen++
|
||||||
if ($l -match '^SWEEP ') { Write-Output ('RESULT ' + $l); if ($l -match '^SWEEP card=') { $rows++ } }
|
if ($l -match '^SWEEP ') { Write-Output ('RESULT ' + $l); if ($l -match '^SWEEP card=') { $rows++ } }
|
||||||
elseif ($l -match '^(URL|STATE) ') { }
|
elseif ($l -match '^(URL|STATE) ') { }
|
||||||
else { Say $l }
|
else { Say $l }
|
||||||
|
|
@ -91,12 +85,14 @@ while (-not $p.HasExited) {
|
||||||
$u = Join-Path $sApp 'app.url'
|
$u = Join-Path $sApp 'app.url'
|
||||||
if (Test-Path $u) { try { Invoke-WebRequest -Uri ((Get-Content -LiteralPath $u -Raw).Trim() + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null } catch { } }
|
if (Test-Path $u) { try { Invoke-WebRequest -Uri ((Get-Content -LiteralPath $u -Raw).Trim() + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null } catch { } }
|
||||||
Start-Sleep -Seconds 20
|
Start-Sleep -Seconds 20
|
||||||
if (-not $p.HasExited) { $p.Kill() }
|
if (-not $p.HasExited) { EndTree $p.Id 'budget' }
|
||||||
Write-Output 'RESULT SWEEP error=budget_exceeded'
|
Write-Output 'RESULT SWEEP error=budget_exceeded'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while ($seen -lt $lines.Count) { $l = [string]$lines[$seen]; $seen++; if ($l -match '^SWEEP ') { Write-Output ('RESULT ' + $l); if ($l -match '^SWEEP card=') { $rows++ } } }
|
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||||
|
while ($seen -lt $all.Count) { $l = [string]$all[$seen]; $seen++; if ($l -match '^SWEEP ') { Write-Output ('RESULT ' + $l); if ($l -match '^SWEEP card=') { $rows++ } } }
|
||||||
Say ("sweep engine exited " + $p.ExitCode + " after " + [int]((Get-Date) - $started).TotalSeconds + " s, " + $rows + " table rows")
|
Say ("sweep engine exited " + $p.ExitCode + " after " + [int]((Get-Date) - $started).TotalSeconds + " s, " + $rows + " table rows")
|
||||||
|
EndTree $p.Id 'end of run'
|
||||||
if (Test-Path $smi) {
|
if (Test-Path $smi) {
|
||||||
$q = (& $smi --query-gpu=index,power.draw,power.limit --format=csv,noheader 2>&1 | Out-String).Trim()
|
$q = (& $smi --query-gpu=index,power.draw,power.limit --format=csv,noheader 2>&1 | Out-String).Trim()
|
||||||
Write-Output ("RESULT SWEEP after " + ($q -replace "`r?`n", ' | '))
|
Write-Output ("RESULT SWEEP after " + ($q -replace "`r?`n", ' | '))
|
||||||
|
|
|
||||||
110
relay/test/ember.test.mjs
Normal file
110
relay/test/ember.test.mjs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// node --test relay/test/ember.test.mjs (no dependencies; CI runs it in the site job)
|
||||||
|
// The fleet aggregation of Ember Tune records (relay/lib/ember.mjs) on a fixture of records in the shape
|
||||||
|
// app/igneum-app/src/ember.rs record_json writes: known-good (five samples converge on one point), known-bad (an
|
||||||
|
// outlier does not move the median), the de-duplication of re-sent logs, the manifest merge and the prior lookup.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { parseRecords, dedupe, aggregate, mergeTuning, priorFor, priorLine, median, spreadPct } from '../lib/ember.mjs';
|
||||||
|
|
||||||
|
const step = (clock_mhz, power_pct, watts, mhs, mark = 'ok') => ({ clock_mhz, power_pct, limit_w: 575 * power_pct / 100, watts, mhs, eff: Number((mhs / watts).toFixed(4)), gclk: clock_mhz || 2800, mclk: 10500, tmax: 68, faults: 0, mark });
|
||||||
|
const rec = (machine, ts, chosen, over = {}) => ({
|
||||||
|
ts, machine, app: '0.3.10', os: 'windows', card: 'NVIDIA_GeForce_RTX_5090', vendor: 'nvidia', driver: '581.57', driver_major: '581', class: 'l128w16',
|
||||||
|
key: 'NVIDIA_GeForce_RTX_5090|581|l128w16', plan: 'full', steps: [step(0, 100, 290, 124.0), chosen], chosen, before: step(0, 100, 290, 124.0), eff: chosen.eff, mhs: chosen.mhs, watts: chosen.watts, ...over,
|
||||||
|
});
|
||||||
|
// five machines, each landing near 2,470 MHz at 100%: 0.55 to 0.57 MH/W
|
||||||
|
const good = [
|
||||||
|
rec('a1', 1000, step(2472, 100, 220, 123.5)),
|
||||||
|
rec('b2', 1001, step(2472, 100, 222, 123.1)),
|
||||||
|
rec('c3', 1002, step(2781, 100, 236, 123.8)),
|
||||||
|
rec('d4', 1003, step(2472, 100, 218, 123.6)),
|
||||||
|
rec('e5', 1004, step(2163, 100, 212, 122.9)),
|
||||||
|
];
|
||||||
|
const outlier = rec('f6', 1005, step(1854, 50, 130, 118.0)); // 0.908 MH/W: a card with a broken draw reading
|
||||||
|
const baseline = rec('g7', 1006, step(0, 80, 290, 122.3), { plan: 'baseline', steps: [step(0, 80, 290, 122.3)], before: null });
|
||||||
|
const amd = (machine, ts, chosen) => rec(machine, ts, chosen, { card: 'AMD_Radeon_RX_9070_XT', vendor: 'amd', driver: '32.0.15801.1', driver_major: '32', key: 'AMD_Radeon_RX_9070_XT|32|l128w16', plan: 'confirm', before: null });
|
||||||
|
|
||||||
|
test('five samples converge on the median point and an outlier does not move it', () => {
|
||||||
|
const { priors, table } = aggregate(good, { minSamples: 5 });
|
||||||
|
const p = priors['NVIDIA_GeForce_RTX_5090|581|l128w16'];
|
||||||
|
assert.ok(p, 'a prior at the sample floor');
|
||||||
|
assert.equal(p.clock_mhz, 2470, 'the median clock cap, rounded to 10 MHz');
|
||||||
|
assert.equal(p.power_pct, 100);
|
||||||
|
assert.equal(p.samples, 5);
|
||||||
|
assert.equal(p.machines, 5);
|
||||||
|
assert.ok(p.eff > 0.55 && p.eff < 0.57, `eff ${p.eff}`);
|
||||||
|
assert.ok(p.spread_pct >= 0 && p.spread_pct < 3, `spread ${p.spread_pct}`);
|
||||||
|
assert.equal(p.before_eff, Number((124 / 290).toFixed(4)));
|
||||||
|
assert.ok(p.gain_pct > 25, `gain ${p.gain_pct}% over the untuned 100% point`);
|
||||||
|
assert.equal(p.updated, '1970-01-01T00:16:44Z');
|
||||||
|
// the outlier: 0.908 MH/W at 1,854 MHz joins; the median moves by one rank at most
|
||||||
|
const with6 = aggregate(good.concat(outlier), { minSamples: 5 }).priors['NVIDIA_GeForce_RTX_5090|581|l128w16'];
|
||||||
|
assert.equal(with6.samples, 6);
|
||||||
|
assert.equal(with6.clock_mhz, 2470);
|
||||||
|
assert.equal(with6.power_pct, 100);
|
||||||
|
assert.ok(with6.eff < 0.58, `the outlier's 0.908 MH/W did not drag the median: ${with6.eff}`);
|
||||||
|
assert.ok(with6.spread_pct < 5, `spread ${with6.spread_pct}`);
|
||||||
|
assert.equal(table[0].key, 'NVIDIA_GeForce_RTX_5090|581|l128w16');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('under the floor there is no prior, and baseline records never make one', () => {
|
||||||
|
const { priors, table } = aggregate(good.slice(0, 4), { minSamples: 5 });
|
||||||
|
assert.deepEqual(priors, {});
|
||||||
|
assert.equal(table[0].samples, 4, 'the table still shows the count');
|
||||||
|
const b = aggregate([baseline, baseline], { minSamples: 1 });
|
||||||
|
assert.deepEqual(b.priors, {}, 'measure-only records say what a card does, never what to set');
|
||||||
|
assert.equal(b.table[0].baseline_samples, 1, 'the duplicate upload counted once');
|
||||||
|
assert.equal(b.table[0].baseline_eff, Number((122.3 / 290).toFixed(4)));
|
||||||
|
// a marked chosen step (a faulted or hot winner cannot exist, but a record with one is ignored)
|
||||||
|
const bad = rec('h8', 1007, step(2000, 100, 200, 120, 'faulted'));
|
||||||
|
assert.deepEqual(aggregate([bad], { minSamples: 1 }).priors, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('records are parsed out of log text and de-duplicated on machine, card and time', () => {
|
||||||
|
const line = `1791230000 TUNE ${JSON.stringify(good[0])}`;
|
||||||
|
const text = ['1791229999 status: x', line, line, `1791230001 TUNE ${JSON.stringify(good[1])}`, '1791230002 TUNE {not json'].join('\n');
|
||||||
|
const rs = parseRecords(text);
|
||||||
|
assert.equal(rs.length, 3);
|
||||||
|
assert.equal(dedupe(rs).length, 2);
|
||||||
|
assert.equal(parseRecords('').length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the manifest merge keeps the kernel-variant cards and carries the settings', () => {
|
||||||
|
const existing = { updated: '2026-10-04T21:00:00Z', window_days: 7, cards: { NVIDIA_GeForce_RTX_5090: { variant: 'u2-ldg', race: true, candidates: ['u2-ldg', 'ldg', 'base'] } }, priors: { 'old|1|v2': { samples: 9 } } };
|
||||||
|
const { priors } = aggregate(good, { minSamples: 5 });
|
||||||
|
const t = mergeTuning(existing, priors, { rate_tolerance_pct: 1 });
|
||||||
|
assert.equal(t.cards.NVIDIA_GeForce_RTX_5090.variant, 'u2-ldg', 'lever 2 untouched');
|
||||||
|
assert.equal(t.window_days, 7);
|
||||||
|
assert.deepEqual(t.ember, { enabled: true, min_samples: 5, rate_tolerance_pct: 1 });
|
||||||
|
assert.ok(!t.priors['old|1|v2'], 'a key without samples in the window drops out');
|
||||||
|
assert.ok(t.priors['NVIDIA_GeForce_RTX_5090|581|l128w16']);
|
||||||
|
// the kill switch rides the same section
|
||||||
|
assert.equal(mergeTuning(existing, {}, { enabled: false }).ember.enabled, false);
|
||||||
|
assert.deepEqual(mergeTuning(null, {}).cards, {});
|
||||||
|
// the round trip: canonical JSON (what publish-manifest.sh signs) parses back to the same prior
|
||||||
|
const back = JSON.parse(JSON.stringify(t));
|
||||||
|
assert.deepEqual(priorFor(back, 'NVIDIA_GeForce_RTX_5090|581|l128w16'), { clock_mhz: 2470, power_pct: 100, eff: priors['NVIDIA_GeForce_RTX_5090|581|l128w16'].eff, samples: 5 });
|
||||||
|
assert.equal(priorFor(back, 'NVIDIA_GeForce_RTX_5090|581|l128w16', 6), null, 'six wanted, five there');
|
||||||
|
assert.equal(priorFor(back, 'nothing|0|v2'), null);
|
||||||
|
assert.equal(priorFor(null, 'x'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AMD confirm records aggregate by their own key, and the line reads', () => {
|
||||||
|
const rs = [amd('p1', 2000, step(2600, 90, 177, 17.7)), amd('p2', 2001, step(2600, 90, 180, 17.6)), amd('p3', 2002, step(2500, 90, 170, 17.4))];
|
||||||
|
const { priors, table } = aggregate(rs, { minSamples: 3 });
|
||||||
|
const p = priors['AMD_Radeon_RX_9070_XT|32|l128w16'];
|
||||||
|
assert.equal(p.clock_mhz, 2600);
|
||||||
|
assert.equal(p.power_pct, 90);
|
||||||
|
assert.equal(p.vendor, 'amd');
|
||||||
|
assert.equal(p.gain_pct, undefined, 'confirm records carry no before step and no baseline was uploaded');
|
||||||
|
assert.match(priorLine(p), /^AMD Radeon RX 9070 XT \| driver 32 \| l128w16: 2600 MHz at 90%, 0\.\d+ MH\/W, 17\.6 MH\/s at 177 W, spread \d+(\.\d+)?%, 3 sample\(s\) from 3 machine\(s\)$/);
|
||||||
|
assert.equal(table.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('median and spread', () => {
|
||||||
|
assert.equal(median([3, 1, 2]), 2);
|
||||||
|
assert.equal(median([4, 1, 2, 3]), 2.5);
|
||||||
|
assert.equal(median([]), 0);
|
||||||
|
assert.equal(spreadPct([1, 1, 1]), 0);
|
||||||
|
assert.equal(spreadPct([10]), 0);
|
||||||
|
assert.equal(spreadPct([9, 10, 11]), 10);
|
||||||
|
});
|
||||||
|
|
@ -160,6 +160,7 @@ p{margin:0;color:var(--ink-2);max-width:52ch}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ main{padding-bottom:var(--sec)}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
194
site/bench.html
194
site/bench.html
File diff suppressed because one or more lines are too long
|
|
@ -234,6 +234,7 @@ main{padding-bottom:var(--sec)}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,42 @@ function stampDownloads(html, file, dl) {
|
||||||
const downloads = await loadDownloads();
|
const downloads = await loadDownloads();
|
||||||
built.push(`downloads (${downloads.source}: ${Object.keys(downloads.files || {}).join(', ') || 'none'})`);
|
built.push(`downloads (${downloads.source}: ${Object.keys(downloads.files || {}).join(', ') || 'none'})`);
|
||||||
|
|
||||||
|
// the evidence page (/evidence): the claims table, the counts and the date are rendered from docs/evidence.md (6 October 2026);
|
||||||
|
// the page's prose stays in site/evidence.html, the rows are judgement edited in the markdown and follow from there.
|
||||||
|
function renderEvidence(html) {
|
||||||
|
const mdp = join(docs, 'evidence.md');
|
||||||
|
if (!existsSync(mdp)) return html;
|
||||||
|
const src = readFileSync(mdp, 'utf8');
|
||||||
|
const start = src.indexOf('\n| # | Claim |'); const end = src.indexOf('\n## Count by status');
|
||||||
|
if (start < 0 || end < 0) throw new Error('evidence.md: claims table not found');
|
||||||
|
const rows = src.slice(start, end).split('\n').filter(l => /^\| \d+ \|/.test(l));
|
||||||
|
const LABELS = ['designed', 'implemented', 'tested by the team', 'reproduced externally', 'reviewed independently'];
|
||||||
|
const inl = t => esc(t.trim()).replace(/`([^`]+)`/g, (m, c) => `<code>${c}</code>`);
|
||||||
|
const cells = l => l.replace(/^\| /, '').replace(/ \|$/, '').split(' | ');
|
||||||
|
const counts = Object.fromEntries(LABELS.map(k => [k, 0]));
|
||||||
|
const tr = rows.map(l => {
|
||||||
|
const c = cells(l); if (c.length !== 8) throw new Error(`evidence.md: row ${c[0]} has ${c.length} cells`);
|
||||||
|
const [n, claim, where, status, ver, test, result, iv] = c;
|
||||||
|
const idx = LABELS.findIndex(k => status.toLowerCase().includes(k)); if (idx < 0) throw new Error(`evidence.md: row ${n} status "${status}"`);
|
||||||
|
counts[LABELS[idx]]++;
|
||||||
|
return `<tr data-status="${LABELS[idx]}"><td class="n">${n}</td><td class="claim">${inl(claim)}<div class="where">${inl(where)}</div></td><td><span class="st st-${idx}">${inl(status)}</span></td><td class="mono">${inl(ver)}</td><td>${inl(test)}</td><td>${inl(result)}</td><td class="iv">${inl(iv)}</td></tr>`;
|
||||||
|
}).join('\n');
|
||||||
|
// the same scrub as /bench: local-time stamps to UTC and the private-string check (the build fails on any hit)
|
||||||
|
const trs = scrubBench(tr);
|
||||||
|
html = html.replace(/(<table id="claims">[\s\S]*?<tbody>)[\s\S]*?(<\/tbody>)/, (m, a, b) => `${a}\n${trs}\n${b}`);
|
||||||
|
for (const k of LABELS) {
|
||||||
|
html = html.replace(new RegExp(`(<div class="label"><div class="k">${k}</div><div class="v">)\\d+(</div>)`), `$1${counts[k]}$2`);
|
||||||
|
html = html.replace(new RegExp(`(data-filter="${k}"><b>)\\d+(</b>)`), `$1${counts[k]}$2`);
|
||||||
|
}
|
||||||
|
html = html.replace(/(data-filter=""><b>)\d+(<\/b>)/, `$1${rows.length}$2`);
|
||||||
|
const d = new Date(); const MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||||
|
const day = `${d.getUTCDate()} ${MON[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
||||||
|
const dayLong = `${d.getUTCDate()} ${['January','February','March','April','May','June','July','August','September','October','November','December'][d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
||||||
|
html = html.replace(/<div class="eyebrow">\d+ claims · 5 labels · [^<]*<\/div>/, `<div class="eyebrow">${rows.length} claims · 5 labels · ${day}</div>`);
|
||||||
|
html = html.replace(/<p class="asof">Statuses are honest as of [^<]*<\/p>/, `<p class="asof">Statuses are honest as of ${dayLong}, the day this page was generated from docs/evidence.md, and change only through that file.</p>`);
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
const PAGES = [['index.html', ''], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['miner.html', 'miner'], ['wallet.html', 'wallet'], ['metamask.html', ''], ['faucet.html', ''], ['404.html', ''],
|
const PAGES = [['index.html', ''], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['miner.html', 'miner'], ['wallet.html', 'wallet'], ['metamask.html', ''], ['faucet.html', ''], ['404.html', ''],
|
||||||
// the DAG explorer (5 Oct 2026): /explorer, /block/<hash> and /address/<addr> (vercel.json rewrites the last two)
|
// the DAG explorer (5 Oct 2026): /explorer, /block/<hash> and /address/<addr> (vercel.json rewrites the last two)
|
||||||
['explorer.html', 'live'], ['block.html', 'live'], ['address.html', 'live']];
|
['explorer.html', 'live'], ['block.html', 'live'], ['address.html', 'live']];
|
||||||
|
|
@ -324,6 +360,7 @@ for (const [file, active] of PAGES) {
|
||||||
html = inject(html, 'footer', FOOT, file);
|
html = inject(html, 'footer', FOOT, file);
|
||||||
html = stampProduct(html, file);
|
html = stampProduct(html, file);
|
||||||
html = stampDownloads(html, file, downloads);
|
html = stampDownloads(html, file, downloads);
|
||||||
|
if (file === 'evidence.html') html = renderEvidence(html);
|
||||||
if (file === 'index.html') html = inject(html, 'journey', `<script type="application/json" id="journey-data">${JSON.stringify(journey).replace(/</g, '\\u003c')}</script>`, file);
|
if (file === 'index.html') html = inject(html, 'journey', `<script type="application/json" id="journey-data">${JSON.stringify(journey).replace(/</g, '\\u003c')}</script>`, file);
|
||||||
writeFileSync(p, html);
|
writeFileSync(p, html);
|
||||||
built.push(file);
|
built.push(file);
|
||||||
|
|
@ -340,6 +377,21 @@ for (const [file, active] of PAGES) {
|
||||||
];
|
];
|
||||||
const table = '<div class="tbl"><table><thead><tr>' + ['Card', 'Generator', 'Best MH/s', 'MH per watt', 'Miner', 'Date', 'Source', 'Who measured it'].map(h => `<th>${h}</th>`).join('') + '</tr></thead><tbody>' +
|
const table = '<div class="tbl"><table><thead><tr>' + ['Card', 'Generator', 'Best MH/s', 'MH per watt', 'Miner', 'Date', 'Source', 'Who measured it'].map(h => `<th>${h}</th>`).join('') + '</tr></thead><tbody>' +
|
||||||
rows.map(r => '<tr>' + cell(r).map(c => `<td>${esc(String(c))}</td>`).join('') + '</tr>').join('') + '</tbody></table></div>';
|
rows.map(r => '<tr>' + cell(r).map(c => `<td>${esc(String(c))}</td>`).join('') + '</tr>').join('') + '</tbody></table></div>';
|
||||||
|
// Ember Tune's fleet priors (site/miner-priors.json, tools/tuning.mjs --priors --site): one row per card model,
|
||||||
|
// driver major and program class; a row under the sample floor shows its count and no point
|
||||||
|
const pj = JSON.parse(readFileSync(join(here, 'miner-priors.json'), 'utf8'));
|
||||||
|
const prows = (pj.rows || []).slice().sort((a, b) => (b.samples - a.samples) || (a.card < b.card ? -1 : 1));
|
||||||
|
const pcell = r => [
|
||||||
|
r.card, r.driver_major, r.class, r.samples + (r.machines ? ' from ' + r.machines + ' machine' + (r.machines === 1 ? '' : 's') : ''),
|
||||||
|
r.prior ? (r.clock_mhz ? fmt(r.clock_mhz) + ' MHz at ' + r.power_pct + '%' : r.power_pct + '%, clock unlocked') : 'under the floor (' + pj.min_samples + ' needed)',
|
||||||
|
r.mh_per_w == null ? 'not yet' : Number(r.mh_per_w).toFixed(3) + (r.spread_pct != null ? ' (spread ' + r.spread_pct + '%)' : ''),
|
||||||
|
r.mh_s == null ? '' : fmt(r.mh_s) + ' MH/s at ' + fmt(r.watts) + ' W',
|
||||||
|
r.untuned_mh_per_w == null ? 'not measured' : Number(r.untuned_mh_per_w).toFixed(3) + (r.gain_pct != null ? ' (' + (r.gain_pct >= 0 ? '+' : '') + r.gain_pct + '%)' : ''),
|
||||||
|
];
|
||||||
|
const ptable = prows.length
|
||||||
|
? '<div class="tbl"><table><thead><tr>' + ['Card', 'Driver', 'Program class', 'Samples', 'Tuned point', 'MH per watt', 'Rate and draw', 'Untuned MH per watt (gain)'].map(h => `<th>${h}</th>`).join('') + '</tr></thead><tbody>' +
|
||||||
|
prows.map(r => '<tr>' + pcell(r).map(c => `<td>${esc(String(c))}</td>`).join('') + '</tr>').join('') + '</tbody></table></div>'
|
||||||
|
: '<p>No tune reports yet. The first rows appear once five machines with the same card model have reported.</p>';
|
||||||
const body = scrubBench([
|
const body = scrubBench([
|
||||||
'<h2 id="table">The table</h2>',
|
'<h2 id="table">The table</h2>',
|
||||||
'<p>One row per card, generator version and miner version. The rate is the best one measured. Integrated GPUs are not listed. Prototype rows are bench numbers from before the devnet and say so in the miner column.</p>',
|
'<p>One row per card, generator version and miner version. The rate is the best one measured. Integrated GPUs are not listed. Prototype rows are bench numbers from before the devnet and say so in the miner column.</p>',
|
||||||
|
|
@ -349,8 +401,12 @@ for (const [file, active] of PAGES) {
|
||||||
'<p>MH per watt needs the card\'s power draw during the run. The app reads it on NVIDIA cards through the driver. Rows get the figure when a run records it.</p>',
|
'<p>MH per watt needs the card\'s power draw during the run. The app reads it on NVIDIA cards through the driver. Rows get the figure when a run records it.</p>',
|
||||||
'<p>There is no other Igneum miner to compare with yet, so this table compares cards, not miners. The app that produces these rows: <a href="/miner">the miner page</a>.</p>',
|
'<p>There is no other Igneum miner to compare with yet, so this table compares cards, not miners. The app that produces these rows: <a href="/miner">the miner page</a>.</p>',
|
||||||
`<p>Rows: ${rows.length}. Source file: <code>site/miner-bench.json</code> in the repository.</p>`,
|
`<p>Rows: ${rows.length}. Source file: <code>site/miner-bench.json</code> in the repository.</p>`,
|
||||||
|
'<h2 id="priors">Fleet tuning priors</h2>',
|
||||||
|
'<p>Ember Tune runs on every card the app mines with: the power limit and the core clock are stepped on the live program and the card keeps the point with the best MH per watt within 1% of its top rate. Every finished tune is reported back without anything that identifies the owner, and the fleet\'s median point per card model, driver major and program class comes back down inside the signed update manifest as the starting point for the next card of that model. A model needs ' + pj.min_samples + ' reports before its prior is used.</p>',
|
||||||
|
ptable,
|
||||||
|
`<p>Rows: ${prows.length}${pj.generated ? ', generated ' + pj.generated : ''}. Source file: <code>site/miner-priors.json</code> in the repository, written from the fleet records by <code>tools/tuning.mjs --priors --site</code>.</p>`,
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
const toc = [{ lvl: 2, t: 'The table', id: 'table' }, { lvl: 2, t: 'How a row gets here', id: 'how' }];
|
const toc = [{ lvl: 2, t: 'The table', id: 'table' }, { lvl: 2, t: 'How a row gets here', id: 'how' }, { lvl: 2, t: 'Fleet tuning priors', id: 'priors' }];
|
||||||
writeFileSync(join(here, 'miners.html'), page('Igneum GPU bench table', 'Measured Igneum hash rates per GPU: card, generator version, best MH/s, MH per watt where measured, miner version, date and the log entry each number came from.', body, toc,
|
writeFileSync(join(here, 'miners.html'), page('Igneum GPU bench table', 'Measured Igneum hash rates per GPU: card, generator version, best MH/s, MH per watt where measured, miner version, date and the log entry each number came from.', body, toc,
|
||||||
'Measured hash rates per card on the Igneum lottery hash, with the generator version, the miner version, the date and the log entry behind each number.',
|
'Measured hash rates per card on the Igneum lottery hash, with the generator version, the miner version, the date and the log entry behind each number.',
|
||||||
{ path: '/miners', heading: 'GPU bench table', active: 'miner' }));
|
{ path: '/miners', heading: 'GPU bench table', active: 'miner' }));
|
||||||
|
|
|
||||||
|
|
@ -175,52 +175,53 @@ code{font-family:var(--f-mono);font-size:.92em;background:var(--obsidian);paddin
|
||||||
<!-- nav:end -->
|
<!-- nav:end -->
|
||||||
<main id="main" class="wrap">
|
<main id="main" class="wrap">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="eyebrow">30 claims · 5 labels · 4 Oct 2026</div>
|
<div class="eyebrow">31 claims · 5 labels · 6 Oct 2026</div>
|
||||||
<h1>Evidence</h1>
|
<h1>Evidence</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="note">Every claim the homepage and the litepaper make, one row each, with one of five labels: <b>designed</b> (a decision, no code), <b>implemented</b> (code with passing test vectors), <b>tested by the team</b> (measured by the project on a named machine, in the engineering log), <b>reproduced externally</b> (a third party ran the published command and got the published result) and <b>reviewed independently</b> (a named outside reviewer published a finding on that version). Nothing on this chain has been reproduced externally or reviewed independently; every row says so. A label belongs to the exact version in the row, and an audit of one version never covers a newer one. The 12-node cloud network of 4 October 2026 is the project's own, so its rows are tested by the team, not reproduced externally.</p>
|
<p class="note">Every claim the homepage and the litepaper make, one row each, with one of five labels: <b>designed</b> (a decision, no code), <b>implemented</b> (code with passing test vectors), <b>tested by the team</b> (measured by the project on a named machine, in the engineering log), <b>reproduced externally</b> (a third party ran the published command and got the published result) and <b>reviewed independently</b> (a named outside reviewer published a finding on that version). Nothing on this chain has been reproduced externally or reviewed independently; every row says so. A label belongs to the exact version in the row, and an audit of one version never covers a newer one. The 12-node cloud network of 4 October 2026 is the project's own, so its rows are tested by the team, not reproduced externally.</p>
|
||||||
<div class="labels">
|
<div class="labels">
|
||||||
<div class="label"><div class="k">designed</div><div class="v">6</div><p>A decision in the design document or the specification. No code, or a stub</p></div>
|
<div class="label"><div class="k">designed</div><div class="v">6</div><p>A decision in the design document or the specification. No code, or a stub</p></div>
|
||||||
<div class="label"><div class="k">implemented</div><div class="v">3</div><p>Code in the repository with test vectors that pass. Not measured as the claim</p></div>
|
<div class="label"><div class="k">implemented</div><div class="v">3</div><p>Code in the repository with test vectors that pass. Not measured as the claim</p></div>
|
||||||
<div class="label"><div class="k">tested by the team</div><div class="v">21</div><p>Measured or exercised by the project on a named machine, with the command in the log</p></div>
|
<div class="label"><div class="k">tested by the team</div><div class="v">22</div><p>Measured or exercised by the project on a named machine, with the command in the log</p></div>
|
||||||
<div class="label"><div class="k">reproduced externally</div><div class="v">0</div><p>None yet. The repository is private until the public testnet</p></div>
|
<div class="label"><div class="k">reproduced externally</div><div class="v">0</div><p>None yet. The repository is private until the public testnet</p></div>
|
||||||
<div class="label"><div class="k">reviewed independently</div><div class="v">0</div><p>None yet. What review would cost and who pays is in the funding plan</p></div>
|
<div class="label"><div class="k">reviewed independently</div><div class="v">0</div><p>None yet. What review would cost and who pays is in the funding plan</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chips" role="group" aria-label="Filter by status"><button type="button" class="chip on" aria-pressed="true" data-filter=""><b>30</b> all</button><button type="button" class="chip" aria-pressed="false" data-filter="designed"><b>6</b> designed</button><button type="button" class="chip" aria-pressed="false" data-filter="implemented"><b>3</b> implemented</button><button type="button" class="chip" aria-pressed="false" data-filter="tested by the team"><b>21</b> tested by the team</button><button type="button" class="chip" aria-pressed="false" data-filter="reproduced externally"><b>0</b> reproduced externally</button><button type="button" class="chip" aria-pressed="false" data-filter="reviewed independently"><b>0</b> reviewed independently</button></div>
|
<div class="chips" role="group" aria-label="Filter by status"><button type="button" class="chip on" aria-pressed="true" data-filter=""><b>31</b> all</button><button type="button" class="chip" aria-pressed="false" data-filter="designed"><b>6</b> designed</button><button type="button" class="chip" aria-pressed="false" data-filter="implemented"><b>3</b> implemented</button><button type="button" class="chip" aria-pressed="false" data-filter="tested by the team"><b>22</b> tested by the team</button><button type="button" class="chip" aria-pressed="false" data-filter="reproduced externally"><b>0</b> reproduced externally</button><button type="button" class="chip" aria-pressed="false" data-filter="reviewed independently"><b>0</b> reviewed independently</button></div>
|
||||||
<p class="hint">The table is wider than this screen. Scroll it sideways.</p>
|
<p class="hint">The table is wider than this screen. Scroll it sideways.</p>
|
||||||
<div class="tbl"><table id="claims">
|
<div class="tbl"><table id="claims">
|
||||||
<thead><tr><th data-col="0" data-num="1">#</th><th data-col="1">Claim</th><th data-col="2" data-status="1">Status</th><th data-col="3">Version or commit</th><th data-col="4">Reproducible test</th><th data-col="5">Result, date, machine</th><th data-col="6">Independent verification</th></tr></thead>
|
<thead><tr><th data-col="0" data-num="1">#</th><th data-col="1">Claim</th><th data-col="2" data-status="1">Status</th><th data-col="3">Version or commit</th><th data-col="4">Reproducible test</th><th data-col="5">Result, date, machine</th><th data-col="6">Independent verification</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr data-status="tested by the team"><td class="n">1</td><td class="claim">A new mining program every hour, compiled by the miner, with no human in the loop and no pause in mining<div class="where">Homepage hero and "This hour's program"; litepaper Mining</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">igneum-pow 0.2.0; repo <code>b27da39</code>, <code>1292110</code>, <code>100c5d7</code>; fork <code>devnet-v4</code> <code>6457ca95</code></td><td>The live devnet v4: the node announces <code>next_epoch_seed</code> 150 DAA past the seed score, <code>igneum-miner</code> sends <code>prepare</code> to its worker, the worker builds the next program while the current one mines; Metal (<code>proto-metal/igneum-bench</code>), CUDA and OpenCL workers; bench-log "first hourly program swap on the live devnet"</td><td>Epoch boundary at DAA 3,600 (11:05:07 BST, 4 October 2026) crossed live on three vendors: the Mac M5 Max (Metal) compiled the next program in 82 ms, 449 DAA before the boundary, swapped in 0.01 ms, 26.7 MH/s before and after; the RTX 5090 compiled in 1,285 ms, swapped in 0.00 ms, 121.8 before and 123.4 MH/s after; the integrated AMD chip 2.74 MH/s before and after. 0 restarts, 0 rejected blocks, 0 rebuilds. The epoch seed is the epoch block hash; the delay of row 2 is not wired in. At a later boundary (DAA 18,000) one OpenCL worker on PC 2 stayed on the previous epoch after an app reinstall and answered 514 jobs with a seed mismatch; fixed in the miner (<code>3bfe346f</code>, workers emit a <code>need</code> line), the swap time of that forced prepare not measured</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">1</td><td class="claim">A new mining program every hour, compiled by the miner, with no human in the loop and no pause in mining<div class="where">Homepage hero and "This hour's program"; litepaper Mining</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">igneum-pow 0.2.0; repo <code>b27da39</code>, <code>1292110</code>, <code>100c5d7</code>; fork <code>devnet-v4</code> <code>6457ca95</code></td><td>The live devnet v4: the node announces <code>next_epoch_seed</code> 150 DAA past the seed score, <code>igneum-miner</code> sends <code>prepare</code> to its worker, the worker builds the next program while the current one mines; Metal (<code>proto-metal/igneum-bench</code>), CUDA and OpenCL workers; bench-log "first hourly program swap on the live devnet"</td><td>Epoch boundary at DAA 3,600 (10:05:07 UTC, 4 October 2026) crossed live on three vendors: the Apple M5 Max M5 Max (Metal) compiled the next program in 82 ms, 449 DAA before the boundary, swapped in 0.01 ms, 26.7 MH/s before and after; the RTX 5090 compiled in 1,285 ms, swapped in 0.00 ms, 121.8 before and 123.4 MH/s after; the integrated AMD chip 2.74 MH/s before and after. 0 restarts, 0 rejected blocks, 0 rebuilds. The epoch seed is the epoch block hash; the delay of row 2 is not wired in. At a later boundary (DAA 18,000) one OpenCL worker on PC 2 stayed on the previous epoch after an app reinstall and answered 514 jobs with a seed mismatch; fixed in the miner (<code>3bfe346f</code>, workers emit a <code>need</code> line), the swap time of that forced prepare not measured</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="implemented"><td class="n">2</td><td class="claim">The program seed passes through a 10-minute verifiable delay from a certified checkpoint, so nobody can grind the seed<div class="where">Litepaper Mining, vs RandomX ("Closed by a verifiable delay")</div></td><td><span class="st st-1">implemented</span></td><td class="mono">repo <code>792776e</code>; <code>proto-vdf/</code></td><td><code>proto-vdf</code> full 10-minute runs and the tamper cases in <code>proto-vdf/README.md</code>; bench-log "proto-vdf"</td><td>Class group 1024-bit: 163,000 squarings per second, 10-min eval 585.4 s, prove 9.1 s on 12 threads, verify 4.47 ms, 516-byte proof; wrong checkpoint, flipped seed bit and T+1 all rejected; grinding model gains 0 blocks per epoch with the delay against +3.62 at a 30% advantage without it. 3 October 2026, Apple M5 Max, one core. Prototype only: not in the node on 4 October either, not reviewed against chiavdf (O-4.1)</td><td class="iv">none yet</td></tr>
|
<tr data-status="implemented"><td class="n">2</td><td class="claim">The program seed passes through a 10-minute verifiable delay from a certified checkpoint, so nobody can grind the seed<div class="where">Litepaper Mining, vs RandomX ("Closed by a verifiable delay")</div></td><td><span class="st st-1">implemented</span></td><td class="mono">repo <code>792776e</code>; <code>proto-vdf/</code></td><td><code>proto-vdf</code> full 10-minute runs and the tamper cases in <code>proto-vdf/README.md</code>; bench-log "proto-vdf"</td><td>Class group 1024-bit: 163,000 squarings per second, 10-min eval 585.4 s, prove 9.1 s on 12 threads, verify 4.47 ms, 516-byte proof; wrong checkpoint, flipped seed bit and T+1 all rejected; grinding model gains 0 blocks per epoch with the delay against +3.62 at a 30% advantage without it. 3 October 2026, Apple M5 Max, one core. Prototype only: not in the node on 4 October either, not reviewed against chiavdf (O-4.1)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">3</td><td class="claim">The dataset is memory-hard: computing an item costs more than loading it, and every hash does 128 distinct dataset reads<div class="where">Litepaper Mining and vs RandomX; homepage vs RandomX ("Memory 2 GB, growing")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>58a5a63</code> (memory-hard), <code>b27da39</code> (generator 2); igneum-pow 0.2.0 (<code>memhard.rs</code>, <code>generator.rs</code>, <code>accept.rs</code>); spec 01 sections 1.4.2 to 1.4.6; <code>proto-metal/MEMHARD.md</code></td><td><code>proto-metal/igneum-bench --inline-dataset</code> against the honest run at 1 GiB and 256 MiB; <code>igneum-census --gen v2 --warps 64</code> over 20,000 programs; bench-log "memory-hard dataset" and "generator version 2 adopted"</td><td>Honest 45.2 Mhash/s, inline (never reads the dataset) 9.49 Mhash/s, ratio 0.21 at 1 GiB, 0.10 at 256 MiB. 3 October 2026, Apple M5 Max. Generator 2, 4 October 2026: 20,000 programs, 128 static loads on every program, distinct addresses per hash mean 127.9, minimum 120.1; 5.2% of candidates rejected by the acceptance rule. The price of the 128 fresh reads is the hash rate: Apple OpenCL 45.0 MH/s on a version 1 program with 80 distinct loads against 27.5 to 27.9 on version 2; the RTX 5090 229 MH/s on a 104-load version 1 program at 1 GiB (3 October) against 121.8 to 124.2 MH/s mining version 2 on the live devnet (4 October). Apple only for the shortcut ratio (O-1.5); the on-die cache question of ledger M16 is unchanged</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">3</td><td class="claim">The dataset is memory-hard: computing an item costs more than loading it, and every hash does 128 distinct dataset reads<div class="where">Litepaper Mining and vs RandomX; homepage vs RandomX ("Memory 2 GB, growing")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>58a5a63</code> (memory-hard), <code>b27da39</code> (generator 2); igneum-pow 0.2.0 (<code>memhard.rs</code>, <code>generator.rs</code>, <code>accept.rs</code>); spec 01 sections 1.4.2 to 1.4.6; <code>proto-metal/MEMHARD.md</code></td><td><code>proto-metal/igneum-bench --inline-dataset</code> against the honest run at 1 GiB and 256 MiB; <code>igneum-census --gen v2 --warps 64</code> over 20,000 programs; bench-log "memory-hard dataset" and "generator version 2 adopted"</td><td>Honest 45.2 Mhash/s, inline (never reads the dataset) 9.49 Mhash/s, ratio 0.21 at 1 GiB, 0.10 at 256 MiB. 3 October 2026, Apple M5 Max. Generator 2, 4 October 2026: 20,000 programs, 128 static loads on every program, distinct addresses per hash mean 127.9, minimum 120.1; 5.2% of candidates rejected by the acceptance rule. The price of the 128 fresh reads is the hash rate: Apple OpenCL 45.0 MH/s on a version 1 program with 80 distinct loads against 27.5 to 27.9 on version 2; the RTX 5090 229 MH/s on a 104-load version 1 program at 1 GiB (3 October) against 121.8 to 124.2 MH/s mining version 2 on the live devnet (4 October). Apple only for the shortcut ratio (O-1.5); the on-die cache question of ledger M16 is unchanged</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">4</td><td class="claim">The same program produces identical hashes on three GPU vendors, cache and dataset included<div class="where">Litepaper vs RandomX ("Bit-exact on Apple, NVIDIA and AMD, measured"), For miners; homepage</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>f2e903e</code>, <code>0f1fdaf</code> (version 1 packs), <code>b27da39</code> (version 2 packs <code>igneum-genesis-mh</code>, <code>igneum-devnet-v4-epoch0</code>); igneum-pow 0.2.0</td><td>The 96 test vectors of a pack through <code>proto-metal/igneum-bench</code>, <code>proto-cuda/host.cu</code>, <code>proto-opencl/host.c</code>; batch fingerprint at <code>--batch-log2 24</code>; the miner's CPU re-check of every share a GPU worker finds on the devnet; bench-log entries "RTX 5090, memory-hard dataset", "AMD gfx1036", "RTX 5090 through NVIDIA OpenCL", "generator version 2 adopted", "the gfx1036 worker fault"</td><td>Version 1: 96/96 on Apple Metal (M5 Max), NVIDIA CUDA and NVIDIA OpenCL (RTX 5090, Windows), AMD OpenCL (Ryzen 7 9800X3D integrated gfx1036, 1 compute unit), Apple OpenCL, pocl and two CPU references; batch fingerprint <code>98af644e993239e2</code> over 16.7 million nonces identical on the AMD chip and the 5090, 3 October 2026. Version 2: 96/96 on Apple Metal, Apple OpenCL and the CUDA and OpenCL emulators with identical fingerprints; on real NVIDIA and AMD silicon the version 2 vectors have not run as a pack, but both mined accepted blocks on the live devnet with the CPU re-check clean on every share (RTX 5090 at 124.2 MH/s, gfx1036 at 3.3 MH/s), 4 October 2026. The AMD device is an integrated chip; no discrete AMD card and no Intel card has run anything (O-1.15)</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">4</td><td class="claim">The same program produces identical hashes on three GPU vendors, cache and dataset included<div class="where">Litepaper vs RandomX ("Bit-exact on Apple, NVIDIA and AMD, measured"), For miners; homepage</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>f2e903e</code>, <code>0f1fdaf</code> (version 1 packs), <code>b27da39</code> (version 2 packs <code>igneum-genesis-mh</code>, <code>igneum-devnet-v4-epoch0</code>); igneum-pow 0.2.0</td><td>The 96 test vectors of a pack through <code>proto-metal/igneum-bench</code>, <code>proto-cuda/host.cu</code>, <code>proto-opencl/host.c</code>; batch fingerprint at <code>--batch-log2 24</code>; the miner's CPU re-check of every share a GPU worker finds on the devnet; bench-log entries "RTX 5090, memory-hard dataset", "AMD gfx1036", "RTX 5090 through NVIDIA OpenCL", "generator version 2 adopted", "the gfx1036 worker fault"</td><td>Version 1: 96/96 on Apple Metal (M5 Max), NVIDIA CUDA and NVIDIA OpenCL (RTX 5090, Windows), AMD OpenCL (Ryzen 7 9800X3D integrated gfx1036, 1 compute unit), Apple OpenCL, pocl and two CPU references; batch fingerprint <code>98af644e993239e2</code> over 16.7 million nonces identical on the AMD chip and the 5090, 3 October 2026. Version 2: 96/96 on Apple Metal, Apple OpenCL and the CUDA and OpenCL emulators with identical fingerprints; on real NVIDIA and AMD silicon the version 2 vectors have not run as a pack, but both mined accepted blocks on the live devnet with the CPU re-check clean on every share (RTX 5090 at 124.2 MH/s, gfx1036 at 3.3 MH/s), 4 October 2026. The AMD device is an integrated chip; no discrete AMD card and no Intel card has run anything (O-1.15)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">5</td><td class="claim">A CPU verifies one hash in under 10 ms by simulating one warp<div class="where">Litepaper Mining ("about ten milliseconds"), vs RandomX; roadmap gate 2</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>75cac18</code>, <code>b27da39</code>; igneum-pow 0.2.0 (<code>verify.rs</code>)</td><td><code>cargo test</code> and the crate bench in <code>igneum-pow/</code>; bench-log "igneum-pow: Rust crate bit-exact with proto-metal" and "generator version 2 adopted"</td><td>0.411 to 0.579 ms per 32-lane warp steady, 0.41 to 0.87 ms cold, average of 20, 1 GiB dataset, cache held, one M5 Max performance core, 3 October 2026; version 2 units 0.631 ms (average of 20), cold 0.67 to 0.81 ms, 4 October 2026. Gate margin about 16x on this core. Not measured on a 2019-class laptop core (O-1.14)</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">5</td><td class="claim">A CPU verifies one hash in under 10 ms by simulating one warp<div class="where">Litepaper Mining ("about ten milliseconds"), vs RandomX; roadmap gate 2</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>75cac18</code>, <code>b27da39</code>; igneum-pow 0.2.0 (<code>verify.rs</code>)</td><td><code>cargo test</code> and the crate bench in <code>igneum-pow/</code>; bench-log "igneum-pow: Rust crate bit-exact with proto-metal" and "generator version 2 adopted"</td><td>0.411 to 0.579 ms per 32-lane warp steady, 0.41 to 0.87 ms cold, average of 20, 1 GiB dataset, cache held, one M5 Max performance core, 3 October 2026; version 2 units 0.631 ms (average of 20), cold 0.67 to 0.81 ms, 4 October 2026. Gate margin about 16x on this core. Not measured on a 2019-class laptop core (O-1.14)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">6</td><td class="claim">The hash is bound to the header: one nonce serves one header, and a wrong nonce is rejected<div class="where">Spec 1.6; litepaper Mining (implied by "checks a hash")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>33f7b33</code>, <code>9812466</code>, <code>b27da39</code>; igneum-pow 0.2.0 (<code>bind.rs</code>, bound vectors re-cut for version 2, 39 crate tests)</td><td><code>igneum-miner bad-nonce</code> against a devnet node; <code>igneum-pow hash-bound</code> for the 96-nonce job across the 2^32 lane boundary; bench-log "first devnet blocks on the real lottery hash" and "generator version 2 adopted"</td><td>833 blocks accepted by <code>igneum-lottery-v1-bound</code> on 3 nodes, 0 rejections; <code>bad-nonce</code> gave Reject(BlockInvalid); Metal, OpenCL and CUDA (emulated) workers bit-exact with the crate on the lane-boundary job, 3 October 2026, Apple M5 Max. Version 2: the node's engine reports <code>igneum-lottery-v2-bound</code>, 39 of 39 crate tests, and the live devnet v4 accepts its blocks under it, 4 October 2026</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">6</td><td class="claim">The hash is bound to the header: one nonce serves one header, and a wrong nonce is rejected<div class="where">Spec 1.6; litepaper Mining (implied by "checks a hash")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>33f7b33</code>, <code>9812466</code>, <code>b27da39</code>; igneum-pow 0.2.0 (<code>bind.rs</code>, bound vectors re-cut for version 2, 39 crate tests)</td><td><code>igneum-miner bad-nonce</code> against a devnet node; <code>igneum-pow hash-bound</code> for the 96-nonce job across the 2^32 lane boundary; bench-log "first devnet blocks on the real lottery hash" and "generator version 2 adopted"</td><td>833 blocks accepted by <code>igneum-lottery-v1-bound</code> on 3 nodes, 0 rejections; <code>bad-nonce</code> gave Reject(BlockInvalid); Metal, OpenCL and CUDA (emulated) workers bit-exact with the crate on the lane-boundary job, 3 October 2026, Apple M5 Max. Version 2: the node's engine reports <code>igneum-lottery-v2-bound</code>, 39 of 39 crate tests, and the live devnet v4 accepts its blocks under it, 4 October 2026</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">7</td><td class="claim">The devnet runs at one block a second<div class="where">Homepage stats ("1 / s"); litepaper Speed; roadmap phase 3</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>9812466</code>, <code>e9328c6</code>, <code>8dae48b</code>; fork <code>devnet-v4</code> <code>dc749905</code></td><td>The merged node's 3-node test network (<code>igneum-devnet-880</code>, 960 s); the live devnet v4 record <code>sim/difficulty/records/live-2026-10-04.csv</code>; the 12-node cloud network's arrival logs; bench-log "devnet-v4 integration", "difficulty rule v2", "first devnet blocks"</td><td>Merged node, 4 October 2026, Apple M5 Max: 1,055 blocks in 960 s, 1.03 blocks/s, sink identical on 3 nodes at 31 of 31 samples, 0 rejected. Live devnet v4 the same day: 49 to 81 blocks a minute while two RTX 5090s joined and left (row 12), 1.1 to 1.2 blocks/s in the oscillating window, then within 1.3% per minute with one PC and the Mac. The 12-node cloud network at one block a second: 644 blocks in a 10-minute window. The 3 October CPU devnet: 1.29 blocks/s over 641 s, 1.03 after the first retarget. The phase 3 gate also asks for proofs under 60 s behind the tip; no proof is on the chain (row 15)</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">7</td><td class="claim">The devnet runs at one block a second<div class="where">Homepage stats ("1 / s"); litepaper Speed; roadmap phase 3</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>9812466</code>, <code>e9328c6</code>, <code>8dae48b</code>; fork <code>devnet-v4</code> <code>dc749905</code></td><td>The merged node's 3-node test network (<code>igneum-devnet-880</code>, 960 s); the live devnet v4 record <code>sim/difficulty/records/live-2026-10-04.csv</code>; the 12-node cloud network's arrival logs; bench-log "devnet-v4 integration", "difficulty rule v2", "first devnet blocks"</td><td>Merged node, 4 October 2026, Apple M5 Max: 1,055 blocks in 960 s, 1.03 blocks/s, sink identical on 3 nodes at 31 of 31 samples, 0 rejected. Live devnet v4 the same day: 49 to 81 blocks a minute while two RTX 5090s joined and left (row 12), 1.1 to 1.2 blocks/s in the oscillating window, then within 1.3% per minute with one PC and the Apple M5 Max. The 12-node cloud network at one block a second: 644 blocks in a 10-minute window. The 3 October CPU devnet: 1.29 blocks/s over 641 s, 1.03 after the first retarget. The phase 3 gate also asks for proofs under 60 s behind the tip; no proof is on the chain (row 15)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">8</td><td class="claim">Blocks are mined by GPUs on Apple and NVIDIA<div class="where">Homepage live strip; journey phase 3 ("GPU miners on three vendors")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>9812466</code>, <code>e9328c6</code>, <code>d7e1f89</code>, <code>2309c8d</code>; fork <code>devnet-v4</code></td><td>Metal worker <code>proto-metal/igneum-bench --serve</code> driven by <code>igneum-miner --worker</code>; the live devnet v4 hash-rate record <code>sim/difficulty/records/live-2026-10-04-hashrate.csv</code> (587 worker STATUS lines by run id); bench-log "first devnet blocks", "devnet v4 cut-over", "difficulty rule v2", "first machine on the Igneum Miner app"</td><td>Metal: 506 jobs, 5,636 blocks found and accepted, 0 rejected, 0 CPU/GPU mismatches, 28.2 MH/s wall, 3 October 2026. Live devnet v4, 4 October 2026: PC 1's RTX 5090 at 122 MH/s with 8 identities, PC 2's at 124 MH/s with 8 identities (117 to 119 MH/s inside the one-click app, 34 accepted blocks in its first minute, CPU re-check OK on every share), the Mac's Metal worker at 26.7 MH/s; 17 vote keys signed the first finality lock (row 10); from the afternoon an Apple silicon laptop outside the project at 21.0 MH/s through the app (row 30). Two RTX 5090s and two Apple chips; no other NVIDIA model has mined</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">8</td><td class="claim">Blocks are mined by GPUs on Apple and NVIDIA<div class="where">Homepage live strip; journey phase 3 ("GPU miners on three vendors")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>9812466</code>, <code>e9328c6</code>, <code>d7e1f89</code>, <code>2309c8d</code>; fork <code>devnet-v4</code></td><td>Metal worker <code>proto-metal/igneum-bench --serve</code> driven by <code>igneum-miner --worker</code>; the live devnet v4 hash-rate record <code>sim/difficulty/records/live-2026-10-04-hashrate.csv</code> (587 worker STATUS lines by run id); bench-log "first devnet blocks", "devnet v4 cut-over", "difficulty rule v2", "first machine on the Igneum Miner app"</td><td>Metal: 506 jobs, 5,636 blocks found and accepted, 0 rejected, 0 CPU/GPU mismatches, 28.2 MH/s wall, 3 October 2026. Live devnet v4, 4 October 2026: PC 1's RTX 5090 at 122 MH/s with 8 identities, PC 2's at 124 MH/s with 8 identities (117 to 119 MH/s inside the one-click app, 34 accepted blocks in its first minute, CPU re-check OK on every share), the Apple M5 Max's Metal worker at 26.7 MH/s; 17 vote keys signed the first finality lock (row 10); from the afternoon an Apple silicon laptop outside the project at 21.0 MH/s through the app (row 30). Two RTX 5090s and two Apple chips; no other NVIDIA model has mined</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">9</td><td class="claim">Blocks are mined by a GPU on AMD<div class="where">Journey phase 3 ("three vendors")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>2c4b30f</code> (generic OpenCL worker, <code>--pack</code>), <code>112acf6</code> (fault guards); bound kernel <code>kernel_bound.cl</code> in the pack</td><td><code>igneum-worker-opencl.exe --pack</code> on PC 2's integrated Radeon against the live devnet v4 through the Windows package; bench-log "the gfx1036 worker fault", "first hourly program swap", "first machine on the Igneum Miner app"</td><td>PC 2's integrated gfx1036 (1 compute unit) mined on the live devnet on 4 October 2026: 8 accepted blocks at 3.3 MH/s over 577 s with the CPU re-check clean, and 2.74 MH/s through the hourly program swap with 0 rejected. At about 600 s the AMD runtime began answering every call with success while running nothing (906 jobs became 56,384 in 30 s, 4.3 GH/s of phantom work); not reproduced on Apple OpenCL in 4,565 jobs with 0 leaked objects; the worker and miner now refuse a job 20x faster than the mean or an unchanged output buffer and restart (<code>112acf6</code>), and the next gfx1036 run names the guard that fires. One integrated chip; no discrete AMD card has run anything</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">9</td><td class="claim">Blocks are mined by a GPU on AMD<div class="where">Journey phase 3 ("three vendors")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>2c4b30f</code> (generic OpenCL worker, <code>--pack</code>), <code>112acf6</code> (fault guards); bound kernel <code>kernel_bound.cl</code> in the pack</td><td><code>igneum-worker-opencl.exe --pack</code> on PC 2's integrated Radeon against the live devnet v4 through the Windows package; bench-log "the gfx1036 worker fault", "first hourly program swap", "first machine on the Igneum Miner app"</td><td>PC 2's integrated gfx1036 (1 compute unit) mined on the live devnet on 4 October 2026: 8 accepted blocks at 3.3 MH/s over 577 s with the CPU re-check clean, and 2.74 MH/s through the hourly program swap with 0 rejected. At about 600 s the AMD runtime began answering every call with success while running nothing (906 jobs became 56,384 in 30 s, 4.3 GH/s of phantom work); not reproduced on Apple OpenCL in 4,565 jobs with 0 leaked objects; the worker and miner now refuse a job 20x faster than the mean or an unchanged output buffer and restart (<code>112acf6</code>), and the next gfx1036 run names the guard that fires. One integrated chip; no discrete AMD card has run anything</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">10</td><td class="claim">Checkpoints lock every 30 s of chain at two thirds of all 30-day weight, and the floor stops conflicting locks in partitions and eclipses for as long as neither side's own new blocks carry it past two thirds of its window (about 10 days of a 30-day window at a 50/50 split)<div class="where">Litepaper Finality, "What Igneum does not claim"; homepage "locked every 30 seconds"</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>a3a9833</code> (2/3 floor, O-3.15), <code>bbb264a</code> (simulation), <code>c16ccf1</code>; fork <code>devnet-v4</code> <code>6457ca95</code> (<code>FLOOR_NUM / FLOOR_DEN</code> 2/3), <code>da1eb889</code> (F17 by-weight sortition, F1 first-month gate <code>min_daa = window</code>); spec 3.3, 3.3.1, 3.7, 3.9</td><td>The live devnet v4 (<code>getFinalityCheckpoints</code>, <code>tools/observer/observer.mjs</code>, <code>/api/checkpoint</code>); <code>sim/finality_v2.py --floor 1.0</code>, scenarios A to L; the three-node, six-voter partition runs <code>igneum-devnet-921</code> to <code>-923</code>; <code>tools/finality-attacks</code> scenarios 1 to 6 and 8; bench-log "first finality lock on the live devnet", "finality floor 2/3", "finality v2 attack harness", "finality fixes F17 and F1"</td><td>Live: the first lock on the live devnet was checkpoint 242 at 12:03:44 BST on 4 October 2026, two hours after genesis (the window and <code>min_daa</code> are 7,200 DAA), with 77.4% of all weight and of active weight signed by 12 aggregated votes from 17 vote keys; <code>observer.mjs</code> saw it 0.7 s after the miner's own lock line. By 13:21 UTC the observer held 280 certificates, indices 241 to 522 (DAA 7,229 to 17,982), 17 to 27 voters, no index with two hashes. Test networks, 4 October 2026, Apple M5 Max: a 4/2 split locked on the 4 side (67.9%) 2 to 8 s after the cut and never on the 2 side, 0 conflicts; a 3/3 split locked on neither side for 150 s with 0 conflicts, where the 3 October floor (56.7%) would have locked both sides at 76 and 106 s; the rule guarantees one lock history for partitions shorter than the window bound W / (3R) (200 s on that test network's 1,800-DAA window, about 40 minutes on the devnet, about 10 days at the 30-day mainnet window); beyond that bound each side can reach two thirds of its own window, so the next finality rule freezes the weight table at the last certified checkpoint and pauses instead. Simulator with the 2/3 floor: 0 conflicts up to a 33% equivocator (34% splits a 50/50 partition), silent weight pauses locks from 34%, a 50/50 partition locks alone from day 10.1. Harness: equivocating keys stripped on every node, Sybil dust at zero weight, a pulsed miner's weight equal to its block share (ratio 0.96 to 1.0), the first-month gate stops a young window locking under one key. Not demonstrated: certificate injection on the wire, an eclipse with a private fork, the 2-hour presence window at mainnet length</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">10</td><td class="claim">Checkpoints lock every 30 s of chain at two thirds of all 30-day weight, and the floor stops conflicting locks in partitions and eclipses for as long as neither side's own new blocks carry it past two thirds of its window (about 10 days of a 30-day window at a 50/50 split)<div class="where">Litepaper Finality, "What Igneum does not claim"; homepage "locked every 30 seconds"</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>a3a9833</code> (2/3 floor, O-3.15), <code>bbb264a</code> (simulation), <code>c16ccf1</code>; fork <code>devnet-v4</code> <code>6457ca95</code> (<code>FLOOR_NUM / FLOOR_DEN</code> 2/3), <code>da1eb889</code> (F17 by-weight sortition, F1 first-month gate <code>min_daa = window</code>); spec 3.3, 3.3.1, 3.7, 3.9</td><td>The live devnet v4 (<code>getFinalityCheckpoints</code>, <code>tools/observer/observer.mjs</code>, <code>/api/checkpoint</code>); <code>sim/finality_v2.py --floor 1.0</code>, scenarios A to L; the three-node, six-voter partition runs <code>igneum-devnet-921</code> to <code>-923</code>; <code>tools/finality-attacks</code> scenarios 1 to 6 and 8; bench-log "first finality lock on the live devnet", "finality floor 2/3", "finality v2 attack harness", "finality fixes F17 and F1"</td><td>Live: the first lock on the live devnet was checkpoint 242 at 11:03:44 UTC on 4 October 2026, two hours after genesis (the window and <code>min_daa</code> are 7,200 DAA), with 77.4% of all weight and of active weight signed by 12 aggregated votes from 17 vote keys; <code>observer.mjs</code> saw it 0.7 s after the miner's own lock line. By 13:21 UTC the observer held 280 certificates, indices 241 to 522 (DAA 7,229 to 17,982), 17 to 27 voters, no index with two hashes. Test networks, 4 October 2026, Apple M5 Max: a 4/2 split locked on the 4 side (67.9%) 2 to 8 s after the cut and never on the 2 side, 0 conflicts; a 3/3 split locked on neither side for 150 s with 0 conflicts, where the 3 October floor (56.7%) would have locked both sides at 76 and 106 s; the rule guarantees one lock history for partitions shorter than the window bound W / (3R) (200 s on that test network's 1,800-DAA window, about 40 minutes on the devnet, about 10 days at the 30-day mainnet window); beyond that bound each side can reach two thirds of its own window, so the next finality rule freezes the weight table at the last certified checkpoint and pauses instead. Simulator with the 2/3 floor: 0 conflicts up to a 33% equivocator (34% splits a 50/50 partition), silent weight pauses locks from 34%, a 50/50 partition locks alone from day 10.1. Harness: equivocating keys stripped on every node, Sybil dust at zero weight, a pulsed miner's weight equal to its block share (ratio 0.96 to 1.0), the first-month gate stops a young window locking under one key. Not demonstrated: certificate injection on the wire, an eclipse with a private fork, the 2-hour presence window at mainnet length</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">11</td><td class="claim">Hashrate that arrived today has almost no vote: ten days of the whole network's hashrate to reach a third of the weight, twenty for two thirds; 51% never reaches two thirds while honest miners stay<div class="where">Litepaper Finality; homepage firsts</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>bbb264a</code>; <code>sim/finality_v2.py</code></td><td>Scenario B of <code>sim/finality_v2.py</code>, seeds 7 and 11</td><td>share(t) = (t/30) x a/(1+a) holds to 0.04 points; a renter equal to the whole honest network (a = 1) crosses 1/3 on day 20 and never reaches 2/3; a = 9 crosses 1/3 on day 11.1 and 2/3 on day 22.2. The ten-day figure is a = infinity, honest miners gone. 3 October 2026, Apple M5 Max. A model with 1,000 Pareto keys and no DAG; the live devnet's window is two hours old, so the claim has no live measurement yet</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">11</td><td class="claim">Hashrate that arrived today has almost no vote: ten days of the whole network's hashrate to reach a third of the weight, twenty for two thirds; 51% never reaches two thirds while honest miners stay<div class="where">Litepaper Finality; homepage firsts</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>bbb264a</code>; <code>sim/finality_v2.py</code></td><td>Scenario B of <code>sim/finality_v2.py</code>, seeds 7 and 11</td><td>share(t) = (t/30) x a/(1+a) holds to 0.04 points; a renter equal to the whole honest network (a = 1) crosses 1/3 on day 20 and never reaches 2/3; a = 9 crosses 1/3 on day 11.1 and 2/3 on day 22.2. The ten-day figure is a = infinity, honest miners gone. 3 October 2026, Apple M5 Max. A model with 1,000 Pareto keys and no DAG; the live devnet's window is two hours old, so the claim has no live measurement yet</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">12</td><td class="claim">The difficulty rule recovers from a hashrate step within minutes, where Kaspa's sampled rule never settles. A step inside an epoch set the rule oscillating on the live devnet on 4 October 2026; rule v2 removes it in the simulator and on a test network and is built but not yet rolled out<div class="where">Spec 2.3; litepaper Speed (implied); bench page</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>e9328c6</code>, <code>abb5a5d</code> (attacks), <code>67bf226</code> (rule v2); fork <code>difficulty</code> branch (timestamp fix) and <code>devnet-v4</code> <code>a21ff239</code> (<code>difficulty_v2_activation_daa</code>, <code>REF_WINDOW_V2 = 600</code>); <code>sim/difficulty/sim.py --live</code></td><td>The live record <code>sim/difficulty/records/live-2026-10-04.csv</code> (8,090 headers, <code>pull_live.py</code>) and the hash-rate record beside it; <code>sim/difficulty/sim.py</code> on the synthetic set and the DAG replay; <code>sim/difficulty/attacks/attacks.py</code>; <code>sim/difficulty/testnet_v2.py</code> (3 nodes, activation at DAA 900); <code>cargo test --release -p kaspa-consensus --lib difficulty</code> (15 pass); bench-log "difficulty controller", "difficulty rule under attack", "timestamp attack fixed", "difficulty rule v2"</td><td>Live devnet v4, 4 October 2026 (UTC): a second RTX 5090 joining 7 minutes into an epoch (about 152 to 280 MH/s) hardened the difficulty 70M to 144M in 90 s and then swung by about a third for 40 minutes around the true level of 139M while the epoch-long reference lane carried the join; that card leaving for 4 minutes eased 116M to 67M and back to 106M; the epoch boundary with both PCs restarting took 152M to 77M in 3 minutes, after which the rule held within 1.3% per minute with no flips. Cause: the reference lane covered the whole epoch, so a mid-epoch step polluted it for the hour and the 25% trigger flipped on the short lane's noise. The DAG replay reproduces the record (std of log difficulty 0.115 against 0.134, 4.3 peaks against 4). Rule v2 (reference window 600 DAA) on the replay: std 0.026, 0 flips, mean 142.6M against 139M true; on a 3-node test network the v2 nodes eased a leave with no peak and held a rejoin within 3% after 60 s, and a node without the activation height forked off at it as designed. Rule v2 rolled onto the 12-node cloud network on 4 October (all nodes crossed the height on one chain; a hash-rate step then settled in 160 to 270 s with no swing) and activates on the devnet at DAA 33,000 the same evening. Timestamp forging (ledger M23) fixed the same day: a 50% forger drifts the rate under 1.1% where the 3 October rule gave it a 9.9x difficulty. Simulator, settled seconds: x50 step 62 to 66 (Kaspa 1,542), /50 step 657 to 753 (Kaspa 12,296). Apple M5 Max under load 7 to 442; the DAG model is fitted on one scale; the pool hopper's 0.7-point excess over Kaspa's rule stays open</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">12</td><td class="claim">The difficulty rule recovers from a hashrate step within minutes, where Kaspa's sampled rule never settles. A step inside an epoch set the rule oscillating on the live devnet on 4 October 2026; rule v2 removes it in the simulator and on a test network and is built but not yet rolled out<div class="where">Spec 2.3; litepaper Speed (implied); bench page</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>e9328c6</code>, <code>abb5a5d</code> (attacks), <code>67bf226</code> (rule v2); fork <code>difficulty</code> branch (timestamp fix) and <code>devnet-v4</code> <code>a21ff239</code> (<code>difficulty_v2_activation_daa</code>, <code>REF_WINDOW_V2 = 600</code>); <code>sim/difficulty/sim.py --live</code></td><td>The live record <code>sim/difficulty/records/live-2026-10-04.csv</code> (8,090 headers, <code>pull_live.py</code>) and the hash-rate record beside it; <code>sim/difficulty/sim.py</code> on the synthetic set and the DAG replay; <code>sim/difficulty/attacks/attacks.py</code>; <code>sim/difficulty/testnet_v2.py</code> (3 nodes, activation at DAA 900); <code>cargo test --release -p kaspa-consensus --lib difficulty</code> (15 pass); bench-log "difficulty controller", "difficulty rule under attack", "timestamp attack fixed", "difficulty rule v2"</td><td>Live devnet v4, 4 October 2026 (UTC): a second RTX 5090 joining 7 minutes into an epoch (about 152 to 280 MH/s) hardened the difficulty 70M to 144M in 90 s and then swung by about a third for 40 minutes around the true level of 139M while the epoch-long reference lane carried the join; that card leaving for 4 minutes eased 116M to 67M and back to 106M; the epoch boundary with both PCs restarting took 152M to 77M in 3 minutes, after which the rule held within 1.3% per minute with no flips. Cause: the reference lane covered the whole epoch, so a mid-epoch step polluted it for the hour and the 25% trigger flipped on the short lane's noise. The DAG replay reproduces the record (std of log difficulty 0.115 against 0.134, 4.3 peaks against 4). Rule v2 (reference window 600 DAA) on the replay: std 0.026, 0 flips, mean 142.6M against 139M true; on a 3-node test network the v2 nodes eased a leave with no peak and held a rejoin within 3% after 60 s, and a node without the activation height forked off at it as designed. Rule v2 rolled onto the 12-node cloud network on 4 October (all nodes crossed the height on one chain; a hash-rate step then settled in 160 to 270 s with no swing) and activates on the devnet at DAA 33,000 the same evening. Timestamp forging (ledger M23) fixed the same day: a 50% forger drifts the rate under 1.1% where the 3 October rule gave it a 9.9x difficulty. Simulator, settled seconds: x50 step 62 to 66 (Kaspa 1,542), /50 step 657 to 753 (Kaspa 12,296). Apple M5 Max under load 7 to 442; the DAG model is fitted on one scale; the pool hopper's 0.7-point excess over Kaspa's rule stays open</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">13</td><td class="claim">Every node executes the ordered transactions natively and reaches the same state root<div class="where">Litepaper Proving ("Every node executes ... natively"), Building ("runs on Igneum unchanged")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>f5f8c80</code>, <code>8dae48b</code>; fork <code>devnet-v4</code> <code>dc749905</code>; revm 43.0.3</td><td><code>node tools/evm-smoke/smoke.mjs</code> against a 3-node <code>igneumd</code>; <code>igneum-exec-diff seq.json</code>; bench-log "execution layer devnet v3" and "devnet-v4 integration"</td><td>Simnet, 3 October 2026: 87 of 87 viem checks, state roots identical on 3 nodes at four heights, 57 executed and 19 skipped transactions agree with plain revm, 0 mismatches. Merged node on real proof of work, 4 October 2026: 84 of 85 checks (the miss needs parallel blocks the network did not produce in 36 s), 59 transfers in 10 chain blocks, state roots identical on 3 nodes, <code>igneum-exec-diff</code> 0 mismatches over 59 transactions; the live devnet v4 runs this execution layer. Apple M5 Max. The prover is a stub; state is rebuilt from genesis at start; no EVM transaction relay between nodes</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">13</td><td class="claim">Every node executes the ordered transactions natively and reaches the same state root<div class="where">Litepaper Proving ("Every node executes ... natively"), Building ("runs on Igneum unchanged")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>f5f8c80</code>, <code>8dae48b</code>; fork <code>devnet-v4</code> <code>dc749905</code>; revm 43.0.3</td><td><code>node tools/evm-smoke/smoke.mjs</code> against a 3-node <code>igneumd</code>; <code>igneum-exec-diff seq.json</code>; bench-log "execution layer devnet v3" and "devnet-v4 integration"</td><td>Simnet, 3 October 2026: 87 of 87 viem checks, state roots identical on 3 nodes at four heights, 57 executed and 19 skipped transactions agree with plain revm, 0 mismatches. Merged node on real proof of work, 4 October 2026: 84 of 85 checks (the miss needs parallel blocks the network did not produce in 36 s), 59 transfers in 10 chain blocks, state roots identical on 3 nodes, <code>igneum-exec-diff</code> 0 mismatches over 59 transactions; the live devnet v4 runs this execution layer. Apple M5 Max. The prover is a stub; state is rebuilt from genesis at start; no EVM transaction relay between nodes</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">14</td><td class="claim">Ethereum bytecode runs unchanged, with the documented differences of spec 7.1<div class="where">Homepage Build card; litepaper Building</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">as row 13; fixes <code>F-exec-A</code>, <code>F-exec-B</code> (spec 7.5)</td><td><code>tools/evm-smoke/smoke.mjs</code>: deploy via viem, <code>increment</code>, <code>hashLoop</code>, <code>eth_estimateGas</code>, <code>eth_getLogs</code>; <code>tools/exec-attacks</code> scenarios 1 and 3; bench-log "execution layer attack fixes"</td><td>Deployment, calls, reverts, logs and gas estimates behave as viem expects; chain id 4463; the prototype pgas table gives 0.0095 to 0.028 pgas per gas, below the design's band before calibration, 3 October 2026. 4 October 2026: a transaction that would cross the block's proving budget is refused by the mempool and, if forced in, aborted and charged with its nonce advanced (25 of 25 checks; 30 of 30 malformed cases). Apple M5 Max. The <code>Prover</code> precompile, proof records and the shard planner are not in the node</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">14</td><td class="claim">Ethereum bytecode runs unchanged, with the documented differences of spec 7.1<div class="where">Homepage Build card; litepaper Building</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">as row 13; fixes <code>F-exec-A</code>, <code>F-exec-B</code> (spec 7.5)</td><td><code>tools/evm-smoke/smoke.mjs</code>: deploy via viem, <code>increment</code>, <code>hashLoop</code>, <code>eth_estimateGas</code>, <code>eth_getLogs</code>; <code>tools/exec-attacks</code> scenarios 1 and 3; bench-log "execution layer attack fixes"</td><td>Deployment, calls, reverts, logs and gas estimates behave as viem expects; chain id 4463; the prototype pgas table gives 0.0095 to 0.028 pgas per gas, below the design's band before calibration, 3 October 2026. 4 October 2026: a transaction that would cross the block's proving budget is refused by the mempool and, if forced in, aborted and charged with its nonce advanced (25 of 25 checks; 30 of 30 malformed cases). Apple M5 Max. The <code>Prover</code> precompile, proof records and the shard planner are not in the node</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="implemented"><td class="n">15</td><td class="claim">Every block is proven, with the proof landing within about a minute at launch<div class="where">Homepage stats ("~60 s to a proof"); litepaper Proving; roadmap phase 3 gate</div></td><td><span class="st st-1">implemented</span></td><td class="mono">repo <code>d7e1f89</code> (GPU proof), <code>e01a3cc</code>, <code>292e800</code>, <code>eedd136</code> (<code>proving/igneum-prove</code>: shard cutter, MPT witnesses, shard and aggregator guests); SP1 6.8.1; spec 7.2, 7.6</td><td><code>proving/windows-wsl2</code> (SETUP-PROVER, PROVE-BLOCK) on the RTX 5090; <code>igneum-prove-host --mode block</code> on <code>proving/fixtures/</code>; bench-log "proving v0 on the RTX 5090" and "proving: devnet v4 shards"</td><td>First GPU proof of an Igneum block, 4 October 2026, RTX 5090 (WSL2, SP1 cuda, mining paused): fixture <code>block-78-increment</code> (2 transactions), core proof 1.4 s (7.3 MB, verify 0.221 s), compressed proof 2.7 s (1.27 MB, verify 0.038 s), post-state and receipts roots identical to the node's; 15.7x and 20.6x faster than a loaded M5 Max CPU. The same day on that CPU (load 38 to 47): a three-shard block proved shard by shard and aggregated by recursion, 19 min (1,139 s) end to end, 245 to 337 s per compressed shard proof, every proof verified. What is not there: no proof is produced, carried or checked on the chain (the devnet prover is a stub that signs claims), the proving pool pays nobody (row 21), the block proven is far below one shard, and the 60-second figure remains a design target; the pass mark is the standard in <code>docs/benchmarks/proving-e2e.md</code>. Second RTX 5090 run, 4 October 2026 evening (job run-20261004-173115): a full shard at the provisional S_p (6.75 M pgas, 60.8 M cycles) executed in 1.63 s, core proof 8.3 s (18.1 MB), compressed proof 10.9 s (1.27 MB, verify 0.040 s); a two-shard block (13.5 M pgas) proved shard by shard (11.7 s and 10.0 s) and aggregated in 2.2 s, 24 s of GPU stages end to end, every proof verified, six tampered witnesses rejected. The two host defects (an abort after the upload, an idle wait that turned out to be an unbuffered 18 MB proof save through the WSL2 file bridge, 24 minutes) are fixed (ledger P20)</td><td class="iv">none yet</td></tr>
|
<tr data-status="implemented"><td class="n">15</td><td class="claim">Every block is proven, with the proof landing within about a minute at launch<div class="where">Homepage stats ("~60 s to a proof"); litepaper Proving; roadmap phase 3 gate</div></td><td><span class="st st-1">implemented</span></td><td class="mono">repo <code>d7e1f89</code> (GPU proof), <code>e01a3cc</code>, <code>292e800</code>, <code>eedd136</code> (<code>proving/igneum-prove</code>: shard cutter, MPT witnesses, shard and aggregator guests); SP1 6.8.1; spec 7.2, 7.6</td><td><code>proving/windows-wsl2</code> (SETUP-PROVER, PROVE-BLOCK) on the RTX 5090; <code>igneum-prove-host --mode block</code> on <code>proving/fixtures/</code>; bench-log "proving v0 on the RTX 5090" and "proving: devnet v4 shards"</td><td>First GPU proof of an Igneum block, 4 October 2026, RTX 5090 (WSL2, SP1 cuda, mining paused): fixture <code>block-78-increment</code> (2 transactions), core proof 1.4 s (7.3 MB, verify 0.221 s), compressed proof 2.7 s (1.27 MB, verify 0.038 s), post-state and receipts roots identical to the node's; 15.7x and 20.6x faster than a loaded M5 Max CPU. The same day on that CPU (load 38 to 47): a three-shard block proved shard by shard and aggregated by recursion, 19 min (1,139 s) end to end, 245 to 337 s per compressed shard proof, every proof verified. What is not there: no proof is produced, carried or checked on the chain (the devnet prover is a stub that signs claims), the proving pool pays nobody (row 21), the block proven is far below one shard, and the 60-second figure remains a design target; the pass mark is the standard in <code>docs/benchmarks/proving-e2e.md</code>. Second RTX 5090 run, 4 October 2026 evening (job run-20261004-173115): a full shard at the provisional S_p (6.75 M pgas, 60.8 M cycles) executed in 1.63 s, core proof 8.3 s (18.1 MB), compressed proof 10.9 s (1.27 MB, verify 0.040 s); a two-shard block (13.5 M pgas) proved shard by shard (11.7 s and 10.0 s) and aggregated in 2.2 s, 24 s of GPU stages end to end, every proof verified, six tampered witnesses rejected. The two host defects (an abort after the upload, an idle wait that turned out to be an unbuffered 18 MB proof save through the WSL2 file bridge, 24 minutes) are fixed (ledger P20) 5 October 2026, live devnet with real transactions (bench-log "real transactions, the first non-empty shard proven and paid"): block 72704 shard 0, 29 transfers, 5,800 pgas, proven on PC 2 in 34 s, verified on the Apple M5 Max in 0.297 s and paid 1.7623 IGN, 53 s after the chain block executed; of about 1,400 blocks in the 20-minute window 36 were proven (the one prover takes the newest shard assigned to it), so "every block" is not yet true; a second content shard (72803, all copies skipped) failed the native-execution veto on the exporter's block structure, fixed with fixtures the same day, the node side pending the 0.3.9 rollout 5 October 2026, evening (bench-log "proving v1"): the aggregated segment record, the chain rule and the unproven rule are implemented behind <code>proving_v1_activation_daa</code> (branch proving-v1, not on the devnet before 0.3.11); on the RTX 5090 a chain of 8 consecutive live blocks proved and aggregated by recursion in 135.6 s with the miner on the card (17 s a block, one proof of 1,272,909 bytes attesting all 8, verified in 0.04 s); the 3-node fast-time harness paid a segment record 1.0 s after submission and refused a late one after its deadline (21 checks); the devnet itself, with one prover, carried proofs for 2.4% of blocks over 30 minutes at a block-to-record latency p50 44 s, p99 52 s. The "within about a minute" holds per proven block; "every block" needs 18 mining 5090s or 6 proving-only cards at empty blocks on the measured rates, and the mandatory rule stays off until the share is one</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="designed"><td class="n">16</td><td class="claim">A 12 GB card proves one shard in about 20 s<div class="where">Litepaper Proving ("The proving budget"); roadmap gate 2</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.1 (Target), 7.6 (<code>S_p</code> provisional, 7,500,000 pgas = <code>B_p</code> / 4)</td><td><code>PROVE-SHARD.bat</code> on the RTX 5090 (pending); the end-to-end standard in <code>docs/benchmarks/proving-e2e.md</code>; bench-log "proving: devnet v4 shards"</td><td>Measured on a 32 GB card, not yet on a 12 GB card. A shard at the provisional <code>S_p</code> is 60.8 M SP1 cycles on the prototype pgas table (9 cycles per pgas, 44 per EVM gas; the modexp entry about 100x its SP1 cost); on an RTX 5090 (4 October 2026 evening, job run-20261004-173115) it executed in 1.63 s and its compressed proof took 10.9 s, verified in 0.040 s, so the 32 GB card is inside the 20 s target with margin. Whether a 12 GB card proves it at all, and in what time, is the next measurement (an RTX 3060 and an RTX 5060 Ti 16 GB are on order). A per-shard time can be met by shrinking the shard, so the project does not use it as a pass mark</td><td class="iv">none yet</td></tr>
|
<tr data-status="designed"><td class="n">16</td><td class="claim">A 12 GB card proves one shard in about 20 s (WITHDRAWN 5 October 2026: a 24 GB card proves a full shard at the adopted size in 4.3 s; 32 GB mines and proves)<div class="where">Litepaper Proving ("The proving budget"); roadmap gate 2</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.1 (Target), 7.6 (<code>S_p</code> provisional, 7,500,000 pgas = <code>B_p</code> / 4)</td><td><code>PROVE-SHARD.bat</code> on the RTX 5090 (pending); the end-to-end standard in <code>docs/benchmarks/proving-e2e.md</code>; bench-log "proving: devnet v4 shards"</td><td>Measured on a 32 GB card, not yet on a 12 GB card. A shard at the provisional <code>S_p</code> is 60.8 M SP1 cycles on the prototype pgas table (9 cycles per pgas, 44 per EVM gas; the modexp entry about 100x its SP1 cost); on an RTX 5090 (4 October 2026 evening, job run-20261004-173115) it executed in 1.63 s and its compressed proof took 10.9 s, verified in 0.040 s, so the 32 GB card is inside the 20 s target with margin. Whether a 12 GB card proves it at all, and in what time, is the next measurement (an RTX 3060 and an RTX 5060 Ti 16 GB are on order). A per-shard time can be met by shrinking the shard, so the project does not use it as a pass mark 5 October 2026, evening (bench-log "proving v1", the S_p curve): measured on the RTX 5090 with SP1 6.8.1's GPU prover, the card to itself, 1-s nvidia-smi samples: an empty shard 13,874 MiB and 2.2 s; a full shard at the ADOPTED v1 budget (30,000 pgas, 4.7 M cycles) 20,434 MiB and 4.3 s; the full prototype shard (6.75 M pgas, 60 M cycles) 28,307 MiB and 10.8 s; beside the miner 15,670 and 30,039 MiB. No environment knob of SP1 moves the 13.9 GB floor and the GPU server has no options of its own, so on this build a 12 GB card proves nothing, a 16 GB card only empty shards, a 24 GB card the adopted full shard alone and beside the miner (22,210 MiB and 13.2 s, measured on the 32 GB card: the 5090's allocation pattern, not yet a run on a 24 GB card) and a 32 GB card the prototype shard beside the miner with 2.5 GB spare. The litepaper line now says so; the 12 GB gate returns when a prover build with a smaller floor is measured on a 12 GB card</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="designed"><td class="n">17</td><td class="claim">The chip resistance target: a chip gains under 2x over a GPU<div class="where">Litepaper Mining, "What Igneum does not claim"; homepage "no chip can be built for it"</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 0.2 (Target); O-1.17</td><td>Public benchmark with a leaderboard by card model and a standing bounty, January 2027 (O-1.17); the on-die-SRAM test on the RTX 5090 (R3.5)</td><td>A target, not a measurement. Review round 3 priced a recompute chip with the 256 MiB cache on die at about 2.4x, approximate, before the usual chip-versus-GPU integer gain; the design answer (cache larger than any die) is open (spec 1.16)</td><td class="iv">none yet</td></tr>
|
<tr data-status="designed"><td class="n">17</td><td class="claim">The chip resistance claim: the strongest recompute chip under 1x per chip against an RTX 5090; the stored-dataset chip 1.2x per chip and 5x to 9x per joule in the model (2.1x to 4.8x by the Ethash precedent); the latency-shadow lever, measured and in its gates, brings it to about 2x<div class="where">Homepage hero and litepaper abstract (draft (a) of <code>docs/plans/counter-asic-3-status.md</code> section 6, chosen 6 October 2026), litepaper "What Igneum does not claim"</div></td><td><span class="st st-0">tested by the team (the model), designed (the target)</span></td><td class="mono">program class v3 (Counter ASIC 2.0, 5 October 2026): branches ca2-v3 d233fa1 and after, ca2-mixer 1ab8b21, ca2-era 78c0ee4; <code>docs/analysis/chip-model-v3.md</code>, <code>docs/analysis/sram-mirror.md</code>, <code>docs/analysis/scratch-soundness.md</code></td><td>The m16 recompute model re-run on the measured v3 rates and verifier times; the on-die-cache chip row</td><td>The on-die-cache recompute chip against the RTX 5090's measured 136.1 MH/s: class v2 2.4x; class v3 (mixer x8) 0.31x bare, 0.92x with a 3x fixed-function allowance (approximate), 0.76x at equal silicon; margin 8% on the allowance, 9% on the budget. 5 October 2026, M5 Max, RTX 5090, RX 9070 XT. The 2x target is a target: no chip has been built; the bounty stands (O-1.17)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">18</td><td class="claim">The chip resistance measurements: the program is random-access bound, not bandwidth bound, and sits beyond a card's on-chip cache<div class="where">Litepaper Mining ("bound by memory bandwidth", to be corrected), vs RandomX "Measured so far"</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>aba248d</code>, <code>f2a1a64</code>, <code>4b95c5e</code></td><td>RTX 5090 dataset sweep 4 MiB to 1 GiB with <code>proto-cuda/host.cu</code>; bench-log "RTX 5090 first run" and "dataset sweep"</td><td>At 1 GiB: 228.1 Mhash/s, 23.7 G random loads/s, 94.9 GB/s useful against a 1,638 GB/s dataset fill; inside the 96 MiB L2 (4 and 64 MiB) 1,340 to 1,353 Mhash/s, about 5.8x faster; 104 against 128 loads per hash gives 228 against 185 Mhash/s, proportional. 3 October 2026, RTX 5090, Windows, CUDA 12.8, version 1 programs. Prototype dataset 1 GiB against 2 GB at genesis; a pure random-read microbenchmark (R3 chip designer, attack 2) has not run; the sweep has not been repeated on version 2</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">18</td><td class="claim">The chip resistance measurements: the program is latency-bound (random reads), not bandwidth-bound, on every card we own, and sits beyond a card's on-chip cache<div class="where">Litepaper Mining ("waits on memory latency, not on maths or bandwidth"), vs RandomX; the numbers page</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">readwidth e752fc7 (<code>docs/plans/read-width.md</code>), ca2-era 78c0ee4, ca2-cache 2de19e5 (<code>docs/plans/hot-table.md</code>)</td><td>The dependent-read probes at 32 to 1,024 MiB and the hash rate per class on the three cards; the latency-bound share = rate over the probe ceiling per load</td><td>Latency-bound share at the 1 GiB dataset: RTX 5090 0.96 (v2) and 1.01 (v3), RX 9070 XT 0.87 and 0.95, M5 Max 1.01 and 1.06; wider reads do not close the AMD gap (the 9070 XT does 2.4 G dependent reads per second at every width; the 5090 goes bandwidth-bound at 64 B, share 0.58); a 32 to 96 MiB hot table is not kept resident by any card while the dataset streams (g 0.80 to 0.87 in the added form). 5 October 2026</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">19</td><td class="claim">The lottery hash is sound as a hash: uniform output, deterministic, no out-of-bounds read, fuzzed<div class="where">Litepaper vs RandomX ("Every number above is measured and logged")</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>c52307e</code>, <code>58a5a63</code>, <code>b27da39</code>; <code>proto-metal/TESTS.md</code></td><td><code>proto-metal/igneum-bench --fuzz --edge --stats --determinism --memcheck</code>; <code>--fuzz 2000</code> on the version 2 generator; <code>igneum-census</code>; bench-log "hardening tests", the re-run on the memory-hard dataset, "generator version 2 adopted"</td><td>Version 1: 10,200 random programs, 1,305,600 hashes, 0 mismatches; 14 of 14 edge cases; bit frequency within 2.90 sigma, avalanche mean 31.99 to 32.04 of 32; deterministic fingerprint across 5 runs; every dataset read masked, 3 October 2026. Version 2, 4 October 2026: 2,000 random programs through the Metal cross-check, 8,000 warps, 0 mismatches, 128 loads per hash on every program; 20,000-program census, 5.2% rejected (4.1% static, 1.1% dynamic). Apple M5 Max. Statistics are not a security proof; the edge, stats and memcheck sections were not re-run on version 2 (they do not depend on the generator); the seed derivation review (O-1.4) is open; the fuzz set has run on Metal and the CPU only</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">19</td><td class="claim">The lottery hash is sound as a hash: uniform output, deterministic, no out-of-bounds read, fuzzed; class v3 bit-exact on the three vendors<div class="where">Litepaper vs RandomX ("Every number above is measured and logged"), the numbers page</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">ca2-mixer 1ab8b21 (<code>tests/mixer.rs</code>, <code>tests/scratch.rs</code>), ca2-era 78c0ee4, ca2-soundness a465881 (<code>docs/analysis/scratch-soundness.md</code>), <code>igneum-pow/tests/packs.rs</code></td><td>The crate suite (53 + 4 + 19 + 7), the Metal fuzz, edge, stats and determinism runs on the v3 construction, the pack vectors and 2^24 fingerprints on Metal, Apple OpenCL, the RTX 5090 and the RX 9070 XT, the 1,024-hash CPU re-check per card</td><td>Class v3 (mixer x8 + era): 200-program fuzz 200 of 200 on Metal, every tenth on Apple OpenCL; the pinned v3 packs 3/3 + 3/3 and 96 of 96 lanes on Metal and Apple OpenCL; the six era packs' fingerprints equal on the three vendors (PC 1 job run-ca2-era-pc1-20261005, 5 October 2026); the v2 exports byte-identical on the v3 crate; the final-class PC rows and the G2 re-check: job run-ca2-era-pc1b-20261005 (pending at the time of writing)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="implemented"><td class="n">20</td><td class="claim">No premine, no pre-sale, no allocation: every coin is minted by the schedule and every coin goes to the block producer (80%) and the proving pool (20%)<div class="where">Homepage stats and Economics tiles; litepaper Supply, Economics</div></td><td><span class="st st-1">implemented</span></td><td class="mono">repo <code>6ac80a3</code>; fork "igneum-node devnet v0"; <code>consensus/core/src/igneum.rs</code>, <code>coinbase.rs</code></td><td><code>cargo test -p kaspa-consensus-core igneum</code> (8 pass: subsidy table, ramp, split, cap) and <code>cargo test -p kaspa-consensus coinbase</code> (8 pass); <code>igneum-miner inspect 40</code>; bench-log "igneum-node devnet v0"</td><td>Coinbases on the devnet: 80/20 exact on 39 of 39 single-payee blocks, the 20% to the <code>igneum-proving-pool-v0</code> output; the per-second schedule sums to under the 4,000,000,000 cap by less than 100 coins; 3,168,808,781 units per DAA second in years 0 to 2, halving at 63,115,200 DAA s. 3 October 2026, Apple M5 Max. The devnet genesis carries no allocation; the mainnet genesis does not exist yet, so the claim is about the code and the stated rule, not a launch that has happened</td><td class="iv">none yet</td></tr>
|
<tr data-status="implemented"><td class="n">20</td><td class="claim">No premine, no pre-sale, no allocation: every coin is minted by the schedule and every coin goes to the block producer (80%) and the proving pool (20%)<div class="where">Homepage stats and Economics tiles; litepaper Supply, Economics</div></td><td><span class="st st-1">implemented</span></td><td class="mono">repo <code>6ac80a3</code>; fork "igneum-node devnet v0"; <code>consensus/core/src/igneum.rs</code>, <code>coinbase.rs</code></td><td><code>cargo test -p kaspa-consensus-core igneum</code> (8 pass: subsidy table, ramp, split, cap) and <code>cargo test -p kaspa-consensus coinbase</code> (8 pass); <code>igneum-miner inspect 40</code>; bench-log "igneum-node devnet v0"</td><td>Coinbases on the devnet: 80/20 exact on 39 of 39 single-payee blocks, the 20% to the <code>igneum-proving-pool-v0</code> output; the per-second schedule sums to under the 4,000,000,000 cap by less than 100 coins; 3,168,808,781 units per DAA second in years 0 to 2, halving at 63,115,200 DAA s. 3 October 2026, Apple M5 Max. The devnet genesis carries no allocation; the mainnet genesis does not exist yet, so the claim is about the code and the stated rule, not a launch that has happened</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="designed"><td class="n">21</td><td class="claim">The proving pool's 20% reaches shard provers and aggregators<div class="where">Litepaper Economics; homepage "20% provers"</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.3; <code>proving/igneum-prove</code> carries the prover's payout address in every shard proof (ledger P12)</td><td>None. The pool output exists (row 20); the payout from it against proof records is unwritten</td><td>The escrow accumulated on the simnet (92.55 IGN at the end of the v3 run) and nothing can draw it. Rule decided: per block, divided among shards by consensus proving cost, sortition to 8 provers for 10 s then open (spec 7.2). The economy model of 4 October 2026 (<code>sim/economy</code>, 1,000 operators, 30 days) kept every block proven within 60 s under six stress scenarios; a model, not hardware</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">21</td><td class="claim">The proving pool's 20% reaches shard provers and aggregators<div class="where">Litepaper Economics; homepage "20% provers"</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">spec 5.3; <code>proving/igneum-prove</code> carries the prover's payout address in every shard proof (ledger P12)</td><td>None. The pool output exists (row 20); the payout from it against proof records is unwritten. Since 5 October 2026: the payout rule is live on the devnet (<code>proving.rs shard_payouts</code>, the carrying segment pays the first valid record per shard its part of the segment's pool credit)</td><td>The escrow accumulated on the simnet (92.55 IGN at the end of the v3 run) and nothing can draw it. Rule decided: per block, divided among shards by consensus proving cost, sortition to 8 provers for 10 s then open (spec 7.2). The economy model of 4 October 2026 (<code>sim/economy</code>, 1,000 operators, 30 days) kept every block proven within 60 s under six stress scenarios; a model, not hardware Live devnet, 5 October 2026: 388 shards paid by 16:02 UTC, 446.13 IGN from the pool to PC 2's payout address, 0.8813 IGN per mergeset block of the proven segment (bench-log entries of 5 October: "the first shards proven, verified and paid" and "real transactions, the first non-empty shard proven and paid")</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">22</td><td class="claim">The base fee is burned in full and the priority fee splits 80% to the miner and provers, 20% to the apps whose code ran<div class="where">Homepage Economics caption and Build card; litepaper "Where fees go"</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>f5f8c80</code>; fork worktree <code>vendor/igneum-node-exec</code></td><td><code>tools/evm-smoke/smoke.mjs</code> receipt checks; bench-log "execution layer devnet v3"</td><td>Transfer receipt: <code>burnedProvingFee</code> 200 gwei, <code>minerTip</code> 16,800 gwei (80%), unregistered developer share 4,200 gwei burned; contract call: 80% to the miner, 20% credited to the payee the constructor registered, balance delta equal. 3 October 2026, Apple M5 Max simnet. The provers' part of the 80% is not split out (no provers exist); the base fee stayed at the 1 gwei floor throughout</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">22</td><td class="claim">The base fee is burned in full and the priority fee splits 80% to the miner and provers, 20% to the apps whose code ran<div class="where">Homepage Economics caption and Build card; litepaper "Where fees go"</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>f5f8c80</code>; fork worktree <code>vendor/igneum-node-exec</code></td><td><code>tools/evm-smoke/smoke.mjs</code> receipt checks; bench-log "execution layer devnet v3"</td><td>Transfer receipt: <code>burnedProvingFee</code> 200 gwei, <code>minerTip</code> 16,800 gwei (80%), unregistered developer share 4,200 gwei burned; contract call: 80% to the miner, 20% credited to the payee the constructor registered, balance delta equal. 3 October 2026, Apple M5 Max simnet. The provers' part of the 80% is not split out (no provers exist); the base fee stayed at the 1 gwei floor throughout</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="designed"><td class="n">23</td><td class="claim">No fee to any team, foundation or fund; 0 admin keys in consensus<div class="where">Homepage Economics tiles and caption; litepaper "No fund, no foundation" and Governance</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.5, 5.6 (decided 3 October 2026); spec 08</td><td>Reading: no coinbase output, fee route or consensus key in the fork names any party (<code>coinbase.rs</code>, <code>docs/fork-divergence.md</code>)</td><td>The emission code has two outputs (row 20) and the fee code has three routes (row 22), none to a team. The 1% fee of the official client is a client setting, not a protocol rule, and is not implemented. The release key of spec 08 signs client updates (the Igneum Miner app's over-the-air manifest since 4 October 2026, Ed25519) and holds no consensus power; its custody policy is open (O-8.1)</td><td class="iv">none yet</td></tr>
|
<tr data-status="designed"><td class="n">23</td><td class="claim">No fee to any team, foundation or fund; 0 admin keys in consensus<div class="where">Homepage Economics tiles and caption; litepaper "No fund, no foundation" and Governance</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.5, 5.6 (decided 3 October 2026); spec 08</td><td>Reading: no coinbase output, fee route or consensus key in the fork names any party (<code>coinbase.rs</code>, <code>docs/fork-divergence.md</code>)</td><td>The emission code has two outputs (row 20) and the fee code has three routes (row 22), none to a team. The 1% fee of the official client is a client setting, not a protocol rule, and is not implemented. The release key of spec 08 signs client updates (the Igneum Miner app's over-the-air manifest since 4 October 2026, Ed25519) and holds no consensus power; its custody policy is open (O-8.1)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="designed"><td class="n">24</td><td class="claim">External proving jobs pay 90% to the provers who delivered and burn 10%, once settled in IGN<div class="where">Homepage "IGN burned from jobs, phase two"; litepaper Proving and Economics</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.4</td><td>None. Needs the proof bridge (spec 7.3, phase two) and the settlement switch (O-5.2)</td><td>At launch jobs are paid on the customer's chain in the customer's currency and nothing is burned (ledger P10). No job market code exists</td><td class="iv">none yet</td></tr>
|
<tr data-status="designed"><td class="n">24</td><td class="claim">External proving jobs pay 90% to the provers who delivered and burn 10%, once settled in IGN<div class="where">Homepage "IGN burned from jobs, phase two"; litepaper Proving and Economics</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 5.4</td><td>None. Needs the proof bridge (spec 7.3, phase two) and the settlement switch (O-5.2)</td><td>At launch jobs are paid on the customer's chain in the customer's currency and nothing is burned (ledger P10). No job market code exists</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">25</td><td class="claim">The 4 billion cap, halving every two years, with a 30-day ramp from 10%<div class="where">Homepage "4B IGN hard cap"; litepaper Supply and the emission chart</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>6ac80a3</code>; fork <code>consensus/core/src/igneum.rs</code></td><td><code>cargo test -p kaspa-consensus-core igneum</code>; bench-log "igneum-node devnet v0"</td><td>Ramp day 0 paid 10.03% of the full rate (317,767,704 units at DAA 806); the schedule table and the cap assert in the crate's own tests. 3 October 2026, Apple M5 Max. Base unit (8 or 18 decimals) is open (O-2.6); the spec was changed to follow the code's 365.25-day year (ledger E9) and a test that reads the published numbers back is still owed</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">25</td><td class="claim">The 4 billion cap, halving every two years, with a 30-day ramp from 10%<div class="where">Homepage "4B IGN hard cap"; litepaper Supply and the emission chart</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>6ac80a3</code>; fork <code>consensus/core/src/igneum.rs</code></td><td><code>cargo test -p kaspa-consensus-core igneum</code>; bench-log "igneum-node devnet v0"</td><td>Ramp day 0 paid 10.03% of the full rate (317,767,704 units at DAA 806); the schedule table and the cap assert in the crate's own tests. 3 October 2026, Apple M5 Max. Base unit (8 or 18 decimals) is open (O-2.6); the spec was changed to follow the code's 365.25-day year (ledger E9) and a test that reads the published numbers back is still owed</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="designed"><td class="n">26</td><td class="claim">A phone or browser verifies the chain from a locked checkpoint, at about 3.44 MB per day in checkpoint mode<div class="where">Homepage "Browser checks Igneum" card; litepaper Building ("Light clients"), firsts row 6</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 10 (10.5 bytes per day: 3.44 MB at 1,000 voters, 6.68 MB at 10,000, derived, approximate); repo <code>f874f80</code> for the browser card; <code>site/api/checkpoint.mjs</code></td><td>None for the byte figure; <code>site/verify/</code> for the card against <code>/api/checkpoint</code>. BLS verification on a phone and in WebAssembly is O-10.3; the full-header mode on a phone is O-10.4</td><td>Since 12:03 BST on 4 October 2026 the homepage card verifies the live devnet's own certificates in the tab (index 522 with 27 voters at 13:42 UTC), BLS aggregate against the voter list the node serves, light client v0; before that it verified the 3 October test network's. The byte figure is arithmetic on designed sizes (header 400 bytes, proof 400 bytes), measured nowhere; the execution proof the card would also check is not on the chain (row 15)</td><td class="iv">none yet</td></tr>
|
<tr data-status="designed"><td class="n">26</td><td class="claim">A phone or browser verifies the chain from a locked checkpoint, at about 3.44 MB per day in checkpoint mode<div class="where">Homepage "Browser checks Igneum" card; litepaper Building ("Light clients"), firsts row 6</div></td><td><span class="st st-0">designed</span></td><td class="mono">spec 10 (10.5 bytes per day: 3.44 MB at 1,000 voters, 6.68 MB at 10,000, derived, approximate); repo <code>f874f80</code> for the browser card; <code>site/api/checkpoint.mjs</code></td><td>None for the byte figure; <code>site/verify/</code> for the card against <code>/api/checkpoint</code>. BLS verification on a phone and in WebAssembly is O-10.3; the full-header mode on a phone is O-10.4</td><td>Since 11:03 UTC on 4 October 2026 the homepage card verifies the live devnet's own certificates in the tab (index 522 with 27 voters at 13:42 UTC), BLS aggregate against the voter list the node serves, light client v0; before that it verified the 3 October test network's. The byte figure is arithmetic on designed sizes (header 400 bytes, proof 400 bytes), measured nowhere; the execution proof the card would also check is not on the chain (row 15)</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">27</td><td class="claim">The node survives malformed input, floods, withholding, partitions and eclipses<div class="where">Litepaper Speed ("GHOSTDAG, the BlockDAG consensus proven on Kaspa"); spec 2</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>394030c</code>, <code>8dae48b</code>, <code>6b5bd92</code>; fork worktree <code>vendor/igneum-node-harness</code> and <code>devnet-v4</code>; <code>tools/harness/</code>; <code>infra/cloud-devnet/experiments/partition.sh</code></td><td><code>tools/harness/</code> against a private <code>igneumd</code> test network; the merged node's harness scenarios 2 and 5; the cloud network's 10-minute partition of Singapore (<code>results/2026-10-04/partition-sin-20261004-110906/partition.md</code>); bench-log "consensus attack harness", "devnet-v4 integration"</td><td>3 October 2026, Apple M5 Max: 63 malformed cases, node up on every one; withholding at 10% to 45% within 2 sigma of share; partitions of 120 s to 3,700 s healed to one chain in 10 s; eclipse victims rejoined in 10 s; 50x floods left template p95 under 4 ms; one FAIL, a 45% withholder releasing every 20 blocks took 50.7% of blues (bound 47.4%). Merged node, 4 October 2026: 63 cases, node up, 0 cache builds; the 10 s timestamp floor and future bound exact. Cloud network, 4 October 2026: 12 nodes in five locations on their own chain, Singapore cut off by iptables for 10 minutes; the two minority nodes adopted the majority chain 10 and 14 s after the heal with reorgs of 445 and 516 blocks, the majority's deepest reorg was 2 blocks, 0 conflicting locks (none were possible: the weight window stood at DAA 3,030 of 7,200). CPU miners only; the finality rules under partition are row 10</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">27</td><td class="claim">The node survives malformed input, floods, withholding, partitions and eclipses<div class="where">Litepaper Speed ("GHOSTDAG, the BlockDAG consensus proven on Kaspa"); spec 2</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>394030c</code>, <code>8dae48b</code>, <code>6b5bd92</code>; fork worktree <code>vendor/igneum-node-harness</code> and <code>devnet-v4</code>; <code>tools/harness/</code>; <code>infra/cloud-devnet/experiments/partition.sh</code></td><td><code>tools/harness/</code> against a private <code>igneumd</code> test network; the merged node's harness scenarios 2 and 5; the cloud network's 10-minute partition of Singapore (<code>results/2026-10-04/partition-sin-20261004-110906/partition.md</code>); bench-log "consensus attack harness", "devnet-v4 integration"</td><td>3 October 2026, Apple M5 Max: 63 malformed cases, node up on every one; withholding at 10% to 45% within 2 sigma of share; partitions of 120 s to 3,700 s healed to one chain in 10 s; eclipse victims rejoined in 10 s; 50x floods left template p95 under 4 ms; one FAIL, a 45% withholder releasing every 20 blocks took 50.7% of blues (bound 47.4%). Merged node, 4 October 2026: 63 cases, node up, 0 cache builds; the 10 s timestamp floor and future bound exact. Cloud network, 4 October 2026: 12 nodes in five locations on their own chain, Singapore cut off by iptables for 10 minutes; the two minority nodes adopted the majority chain 10 and 14 s after the heal with reorgs of 445 and 516 blocks, the majority's deepest reorg was 2 blocks, 0 conflicting locks (none were possible: the weight window stood at DAA 3,030 of 7,200). CPU miners only; the finality rules under partition are row 10</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">28</td><td class="claim">Headers are validated cheaply before the lottery engine runs, so forged timestamps cannot force 256 MiB cache builds<div class="where">Spec 2.4; ledger M15</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>0953ec7</code>, <code>8dae48b</code>; fork worktree <code>vendor/igneum-node-r3</code> branch <code>r3-fixes</code> at <code>5166ee26</code>, merged into <code>devnet-v4</code></td><td><code>measure_m15_attack_before_and_after</code> (ignored test, release, <code>--features igneum-pow</code>); kaspa-pow 8, header_processor 1, p2p <code>pow_guard</code> 2 tests; harness scenario 5 on the merged node</td><td>50 forged headers: before, 50 cold builds in 10,595 ms and the live day evicted; after, 0 builds, all 50 rejected in 14 ms, 3 October 2026, Apple M5 Max under load 60 to 110. Merged node, 4 October 2026: 63 harness cases with 0 cache builds (the node log shows one build, the honest day) and the M15 p2p cases disconnected by the strike guard; the live devnet v4 runs it. Measured through the validate path with <code>skip_proof_of_work</code>, not the daemon RPC</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">28</td><td class="claim">Headers are validated cheaply before the lottery engine runs, so forged timestamps cannot force 256 MiB cache builds<div class="where">Spec 2.4; ledger M15</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>0953ec7</code>, <code>8dae48b</code>; fork worktree <code>vendor/igneum-node-r3</code> branch <code>r3-fixes</code> at <code>5166ee26</code>, merged into <code>devnet-v4</code></td><td><code>measure_m15_attack_before_and_after</code> (ignored test, release, <code>--features igneum-pow</code>); kaspa-pow 8, header_processor 1, p2p <code>pow_guard</code> 2 tests; harness scenario 5 on the merged node</td><td>50 forged headers: before, 50 cold builds in 10,595 ms and the live day evicted; after, 0 builds, all 50 rejected in 14 ms, 3 October 2026, Apple M5 Max under load 60 to 110. Merged node, 4 October 2026: 63 harness cases with 0 cache builds (the node log shows one build, the honest day) and the M15 p2p cases disconnected by the strike guard; the live devnet v4 runs it. Measured through the validate path with <code>skip_proof_of_work</code>, not the daemon RPC</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">29</td><td class="claim">Blocks reach every node well inside GHOSTDAG's delay bound across continents<div class="where">Litepaper Speed (GHOSTDAG at one block a second); spec 03 C1 (lock latency); <code>infra/cloud-devnet/README.md</code></div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>6b5bd92</code>; <code>infra/cloud-devnet/experiments/latency.sh</code>, <code>analyze.py</code>; the Linux cross-build <code>infra/cross/build-linux.sh</code></td><td>12 <code>igneumd</code> nodes on Hetzner VMs in Helsinki, Falkenstein, Ashburn, Hillsboro and Singapore (own chain <code>igneum-devnet-20</code>, one CPU trickle miner each), a ping matrix, then 10 minutes of per-node arrival logs joined on block hash; <code>results/2026-10-04/latency/propagation.md</code> and <code>rtt-by-region.md</code></td><td>644 blocks in the window, 642 seen by at least 80% of nodes; arrival at a node minus the first arrival anywhere: p50 343 ms, p90 497 ms, p99 666 ms, max 2,313 ms; by region p50 239 ms (Falkenstein) to 413 ms (Singapore), p90 455 to 632 ms; inter-region RTT 35 ms (Helsinki to Falkenstein) to 289 ms (Ashburn to Singapore); first arrival minus header time median 490 ms. 4 October 2026. The network is the project's own: 12 nodes not 20 (a new account's limits), CPU hash rate only, clocks by chrony, one evening of data; the 5 s bound behind GHOSTDAG k is a design parameter this run did not challenge</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">29</td><td class="claim">Blocks reach every node well inside GHOSTDAG's delay bound across continents<div class="where">Litepaper Speed (GHOSTDAG at one block a second); spec 03 C1 (lock latency); <code>infra/cloud-devnet/README.md</code></div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>6b5bd92</code>; <code>infra/cloud-devnet/experiments/latency.sh</code>, <code>analyze.py</code>; the Linux cross-build <code>infra/cross/build-linux.sh</code></td><td>12 <code>igneumd</code> nodes on cloud VMs in Helsinki, Falkenstein, Ashburn, Hillsboro and Singapore (own chain <code>igneum-devnet-20</code>, one CPU trickle miner each), a ping matrix, then 10 minutes of per-node arrival logs joined on block hash; <code>results/2026-10-04/latency/propagation.md</code> and <code>rtt-by-region.md</code></td><td>644 blocks in the window, 642 seen by at least 80% of nodes; arrival at a node minus the first arrival anywhere: p50 343 ms, p90 497 ms, p99 666 ms, max 2,313 ms; by region p50 239 ms (Falkenstein) to 413 ms (Singapore), p90 455 to 632 ms; inter-region RTT 35 ms (Helsinki to Falkenstein) to 289 ms (Ashburn to Singapore); first arrival minus header time median 490 ms. 4 October 2026. The network is the project's own: 12 nodes not 20 (a new account's limits), CPU hash rate only, clocks by chrony, one evening of data; the 5 s bound behind GHOSTDAG k is a design parameter this run did not challenge</td><td class="iv">none yet</td></tr>
|
||||||
<tr data-status="tested by the team"><td class="n">30</td><td class="claim">One click: install, press start, the card mines; the app looks after its node<div class="where">Homepage Mine section ("One click: install, press start"); litepaper "One click, for everyone else"; journey phase 5</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>3bb50d6</code>, <code>2c4b30f</code>, <code>6461540</code> (package 0.3.0: prebuilt NVRTC CUDA worker and generic OpenCL worker, driver only), <code>a1a33cb</code>, <code>7c794df</code>, <code>0d4498e</code>, <code>6c083db</code> (Igneum Miner 0.3.0), <code>78903cd</code> (0.3.1, over-the-air updates)</td><td><code>Igneum-Miner-Setup-0.3.0.exe</code> (runner-built, unsigned) on a Windows PC with an RTX 5090 and no toolchain; <code>proto-cuda/nvrtc/emu/serve-check.sh</code> on the Mac; <code>proto-cuda/windows-app/TEST.md</code>; bench-log "one-click Windows workers", "first machine on the Igneum Miner app", "a node 60 s behind the clock is silently dead", "the gfx1036 worker fault"</td><td>Four machines by 15:45 BST on 4 October 2026: PC 2, then PC 1 (RTX 5090 at 110 MH/s under the 80% power cap), the project's Apple M5 Max (25 MH/s) and the outside Apple silicon laptop (row 29), all on Igneum Miner 0.3.1. The NVRTC worker compiled the pack on the card with no toolchain installed and mined at 124.2 MH/s, equal to the nvcc-built worker, 0 rejected, CPU re-check clean; inside the app 117 to 119 MH/s with 34 accepted blocks in the first minute, the integrated AMD chip at 3.3 MH/s beside it (row 9). Two defects found by the install, both fixed the same hour: a clock 62 s slow after a power cut made the node reject every relayed block for 12 minutes with no visible reason (the app now reads the skew from the node's warnings, the block timestamps over the EVM RPC and an HTTPS Date header, warns over 5 s and blocks Start over 10 s, with a one-click clock sync; checked on the Mac with a fake 60 s skew; a one-line node warning is filed), and the node card said "syncing" while the miner was already accepted. The Mac could only emulate the NVIDIA path (17 of 17 sampled hashes) and the AMD path on Apple OpenCL (15 of 15). Over-the-air updates were dry-run on a private devnet (0.3.0 to 0.3.1 and back), not on a user's machine. The installer is unsigned (SmartScreen "run anyway"). Second machine, the same afternoon: a friend of the project installed Igneum Miner 0.3.1 from the DMG on an Apple silicon laptop with no toolchain and no instructions beyond five steps; the node synced from the seed, the Metal worker reported ready, 33 accepted blocks and 0 rejected in 7 minutes at 21.0 MH/s average, CPU re-check OK on every share, uploads arriving every minute under its per-install id. That laptop is not the project's hardware, but the result is observed through the project's own log intake and reported by the project, so it stays tested by the team until an outsider publishes a run of their own. The devnet's other GPU machines (PC 1 and the Mac) run the same workers through the launcher, not the app</td><td class="iv">none yet</td></tr>
|
<tr data-status="tested by the team"><td class="n">30</td><td class="claim">One click: install, press start, the card mines; the app looks after its node<div class="where">Homepage Mine section ("One click: install, press start"); litepaper "One click, for everyone else"; journey phase 5</div></td><td><span class="st st-2">tested by the team</span></td><td class="mono">repo <code>3bb50d6</code>, <code>2c4b30f</code>, <code>6461540</code> (package 0.3.0: prebuilt NVRTC CUDA worker and generic OpenCL worker, driver only), <code>a1a33cb</code>, <code>7c794df</code>, <code>0d4498e</code>, <code>6c083db</code> (Igneum Miner 0.3.0), <code>78903cd</code> (0.3.1, over-the-air updates)</td><td><code>Igneum-Miner-Setup-0.3.0.exe</code> (runner-built, unsigned) on a an RTX 5090 on Windows with an RTX 5090 and no toolchain; <code>proto-cuda/nvrtc/emu/serve-check.sh</code> on the Apple M5 Max; <code>proto-cuda/windows-app/TEST.md</code>; bench-log "one-click Windows workers", "first machine on the Igneum Miner app", "a node 60 s behind the clock is silently dead", "the gfx1036 worker fault"</td><td>Four machines by 14:45 UTC on 4 October 2026: PC 2, then PC 1 (RTX 5090 at 110 MH/s under the 80% power cap), the project's Apple M5 Max (25 MH/s) and the outside Apple silicon laptop (row 29), all on Igneum Miner 0.3.1. The NVRTC worker compiled the pack on the card with no toolchain installed and mined at 124.2 MH/s, equal to the nvcc-built worker, 0 rejected, CPU re-check clean; inside the app 117 to 119 MH/s with 34 accepted blocks in the first minute, the integrated AMD chip at 3.3 MH/s beside it (row 9). Two defects found by the install, both fixed the same hour: a clock 62 s slow after a power cut made the node reject every relayed block for 12 minutes with no visible reason (the app now reads the skew from the node's warnings, the block timestamps over the EVM RPC and an HTTPS Date header, warns over 5 s and blocks Start over 10 s, with a one-click clock sync; checked on the Apple M5 Max with a fake 60 s skew; a one-line node warning is filed), and the node card said "syncing" while the miner was already accepted. The Mac could only emulate the NVIDIA path (17 of 17 sampled hashes) and the AMD path on Apple OpenCL (15 of 15). Over-the-air updates were dry-run on a private devnet (0.3.0 to 0.3.1 and back), not on a user's machine. The installer is unsigned (SmartScreen "run anyway"). Second machine, the same afternoon: a friend of the project installed Igneum Miner 0.3.1 from the DMG on an Apple silicon laptop with no toolchain and no instructions beyond five steps; the node synced from the seed, the Metal worker reported ready, 33 accepted blocks and 0 rejected in 7 minutes at 21.0 MH/s average, CPU re-check OK on every share, uploads arriving every minute under its per-install id. That laptop is not the project's hardware, but the result is observed through the project's own log intake and reported by the project, so it stays tested by the team until an outsider publishes a run of their own. The devnet's other GPU machines (PC 1 and the Apple M5 Max) run the same workers through the launcher, not the app</td><td class="iv">none yet</td></tr>
|
||||||
|
<tr data-status="designed"><td class="n">31</td><td class="claim">Card lifetime: a 4 GB card mines about four years and an 8 GB card about twelve, under the dataset's step schedule (2 GB at genesis, doubling at years 4, 12, 28, 60) with the cache freed after the daily build<div class="where">Litepaper Hardware and vs RandomX ("Dataset" row); homepage Mine card and "Memory" row</div></td><td><span class="st st-0">designed</span></td><td class="mono"><code>docs/analysis/card-lifetime-2026-10-05.md</code> (branch card-lifetime 1fecfe2); spec 1.13.3 option (b) recommended to the owner 5 October 2026 (<code>docs/plans/counter-asic-2-rollout.md</code> 6c)</td><td>The per-tier working-set arithmetic of that document (GTX 1650, RTX 3050, RTX 3060, RTX 4090 tiers) against the step schedule</td><td>A design claim: under the continuous mapping (a) a 4 GB card is out within 1 to 1.5 years and an 8 GB card at 6 to 7.5 years, so the sentence is true only under the step schedule (b), which the spec has not yet fixed (O-1.13)</td><td class="iv">none yet</td></tr>
|
||||||
</tbody></table></div>
|
</tbody></table></div>
|
||||||
<p class="note">Click a column heading to sort; click again to reverse. Versions: <code>igneum-pow</code> is the Rust crate at version 0.2.0 (generator version 2, 4 October 2026); repo commits are this repository's; fork commits are the node fork and its worktrees, named by message as the engineering log names them. Source of every number: the <a href="/bench">engineering log</a>. The source of this page is <code>docs/evidence.md</code> in the repository.</p>
|
<p class="note">Click a column heading to sort; click again to reverse. Versions: <code>igneum-pow</code> is the Rust crate at version 0.2.0 (generator version 2, 4 October 2026); repo commits are this repository's; fork commits are the node fork and its worktrees, named by message as the engineering log names them. Source of every number: the <a href="/bench">engineering log</a>. The source of this page is <code>docs/evidence.md</code> in the repository.</p>
|
||||||
<h2>What would move a row</h2>
|
<h2>What would move a row</h2>
|
||||||
|
|
@ -231,7 +232,7 @@ code{font-family:var(--f-mono);font-size:.92em;background:var(--obsidian);paddin
|
||||||
<tr><td>reproduced externally</td><td>reviewed independently</td><td>A named reviewer's published finding on that version. Funding for review is <code>docs/plans/funding.md</code></td></tr>
|
<tr><td>reproduced externally</td><td>reviewed independently</td><td>A named reviewer's published finding on that version. Funding for review is <code>docs/plans/funding.md</code></td></tr>
|
||||||
<tr><td>any</td><td>the row's status falls back</td><td>A new version of the code or rule the row names</td></tr>
|
<tr><td>any</td><td>the row's status falls back</td><td>A new version of the code or rule the row names</td></tr>
|
||||||
</tbody></table></div>
|
</tbody></table></div>
|
||||||
<p class="asof">Statuses are honest as of 4 October 2026 and change only through this page.</p>
|
<p class="asof">Statuses are honest as of 6 October 2026, the day this page was generated from docs/evidence.md, and change only through that file.</p>
|
||||||
</main>
|
</main>
|
||||||
<!-- footer:start -->
|
<!-- footer:start -->
|
||||||
<footer class="foot">
|
<footer class="foot">
|
||||||
|
|
@ -249,6 +250,7 @@ code{font-family:var(--f-mono);font-size:.92em;background:var(--obsidian);paddin
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,7 @@ main{padding-bottom:var(--sec)}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ dt{color:var(--ash)}dd{margin:0;font-family:var(--f-mono);font-size:14px;overflo
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
1286
site/ledger.html
Normal file
1286
site/ledger.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -298,7 +298,7 @@ body.all .pager{display:none}
|
||||||
<article>
|
<article>
|
||||||
<section id="abstract">
|
<section id="abstract">
|
||||||
<h2>Abstract</h2>
|
<h2>Abstract</h2>
|
||||||
<p class="lead">Igneum is a proof-of-work blockchain built for graphics cards, where NVIDIA cards also prove every block with zero-knowledge proofs and sell proving to other chains. A custom chip gains under 2x, and the model is public; a bounty follows the external review.</p>
|
<p class="lead">Igneum is a proof-of-work blockchain built for graphics cards, where NVIDIA cards also prove every block with zero-knowledge proofs and sell proving to other chains. The strongest recompute chip we can price, holding the whole 256 MiB cache on-die, reaches under 1x per chip against an RTX 5090. A memory-controller chip that stores the whole dataset reaches 1.2x per chip and, in our model, 5x to 9x per joule; the Ethash chips of this class reached 2.1x to 4.8x. The lever against it, program work in the latency shadow, is measured and in its gates: it brings the chip to about 2x. Sources: the chip model (<code>docs/analysis/chip-model-v3.md</code> section 5, 6 October 2026); the Ethash rows of the ASIC history (<code>docs/analysis/asic-resistance-history.md</code>, Linzhi Phoenix 2020, Jasminer X4 2021, Antminer E9 2022); Counter ASIC 3.0 item 8 (100,000 ops per hash: the chip's per-joule edge over the RTX 5090 falls from 5.6x to 2.1x on GDDR7 at a chip core equal to the GPU's, the 5090 at 0.2% less rate, gates G1 to G6 in progress). The model is public; the claim is tested by paid independent cryptanalysis and the public benchmark.</p>
|
||||||
<p>It runs the Ethereum virtual machine, so anything built for Ethereum runs on Igneum unchanged. Transactions are included in about one second, proven within about a minute at launch, and locked by miners within about two. There is no premine, no pre-sale, no treasury taken from emission, no stake anywhere in consensus, and no dependence on any other chain. Mining stays open to anyone with a GPU because the mining program changes every hour, so a chip built for one program is useless for the next, and a chip for the whole program space is a GPU without the graphics parts. No scheduled human release is needed to keep it that way. Writing new code, including an emergency fix to the proof system, is the one thing that takes a person, and it activates only on miner signalling.</p>
|
<p>It runs the Ethereum virtual machine, so anything built for Ethereum runs on Igneum unchanged. Transactions are included in about one second, proven within about a minute at launch, and locked by miners within about two. There is no premine, no pre-sale, no treasury taken from emission, no stake anywhere in consensus, and no dependence on any other chain. Mining stays open to anyone with a GPU because the mining program changes every hour, so a chip built for one program is useless for the next, and a chip for the whole program space is a GPU without the graphics parts. No scheduled human release is needed to keep it that way. Writing new code, including an emergency fix to the proof system, is the one thing that takes a person, and it activates only on miner signalling.</p>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat"><div class="v">1 / s</div><div class="k">blocks, rising to 10</div></div>
|
<div class="stat"><div class="v">1 / s</div><div class="k">blocks, rising to 10</div></div>
|
||||||
|
|
@ -393,11 +393,11 @@ body.all .pager{display:none}
|
||||||
<thead><tr><th>Layer</th><th>State</th><th>Since</th></tr></thead>
|
<thead><tr><th>Layer</th><th>State</th><th>Since</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Mining lottery</td><td>A new program every hour on Apple, NVIDIA and AMD cards, compiled ahead, no pause and no rejected block at the boundary</td><td class="num">4 Oct 2026, first live swap</td></tr>
|
<tr><td>Mining lottery</td><td>A new program every hour on Apple, NVIDIA and AMD cards, compiled ahead, no pause and no rejected block at the boundary</td><td class="num">4 Oct 2026, first live swap</td></tr>
|
||||||
<tr><td>Blocks</td><td>About one a second</td><td class="num">genesis, 3 Oct 2026</td></tr>
|
<tr><td>Blocks</td><td>About one a second with the full fleet; 0.65 a second over the hour to 16:00 UTC on 6 Oct 2026 with one PC off (the live page's hour count, 2,325 blocks)</td><td class="num">genesis, 3 Oct 2026</td></tr>
|
||||||
<tr><td>Difficulty</td><td>Rule v2, a 600-second reference window, switched on by height under the running chain with no fork and no restart of the chain</td><td class="num">DAA 33,000, 4 Oct 2026</td></tr>
|
<tr><td>Difficulty</td><td>Rule v2, a 600-second reference window, switched on by height under the running chain with no fork and no restart of the chain</td><td class="num">DAA 33,000, 4 Oct 2026</td></tr>
|
||||||
<tr><td>Finality</td><td>Rule v2: a checkpoint every 30 s of chain, locked at two thirds of all 30-day weight. First live lock: checkpoint 242 at 77.4% of all weight, 17 vote keys</td><td class="num">4 Oct 2026</td></tr>
|
<tr><td>Finality</td><td>Rule v2: a checkpoint every 30 s of chain, locked at two thirds of all 30-day weight. First live lock: checkpoint 242 at 77.4% of all weight, 17 vote keys</td><td class="num">4 Oct 2026</td></tr>
|
||||||
<tr><td>Proving</td><td>v0 active: shards are assigned to miners' keys, proven on their cards, and the records are carried in blocks</td><td class="num">DAA 84,100, 5 Oct 2026</td></tr>
|
<tr><td>Proving</td><td>v0 active: shards are assigned to miners' keys, proven on their cards, and the records are carried in blocks</td><td class="num">DAA 84,100, 5 Oct 2026</td></tr>
|
||||||
<tr><td>Ember</td><td>The one-click miner on the fleet, version 0.3.5; 0.3.6 staged</td><td class="num">4 Oct 2026, first install</td></tr>
|
<tr><td>Ember</td><td>The one-click miner on the fleet, version 0.3.13 (6 Oct 2026); the app window still says Igneum Miner</td><td class="num">4 Oct 2026, first install</td></tr>
|
||||||
<tr><td>Wallet</td><td>Igneum Wallet 0.1.1 on macOS</td><td class="num">5 Oct 2026</td></tr>
|
<tr><td>Wallet</td><td>Igneum Wallet 0.1.1 on macOS</td><td class="num">5 Oct 2026</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
|
|
@ -408,7 +408,7 @@ body.all .pager{display:none}
|
||||||
<section id="mining">
|
<section id="mining">
|
||||||
<h2>Mining: a program that never holds still</h2>
|
<h2>Mining: a program that never holds still</h2>
|
||||||
<p>Every GPU chain that promised ASIC resistance shipped a fixed algorithm, and a fixed algorithm gets a chip the moment the prize pays for one. Igneum does not have a fixed algorithm.</p>
|
<p>Every GPU chain that promised ASIC resistance shipped a fixed algorithm, and a fixed algorithm gets a chip the moment the prize pays for one. Igneum does not have a fixed algorithm.</p>
|
||||||
<p>Each hour the chain derives a seed from a locked checkpoint one epoch back, passes it through a ten-minute verifiable delay so no miner can see which program a seed implies before choosing whether to publish a block, and feeds it to a deterministic generator. The generator emits a random integer program built from what graphics cards are uniquely good at: wide parallel integer maths, shuffles between the 32 lanes of a warp, and random reads over a multi-gigabyte dataset that changes daily, so the program waits on memory latency, not on maths or bandwidth. The memory footprint and instruction count are fixed and only the maths sequence is random, so no hour favours one vendor's cards and nobody gains by grinding the seed. Miners compile the program once per hour. Anyone running a node, a wallet or an exchange checks a hash on an ordinary CPU in under ten milliseconds by simulating one warp, so nobody needs a GPU except to mine. Measured: 0.61 ms per warp on one Apple M5 Max core for class v2 and 2.1 ms for class v3 (the mixer at x8, 5 October 2026, one core at load average 5.5, worst cold unit 2.15 ms), 3.4x the class v2 verifier; the 10 ms gate leaves 4.8x (4.6x on the worst cold unit); a 2019-class laptop core is not yet measured.</p>
|
<p>Each hour the chain derives a seed from a locked checkpoint one epoch back, passes it through a ten-minute verifiable delay so no miner can see which program a seed implies before choosing whether to publish a block, and feeds it to a deterministic generator. The generator emits a random integer program built from what graphics cards are uniquely good at: wide parallel integer maths, shuffles between the 32 lanes of a warp, and random reads over a multi-gigabyte dataset that changes daily, so the program waits on memory latency, not on maths or bandwidth. Measured: an RTX 5090 hashes at 95 GB/s of useful 4-byte loads against 1,638 GB/s of sequential writes (engineering log, the RTX 5090 entries). The memory footprint and instruction count are fixed and only the maths sequence is random, so no hour favours one vendor's cards and nobody gains by grinding the seed. Miners compile the program once per hour. Anyone running a node, a wallet or an exchange checks a hash on an ordinary CPU in under ten milliseconds by simulating one warp, so nobody needs a GPU except to mine. Measured: 0.61 ms per warp on one Apple M5 Max core for class v2 and 2.1 ms for class v3 (the mixer at x8, 5 October 2026, one core at load average 5.5, worst cold unit 2.15 ms), 3.4x the class v2 verifier; the 10 ms gate leaves 4.8x (4.6x on the worst cold unit); a 2019-class laptop core is not yet measured.</p>
|
||||||
<p>The hash is a lottery, not a general-purpose cryptographic hash. It has to be unpredictable per nonce, free of any shortcut cheaper than honest evaluation, and free of bias a miner can exploit. It does not need preimage or collision resistance. Open: no analysis of the lottery properties exists yet. It is the first job of the external review in phase 1, and until then the hash is a design claim backed by the measurements below.</p>
|
<p>The hash is a lottery, not a general-purpose cryptographic hash. It has to be unpredictable per nonce, free of any shortcut cheaper than honest evaluation, and free of bias a miner can exploit. It does not need preimage or collision resistance. Open: no analysis of the lottery properties exists yet. It is the first job of the external review in phase 1, and until then the hash is a design claim backed by the measurements below.</p>
|
||||||
<div class="tbl"><table>
|
<div class="tbl"><table>
|
||||||
<thead><tr><th>Clock</th><th>What changes</th><th>Miner update needed?</th></tr></thead>
|
<thead><tr><th>Clock</th><th>What changes</th><th>Miner update needed?</th></tr></thead>
|
||||||
|
|
@ -421,7 +421,7 @@ body.all .pager{display:none}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<p>Three ideas carry the chip resistance. <strong>The hash rewrites itself.</strong> A new program every hour, drawn from the chain. Its memory pattern changes with it. The rules change on a schedule fixed at launch. No release, no vote. <strong>It waits on memory, not maths.</strong> Every hash is a chain of random reads into a table too big for a chip to carry. The wait is the same physics for everyone. <strong>Miners hold the switch.</strong> Spare defences are written into the rules, switched off. A 90% miner signal turns one on. No fork.</p>
|
<p>Three ideas carry the chip resistance. <strong>The hash rewrites itself.</strong> A new program every hour, drawn from the chain. Its memory pattern changes with it. The rules change on a schedule fixed at launch. No release, no vote. <strong>It waits on memory, not maths.</strong> Every hash is a chain of random reads into a table too big for a chip to carry. The wait is the same physics for everyone. <strong>Miners hold the switch.</strong> Spare defences are written into the rules, switched off. A 90% miner signal turns one on. No fork.</p>
|
||||||
<p>No hash has stayed free of chips forever. Igneum does not claim to. It claims the gain is small, the response takes a week, and both are measured. The model is public: <a href="/bench#counter-asic-2-0-the-numbers">the numbers</a>; a bounty follows the external review. Monero has run on RandomX since 2019 with no chip publicly shipped, approximate; that is precedent, not proof.</p>
|
<p>No hash has stayed free of chips forever. Igneum does not claim to. It states the gain its own model finds, the response takes a week, and both are measured. The model is public: <a href="/bench#counter-asic-2-0-the-numbers">the numbers</a>; the claim is tested by paid independent cryptanalysis and the public benchmark. Monero has run on RandomX since 2019 with no chip publicly shipped, approximate; that is precedent, not proof.</p>
|
||||||
<p>One thing takes a person, here and on every chain that exists: writing new code. A chain cannot safely write its own generator, and it cannot safely tell a chip from a wave of honest new cards by hashrate alone. If the design above ever failed, anyone could publish a new generator and miners would switch it on by signalling, as Monero's community can fork. Igneum is built to make that day unlikely, and does not depend on avoiding it.</p>
|
<p>One thing takes a person, here and on every chain that exists: writing new code. A chain cannot safely write its own generator, and it cannot safely tell a chip from a wave of honest new cards by hashrate alone. If the design above ever failed, anyone could publish a new generator and miners would switch it on by signalling, as Monero's community can fork. Igneum is built to make that day unlikely, and does not depend on avoiding it.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -437,7 +437,7 @@ body.all .pager{display:none}
|
||||||
<tr><td>Light verification</td><td>256 MB cache on a CPU, milliseconds</td><td>256 MB cache on a CPU (512 MB from year 4), one warp under 10 ms, the gate. Measured 2.1 ms on one Apple M5 Max core for class v3 (3.4x class v2's 0.61 ms); a 2019-class core not yet</td></tr>
|
<tr><td>Light verification</td><td>256 MB cache on a CPU, milliseconds</td><td>256 MB cache on a CPU (512 MB from year 4), one warp under 10 ms, the gate. Measured 2.1 ms on one Apple M5 Max core for class v3 (3.4x class v2's 0.61 ms); a 2019-class core not yet</td></tr>
|
||||||
<tr><td>Changes over time</td><td>None. A fixed design, unchanged for seven years</td><td>A new program every hour, its memory pattern with it; era draws and reserved families on a schedule fixed at genesis. Nobody touches it</td></tr>
|
<tr><td>Changes over time</td><td>None. A fixed design, unchanged for seven years</td><td>A new program every hour, its memory pattern with it; era draws and reserved families on a schedule fixed at genesis. Nobody touches it</td></tr>
|
||||||
<tr><td>Seed grinding</td><td>Not applicable, the program comes from the hash input</td><td>Closed by a verifiable delay between seed and program</td></tr>
|
<tr><td>Seed grinding</td><td>Not applicable, the program comes from the hash input</td><td>Closed by a verifiable delay between seed and program</td></tr>
|
||||||
<tr><td>Useful work</td><td>None. Hashing only</td><td>NVIDIA cards with 24 GB or more prove every block and sell proofs to other chains; AMD and Apple cards mine, and a prover for them lands when a zkVM ships one</td></tr>
|
<tr><td>Useful work</td><td>None. Hashing only</td><td>Every NVIDIA card from 8 GB proves; 12 GB and up mine and prove; 24 GB on the stock server (eleven rented cards, RTX 3060 to RTX 5090, 6 October 2026, <code>docs/analysis/prover-tiers-real-cards.md</code>); they sell proofs to other chains. AMD and Apple cards mine, and a prover for them lands when a zkVM ships one</td></tr>
|
||||||
<tr><td>Track record</td><td>No chip publicly shipped in seven years, approximate</td><td>Zero years. Every number above is measured and logged with the commands that produced it. The specification, reference hash, test vectors and simulators are public now (github.com/igneum-network/spec). The node, the miner and the wallet are in a private repository until the public testnet</td></tr>
|
<tr><td>Track record</td><td>No chip publicly shipped in seven years, approximate</td><td>Zero years. Every number above is measured and logged with the commands that produced it. The specification, reference hash, test vectors and simulators are public now (github.com/igneum-network/spec). The node, the miner and the wallet are in a private repository until the public testnet</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
|
|
@ -446,12 +446,12 @@ body.all .pager{display:none}
|
||||||
|
|
||||||
<section id="proving">
|
<section id="proving">
|
||||||
<h2>Proving: the miners are the provers</h2>
|
<h2>Proving: the miners are the provers</h2>
|
||||||
<p>Every Igneum block is proven with a zero-knowledge proof, and the miners produce it. Proving is a useful GPU workload that is cheaply verifiable by construction. A proof is right or it is not, and a phone can check it in milliseconds.</p>
|
<p>Every Igneum block is proven with a zero-knowledge proof, and the miners produce it. Proving is a useful GPU workload that is cheaply verifiable by construction. A proof is right or it is not. Wrapped for light clients, a phone checks it in milliseconds; the wrapping cost is a phase two measurement. Measured so far, the certificate half only: the browser verifier on the home page checks a devnet finality certificate, one BLS aggregate signature over 16 keys and 21 header hashes, in 139 to 155 ms cold and 58 to 68 ms warm in a phone-sized tab on a laptop core (5 October 2026). No phone has been measured, and no wrapped block proof exists yet.</p>
|
||||||
<h3>How a block gets proven</h3>
|
<h3>How a block gets proven</h3>
|
||||||
<p>Blocks carry transactions only and make no claim about state. Every node executes the ordered transactions natively at once, so users see their transaction land in about a second. The execution is then split into shards of a fixed proving cost. Shards are assigned by lot to eight provers for ten seconds, then open to anyone; there is no bond. Provers run them on consumer cards, and the shard proofs are folded by recursive aggregation into one proof for the block. That proof lands on-chain within about a minute at launch. Because the proof computes the state from the ordered sequence, no node accepts a block with a wrong state root. Full nodes also execute every block natively and reject a proof record whose result differs from their own execution, so a forged proof is a light-client problem and never a chain split. Implemented: the native-execution check on every carried proof record, proving v0 on the devnet (specification section 7). The emergency path for a soundness bug in the proof system is a human one: a new proof-system version is written by people and activates only on miner signalling. Invalid transactions are skipped by rule, the way Kaspa skips conflicting spends.</p>
|
<p>Blocks carry transactions only and make no claim about state. Every node executes the ordered transactions natively at once, so users see their transaction land in about a second. The execution is then split into shards of a fixed proving cost. Shards are assigned by lot to eight provers for ten seconds, then open to anyone; there is no bond. Provers run them on consumer cards, and the shard proofs are folded by recursive aggregation into one proof for the block. That proof lands on-chain within about a minute at launch. Because the proof computes the state from the ordered sequence, no node accepts a block with a wrong state root. Full nodes also execute every block natively and reject a proof record whose result differs from their own execution, so a forged proof is a light-client problem and never a chain split. Implemented: the native-execution check on every carried proof record, proving v0 on the devnet (specification section 7). The emergency path for a soundness bug in the proof system is a human one: a new proof-system version is written by people and activates only on miner signalling. Invalid transactions are skipped by rule, the way Kaspa skips conflicting spends.</p>
|
||||||
<p>Proving needs an NVIDIA card with 24 GB or more (32 GB until the fee switch of 6 October 2026; from it a 24 GB card mines and proves on the same card: 22.2 GB peak measured with the miner on, 5 October 2026). AMD and Apple cards mine. A prover for them lands when a zkVM ships one.</p>
|
<p>Proving: every NVIDIA card from 8 GB proves; 12 GB and up mine and prove; 24 GB on the stock server. Measured on eleven rented cards, RTX 3060 to RTX 5090, 6 October 2026, <code>docs/analysis/prover-tiers-real-cards.md</code>: the RTX 3060 (12 GB) mines at 23.78 MH/s and proves the v1 shard beside its miner at an 8.9 GB peak in 37.5 s; the RTX 4060 (8 GB) proves it alone at 7.4 GB in 18.4 s; the RTX 4090 (24 GB) proves it on the stock SP1 server in 5.6 s at 17.4 GB. The patched server that fits the smaller cards is not yet in the shipped app. AMD and Apple cards mine. A prover for them lands when a zkVM ships one.</p>
|
||||||
<h3>The proving budget</h3>
|
<h3>The proving budget</h3>
|
||||||
<p>Gas prices execution. Proving cost is a different number, so Igneum meters it separately: every transaction pays in both dimensions, and each block has a proving-cost budget set in consensus from measured prover throughput. A transaction that is cheap to run and expensive to prove pays for what it costs the provers. Measured on 5 October 2026 (an RTX 5090 under SP1 6.8.1's GPU prover, the shard size the chain adopts from its fee switch, 30,000 proving gas, about 4.7 million prover cycles): one full shard proves in 4.3 seconds and needs 20.4 GB of GPU memory with the card to itself, so a 24 GB card proves full shards and a 12 GB or 16 GB card does not on this prover build, whose floor is 13.9 GB for even an empty shard; mining and proving on one card needs 32 GB today (the prototype-size shard beside the miner peaked at 30.1 GB) and 24 GB once the adopted shard size is live (22.2 GB beside the miner, 13.2 seconds a shard, measured on the 32 GB card; a 24 GB card has not run it yet). The old 12 GB gate on the roadmap is withdrawn until a prover build with a smaller floor is measured. The first proofs exist: on 4 October 2026 an RTX 5090 proved a small two-transaction block in 1.4 seconds (2.7 seconds compressed), verified in 0.22 and 0.038 seconds, and a laptop CPU proved a three-shard block end to end in 19 minutes. Later that day the same card proved a full shard at the provisional size, 6.75 million prover gas, which executed in 60.8 million cycles: core proof 8.3 seconds, compressed proof 10.9 seconds, verified in 0.040 seconds; a four-shard block took 44.5 seconds of GPU stages end to end. Since 5 October 2026 shards are assigned and proven on the live devnet. The gate asks for a mid-range card, and an RTX 5090 is not one, so the gate stands open. Once the gate is measured, the budget rises by schedule as hardware improves. The proof system is hash-based, which is what runs on consumer cards, and sits behind a versioned interface, so Igneum can adopt a better proof system when one exists by a miner-signalled release, and runs for ever on the current one if none is adopted.</p>
|
<p>Gas prices execution. Proving cost is a different number, so Igneum meters it separately: every transaction pays in both dimensions, and each block has a proving-cost budget set in consensus from measured prover throughput. A transaction that is cheap to run and expensive to prove pays for what it costs the provers. Measured on 5 October 2026 (an RTX 5090 under SP1 6.8.1's GPU prover, the shard size the chain adopts from its fee switch, 30,000 proving gas, about 4.7 million prover cycles): one full shard proves in 4.3 seconds and needs 20.4 GB of GPU memory with the card to itself, so a 24 GB card proves full shards and a 12 GB or 16 GB card does not on this prover build, whose floor is 13.9 GB for even an empty shard; mining and proving on one card needs 32 GB today (the prototype-size shard beside the miner peaked at 30.1 GB) and 24 GB once the adopted shard size is live (22.2 GB beside the miner, 13.2 seconds a shard, measured on the 32 GB card; a 24 GB card has not run it yet). The old 12 GB gate on the roadmap was withdrawn on 5 October until a prover build with a smaller floor was measured; on 6 October a patched server proved the same shard at 7.4 to 8.0 GB alone on eleven rented cards from the RTX 3060 to the RTX 5090 (the real-card table), so the gate returns as measured and the patched server is not yet in the shipped app. The first proofs exist: on 4 October 2026 an RTX 5090 proved a small two-transaction block in 1.4 seconds (2.7 seconds compressed), verified in 0.22 and 0.038 seconds, and a laptop CPU proved a three-shard block end to end in 19 minutes. Later that day the same card proved a full shard at the provisional size, 6.75 million prover gas, which executed in 60.8 million cycles: core proof 8.3 seconds, compressed proof 10.9 seconds, verified in 0.040 seconds; a four-shard block took 44.5 seconds of GPU stages end to end. Since 5 October 2026 shards are assigned and proven on the live devnet. The gate asks for a mid-range card, and an RTX 5090 is not one, so the gate stands open. Once the gate is measured, the budget rises by schedule as hardware improves. The proof system is hash-based, which is what runs on consumer cards, and sits behind a versioned interface, so Igneum can adopt a better proof system when one exists by a miner-signalled release, and runs for ever on the current one if none is adopted.</p>
|
||||||
<h3>Proving for everyone else</h3>
|
<h3>Proving for everyone else</h3>
|
||||||
<p>The same miners accept proving jobs from other chains. Rollups post a job, a miner wins it, proves it, and is paid. The job market is permissionless and is Designed, not yet built. At launch a job is paid on the customer's own chain, in the customer's currency, to a payout contract keyed by miner address, because Igneum cannot yet see Ethereum. Settlement in IGN, with 10% of each fee burned, follows when the proof bridge lets Igneum see the payment, in phase two. The Igneum miner client can also bid on other proving networks and take the best price, where a miner chooses to hold their collateral: Boundless provers post ZKC and Succinct provers stake PROVE (approximate, from their documentation). The proving market is small today. Igneum does not depend on it. We know of no other proof-of-work chain selling proofs to other chains.</p>
|
<p>The same miners accept proving jobs from other chains. Rollups post a job, a miner wins it, proves it, and is paid. The job market is permissionless and is Designed, not yet built. At launch a job is paid on the customer's own chain, in the customer's currency, to a payout contract keyed by miner address, because Igneum cannot yet see Ethereum. Settlement in IGN, with 10% of each fee burned, follows when the proof bridge lets Igneum see the payment, in phase two. The Igneum miner client can also bid on other proving networks and take the best price, where a miner chooses to hold their collateral: Boundless provers post ZKC and Succinct provers stake PROVE (approximate, from their documentation). The proving market is small today. Igneum does not depend on it. We know of no other proof-of-work chain selling proofs to other chains.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -484,7 +484,7 @@ body.all .pager{display:none}
|
||||||
<ol>
|
<ol>
|
||||||
<li><strong>Proofs at the cost of power.</strong> A contract requests a proof of any computation and the miners produce it. Their cards already run and are paid by emission, so a job only has to beat a few seconds of lottery income. Verification is folded into the chain's own proof; you ship no verifier. The price is a base fee that rises with the backlog, published at the phase 4 job market.</li>
|
<li><strong>Proofs at the cost of power.</strong> A contract requests a proof of any computation and the miners produce it. Their cards already run and are paid by emission, so a job only has to beat a few seconds of lottery income. Verification is folded into the chain's own proof; you ship no verifier. The price is a base fee that rises with the backlog, published at the phase 4 job market.</li>
|
||||||
<li><strong>Users who were not paid to arrive.</strong> Every miner is a funded wallet. Pools, payout contracts, hardware finance and hashrate forwards have customers before any consumer app does. Block rewards can pay straight to a contract.</li>
|
<li><strong>Users who were not paid to arrive.</strong> Every miner is a funded wallet. Pools, payout contracts, hardware finance and hashrate forwards have customers before any consumer app does. Block rewards can pay straight to a contract.</li>
|
||||||
<li><strong>A share of fees, with the number stated.</strong> 20% of every priority fee goes to the contracts whose code ran, per call frame, to the payee registered at deployment. Libraries are paid at their code address. Factories pass their registration to what they deploy. At launch fee levels this is a property, not an income: a million calls a day at a 1 gwei tip pays about 7,300 IGN a year. It grows with traffic and nothing else.</li>
|
<li><strong>A share of fees, with the number stated.</strong> 20% of every priority fee goes to the contracts whose code ran, per call frame, to the payee registered at deployment. Libraries are paid at their code address. Factories pass their registration to what they deploy. At launch fee levels this is a property, not an income: a million 100,000-gas calls a day at a 1 gwei tip pays about 7,300 IGN a year, with 1 gwei taken as a billionth of an IGN (the base unit is Open). It grows with traffic and nothing else.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p>Ethereum stays your settlement. Move heavy compute to Igneum and return the result as a proof Ethereum verifies for about 250,000 gas. Igneum reads Ethereum's finality trustlessly from launch. Ethereum reads Igneum trustlessly in phase two. Until then, trust the bridge's operator.</p>
|
<p>Ethereum stays your settlement. Move heavy compute to Igneum and return the result as a proof Ethereum verifies for about 250,000 gas. Igneum reads Ethereum's finality trustlessly from launch. Ethereum reads Igneum trustlessly in phase two. Until then, trust the bridge's operator.</p>
|
||||||
<p>No stablecoin and no official bridge at genesis. Grants come from founders' mined coins, for ports, audits and integrations, never for a user count. Execution is immediate, the miner lock lands in about two minutes; a withdrawal waits for the lock.</p>
|
<p>No stablecoin and no official bridge at genesis. Grants come from founders' mined coins, for ports, audits and integrations, never for a user count. Execution is immediate, the miner lock lands in about two minutes; a withdrawal waits for the lock.</p>
|
||||||
|
|
@ -494,7 +494,7 @@ body.all .pager{display:none}
|
||||||
<h2>Economics</h2>
|
<h2>Economics</h2>
|
||||||
<p>The coin is IGN. It is gas and the proving currency, and part of every payment on Igneum is burned. Outside customers pay in their own currency on their own chain at launch; settlement in IGN with a 10% burn follows when the proof bridge lets Igneum see the payment, in phase two.</p>
|
<p>The coin is IGN. It is gas and the proving currency, and part of every payment on Igneum is burned. Outside customers pay in their own currency on their own chain at launch; settlement in IGN with a 10% burn follows when the proof bridge lets Igneum see the payment, in phase two.</p>
|
||||||
<h3>Supply</h3>
|
<h3>Supply</h3>
|
||||||
<p>Fair launch. No premine, no pre-sale, no allocation to anyone. Hard cap of 4 billion IGN, approached and never reached, because emission starts at 1 billion a year and halves every two years for ever. Nearly a quarter of all supply is mined in the first year and half in the first two, so the people who show up early get the most. Emission ramps from 10% to 100% over the first 30 days so that nobody takes the first month before the rest of the world hears about it.</p>
|
<p>Fair launch. No premine, no pre-sale, no allocation to anyone. Hard cap of 4 billion IGN, approached and never reached, because emission starts at 1 billion a year and halves every two years for ever. Nearly a quarter of all supply is mined in the first year and half in the first two. Emission ramps from 10% to 100% over the first 30 days so that nobody takes the first month before the rest of the world hears about it.</p>
|
||||||
<div class="figure">
|
<div class="figure">
|
||||||
<svg viewBox="0 0 760 300" role="img" aria-label="Half of the 4 billion cap is mined in the first two years" font-family="IBM Plex Mono, monospace" font-size="12">
|
<svg viewBox="0 0 760 300" role="img" aria-label="Half of the 4 billion cap is mined in the first two years" font-family="IBM Plex Mono, monospace" font-size="12">
|
||||||
<text x="40" y="26" font-family="IBM Plex Sans, system-ui, sans-serif" font-size="15" font-weight="600" fill="var(--ink)">Half of the 4 billion cap is mined in the first two years</text>
|
<text x="40" y="26" font-family="IBM Plex Sans, system-ui, sans-serif" font-size="15" font-weight="600" fill="var(--ink)">Half of the 4 billion cap is mined in the first two years</text>
|
||||||
|
|
@ -525,14 +525,28 @@ body.all .pager{display:none}
|
||||||
<thead><tr><th>Share</th><th>Goes to</th><th>Why</th></tr></thead>
|
<thead><tr><th>Share</th><th>Goes to</th><th>Why</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="num">80%</td><td>The miner who wins the block</td><td>Pays the hashrate that secures the chain</td></tr>
|
<tr><td class="num">80%</td><td>The miner who wins the block</td><td>Pays the hashrate that secures the chain</td></tr>
|
||||||
<tr><td class="num">20%</td><td>The proving pool: shard provers and aggregators</td><td>Pays a standing prover population that does not have to hash</td></tr>
|
<tr><td class="num">20%</td><td>The proving pool: shard provers and aggregators</td><td>For a standing prover population that does not have to hash. On the devnet today the coinbase's 20% output goes to an unspendable script tagged <code>igneum-proving-pool-v0</code> and is burned there. Provers are paid from a separate escrow in the execution state, credited by rule with the same 20% of each blue block's subsidy and released per shard against valid proof records (Implemented, proving v0, since 5 October 2026). Open: the single coinbase payout that replaces the burn, and whether it reclaims the share burned so far</td></tr>
|
||||||
<tr><td class="num">0%</td><td>Treasury, foundation, team or stake</td><td>There is no coin-holder class in consensus and no tax on emission</td></tr>
|
<tr><td class="num">0%</td><td>Treasury, foundation, team or stake</td><td>There is no coin-holder class in consensus and no tax on emission</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<h3>Where fees go</h3>
|
<h3>Where fees go</h3>
|
||||||
<p>The base fee of every transaction is burned in full, Ethereum's rule, so a miner cannot fill blocks with its own transactions for free. The priority fee splits two ways: 80% to the miner and provers of that block, 20% to the apps whose code ran, by gas consumed inside each. External proving fees, once they settle on Igneum, pay 90% to the provers who delivered and burn 10%. The hard cap fixes supply. Emission is untouched by any of this: every coin minted still goes to miners and provers.</p>
|
<p>The base fee of every transaction is burned in full, Ethereum's rule, so a miner cannot fill blocks with its own transactions for free. The priority fee splits two ways: 80% to the miner and provers of that block, 20% to the apps whose code ran, by gas consumed inside each. External proving fees, once they settle on Igneum, pay 90% to the provers who delivered and burn 10%. The hard cap fixes supply. Emission is untouched by any of this: every coin minted still goes to miners and provers.</p>
|
||||||
|
<h3>Every payment route</h3>
|
||||||
|
<p>One row per route, so operator income and protocol income never blur. The protocol pays no address of its own, and a burn pays nobody. Rows 1 to 5 are the protocol. Row 6 is the project's software, outside the protocol, and is never added to the other five.</p>
|
||||||
|
<div class="tbl"><table>
|
||||||
|
<thead><tr><th>Route</th><th>Currency</th><th>Recipient</th><th>Fee</th><th>Burn</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>1. Emission, per block</td><td>IGN, new coins on the schedule above</td><td>80% the block's miner, 20% the proving pool for the provers of that block</td><td>None</td><td>None. Implemented in consensus: the 80/20 coinbase on the devnet</td></tr>
|
||||||
|
<tr><td>2. Base fee, both gas dimensions</td><td>IGN</td><td>Nobody</td><td>The base fee the chain sets per block</td><td>All of it. Implemented on the devnet</td></tr>
|
||||||
|
<tr><td>3. Priority fee</td><td>IGN</td><td>80% the block's miner and provers; 20% the apps whose code ran, per call frame</td><td>The tip the sender sets</td><td>The share of any frame in an unregistered contract. Implemented on the devnet</td></tr>
|
||||||
|
<tr><td>4. External job, at launch</td><td>The customer's currency, on the customer's chain</td><td>The miner who delivered, through a payout contract keyed by miner address</td><td>Priced in dollars per proof; the customer chain's own bond and slashing apply</td><td>None; Igneum cannot see the payment. Designed</td></tr>
|
||||||
|
<tr><td>5. External job, after the proof bridge</td><td>IGN, on Igneum</td><td>90% the provers who delivered</td><td>The job fee</td><td>10%. Designed, phase two</td></tr>
|
||||||
|
<tr><td>6. The official client's dev fee</td><td>IGN</td><td>The project, as operator income, never the protocol</td><td>1 block template in 100 requested with the dev address; off with one flag</td><td>None. Implemented, measured on a test network 4 October 2026</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table></div>
|
||||||
|
<p class="src"><b>Sources:</b> specification sections 2.5 and 5.1 to 5.4; the engineering log for the devnet receipts and the dev-fee count.</p>
|
||||||
<h3>Security after the subsidy</h3>
|
<h3>Security after the subsidy</h3>
|
||||||
<p>The cap stays at 4 billion. There is no tail emission. Long term, security is paid for by the proving market and by fees. Outside customers buy proofs as dollars-priced work settled in IGN, and 90% of every job goes to the provers who delivered it, so a prover's income does not depend on emission. The table shows the first year in which the block subsidy on its own pays miners less than the power of about 3,000 consumer cards, at three flat prices. The prices are inputs chosen to span two orders of magnitude. The model runs a 300 W card at 124 MH/s on electricity at USD 0.12 per kWh. One rule sits beside the cap. If external proving revenue is under one fifth of the block subsidy over any 90-day window after year 5, the question of a tail reward goes to the miners' signalling vote. The protocol never changes emission by itself.</p>
|
<p>The cap stays at 4 billion. There is no tail emission. The schedule is a bet, not a measurement: a halving halves emission income overnight if price and fees do nothing. Kaspa's steeper monthly reduction kept its hashrate while its price rose (approximate). Long term, security is paid for by the proving market and by fees. Outside customers buy proofs as dollars-priced work settled in IGN, and 90% of every job goes to the provers who delivered it, so a prover's income does not depend on emission. The table shows the first year in which the block subsidy on its own pays miners less than the power of about 3,000 consumer cards, at three flat prices. The prices are inputs chosen to span two orders of magnitude. The model runs a 300 W card at 124 MH/s on electricity at USD 0.12 per kWh. One rule sits beside the cap. If external proving revenue is under one fifth of the block subsidy over any 90-day window after year 5, the question of a tail reward goes to the miners' signalling vote. The protocol never changes emission by itself.</p>
|
||||||
<div class="tbl"><table>
|
<div class="tbl"><table>
|
||||||
<thead><tr><th>Price per IGN</th><th>First year the subsidy alone pays under the power of 3,000 cards</th><th>Subsidy to miners that year</th></tr></thead>
|
<thead><tr><th>Price per IGN</th><th>First year the subsidy alone pays under the power of 3,000 cards</th><th>Subsidy to miners that year</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -553,18 +567,18 @@ body.all .pager{display:none}
|
||||||
<thead><tr><th>Stream</th><th>Paid by</th><th>Moves with the IGN price?</th></tr></thead>
|
<thead><tr><th>Stream</th><th>Paid by</th><th>Moves with the IGN price?</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Block reward</td><td>Emission, 80% to the winner</td><td>Yes</td></tr>
|
<tr><td>Block reward</td><td>Emission, 80% to the winner</td><td>Yes</td></tr>
|
||||||
<tr><td>In-chain proving</td><td>The 20% proving pool plus the proving share of every block's gas</td><td>Yes, mostly</td></tr>
|
<tr><td>In-chain proving</td><td>The 20% proving pool plus the proving share of every block's gas (on the devnet, paid from the execution-state escrow; see Economics)</td><td>Yes, mostly</td></tr>
|
||||||
<tr><td>External proving jobs</td><td>Rollups and apps on other chains, priced in their money</td><td>No, but the market is small today and is upside, not a promise</td></tr>
|
<tr><td>External proving jobs</td><td>Rollups and apps on other chains, priced in their money</td><td>No, but the market is small today and is upside, not a promise</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<p>The honest bear-market case rests on cost. A miner's card is already running and the power is often domestic, so Igneum miners' marginal cost in the proving market is close to power, which is an edge over data-centre provers and nothing more.</p>
|
<p>The honest bear-market case rests on cost. A miner's card is already running and the power is often domestic, so Igneum miners' marginal cost in the proving market is close to power, which is an edge over data-centre provers and nothing more. Which of the two in-chain streams pays more per GPU-second depends on the size of the fleet: on the devnet of 4 October 2026, three machines at 275 million hashes a second, a second of hashing paid about 4.9x a second of proving the pool share; at 10,000 cards the same arithmetic favours proving by about 930x. That is arithmetic on measured devnet rates, approximate, not a market measurement.</p>
|
||||||
<h3>Hardware</h3>
|
<h3>Hardware</h3>
|
||||||
<p>The dataset starts at 2 GB and grows (the proposed schedule, fixed at the testnet genesis: 2 GB, doubling at years 4, 12 and 28, the average of half a gigabyte a year), so a 4 GB card mines for about four years and an 8 GB card for about twelve, approximate. 24 GB proves full shards (measured on a 32 GB card's allocation; a 24 GB card has not run it yet) and 32 GB mines and proves on one card, measured 5 October 2026 on this prover build (a 12 GB card does not prove on it: the GPU prover's floor is 13.9 GB). NVIDIA and AMD both work, because the mining program is generated for the architecture both share and the proof system is hash-based. Apple's chips are GPUs with unified memory, so Macs mine too, at about a fifth of a flagship card: Measured, 26.7 against 123 million hashes a second, an Apple M5 Max beside an RTX 5090 on the live devnet, 4 October 2026. A Mac is a poor miner per dollar. There is no CPU mining lane, on purpose, because CPU mining is what botnets farm. Nodes, wallets and exchanges need no GPU at all.</p>
|
<p>The dataset starts at 2 GB and grows (the proposed schedule, fixed at the testnet genesis: 2 GB, doubling at years 4, 12 and 28, the average of half a gigabyte a year), so a 4 GB card mines for about four years and an 8 GB card for about twelve, approximate. Every NVIDIA card from 8 GB proves; 12 GB and up mine and prove; 24 GB on the stock server (eleven rented cards, RTX 3060 to RTX 5090, 6 October 2026, <code>docs/analysis/prover-tiers-real-cards.md</code>). NVIDIA and AMD both work, because the mining program is generated for the architecture both share and the proof system is hash-based. Apple's chips are GPUs with unified memory, so Macs mine too, at about a fifth of a flagship card: Measured, 26.7 against 123 million hashes a second, an Apple M5 Max beside an RTX 5090 on the live devnet, 4 October 2026. A Mac is a poor miner per dollar. There is no CPU mining lane, on purpose, because CPU mining is what botnets farm. Nodes, wallets and exchanges need no GPU at all.</p>
|
||||||
<h3>What a miner's hour looks like</h3>
|
<h3>What a miner's hour looks like</h3>
|
||||||
<p>The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance.</p>
|
<p>The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance.</p>
|
||||||
<p>The protocol carries no fee: no dev fund, no cut to any team. Ember, the miner software, takes an optional 1% dev fee, the way other GPU miners do. One block template in 100 is requested with the dev address instead of yours, by a counter, not a random draw, so it is exactly 1 in 100 and anyone can check it from the source or from the chain. One flag turns it off (<code>--dev-fee 0</code>, a switch in the app, a line in the HiveOS config). The miner prints the fee and the address when it starts. Any other client is welcome.</p>
|
<p>The protocol carries no fee: no dev fund, no cut to any team. Ember, the miner software, takes an optional 1% dev fee, the way other GPU miners do. One block template in 100 is requested with the dev address instead of yours, by a counter, not a random draw, so it is exactly 1 in 100 and anyone can check it from the source or from the chain. One flag turns it off (<code>--dev-fee 0</code>, a switch in the app, a line in the HiveOS config). The miner prints the fee and the address when it starts. Any other client is welcome.</p>
|
||||||
<h3>One click, for everyone else</h3>
|
<h3>One click, for everyone else</h3>
|
||||||
<p>Farm operators get a HiveOS package. Everyone else gets Igneum Ember: install it on Windows, macOS or Linux, press one button, and the card is mining and proving to a key the app made for you, with earnings shown in IGN and in your currency, and mining paused while you game. It is the same client with a face on it. The app shows you the key and has you save it before mining starts, offers a hardware wallet for your earnings, updates itself from releases signed by the project's release key with a switch to turn that off, and is downloaded only from this domain or the repository with its hash shown beside the button. The next section says what is shipped and what is still a design. Nobody from Igneum will ever ask for your seed. Mining never runs in a browser, because browser compute is slow and browser mining has meant malware since Coinhive. The browser is for the dashboard, and for verifying the chain.</p>
|
<p>Farm operators get a HiveOS package. Everyone else gets Igneum Ember: install it on Windows, macOS or Linux, press Start, and the card is mining to a key the app made for you. It is the same client with a face on it. Implemented, Ember 0.3.9 (5 October 2026): the app shows the key once and has you save it before mining starts, or takes an address you already have; the dashboard shows each card's hash rate, blocks found and accepted by the node, the node's height and peers, the next hourly program, the finality votes sent, and a proving tile with shards assigned, submitted and paid and the verifier state; the chain label reads devnet v4 and the welcome screen says nothing is bought or sold; NVIDIA cards are capped at 80% of their default power limit for stability, with a sweep that looks for the best hash per watt, not yet measured on a card; proving the shards the chain assigns is a switch in Settings (proving v0), beside the 1% dev fee switch, finality voting, and signed updates that install themselves at a quiet moment with a switch to turn that off. It shows no earnings in IGN or in any currency, and it has no hardware-wallet path. Roadmap, Designed and not in the app: earnings per block in IGN with the network named, a figure in your currency, mining paused while you game, and a hardware wallet for your key. Ember is downloaded only from this domain, with the version and size on the button and the hash in the signed manifest the app checks. The next section says what is shipped and what is still a design. Nobody from Igneum will ever ask for your seed. Mining never runs in a browser, because browser compute is slow and browser mining has meant malware since Coinhive. The browser is for the dashboard, and for verifying the chain.</p>
|
||||||
<h3>Fair launch, announced</h3>
|
<h3>Fair launch, announced</h3>
|
||||||
<p>Launch date and miner software published a month ahead. Pools live on testnet. HiveOS support on day one. The founders mine from genesis like everyone else, with disclosed addresses and the same software. Nobody has coins before block one. The first 30 days of mainnet run on proof of work alone, with no locked checkpoint, while vote weights build; anyone crediting deposits in that month should treat Igneum as plain proof of work with a 12-hour depth.</p>
|
<p>Launch date and miner software published a month ahead. Pools live on testnet. HiveOS support on day one. The founders mine from genesis like everyone else, with disclosed addresses and the same software. Nobody has coins before block one. The first 30 days of mainnet run on proof of work alone, with no locked checkpoint, while vote weights build; anyone crediting deposits in that month should treat Igneum as plain proof of work with a 12-hour depth.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -642,7 +656,7 @@ body.all .pager{display:none}
|
||||||
<li><strong>Nothing needs a scheduled upgrade.</strong> The mining program, the dataset and the finality rules run themselves for ever. If the community ever ships an improvement, a better proof system or a block-rate step, it is published with test vectors at least three months ahead and activates only when 90% of blocks signal readiness. Developers can write code. Only miners can turn it on.</li>
|
<li><strong>Nothing needs a scheduled upgrade.</strong> The mining program, the dataset and the finality rules run themselves for ever. If the community ever ships an improvement, a better proof system or a block-rate step, it is published with test vectors at least three months ahead and activates only when 90% of blocks signal readiness. Developers can write code. Only miners can turn it on.</li>
|
||||||
<li><strong>Miners set what genesis leaves open.</strong> A parameter that the genesis rules leave to miners is set by signalling: a proposal passes or fails on 60% of hashrate over two weeks. There is no fund to vote on and no fee to any team, foundation or fund.</li>
|
<li><strong>Miners set what genesis leaves open.</strong> A parameter that the genesis rules leave to miners is set by signalling: a proposal passes or fails on 60% of hashrate over two weeks. There is no fund to vote on and no fee to any team, foundation or fund.</li>
|
||||||
<li><strong>Pools can be bypassed on transaction choice.</strong> Igneum ships Stratum v2 job declaration from day one, so a miner chooses its own transactions when its pool supports it. Pools can decline, and vote keys stay with the pool. Designed: the pool protocol is specification section 9, not yet run by any pool.</li>
|
<li><strong>Pools can be bypassed on transaction choice.</strong> Igneum ships Stratum v2 job declaration from day one, so a miner chooses its own transactions when its pool supports it. Pools can decline, and vote keys stay with the pool. Designed: the pool protocol is specification section 9, not yet run by any pool.</li>
|
||||||
<li><strong>There are no admin keys in consensus.</strong> Nothing in consensus can be paused, upgraded or reversed by any key. There is no foundation allocation to vote with and no stake to buy. The genesis apps are contracts, and each publishes its own upgrade and key policy before launch; the bridge's is the one to read. Designed, open item O-5.4.</li>
|
<li><strong>There are no admin keys in consensus.</strong> Nothing in consensus can be paused, upgraded or reversed by any key. There is no foundation allocation to vote with and no stake to buy. The genesis apps are contracts, and each publishes its own upgrade and key policy before launch; the bridge's is the one to read. Designed, open item O-5.4. On the devnet the activation heights and one execution-state restart (6 October 2026) reach every node through the signed update manifest, so on the devnet the release key acts as the operator; the sentence above holds for mainnet consensus only once that path is closed, and the public testnet terms will say which parameters still travel that way.</li>
|
||||||
<li><strong>The chain runs without its founders.</strong> Blocks, proofs and finality need no one. A second independent node client is the first priority after launch, and anyone can build it.</li>
|
<li><strong>The chain runs without its founders.</strong> Blocks, proofs and finality need no one. A second independent node client is the first priority after launch, and anyone can build it.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -681,9 +695,23 @@ body.all .pager{display:none}
|
||||||
<section id="miners-ask">
|
<section id="miners-ask">
|
||||||
<h2>Questions miners ask</h2>
|
<h2>Questions miners ask</h2>
|
||||||
<h3>Kaspa was GPU-mined too, and IceRiver shipped a chip within two years.</h3>
|
<h3>Kaspa was GPU-mined too, and IceRiver shipped a chip within two years.</h3>
|
||||||
<p>Kaspa never promised chip resistance, and its hash was one fixed function, simple enough to put on silicon. Igneum's program is different every hour, its dataset grows past any fixed memory, and its program space widens every era, with no human involved. The benchmark tool ships in January 2027, and its source is public with the repository at the public testnet, so you run it on your own card and post the number to a leaderboard by card model. A standing bounty pays anyone who can show a chip design that beats a GPU by more than 2x. And if a chip ever appears, miners are the ones who signal the response.</p>
|
<p>Kaspa never promised chip resistance, and its hash was one fixed function, simple enough to put on silicon. Igneum's program is different every hour, its dataset grows past any fixed memory, and its program space widens every era, with no human involved. The benchmark tool ships in January 2027, and its source is public with the repository at the public testnet, so you run it on your own card and post the number to a leaderboard by card model. The paid independent cryptanalysis and the public benchmark are where a chip design that beats a GPU by more than 2x would show. And if a chip ever appears, miners are the ones who signal the response.</p>
|
||||||
|
<h3>Don't ASICs make a chain safer?</h3>
|
||||||
|
<p>Three parts. First, what the chain asks hash to do. Hash picks who makes the next block. It does not protect history. A checkpoint locks when signatures reach two thirds of the weight of the last 30 days of blocks (specification section 3). A locked checkpoint is never reorganised by any amount of hash: fork choice runs among the tips that pass through every certified checkpoint. Rented hash has no weight today. It can mine blocks. It cannot rewrite anything older than a lock. A renter with 60% of the network holds 0.0% of the vote on day one (the finality simulator, table B); one matching the whole honest network reaches a third of the weight on day 20 and never two thirds. The lock is fast: median 1,018 ms behind the checkpoint on the three-node test network (engineering log, the finality harness), and on the live devnet the first lock came two hours after genesis, once the window was full. Reorganisations under the lock are shallow: at 1, 2 and 5 blocks a second the deepest honest reorganisation measured was 2, 3 and 7 blocks against a determination depth of 20 (ledger F7, round 2, the fast-time network with 100-ms links); across five continents blocks reached every node at p50 343 ms and p99 666 ms (the 12-node cloud network, 4 October 2026).</p>
|
||||||
|
<p>Second, the cost the ASIC argument skips. Kaspa's hash went to a handful of chip owners within months: the IceRiver KS0 shipped in July 2023, 17 to 20 months after launch; hashrate went from under 100 PH/s to over 700 PH/s in months and the GPU share was negligible by late 2023 (the ASIC history, row 23, approximate for the share). Bitcoin's hash comes from two manufacturers and a few pools (approximate, from memory). The first chip's owner mines in secret with an edge for months: on Monero, 85% of the hashrate vanished at the April 2018 fork, and chips were found at over 85% again four months after the next fork (row 16). A chip does not add security to a chain; it moves the chain's security to whoever owns the chip first.</p>
|
||||||
|
<p>Third, the honest part. A young GPU chain's hash is cheap to rent, and we publish the number beside the chain's own. The locks are what make that rental unable to buy a double-spend: a deposit under a lock stays, whatever the renter mines on top. Ethereum Classic (January 2019 and August 2020), Bitcoin Gold (May 2018 and January 2020) and Vertcoin (October to December 2018, December 2019) were reorganised with rented hash (the ASIC history rows 6 and 7 for Bitcoin Gold and Vertcoin; the Ethereum Classic dates approximate, from memory); Verge's 2018 reorganisations used a timestamp flaw in its multi-algorithm rule as well as hash (approximate). On those chains the rented hash rewrote history because nothing but hash held it. Here the same rental mines blocks for its hour and leaves the locks where they were. The exception is stated above: in the first 30 days of mainnet no checkpoint locks, the chain is plain proof of work with a 12-hour depth, and a rental can reorganise inside that depth; anyone crediting deposits in that month treats it so.</p>
|
||||||
|
<div class="tbl"><table>
|
||||||
|
<thead><tr><th>What</th><th>Number</th><th>Source</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Rented NVIDIA hash, 6 October 2026</td><td class="num">2 GH/s for USD 13 an hour</td><td>The fleet's bench entry (the rented cards of 6 October 2026); the entry lands tonight with the wave measurement below</td></tr>
|
||||||
|
<tr><td>The devnet's hash the same afternoon</td><td class="num">282 MH/s</td><td><code>/api/stats</code>, 6 October 2026, 16:40 UTC; 181 MH/s an hour earlier with one PC off</td></tr>
|
||||||
|
<tr><td>Weight a renter holds on day one</td><td class="num">0.0%</td><td>The finality simulator, table B (ledger M12)</td></tr>
|
||||||
|
<tr><td>Lock latency behind the checkpoint</td><td class="num">1,018 ms median</td><td>Engineering log, the finality harness, three nodes</td></tr>
|
||||||
|
<tr><td>A 50-miner wave against the devnet: blocks taken, locks moved</td><td class="num">measurement tonight</td><td>The fleet's bench entry, 6 October 2026</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table></div>
|
||||||
<h3>Finality weighted by mining history is new. New gets attacked.</h3>
|
<h3>Finality weighted by mining history is new. New gets attacked.</h3>
|
||||||
<p>Correct, and it is the first thing the external review will be paid to break. The specification is public; reviewers will be named and paid before gate 3, and a bounty is attached. Until then every finality claim here is a design claim backed by simulations and by the devnet, and the chain runs on plain GHOSTDAG without the rule, so it can be fixed without stopping the chain.</p>
|
<p>Correct, and it is the first thing the external review will be paid to break. The specification is public; reviewers will be named and paid before gate 3, and the public benchmark carries the metrics they test against. Until then every finality claim here is a design claim backed by simulations and by the devnet, and the chain runs on plain GHOSTDAG without the rule, so it can be fixed without stopping the chain.</p>
|
||||||
<h3>Who are you?</h3>
|
<h3>Who are you?</h3>
|
||||||
<p>One founder, pseudonymous, working with AI systems. The design, the hostile reviews, the code, the simulators and this document were produced that way, and the commit history says so. The software is shipped by Igneum Labs LTD, Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. The design remains the work of one founder working with AI systems, reviewed in public through the ledger. What that does and does not mean: the measurements are measurements, reproducible from the commands in the engineering log; the simulators are code anyone can run; the design claims stay design claims until people with names have tried to break them. Every criticism the project expects is kept in a ledger with its honest answer, and the entries that were right are marked conceded; the ledger is published with the repository at the public testnet. No cryptographer is hired yet; the plan budgets one for phases 1 and 2, and external reviewers are named and paid before gate 3.</p>
|
<p>One founder, pseudonymous, working with AI systems. The design, the hostile reviews, the code, the simulators and this document were produced that way, and the commit history says so. The software is shipped by Igneum Labs LTD, Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. The design remains the work of one founder working with AI systems, reviewed in public through the ledger. What that does and does not mean: the measurements are measurements, reproducible from the commands in the engineering log; the simulators are code anyone can run; the design claims stay design claims until people with names have tried to break them. Every criticism the project expects is kept in a ledger with its honest answer, and the entries that were right are marked conceded; the ledger is published with the repository at the public testnet. No cryptographer is hired yet; the plan budgets one for phases 1 and 2, and external reviewers are named and paid before gate 3.</p>
|
||||||
<p>The founders mine from genesis with disclosed addresses and the same software as everyone else, and hold no coins before block one. The team is pseudonymous and there is no team page. The mining addresses and the code history are published with the repository at the public testnet.</p>
|
<p>The founders mine from genesis with disclosed addresses and the same software as everyone else, and hold no coins before block one. The team is pseudonymous and there is no team page. The mining addresses and the code history are published with the repository at the public testnet.</p>
|
||||||
|
|
@ -700,7 +728,7 @@ body.all .pager{display:none}
|
||||||
<h3>What does a one-minute proof mean for my app?</h3>
|
<h3>What does a one-minute proof mean for my app?</h3>
|
||||||
<p>Nothing you wait for. Your transaction executes in about a second. A miner lock arrives in about two minutes and that is the finality your contract sees. The proof follows and makes the state unforgeable. Liquidations and trades act on executed state at once, as on any chain. A bridge built on Igneum waits for the lock, about two minutes.</p>
|
<p>Nothing you wait for. Your transaction executes in about a second. A miner lock arrives in about two minutes and that is the finality your contract sees. The proof follows and makes the state unforgeable. Liquidations and trades act on executed state at once, as on any chain. A bridge built on Igneum waits for the lock, about two minutes.</p>
|
||||||
<h3>Which stablecoin, and is there liquidity?</h3>
|
<h3>Which stablecoin, and is there liquidity?</h3>
|
||||||
<p>None is bridged at genesis, and no bridge is official. Native USDC is requested from Circle during testnet, and anyone may run a bridge at their own risk until the proof bridge arrives with the consensus proof in phase two. The DEX is seeded at launch by the founders' own mined coins and by miners, and every miner is a funded wallet.</p>
|
<p>None is bridged at genesis, and no bridge is official. The project will ask Circle for native USDC during the public testnet; whether it is issued is Circle's decision. Anyone may run a bridge at their own risk until the proof bridge arrives with the consensus proof in phase two. The DEX is seeded at launch by the founders' own mined coins and by miners, and every miner is a funded wallet.</p>
|
||||||
<h3>What do I get for being early?</h3>
|
<h3>What do I get for being early?</h3>
|
||||||
<p>20% of the priority fee on every transaction that runs your code, paid to you every block, which at launch fee levels is small and stated as such above. A place in the wallet's Apps tab and the explorer from day one. First access to the proving precompile and the job market. And a user base that was not paid to arrive: the miners.</p>
|
<p>20% of the priority fee on every transaction that runs your code, paid to you every block, which at launch fee levels is small and stated as such above. A place in the wallet's Apps tab and the explorer from day one. First access to the proving precompile and the job market. And a user base that was not paid to arrive: the miners.</p>
|
||||||
<h3>How do I deploy?</h3>
|
<h3>How do I deploy?</h3>
|
||||||
|
|
@ -730,7 +758,7 @@ body.all .pager{display:none}
|
||||||
<p>Here are the limits, stated before anyone else states them.</p>
|
<p>Here are the limits, stated before anyone else states them.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>A proof in seconds.</strong> Not at launch. Proving a full block today needs a cluster of 100 to 200 consumer GPUs, approximate, so Igneum launches with proofs within about a minute and tightens as hardware improves. Users still see their transaction land in one second.</li>
|
<li><strong>A proof in seconds.</strong> Not at launch. Proving a full block today needs a cluster of 100 to 200 consumer GPUs, approximate, so Igneum launches with proofs within about a minute and tightens as hardware improves. Users still see their transaction land in one second.</li>
|
||||||
<li><strong>A chip is impossible.</strong> No. A chip is a bad bet, because the target moves before it ships. The published model (5 October 2026) prices the strongest chip we can name, one with the whole cache on-die computing dataset items on the fly, at 0.92x the hash rate of an RTX 5090 per unit of silicon with a 3x fixed-function allowance, approximate; the claim is under 2x, the margin is stated on the numbers page, and the next lever is named there. No hash has stayed free of chips forever; Igneum does not claim to. Monero's seven years without a public chip are precedent, not proof.</li>
|
<li><strong>A chip is impossible.</strong> No. A chip is a bad bet, because the target moves before it ships. The published model (5 October 2026) prices the strongest chip we can name, one with the whole cache on-die computing dataset items on the fly, at 0.92x the hash rate of an RTX 5090 per unit of silicon with a 3x fixed-function allowance, approximate. The same model, drawn out to the chip that stores the dataset (6 October 2026): The strongest recompute chip we can price, holding the whole 256 MiB cache on-die, reaches under 1x per chip against an RTX 5090. A memory-controller chip that stores the whole dataset reaches 1.2x per chip and, in our model, 5x to 9x per joule; the Ethash chips of this class reached 2.1x to 4.8x. The lever against it, program work in the latency shadow, is measured and in its gates: it brings the chip to about 2x. Sources: the chip model (<code>docs/analysis/chip-model-v3.md</code> section 5, 6 October 2026); the Ethash rows of the ASIC history (<code>docs/analysis/asic-resistance-history.md</code>, Linzhi Phoenix 2020, Jasminer X4 2021, Antminer E9 2022); Counter ASIC 3.0 item 8 (100,000 ops per hash: the chip's per-joule edge over the RTX 5090 falls from 5.6x to 2.1x on GDDR7 at a chip core equal to the GPU's, the 5090 at 0.2% less rate, gates G1 to G6 in progress). No hash has stayed free of chips forever; Igneum does not claim to. Monero's seven years without a public chip are precedent, not proof, and a small prize: they say nothing about the price of a chip with the 256 MB cache on its die, and that price is a cost model, not a measurement.</li>
|
||||||
<li><strong>A guaranteed income floor.</strong> No. External proving is a small market today. Igneum's miners' marginal cost in it is close to power, which is an edge and nothing more.</li>
|
<li><strong>A guaranteed income floor.</strong> No. External proving is a small market today. Igneum's miners' marginal cost in it is close to power, which is an edge and nothing more.</li>
|
||||||
<li><strong>A memory-hard prototype on every vendor.</strong> Not yet. The 256 MB cache closed the shortcut on Apple silicon (computing items runs 4.8x slower than loading them, measured 3 October 2026). The same ratio on NVIDIA and on a discrete AMD card is Open.</li>
|
<li><strong>A memory-hard prototype on every vendor.</strong> Not yet. The 256 MB cache closed the shortcut on Apple silicon (computing items runs 4.8x slower than loading them, measured 3 October 2026). The same ratio on NVIDIA and on a discrete AMD card is Open.</li>
|
||||||
<li><strong>Finality in the first month.</strong> No. No checkpoint locks until the 30-day window has 30 days of history. The first month of mainnet is proof of work with a 12-hour depth, and the text above says so wherever a day count appears.</li>
|
<li><strong>Finality in the first month.</strong> No. No checkpoint locks until the 30-day window has 30 days of history. The first month of mainnet is proof of work with a 12-hour depth, and the text above says so wherever a day count appears.</li>
|
||||||
|
|
@ -761,6 +789,7 @@ body.all .pager{display:none}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,7 @@ main{padding-bottom:var(--sec)}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ pre{margin:0 0 16px;padding:16px 18px;background:var(--graphite);border-radius:v
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
6
site/miner-priors.json
Normal file
6
site/miner-priors.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"_about": "Rows of the fleet priors table at /miners (site/build.mjs), written by tools/tuning.mjs --priors --site from the TUNE records every Igneum Miner uploads. One row per card model, driver major and program class: the median tuned point, MH per watt, the spread and the sample count. No machine names, no addresses.",
|
||||||
|
"generated": null,
|
||||||
|
"min_samples": 5,
|
||||||
|
"rows": []
|
||||||
|
}
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<title>The Igneum miner app</title>
|
<title>The Igneum miner app</title>
|
||||||
<meta name="description" content="One click: install, start, the card mines, and an NVIDIA card with 24 GB proves. Auto GPU detection on NVIDIA, AMD and Apple silicon, a wallet made for you, hourly program swaps with no pause, signed updates, a visible 1% software fee you can switch off.">
|
<meta name="description" content="One click: install, start, the card mines, and every NVIDIA card from 8 GB proves. Auto GPU detection on NVIDIA, AMD and Apple silicon, a wallet made for you, hourly program swaps with no pause, signed updates, a visible 1% software fee you can switch off.">
|
||||||
<link rel="canonical" href="https://igneum.network/miner">
|
<link rel="canonical" href="https://igneum.network/miner">
|
||||||
<meta name="theme-color" content="#0C0C0E">
|
<meta name="theme-color" content="#0C0C0E">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:site_name" content="Igneum">
|
<meta property="og:site_name" content="Igneum">
|
||||||
<meta property="og:title" content="The Igneum miner app">
|
<meta property="og:title" content="The Igneum miner app">
|
||||||
<meta property="og:description" content="One click: install, start, the card mines, and an NVIDIA card with 24 GB proves. Auto GPU detection on NVIDIA, AMD and Apple silicon, a wallet made for you, hourly program swaps with no pause, signed updates, a visible 1% software fee you can switch off.">
|
<meta property="og:description" content="One click: install, start, the card mines, and every NVIDIA card from 8 GB proves. Auto GPU detection on NVIDIA, AMD and Apple silicon, a wallet made for you, hourly program swaps with no pause, signed updates, a visible 1% software fee you can switch off.">
|
||||||
<meta property="og:url" content="https://igneum.network/miner">
|
<meta property="og:url" content="https://igneum.network/miner">
|
||||||
<meta property="og:image" content="https://igneum.network/og.png?v=3">
|
<meta property="og:image" content="https://igneum.network/og.png?v=3">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="The Igneum miner app">
|
<meta name="twitter:title" content="The Igneum miner app">
|
||||||
<meta name="twitter:description" content="One click: install, start, the card mines, and an NVIDIA card with 24 GB proves. Auto GPU detection, a wallet made for you, hourly program swaps with no pause, signed updates, a visible 1% software fee you can switch off.">
|
<meta name="twitter:description" content="One click: install, start, the card mines, and every NVIDIA card from 8 GB proves. Auto GPU detection, a wallet made for you, hourly program swaps with no pause, signed updates, a visible 1% software fee you can switch off.">
|
||||||
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
|
<meta name="twitter:image" content="https://igneum.network/og.png?v=3">
|
||||||
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
|
||||||
|
|
@ -249,7 +249,7 @@ pre b{color:var(--molten);font-weight:500}
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
<div class="eyebrow ember"><span data-product="full">Igneum Ember</span> · one click</div>
|
<div class="eyebrow ember"><span data-product="full">Igneum Ember</span> · one click</div>
|
||||||
<h1>Install. Start. The card mines. An NVIDIA card with 24 GB proves too.</h1>
|
<h1>Install. Start. The card mines. Every NVIDIA card from 8 GB proves; 12 GB and up mine and prove.</h1>
|
||||||
<p class="lead"><span data-product="name">Ember</span> finds your GPU, makes a wallet for you and runs the node, the miner and the prover as one app. Every hour it compiles the next program while the current one mines, so the card never stops.</p>
|
<p class="lead"><span data-product="name">Ember</span> finds your GPU, makes a wallet for you and runs the node, the miner and the prover as one app. Every hour it compiles the next program while the current one mines, so the card never stops.</p>
|
||||||
<div class="cta">
|
<div class="cta">
|
||||||
<a href="#get" class="btn primary">Downloads: public testnet</a>
|
<a href="#get" class="btn primary">Downloads: public testnet</a>
|
||||||
|
|
@ -305,7 +305,7 @@ pre b{color:var(--molten);font-weight:500}
|
||||||
<div class="group reveal" id="start">
|
<div class="group reveal" id="start">
|
||||||
<div class="g-head"><div class="eyebrow ember">01 · One click</div><h3>Install, press Start</h3><p>Five steps from download to the first block on a friend's laptop: drag to Applications, open, Get started, Make me an address, Start mining.</p></div>
|
<div class="g-head"><div class="eyebrow ember">01 · One click</div><h3>Install, press Start</h3><p>Five steps from download to the first block on a friend's laptop: drag to Applications, open, Get started, Make me an address, Start mining.</p></div>
|
||||||
<div class="feats">
|
<div class="feats">
|
||||||
<div class="feat"><b>The card mines; an NVIDIA card proves</b><p>One button starts the node, waits for sync, starts one miner per card and, on an NVIDIA card with 24 GB or more, the prover. AMD and Apple cards mine; a prover for them lands when a zkVM ships one. Quit stops them in order.</p></div>
|
<div class="feat"><b>The card mines; an NVIDIA card proves</b><p>One button starts the node, waits for sync, starts one miner per card and, on an NVIDIA card, the prover: every NVIDIA card from 8 GB proves; 12 GB and up mine and prove; 24 GB on the stock server (eleven rented cards, RTX 3060 to RTX 5090, 6 October 2026, docs/analysis/prover-tiers-real-cards.md); the patched server for the smaller cards is not yet in the shipped app. AMD and Apple cards mine; a prover for them lands when a zkVM ships one. Quit stops them in order.</p></div>
|
||||||
<div class="feat"><b>Finds your GPU by itself</b><p>NVIDIA through the driver, AMD and Intel through OpenCL, Apple silicon through Metal. Real card names, one worker per card.</p></div>
|
<div class="feat"><b>Finds your GPU by itself</b><p>NVIDIA through the driver, AMD and Intel through OpenCL, Apple silicon through Metal. Real card names, one worker per card.</p></div>
|
||||||
<div class="feat"><b>A wallet made for you</b><p>A payout key made on your machine and locked to your user, with a backup step before mining starts. Or paste an address you already hold.</p></div>
|
<div class="feat"><b>A wallet made for you</b><p>A payout key made on your machine and locked to your user, with a backup step before mining starts. Or paste an address you already hold.</p></div>
|
||||||
<div class="feat"><b>Earnings in IGN</b><p>Every block this machine finds pays one address in IGN. The dashboard counts them per run, per hour and for life. The <a href="/wallet" style="color:var(--ember)">wallet</a> shows the balance.</p></div>
|
<div class="feat"><b>Earnings in IGN</b><p>Every block this machine finds pays one address in IGN. The dashboard counts them per run, per hour and for life. The <a href="/wallet" style="color:var(--ember)">wallet</a> shows the balance.</p></div>
|
||||||
|
|
@ -353,7 +353,7 @@ pre b{color:var(--molten);font-weight:500}
|
||||||
<div class="feat"><b>Variant racing every hour</b><p>At every hourly prepare the worker compiles the program in several shapes (unroll, load path, register budget, threads per group), times each for 2 seconds and keeps the fastest for the hour. Base keeps its place unless beaten.</p><a class="src" href="/bench#4-october-2026-miner-performance-variant-racing-metal-worker-on-the-m5-max-the-rtx-5090-job-is-ready-not-run">log · 4 Oct 2026</a></div>
|
<div class="feat"><b>Variant racing every hour</b><p>At every hourly prepare the worker compiles the program in several shapes (unroll, load path, register budget, threads per group), times each for 2 seconds and keeps the fastest for the hour. Base keeps its place unless beaten.</p><a class="src" href="/bench#4-october-2026-miner-performance-variant-racing-metal-worker-on-the-m5-max-the-rtx-5090-job-is-ready-not-run">log · 4 Oct 2026</a></div>
|
||||||
<div class="feat"><b>Measured on Apple silicon</b><p>On the M5 Max, 256-thread groups ran 17.3% and 21.2% faster than base on two programs, under load from other work. The NVIDIA race is built and has not yet run on a GPU.</p><a class="src" href="/bench#4-october-2026-miner-performance-variant-racing-metal-worker-on-the-m5-max-the-rtx-5090-job-is-ready-not-run">log · 4 Oct 2026, ratios under contention</a></div>
|
<div class="feat"><b>Measured on Apple silicon</b><p>On the M5 Max, 256-thread groups ran 17.3% and 21.2% faster than base on two programs, under load from other work. The NVIDIA race is built and has not yet run on a GPU.</p><a class="src" href="/bench#4-october-2026-miner-performance-variant-racing-metal-worker-on-the-m5-max-the-rtx-5090-job-is-ready-not-run">log · 4 Oct 2026, ratios under contention</a></div>
|
||||||
<div class="feat"><b>Fleet tuning manifest</b><p>Every race is one record in the app log. The fleet's best variant per card model goes back out inside the signed update manifest, so a card starts from the known best and keeps racing.</p></div>
|
<div class="feat"><b>Fleet tuning manifest</b><p>Every race is one record in the app log. The fleet's best variant per card model goes back out inside the signed update manifest, so a card starts from the known best and keeps racing.</p></div>
|
||||||
<div class="feat"><b>Efficiency mode</b><p>Hash per watt, the number miners compare. The app steps an NVIDIA card's power cap from 100% to 50%, holds each step for 60 seconds on the live kernel and leaves the cap at the best MH per watt. Built and unit-tested; not yet run on a card.</p></div>
|
<div class="feat"><b>Ember Tune</b><p>Hash per watt, the number miners compare. Out of the box the app steps every card's power limit and core clock on the live kernel, 60 seconds a step, and keeps the point with the best MH per watt within 1% of the card's top rate. The memory clock is never touched; a step with a rejected hash, a hot GPU or a dragged memory clock is reverted and marked. Every result feeds a fleet prior per card model that the next card of that model starts from. Built and unit-tested; the first measured tune is owed.</p></div>
|
||||||
<div class="feat"><b>Latency work</b><p>A block built on a stale tip earns less. The miner is moving from polling to a template subscription and shorter jobs, so a new tip reaches the card in milliseconds. In progress, no number published yet.</p></div>
|
<div class="feat"><b>Latency work</b><p>A block built on a stale tip earns less. The miner is moving from polling to a template subscription and shorter jobs, so a new tip reaches the card in milliseconds. In progress, no number published yet.</p></div>
|
||||||
<div class="feat"><b>Remote signed jobs, opt in</b><p>Our own fleet only. A switch, "Allow remote jobs from Igneum (signed)", with the key's fingerprint beside it. Off aborts the running job and stops polling. Jobs run at most once each and only on the machines they name.</p></div>
|
<div class="feat"><b>Remote signed jobs, opt in</b><p>Our own fleet only. A switch, "Allow remote jobs from Igneum (signed)", with the key's fingerprint beside it. Off aborts the running job and stops polling. Jobs run at most once each and only on the machines they name.</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -403,8 +403,8 @@ pre b{color:var(--molten);font-weight:500}
|
||||||
<thead><tr><th>Lever</th><th>The idea</th><th>Measured state</th></tr></thead>
|
<thead><tr><th>Lever</th><th>The idea</th><th>Measured state</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><b>1 · Race the compiler every hour</b></td><td>For each new program, five to ten kernel variants are compiled, benched for two seconds each, and the winner is kept for the hour.</td><td>Measured on the Mac's Metal worker: the winning variant +17.3% on the genesis seed and +21.2% on the hourly seed over the base compile, under load from other work. The RTX 5090 race is prepared and not yet run. <a href="/bench#4-october-2026-miner-performance-variant-racing-metal-worker-on-the-m5-max-the-rtx-5090-job-is-ready-not-run">Log, 4 Oct 2026</a></td></tr>
|
<tr><td><b>1 · Race the compiler every hour</b></td><td>For each new program, five to ten kernel variants are compiled, benched for two seconds each, and the winner is kept for the hour.</td><td>Measured on the Mac's Metal worker: the winning variant +17.3% on the genesis seed and +21.2% on the hourly seed over the base compile, under load from other work. The RTX 5090 race is prepared and not yet run. <a href="/bench#4-october-2026-miner-performance-variant-racing-metal-worker-on-the-m5-max-the-rtx-5090-job-is-ready-not-run">Log, 4 Oct 2026</a></td></tr>
|
||||||
<tr><td><b>2 · Auto-tune that learns from the fleet</b></td><td>Apps report the race per card and program class. The best settings come back down through the signed update manifest as defaults, so the miner gets faster for everyone as the fleet grows.</td><td>Shipped. The fleet is still small, so no fleet table yet.</td></tr>
|
<tr><td><b>2 · Auto-tune that learns from the fleet</b></td><td>Apps report the race per card and program class, and every finished Ember Tune. The best settings come back down through the signed update manifest as defaults, so the miner gets faster and more efficient for everyone as the fleet grows.</td><td>Shipped. The fleet is still small, so no fleet table yet; the priors table on <a href="/miners#priors">/miners</a> fills as machines report.</td></tr>
|
||||||
<tr><td><b>3 · Hash per watt, not hash</b></td><td>A sweep per card finds the power point with the best MH per watt and holds it. Miners pay for electricity; that is the number they compare.</td><td>Shipped for NVIDIA cards through the power cap. Not yet run on a card; the first sweep on a 5090 is the owed measurement. Clocks are the next lever.</td></tr>
|
<tr><td><b>3 · Hash per watt, not hash</b></td><td>Ember Tune: two knobs per card (power limit, core clock), the memory clock held, the point with the best MH per watt within 1% of the top rate kept and pinned. Miners pay for electricity; that is the number they compare.</td><td>Built for NVIDIA (through the driver, with Power control on) and AMD (through the app's own helper, no administrator rights), measure only on Apple silicon. Unit-tested on every rule; the first tune measured on a card is the owed number.</td></tr>
|
||||||
<tr><td><b>4 · Template latency</b></td><td>Solo against the local node, a new template within 50 ms of a new tip, because a late block on a BlockDAG goes red and earns nothing.</td><td>Approximate, from the 0.3.6 release plan and not yet in the engineering log: switched p50 46 to 52 ms on a three-node CPU run, 5 Oct 2026. Ships in 0.3.6.</td></tr>
|
<tr><td><b>4 · Template latency</b></td><td>Solo against the local node, a new template within 50 ms of a new tip, because a late block on a BlockDAG goes red and earns nothing.</td><td>Approximate, from the 0.3.6 release plan and not yet in the engineering log: switched p50 46 to 52 ms on a three-node CPU run, 5 Oct 2026. Ships in 0.3.6.</td></tr>
|
||||||
<tr><td><b>5 · Never lose a second</b></td><td>Zero-loss hourly program swaps, fault guards, automatic restart, a CPU re-check of every found hash, per-worker health.</td><td>Shipped. Swap 0.01 ms on Metal and 0.00 ms on CUDA with 0 rejected blocks; recoveries in the <a href="#reliability">table above</a>. <a href="/bench#4-october-2026-first-hourly-program-swap-on-the-live-devnet-compile-ahead-no-pause-two-cards">Log, 4 Oct 2026</a></td></tr>
|
<tr><td><b>5 · Never lose a second</b></td><td>Zero-loss hourly program swaps, fault guards, automatic restart, a CPU re-check of every found hash, per-worker health.</td><td>Shipped. Swap 0.01 ms on Metal and 0.00 ms on CUDA with 0 rejected blocks; recoveries in the <a href="#reliability">table above</a>. <a href="/bench#4-october-2026-first-hourly-program-swap-on-the-live-devnet-compile-ahead-no-pause-two-cards">Log, 4 Oct 2026</a></td></tr>
|
||||||
<tr><td><b>6 · Prove it in public</b></td><td>The bench table per card, fed from the job channel. We claim fastest only when the table says so.</td><td>Live at <a href="/miners">/miners</a>, one row per card, generator version and miner version, each with its log entry.</td></tr>
|
<tr><td><b>6 · Prove it in public</b></td><td>The bench table per card, fed from the job channel. We claim fastest only when the table says so.</td><td>Live at <a href="/miners">/miners</a>, one row per card, generator version and miner version, each with its log entry.</td></tr>
|
||||||
|
|
@ -456,18 +456,19 @@ pre b{color:var(--molten);font-weight:500}
|
||||||
<h2>Get the miner</h2>
|
<h2>Get the miner</h2>
|
||||||
<p>Download only from this domain. Nobody from Igneum will ask for your seed. Every file below is the one the app's signed manifest names, with its version and size.</p>
|
<p>Download only from this domain. Nobody from Igneum will ask for your seed. Every file below is the one the app's signed manifest names, with its version and size.</p>
|
||||||
<p data-testnet-notice style="margin-top:10px;font-weight:600;color:var(--molten)">Public testnet: not yet open; the devnet build is here for people who want to look.</p>
|
<p data-testnet-notice style="margin-top:10px;font-weight:600;color:var(--molten)">Public testnet: not yet open; the devnet build is here for people who want to look.</p>
|
||||||
|
<p data-devnet-notice style="margin-top:6px;font-weight:600;color:var(--molten)">Devnet: coins have no value and the chain may be reset.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cta reveal" style="align-items:center">
|
<div class="cta reveal" style="align-items:center">
|
||||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M3 5.5l7.5-1v7H3zM11.5 4.3L21 3v8.5h-9.5zM3 12.5h7.5v7L3 18.5zM11.5 12.5H21V21l-9.5-1.3z"/></svg>Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.85">v0.3.10 · 49.9 MB</span></a>
|
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M3 5.5l7.5-1v7H3zM11.5 4.3L21 3v8.5h-9.5zM3 12.5h7.5v7L3 18.5zM11.5 12.5H21V21l-9.5-1.3z"/></svg>Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.85">v0.3.13 · 45.4 MB</span></a>
|
||||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.8-3.5.8-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.8 2.3-1.6 2.8-.4 7 1.2 9.3.8 1.1 1.7 2.4 2.9 2.3 1.2 0 1.6-.7 3-.7s1.8.7 3 .7c1.3 0 2-1.1 2.8-2.3.9-1.3 1.2-2.6 1.3-2.6-.1 0-2.5-.9-2.5-3.7zM14.1 5.8c.6-.8 1.1-1.9.9-3-.9 0-2 .6-2.7 1.4-.6.7-1.1 1.8-1 2.9 1.1.1 2.1-.5 2.8-1.3z"/></svg>macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.85">v0.3.10 · 41.4 MB</span></a>
|
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.8-3.5.8-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.8 2.3-1.6 2.8-.4 7 1.2 9.3.8 1.1 1.7 2.4 2.9 2.3 1.2 0 1.6-.7 3-.7s1.8.7 3 .7c1.3 0 2-1.1 2.8-2.3.9-1.3 1.2-2.6 1.3-2.6-.1 0-2.5-.9-2.5-3.7zM14.1 5.8c.6-.8 1.1-1.9.9-3-.9 0-2 .6-2.7 1.4-.6.7-1.1 1.8-1 2.9 1.1.1 2.1-.5 2.8-1.3z"/></svg>macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.85">v0.3.13 · 41.9 MB</span></a>
|
||||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg>Linux <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.9 · 24.2 MB</span></a>
|
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg>Linux <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.13 · 24.6 MB</span></a>
|
||||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.5l8.2 4.75v9.5L12 21.5l-8.2-4.75v-9.5z"/><path d="M12 7.5l4.3 2.5v5L12 17.5l-4.3-2.5v-5z"/></svg>HiveOS <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.9 · 24.2 MB</span></a>
|
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.5l8.2 4.75v9.5L12 21.5l-8.2-4.75v-9.5z"/><path d="M12 7.5l4.3 2.5v5L12 17.5l-4.3-2.5v-5z"/></svg>HiveOS <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.13 · 24.6 MB</span></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card reveal" id="hive" style="margin-top:28px;overflow-wrap:anywhere;word-break:break-word;min-width:0">
|
<div class="card reveal" id="hive" style="margin-top:28px;overflow-wrap:anywhere;word-break:break-word;min-width:0">
|
||||||
<div class="eyebrow">HiveOS · Flight Sheet</div>
|
<div class="eyebrow">HiveOS · Flight Sheet</div>
|
||||||
<h3 style="margin:8px 0 10px">Install on a Hive rig</h3>
|
<h3 style="margin:8px 0 10px">Install on a Hive rig</h3>
|
||||||
<p style="font-size:15px;color:var(--ink-2)">Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.9.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>, pool URL <code>grpc://<your node>:26610</code> or <code>local</code> for the bundled node, extra config <code>DEV_FEE=1 IDENTITIES=8 WORKER=auto VOTE=1</code>. The Linux button above is the same archive: the Linux miner, node and both GPU workers. Hive itself is untested so far. Report what breaks.</p>
|
<p style="font-size:15px;color:var(--ink-2)">Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.13.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>, pool URL <code>grpc://<your node>:26610</code> or <code>local</code> for the bundled node, extra config <code>DEV_FEE=1 IDENTITIES=8 WORKER=auto VOTE=1</code>. The Linux button above is the same archive: the Linux miner, node and both GPU workers. Hive itself is untested so far. Report what breaks.</p>
|
||||||
<p style="font-size:13px;color:var(--ash);margin-top:8px;overflow-wrap:anywhere">sha256 <span class="mono" data-dl-sha="miner-hive" style="word-break:break-all">7a58a30fd47c9ecb3d4aeaa0c0a464550f7e4b2b33eacf87512f1b72164c829e</span></p>
|
<p style="font-size:13px;color:var(--ash);margin-top:8px;overflow-wrap:anywhere">sha256 <span class="mono" data-dl-sha="miner-hive" style="word-break:break-all">65ea42600236c7024844d85fc401ef2c4650e671d92061c8db6415038bac9530</span></p>
|
||||||
</div>
|
</div>
|
||||||
<p class="pt reveal" style="margin-top:20px">Testnet coin for testing: <a href="/faucet" style="color:var(--ember)">the faucet</a> sends 10 IGN per address per day. Any 4 GB card at launch. The dataset starts at 2 GB and grows (the proposed schedule, fixed at the testnet genesis: 2 GB, doubling at years 4, 12 and 28), so a 4 GB card mines for about four years, approximate. Updates arrive signed; the app installs them itself. The <a href="/wallet" style="color:var(--ember)">wallet</a> reads this machine's node when the miner is installed.</p>
|
<p class="pt reveal" style="margin-top:20px">Testnet coin for testing: <a href="/faucet" style="color:var(--ember)">the faucet</a> sends 10 IGN per address per day. Any 4 GB card at launch. The dataset starts at 2 GB and grows (the proposed schedule, fixed at the testnet genesis: 2 GB, doubling at years 4, 12 and 28), so a 4 GB card mines for about four years, approximate. Updates arrive signed; the app installs them itself. The <a href="/wallet" style="color:var(--ember)">wallet</a> reads this machine's node when the miner is installed.</p>
|
||||||
<a href="/litepaper#miners" class="lp reveal">Read more in the litepaper</a>
|
<a href="/litepaper#miners" class="lp reveal">Read more in the litepaper</a>
|
||||||
|
|
@ -501,6 +502,7 @@ pre b{color:var(--molten);font-weight:500}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -172,12 +172,12 @@ th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:
|
||||||
<!-- nav:end -->
|
<!-- nav:end -->
|
||||||
<main id="main" class="wrap">
|
<main id="main" class="wrap">
|
||||||
<div class="head">
|
<div class="head">
|
||||||
<div class="eyebrow">2 entries, newest at the bottom</div>
|
<div class="eyebrow">3 entries, newest at the bottom</div>
|
||||||
<h1>GPU bench table</h1>
|
<h1>GPU bench table</h1>
|
||||||
<p class="note">Measured hash rates per card on the Igneum lottery hash, with the generator version, the miner version, the date and the log entry behind each number.</p>
|
<p class="note">Measured hash rates per card on the Igneum lottery hash, with the generator version, the miner version, the date and the log entry behind each number.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<nav class="toc" aria-label="Contents"><div class="lbl">Contents</div><ol><li><a href="#table">The table</a></li><li><a href="#how">How a row gets here</a></li></ol></nav>
|
<nav class="toc" aria-label="Contents"><div class="lbl">Contents</div><ol><li><a href="#table">The table</a></li><li><a href="#how">How a row gets here</a></li><li><a href="#priors">Fleet tuning priors</a></li></ol></nav>
|
||||||
<article><h2 id="table">The table</h2>
|
<article><h2 id="table">The table</h2>
|
||||||
<p>One row per card, generator version and miner version. The rate is the best one measured. Integrated GPUs are not listed. Prototype rows are bench numbers from before the devnet and say so in the miner column.</p>
|
<p>One row per card, generator version and miner version. The rate is the best one measured. Integrated GPUs are not listed. Prototype rows are bench numbers from before the devnet and say so in the miner column.</p>
|
||||||
<div class="tbl"><table><thead><tr><th>Card</th><th>Generator</th><th>Best MH/s</th><th>MH per watt</th><th>Miner</th><th>Date</th><th>Source</th><th>Who measured it</th></tr></thead><tbody><tr><td>Apple M5 Max (40 GPU cores, Metal)</td><td>v1</td><td>45.2</td><td>not measured</td><td>proto-metal bench (prototype, not mining)</td><td>2026-10-03</td><td>bench log: 3 October 2026, RTX 5090 first run (the Apple row of the same table)</td><td>measured by the team. genesis program, 1 GiB dataset</td></tr><tr><td>Apple M5 Max (40 GPU cores, Metal)</td><td>v2</td><td>26.7</td><td>not measured</td><td>igneum-miner devnet v4, Metal worker with prepare</td><td>2026-10-04</td><td>bench log: 4 October 2026, first hourly program swap on the live devnet: compile-ahead, no pause, two cards</td><td>measured by the team. live devnet v4, unbroken through the hour boundary</td></tr><tr><td>Apple silicon laptop (model not reported)</td><td>v2</td><td>24.3</td><td>not measured</td><td>Igneum Miner 0.3.1 (DMG)</td><td>2026-10-04</td><td>bench log: 4 October 2026, first outside machine on the devnet: an Apple silicon laptop through the Igneum Miner app</td><td>reported by the fleet. 21.0 MH/s average over 7 minutes, 24.3 MH/s at the moment of the report, 33 accepted blocks</td></tr><tr><td>NVIDIA RTX 5090 (32 GB)</td><td>v1</td><td>229</td><td>not measured</td><td>proto-cuda bench (prototype, not mining)</td><td>2026-10-03</td><td>bench log: 3 October 2026, RTX 5090, memory-hard dataset (pack igneum-genesis-mh)</td><td>measured by the team. genesis program, 104 loads per hash, 1 GiB dataset</td></tr><tr><td>NVIDIA RTX 5090 (32 GB)</td><td>v1</td><td>185.3</td><td>not measured</td><td>proto-cuda bench (prototype, not mining)</td><td>2026-10-03</td><td>bench log: 3 October 2026, RTX 5090 first run, dataset sweep and second program</td><td>measured by the team. hourly program, 128 loads per hash, 1 GiB dataset</td></tr><tr><td>NVIDIA RTX 5090 (32 GB)</td><td>v2</td><td>124.2</td><td>not measured</td><td>Igneum Miner 0.3.0 package, prebuilt NVRTC worker</td><td>2026-10-04</td><td>bench log: 4 October 2026, the gfx1036 worker fault and what the Apple M5 Max could and could not reproduce</td><td>measured by the team. live devnet v4, 128 loads per hash, CPU re-check clean, 0 rejected</td></tr></tbody></table></div>
|
<div class="tbl"><table><thead><tr><th>Card</th><th>Generator</th><th>Best MH/s</th><th>MH per watt</th><th>Miner</th><th>Date</th><th>Source</th><th>Who measured it</th></tr></thead><tbody><tr><td>Apple M5 Max (40 GPU cores, Metal)</td><td>v1</td><td>45.2</td><td>not measured</td><td>proto-metal bench (prototype, not mining)</td><td>2026-10-03</td><td>bench log: 3 October 2026, RTX 5090 first run (the Apple row of the same table)</td><td>measured by the team. genesis program, 1 GiB dataset</td></tr><tr><td>Apple M5 Max (40 GPU cores, Metal)</td><td>v2</td><td>26.7</td><td>not measured</td><td>igneum-miner devnet v4, Metal worker with prepare</td><td>2026-10-04</td><td>bench log: 4 October 2026, first hourly program swap on the live devnet: compile-ahead, no pause, two cards</td><td>measured by the team. live devnet v4, unbroken through the hour boundary</td></tr><tr><td>Apple silicon laptop (model not reported)</td><td>v2</td><td>24.3</td><td>not measured</td><td>Igneum Miner 0.3.1 (DMG)</td><td>2026-10-04</td><td>bench log: 4 October 2026, first outside machine on the devnet: an Apple silicon laptop through the Igneum Miner app</td><td>reported by the fleet. 21.0 MH/s average over 7 minutes, 24.3 MH/s at the moment of the report, 33 accepted blocks</td></tr><tr><td>NVIDIA RTX 5090 (32 GB)</td><td>v1</td><td>229</td><td>not measured</td><td>proto-cuda bench (prototype, not mining)</td><td>2026-10-03</td><td>bench log: 3 October 2026, RTX 5090, memory-hard dataset (pack igneum-genesis-mh)</td><td>measured by the team. genesis program, 104 loads per hash, 1 GiB dataset</td></tr><tr><td>NVIDIA RTX 5090 (32 GB)</td><td>v1</td><td>185.3</td><td>not measured</td><td>proto-cuda bench (prototype, not mining)</td><td>2026-10-03</td><td>bench log: 3 October 2026, RTX 5090 first run, dataset sweep and second program</td><td>measured by the team. hourly program, 128 loads per hash, 1 GiB dataset</td></tr><tr><td>NVIDIA RTX 5090 (32 GB)</td><td>v2</td><td>124.2</td><td>not measured</td><td>Igneum Miner 0.3.0 package, prebuilt NVRTC worker</td><td>2026-10-04</td><td>bench log: 4 October 2026, the gfx1036 worker fault and what the Apple M5 Max could and could not reproduce</td><td>measured by the team. live devnet v4, 128 loads per hash, CPU re-check clean, 0 rejected</td></tr></tbody></table></div>
|
||||||
|
|
@ -185,7 +185,11 @@ th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:
|
||||||
<p>Every row names the engineering log entry or the job it came from. "Measured by the team" means our own hardware and our own log. "Reported by the fleet" means a machine we do not own, read from the status lines its miner uploads.</p>
|
<p>Every row names the engineering log entry or the job it came from. "Measured by the team" means our own hardware and our own log. "Reported by the fleet" means a machine we do not own, read from the status lines its miner uploads.</p>
|
||||||
<p>MH per watt needs the card's power draw during the run. The app reads it on NVIDIA cards through the driver. Rows get the figure when a run records it.</p>
|
<p>MH per watt needs the card's power draw during the run. The app reads it on NVIDIA cards through the driver. Rows get the figure when a run records it.</p>
|
||||||
<p>There is no other Igneum miner to compare with yet, so this table compares cards, not miners. The app that produces these rows: <a href="/miner">the miner page</a>.</p>
|
<p>There is no other Igneum miner to compare with yet, so this table compares cards, not miners. The app that produces these rows: <a href="/miner">the miner page</a>.</p>
|
||||||
<p>Rows: 6. Source file: <code>site/miner-bench.json</code> in the repository.</p></article>
|
<p>Rows: 6. Source file: <code>site/miner-bench.json</code> in the repository.</p>
|
||||||
|
<h2 id="priors">Fleet tuning priors</h2>
|
||||||
|
<p>Ember Tune runs on every card the app mines with: the power limit and the core clock are stepped on the live program and the card keeps the point with the best MH per watt within 1% of its top rate. Every finished tune is reported back without anything that identifies the owner, and the fleet's median point per card model, driver major and program class comes back down inside the signed update manifest as the starting point for the next card of that model. A model needs 5 reports before its prior is used.</p>
|
||||||
|
<p>No tune reports yet. The first rows appear once five machines with the same card model have reported.</p>
|
||||||
|
<p>Rows: 0. Source file: <code>site/miner-priors.json</code> in the repository, written from the fleet records by <code>tools/tuning.mjs --priors --site</code>.</p></article>
|
||||||
</div>
|
</div>
|
||||||
<p class="gen">Generated from the repository at build time. Times are UTC. Machine names are model names.</p>
|
<p class="gen">Generated from the repository at build time. Times are UTC. Machine names are model names.</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -205,6 +209,7 @@ th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"cleanUrls": true,
|
"cleanUrls": true,
|
||||||
"trailingSlash": false,
|
"trailingSlash": false,
|
||||||
"redirects": [{"source": "/ledger", "destination": "/", "permanent": false}],
|
"redirects": [],
|
||||||
"rewrites": [{"source": "/block/:id", "destination": "/block"}, {"source": "/address/:addr", "destination": "/address"}],
|
"rewrites": [{"source": "/block/:id", "destination": "/block"}, {"source": "/address/:addr", "destination": "/address"}],
|
||||||
"headers": [
|
"headers": [
|
||||||
{"source": "/(.*)", "headers": [{"key": "X-Content-Type-Options", "value": "nosniff"}, {"key": "X-Frame-Options", "value": "DENY"}, {"key": "Referrer-Policy", "value": "strict-origin-when-cross-origin"}, {"key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload"}]},
|
{"source": "/(.*)", "headers": [{"key": "X-Content-Type-Options", "value": "nosniff"}, {"key": "X-Frame-Options", "value": "DENY"}, {"key": "Referrer-Policy", "value": "strict-origin-when-cross-origin"}, {"key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload"}]},
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,7 @@ td.num{font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||||
<h2>Get the wallet</h2>
|
<h2>Get the wallet</h2>
|
||||||
<p>Download only from this domain. Nobody from Igneum will ask for your seed. The file below is the one the wallet's signed manifest names, with its version and size.</p>
|
<p>Download only from this domain. Nobody from Igneum will ask for your seed. The file below is the one the wallet's signed manifest names, with its version and size.</p>
|
||||||
<p data-testnet-notice style="margin-top:10px;font-weight:600;color:var(--molten)">Public testnet: not yet open; the devnet build is here for people who want to look.</p>
|
<p data-testnet-notice style="margin-top:10px;font-weight:600;color:var(--molten)">Public testnet: not yet open; the devnet build is here for people who want to look.</p>
|
||||||
|
<p data-devnet-notice style="margin-top:6px;font-weight:600;color:var(--molten)">Devnet: coins have no value and the chain may be reset.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="cta reveal" style="align-items:center">
|
<div class="cta reveal" style="align-items:center">
|
||||||
<a data-dl="wallet-mac" href="https://dl.igneum.network/public/igneum-wallet-mac.dmg" class="btn primary"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.8-3.5.8-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.8 2.3-1.6 2.8-.4 7 1.2 9.3.8 1.1 1.7 2.4 2.9 2.3 1.2 0 1.6-.7 3-.7s1.8.7 3 .7c1.3 0 2-1.1 2.8-2.3.9-1.3 1.2-2.6 1.3-2.6-.1 0-2.5-.9-2.5-3.7zM14.1 5.8c.6-.8 1.1-1.9.9-3-.9 0-2 .6-2.7 1.4-.6.7-1.1 1.8-1 2.9 1.1.1 2.1-.5 2.8-1.3z"/></svg>macOS <span data-dl-meta="wallet-mac" style="font-weight:400;opacity:.85">v0.1.4 · 19.6 MB</span></a>
|
<a data-dl="wallet-mac" href="https://dl.igneum.network/public/igneum-wallet-mac.dmg" class="btn primary"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.8-3.5.8-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.8 2.3-1.6 2.8-.4 7 1.2 9.3.8 1.1 1.7 2.4 2.9 2.3 1.2 0 1.6-.7 3-.7s1.8.7 3 .7c1.3 0 2-1.1 2.8-2.3.9-1.3 1.2-2.6 1.3-2.6-.1 0-2.5-.9-2.5-3.7zM14.1 5.8c.6-.8 1.1-1.9.9-3-.9 0-2 .6-2.7 1.4-.6.7-1.1 1.8-1 2.9 1.1.1 2.1-.5 2.8-1.3z"/></svg>macOS <span data-dl-meta="wallet-mac" style="font-weight:400;opacity:.85">v0.1.4 · 19.6 MB</span></a>
|
||||||
|
|
@ -403,6 +404,7 @@ td.num{font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||||
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
<a href="/litepaper#shoulders">Built on the shoulders</a>
|
||||||
<a href="/bench">Engineering log</a>
|
<a href="/bench">Engineering log</a>
|
||||||
<a href="/evidence">Evidence</a>
|
<a href="/evidence">Evidence</a>
|
||||||
|
<a href="/ledger">Ledger: every criticism</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="foot-col" aria-label="Run">
|
<nav class="foot-col" aria-label="Run">
|
||||||
<div class="eyebrow">Run</div>
|
<div class="eyebrow">Run</div>
|
||||||
|
|
|
||||||
112
tools/build-remote.sh
Executable file
112
tools/build-remote.sh
Executable file
|
|
@ -0,0 +1,112 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build on igneum-build-1 instead of this Mac. Run from any crate directory of any worktree on the Mac (a fork worktree under
|
||||||
|
# vendor/, igneum-pow, app/igneum-app, proving/igneum-prove): the sources go to /srv/builds/<worktree>/<same relative path> on
|
||||||
|
# the box (HEAD through the bare mirror, uncommitted changes by rsync, changed files re-stamped with touch in lib.sh's
|
||||||
|
# bs_overlay_dir: the copied-sources rule), the cargo command runs there
|
||||||
|
# with sccache and -j 90 under one of the box's build slots, and the artefacts come back into target-remote/ here.
|
||||||
|
#
|
||||||
|
# tools/build-remote.sh the default command for this crate (below)
|
||||||
|
# tools/build-remote.sh -- build --release -p kaspad --features kaspad/igneum-pow
|
||||||
|
# tools/build-remote.sh -- test --release -p kaspa-consensus-core --lib
|
||||||
|
# tools/build-remote.sh --artefacts "target/release/igneumd" --out /tmp/x -- build --release -p kaspad --features kaspad/igneum-pow
|
||||||
|
# tools/build-remote.sh --jobs 48 -- check
|
||||||
|
# tools/build-remote.sh --target-dir target-exp -- build --release another persistent target dir on the box
|
||||||
|
# tools/build-remote.sh --no-fetch -- clippy --all-targets nothing comes back (tests, check, clippy)
|
||||||
|
#
|
||||||
|
# Defaults by crate: a fork worktree builds `-p kaspad -p igneum-miner --features kaspad/igneum-pow` in release and fetches
|
||||||
|
# target/release/{igneumd,igneum-miner} (what packaging/README-ship.md and infra/cross expect); app/igneum-app builds release
|
||||||
|
# and fetches igneum-app, igneum-ota-sign, igneum-prove-verify; proving/igneum-prove fetches igneum-prove-host and
|
||||||
|
# igneum-prove-export; any other crate builds release and fetches nothing unless --artefacts names files.
|
||||||
|
#
|
||||||
|
# Artefacts land in <crate>/target-remote/<path without the leading target/> (target-remote/release/igneumd), NEVER in
|
||||||
|
# target/: the box builds x86_64 Linux ELF binaries (glibc 2.39, Ubuntu 24.04), which do not run on this Mac. Each one is
|
||||||
|
# reported with size and sha256. For Windows exes use tools/cross-remote.sh.
|
||||||
|
#
|
||||||
|
# Slots: the box has its own slot files (/srv/builds/_locks/build-<k>, count in /srv/builds/_locks/slots, default 1); this
|
||||||
|
# script takes one of THOSE, never the Mac's ~/.config/igneum/build-slots or tools/lock/with-lock.sh, so a remote build does
|
||||||
|
# not hold a Mac slot. A build waits up to 2 h for a remote slot, as with-lock.sh does.
|
||||||
|
#
|
||||||
|
# Needs: ~/.config/igneum/build-server (build@<ip>, written by infra/build-server/run-from-mac.sh), ~/.ssh/igneum_ed25519,
|
||||||
|
# the same rustc version on both sides (refused otherwise; IGNEUM_TOOLCHAIN_MISMATCH=ok overrides). IGNEUM_AGENT names the
|
||||||
|
# agent in the slot-file label and the box's JSONL log (/srv/builds/_log/builds.jsonl); default the worktree name.
|
||||||
|
set -euo pipefail
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck disable=SC2034 # shared with the scripts that source lib.sh
|
||||||
|
BS_TOOL=build-remote
|
||||||
|
# shellcheck source=../infra/build-server/lib.sh
|
||||||
|
. "$HERE/../infra/build-server/lib.sh"
|
||||||
|
|
||||||
|
JOBS="${JOBS:-90}"; OUT=""; ARTEFACTS=""; TARGET_DIR="target"; FETCH=1; CARGO_ARGS=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--jobs) JOBS="$2"; shift 2 ;;
|
||||||
|
--out) OUT="$2"; shift 2 ;;
|
||||||
|
--artefacts) ARTEFACTS="$2"; ARTEFACTS_SET=1; shift 2 ;;
|
||||||
|
--target-dir) TARGET_DIR="$2"; shift 2 ;;
|
||||||
|
--no-fetch) FETCH=0; shift ;;
|
||||||
|
--) shift; CARGO_ARGS=("$@"); break ;;
|
||||||
|
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
|
||||||
|
*) CARGO_ARGS=("$@"); break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ "${CARGO_ARGS[0]:-}" = cargo ] && CARGO_ARGS=("${CARGO_ARGS[@]:1}")
|
||||||
|
|
||||||
|
bs_host
|
||||||
|
bs_context
|
||||||
|
|
||||||
|
# defaults per crate
|
||||||
|
case "$BS_KIND:$BS_CRATE_REL" in
|
||||||
|
node:*)
|
||||||
|
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow)
|
||||||
|
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneumd $TARGET_DIR/release/igneum-miner" ;;
|
||||||
|
repo:app/igneum-app)
|
||||||
|
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release)
|
||||||
|
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-app $TARGET_DIR/release/igneum-ota-sign $TARGET_DIR/release/igneum-prove-verify" ;;
|
||||||
|
repo:proving/igneum-prove)
|
||||||
|
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release)
|
||||||
|
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-prove-host $TARGET_DIR/release/igneum-prove-export" ;;
|
||||||
|
*)
|
||||||
|
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release) ;;
|
||||||
|
esac
|
||||||
|
case "${CARGO_ARGS[0]}" in build) ;; *) [ -n "${ARTEFACTS_SET:-}" ] || { FETCH=0; ARTEFACTS=""; } ;; esac # test, check, clippy: nothing to fetch
|
||||||
|
[ -n "$OUT" ] || OUT="$BS_CRATE/target-remote"
|
||||||
|
|
||||||
|
bs_log "$BS_KIND crate $BS_WT/$BS_CRATE_REL at $BS_SHA ($BS_BRANCH) -> $BS_HOST:$BS_REMOTE_CRATE; cargo ${CARGO_ARGS[*]} -j $JOBS; target dir $TARGET_DIR"
|
||||||
|
bs_toolchain_check
|
||||||
|
t_sync0=$(date +%s)
|
||||||
|
bs_sync_sources
|
||||||
|
bs_log "sources in place after $(( $(date +%s) - t_sync0 )) s"
|
||||||
|
|
||||||
|
# the fork's kaspa-build-info embeds the commit through a build script that, having once found no branch, emits no
|
||||||
|
# rerun-if-changed and is never run again by cargo (release-0.3.11 plan: `cargo clean -p kaspa-build-info` first); so when
|
||||||
|
# the commit the box builds differs from the last one built in this target dir, that one crate is cleaned (a relink, seconds)
|
||||||
|
pre=""
|
||||||
|
if [ "$BS_KIND" = node ] && [ "${CARGO_ARGS[0]}" = build ]; then
|
||||||
|
pre="[ \"\$(cat '.build-remote-sha-$TARGET_DIR' 2>/dev/null)\" = '$BS_SHA' ] || CARGO_TARGET_DIR='$TARGET_DIR' cargo clean -q --release -p kaspa-build-info 2>/dev/null; "
|
||||||
|
fi
|
||||||
|
cmd="${pre}CARGO_TARGET_DIR='$TARGET_DIR' cargo $(printf '%q ' "${CARGO_ARGS[@]}")-j $JOBS 2>&1 | tee -a '$BS_REMOTE_WT/.build-remote.log'; rc=\${PIPESTATUS[0]}; [ \$rc = 0 ] && echo '$BS_SHA' > '.build-remote-sha-$TARGET_DIR'; ( exit \$rc )" # a subshell exit: the runner reads \$? and still prints its RESULT line
|
||||||
|
label="$BS_WT/$BS_CRATE_REL cargo ${CARGO_ARGS[*]}"
|
||||||
|
BR_KIND=$(bs_kind build-remote "${CARGO_ARGS[0]}"); BR_COMMAND="cargo ${CARGO_ARGS[*]}"; BR_TARGET=x86_64-unknown-linux-gnu
|
||||||
|
for ((i = 0; i < ${#CARGO_ARGS[@]}; i++)); do [ "${CARGO_ARGS[$i]}" = --target ] && BR_TARGET="${CARGO_ARGS[$((i + 1))]:-}"; done
|
||||||
|
BR_ARTEFACTS=""; [ "$FETCH" = 1 ] && BR_ARTEFACTS="$ARTEFACTS"
|
||||||
|
export BR_KIND BR_COMMAND BR_TARGET BR_ARTEFACTS
|
||||||
|
t0=$(date +%s)
|
||||||
|
set +e
|
||||||
|
bs_remote_run "$BS_REMOTE_CRATE" "$label" "$cmd" 2>&1 | tee "/tmp/build-remote-$$.log"
|
||||||
|
rc=${PIPESTATUS[0]}
|
||||||
|
set -e
|
||||||
|
secs=$(( $(date +%s) - t0 ))
|
||||||
|
result=$(grep -m1 '^build-remote: RESULT' "/tmp/build-remote-$$.log" || true); rm -f "/tmp/build-remote-$$.log"
|
||||||
|
if [ "$rc" != 0 ]; then bs_die "remote cargo failed (rc $rc) after $(bs_fmt_secs "$secs"); $result"; fi
|
||||||
|
bs_log "remote cargo ${CARGO_ARGS[0]} done in $(bs_fmt_secs "$secs") wall from the Mac; ${result#build-remote: RESULT }"
|
||||||
|
|
||||||
|
if [ "$FETCH" = 1 ] && [ -n "$ARTEFACTS" ]; then
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
for a in $ARTEFACTS; do
|
||||||
|
rel="${a#"$TARGET_DIR"/}"; dest="$OUT/$rel"; mkdir -p "$(dirname "$dest")"
|
||||||
|
bs_rsync -p "$BS_HOST:$BS_REMOTE_CRATE/$a" "$dest" || bs_die "no $a on the box after the build"
|
||||||
|
bs_log "artefact $dest: $(bs_size "$dest") bytes, sha256 $(bs_sha256 "$dest"), $(file -b "$dest" | cut -c1-60)"
|
||||||
|
# the commit-string gate (rule of 6 October 2026): a node binary without its commit in its strings fails the run
|
||||||
|
case "$BS_KIND:$(basename "$dest")" in node:igneumd) "$HERE/ci/commit-string-check.sh" "$dest" "$BS_SHA" || bs_die "commit-string gate failed for $a" ;; esac # only kaspad depends on kaspa-build-info
|
||||||
|
done
|
||||||
|
fi
|
||||||
33
tools/ci/commit-string-check.sh
Executable file
33
tools/ci/commit-string-check.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# The empty-commit class (6 October 2026, found on igneum-build-1): the fork's kaspa-build-info embeds the git commit in
|
||||||
|
# igneumd and igneum-miner only when the source tree's .git is a DIRECTORY whose HEAD is a symbolic ref to a branch file,
|
||||||
|
# and once its build script has found nothing it emits no rerun-if-changed, so a persistent target dir keeps the empty hash
|
||||||
|
# for ever. Every Mac worktree build (.git is a file there) and every PC job build (the zip has no .git) shipped with no
|
||||||
|
# commit string while the release plans' "commit in its strings" line was being ticked from main-checkout builds.
|
||||||
|
# Rule: an igneumd binary whose strings do not contain its short commit fails (igneum-miner has no kaspa-build-info
|
||||||
|
# dependency and never carried one; checked 6 October 2026). tools/build-remote.sh, tools/cross-remote.sh and
|
||||||
|
# proto-cuda/windows-node/cross-build.sh run this on every igneumd they produce; the shipper's preflight should too.
|
||||||
|
#
|
||||||
|
# tools/ci/commit-string-check.sh <binary> <commit sha, 7 to 40 hex> exit 0 when the binary carries the commit
|
||||||
|
# tools/ci/commit-string-check.sh --self-test fires on a known-bad and passes a known-good case
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "${1:-}" = --self-test ]; then
|
||||||
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||||
|
printf 'igneumd v2.1.0-3bfe346f\0junk\0' > "$tmp/good"; printf 'igneumd v2.1.0\0junk\0' > "$tmp/bad"
|
||||||
|
"$0" "$tmp/good" 3bfe346f4de0d9a982627270c791b7855abc5af6 >/dev/null || { echo "commit-string self-test: the good case FAILED"; exit 1; }
|
||||||
|
if "$0" "$tmp/bad" 3bfe346f4de0d9a982627270c791b7855abc5af6 >/dev/null 2>&1; then echo "commit-string self-test: the bad case PASSED (the gate is blind)"; exit 1; fi
|
||||||
|
echo "commit-string self-test: fires on the empty binary, passes the stamped one"; exit 0
|
||||||
|
fi
|
||||||
|
bin="${1:-}"; sha="${2:-}"
|
||||||
|
[ -f "$bin" ] && [ -n "$sha" ] || { echo "usage: commit-string-check.sh <binary> <commit sha> | --self-test" >&2; exit 2; }
|
||||||
|
printf '%s' "$sha" | grep -qE '^[0-9a-f]{7,40}$' || { echo "commit-string: '$sha' is not a commit sha" >&2; exit 2; }
|
||||||
|
short="${sha:0:7}" # build-info's fallback embeds 7 characters; git rev-parse --short gives 7 or more, all starting the same
|
||||||
|
# grep -c, not grep -q: -q closes the pipe at the first match, strings dies with SIGPIPE and pipefail turns a hit into a miss
|
||||||
|
# (the first version of this gate failed a stamped binary that way, 6 October 2026)
|
||||||
|
hits=$(strings "$bin" | grep -c "$short" || true)
|
||||||
|
if [ "${hits:-0}" -gt 0 ]; then
|
||||||
|
echo "commit-string: $(basename "$bin") carries $short"
|
||||||
|
else
|
||||||
|
echo "commit-string: $(basename "$bin") does NOT carry its commit $short (kaspa-build-info found no .git directory on a branch; see the header)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue