Merge commit 'd354d5f' into ca3-pc1-amd
# Conflicts: # app/igneum-app/src/engine.rs # tools/ci/playbook-quit-check.sh
This commit is contained in:
commit
e6f2e689ae
150 changed files with 14495 additions and 548 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
|
||||
- name: override params files parse with no duplicate key (the duplicate-field class, 6 October 2026)
|
||||
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
|
||||
run: bash tools/ci/signer-pipe-check.sh
|
||||
- name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree)
|
||||
run: bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh
|
||||
- name: run jobs test their fetched kit before use, the wiped-jobs-folder class (self-test first, then the tree)
|
||||
run: bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh
|
||||
- name: every Windows spawn of the app runs with a hidden console (self-test first, then the tree)
|
||||
run: node tools/ci/windows-spawn-check.mjs --self-test && node tools/ci/windows-spawn-check.mjs
|
||||
- name: pinned guest programs match their manifest and are built only by pin-guests.sh
|
||||
run: bash tools/ci/pinned-guests-check.sh
|
||||
- name: root prover playbooks kill the GPU server and unlink its socket (the root-socket class, 5 October 2026)
|
||||
run: bash tools/ci/prover-socket-check.sh
|
||||
- name: 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)
|
||||
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)
|
||||
|
|
@ -95,6 +103,6 @@ jobs:
|
|||
- name: ship tool self-test (version bump, the dl-both and public manifest helpers)
|
||||
run: node tools/ship-app.mjs --self-test
|
||||
- name: relay unit tests (parsers, secret compare, the wake endpoint)
|
||||
run: node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs
|
||||
run: node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
||||
- name: miner app notice strip and update card (ordering, keys, wording, timers, when the card shows)
|
||||
run: node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs
|
||||
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
|
||||
|
|
|
|||
2
app/igneum-app/Cargo.lock
generated
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.11"
|
||||
version = "0.3.13"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.11"
|
||||
version = "0.3.13"
|
||||
edition = "2021"
|
||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,3,11,0
|
||||
PRODUCTVERSION 0,3,11,0
|
||||
FILEVERSION 0,3,13,0
|
||||
PRODUCTVERSION 0,3,13,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Miner engine"
|
||||
VALUE "FileVersion", "0.3.11"
|
||||
VALUE "FileVersion", "0.3.13"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.11"
|
||||
VALUE "ProductVersion", "0.3.13"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@ pub struct CardPref {
|
|||
pub sweep_watts: f64,
|
||||
#[serde(default)]
|
||||
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)]
|
||||
|
|
@ -70,9 +80,16 @@ pub struct Settings {
|
|||
#[serde(default)]
|
||||
pub prove: bool,
|
||||
/// 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.
|
||||
#[serde(default = "yes")]
|
||||
/// 100% to 50% on the live program and held at the best MH per watt. Default off; implied by `power_control`
|
||||
/// (on when that is switched on, never effective while it is off). A pinned card is skipped.
|
||||
#[serde(default)]
|
||||
pub sweep: bool,
|
||||
/// Power control (the project lead, 5 October 2026: "if we don't have to ask then don't ask"): the NVIDIA power cap and the
|
||||
/// 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.
|
||||
#[serde(default)]
|
||||
pub installed_at: u64,
|
||||
|
|
@ -102,16 +119,48 @@ fn yes() -> bool {
|
|||
|
||||
impl Default for Settings {
|
||||
fn default() -> Settings {
|
||||
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, 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 {
|
||||
/// 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 {
|
||||
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 {
|
||||
// an install from before the sweep existed counts as installed now: it gets its first-hour sweep
|
||||
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
|
||||
|
|
@ -347,6 +396,18 @@ mod tests {
|
|||
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]
|
||||
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");
|
||||
|
|
@ -439,3 +500,18 @@ mod tests {
|
|||
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 cuda: 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,
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +101,7 @@ fn card(index: usize, name: &str, vendor: &str, worker: &str, detail: &str, devi
|
|||
device: device.into(),
|
||||
enabled: true,
|
||||
state: "off".into(),
|
||||
amd_ordinal: -1,
|
||||
..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)
|
||||
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)));
|
||||
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(", ")))
|
||||
|
|
|
|||
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_mhs = p.sweep_mhs;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ pub struct Manifest {
|
|||
pub created_at: String,
|
||||
pub node_branch: String,
|
||||
pub node_commit: String,
|
||||
/// the 40-hex commit (manifest node.commit_full, packer of 6 October 2026); empty in older manifests
|
||||
pub node_commit_full: String,
|
||||
pub node_dirty: bool,
|
||||
pub app_version: String,
|
||||
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 node = v.get("node").cloned().unwrap_or(Value::Null);
|
||||
let ns = |k: &str| node.get(k).and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||
let mut m = Manifest { created_at: s("created_at"), node_branch: ns("branch"), node_commit: ns("commit"), node_dirty: node.get("dirty").and_then(|x| x.as_bool()).unwrap_or(false), app_version: s("app_version"), ..Default::default() };
|
||||
let mut m = Manifest { created_at: s("created_at"), node_branch: ns("branch"), node_commit: ns("commit"), node_commit_full: ns("commit_full"), node_dirty: node.get("dirty").and_then(|x| x.as_bool()).unwrap_or(false), app_version: s("app_version"), ..Default::default() };
|
||||
let builds = v.get("builds").and_then(|b| b.as_array()).ok_or("manifest.json has no \"builds\" list")?;
|
||||
for (i, b) in builds.iter().enumerate() {
|
||||
let dir = b.get("dir").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
|
||||
|
|
@ -243,7 +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 AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar\n");
|
||||
s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc-posix\n");
|
||||
s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS=\"-C link-arg=-static -C link-arg=-static-libgcc\"\n");
|
||||
s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS=\"-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-Wl,--no-insert-timestamp\"\n"); // no PE timestamp: reproducible exes (6 Oct 2026)
|
||||
s.push_str("export IGNEUM_WINDRES=x86_64-w64-mingw32-windres\n");
|
||||
s.push_str("LLVM_LIB=$(ls -d /usr/lib/llvm-*/lib 2>/dev/null | sort -V | tail -1)\n");
|
||||
s.push_str("export LIBCLANG_PATH=\"${LLVM_LIB:-/usr/lib/llvm-18/lib}\"\n");
|
||||
|
|
@ -293,6 +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.
|
||||
s.push_str("find \"$B/src\" -type f -exec touch {} + || { echo \"RESULT extract cannot stamp the sources\"; exit 2; }\n");
|
||||
s.push_str("[ -f \"$SRC/manifest.json\" ] || { echo \"RESULT extract no manifest.json under $SRC\"; exit 2; }\n");
|
||||
// the commit hash (6 October 2026, found on igneum-build-1): the zip has no .git, so kaspa-build-info's build.rs embedded
|
||||
// nothing in every PC build. It needs .git to be a directory with HEAD a symbolic ref to a branch file holding the hash
|
||||
// (git rev-parse reads it; its fallback reads the file itself). A minimal .git with the manifest's full commit gives the
|
||||
// binary its commit string; tools/ci/commit-string-check.sh then passes on the fetched binaries.
|
||||
s.push_str("ncf=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"node\",{}).get(\"commit_full\",\"\"))' \"$SRC/manifest.json\" 2>/dev/null || true)\n");
|
||||
s.push_str("if [ -d \"$SRC/node\" ] && printf '%s' \"$ncf\" | grep -qE '^[0-9a-f]{40}$'; then mkdir -p \"$SRC/node/.git/refs/heads\" && printf 'ref: refs/heads/build\\n' > \"$SRC/node/.git/HEAD\" && printf '%s\\n' \"$ncf\" > \"$SRC/node/.git/refs/heads/build\" && echo \"commit $ncf written to node/.git for kaspa-build-info\"; else echo \"no full node commit in the manifest: the node binaries will carry no commit string\"; fi\n");
|
||||
// the stale-build class (5 October 2026): the target dir persists and cargo rebuilds by mtime, so every extracted
|
||||
// source is stamped now, else a file older than the last build links against the cached crate of the old version
|
||||
s.push_str("find \"$B/src\" -type f -exec touch {} + 2>/dev/null || true\n");
|
||||
|
|
@ -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!("mkdir -p \"$OUT/{target}\"\n"));
|
||||
s.push_str("rc_all=0\n");
|
||||
// the node's full commit from the manifest (each stage is its own script; the extract stage read it too)
|
||||
s.push_str("ncf=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"node\",{}).get(\"commit_full\",\"\"))' \"$SRC/manifest.json\" 2>/dev/null || true)\n");
|
||||
for u in m.builds.iter().filter(|u| u.targets.iter().any(|t| t == target)) {
|
||||
let optional = u.optional_on.iter().any(|t| t == target);
|
||||
let rel = if windows { "x86_64-pc-windows-gnu/release" } else { "release" };
|
||||
let tflag = if windows { " --target x86_64-pc-windows-gnu" } else { "" };
|
||||
s.push_str(&format!("echo \"STAGE {target} {dir} start $(now)\"\n", dir = u.dir));
|
||||
s.push_str(&format!("t0=$(date +%s); rc=1\n"));
|
||||
if u.dir == "node" {
|
||||
// kaspa-build-info emits no rerun-if-changed once it found nothing, so the persistent target dir keeps an empty
|
||||
// hash forever unless that one crate is cleaned when the commit differs from the last one built here (6 Oct 2026)
|
||||
s.push_str(&format!("if cd \"$SRC/{dir}\"; then [ \"$(cat \"$CARGO_TARGET_DIR/.node-commit-{target}\" 2>/dev/null)\" = \"$ncf\" ] || cargo clean -q --release -p kaspa-build-info{tflag} 2>/dev/null || true; fi\n", dir = u.dir));
|
||||
}
|
||||
s.push_str(&format!("if cd \"$SRC/{dir}\"; then $NICE cargo build --release $JOBS{args}{tflag} 2>&1; rc=$?; else echo \"no $SRC/{dir}\"; rc=2; fi\n", dir = u.dir, args = cargo_unit_args(u)));
|
||||
if u.dir == "node" {
|
||||
s.push_str(&format!("[ \"$rc\" = 0 ] && printf '%s\\n' \"$ncf\" > \"$CARGO_TARGET_DIR/.node-commit-{target}\"\n"));
|
||||
}
|
||||
s.push_str(&format!("echo \"RESULT {target} {dir} build exit $rc $(( $(date +%s) - t0 )) s at $(now)\"\n", dir = u.dir));
|
||||
s.push_str("if [ \"$rc\" = 0 ]; then\n");
|
||||
for b in &u.bins {
|
||||
|
|
|
|||
|
|
@ -1234,16 +1234,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('\'', "''")))
|
||||
.collect();
|
||||
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())?;
|
||||
let _ = std::fs::remove_file(&out_file);
|
||||
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`
|
||||
// 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]);
|
||||
cmd = crate::platform::elevated_command("powershell.exe", &inner);
|
||||
} else if shell == "powershell" {
|
||||
cmd = Command::new(crate::platform::tool("powershell"));
|
||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
||||
|
|
@ -1253,10 +1248,17 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
|||
}
|
||||
cmd.current_dir(&dir);
|
||||
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")?;
|
||||
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) {
|
||||
for l in t.lines() {
|
||||
for l in t.lines().skip(seen) {
|
||||
sink.line(l);
|
||||
}
|
||||
}
|
||||
|
|
@ -1264,6 +1266,56 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
|||
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> {
|
||||
match ran.code {
|
||||
Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }),
|
||||
|
|
@ -1430,10 +1482,24 @@ mod tests {
|
|||
assert!(d.summary.contains("administrator prompt"), "{}", d.summary);
|
||||
let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap();
|
||||
assert_eq!(d.status, "done");
|
||||
// the launcher string itself: a thrown Start-Process must not fall through to `exit $p.ExitCode`
|
||||
let src = include_str!("jobrun.rs");
|
||||
assert!(src.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{"));
|
||||
assert!(src.contains("if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}"));
|
||||
// the launcher string itself (platform::elevated_ps_line since 13755b9): a thrown Start-Process must not fall
|
||||
// through to `exit $p.ExitCode`
|
||||
let l = crate::platform::elevated_ps_line("powershell.exe", "-NoProfile -File x.ps1");
|
||||
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]
|
||||
|
|
@ -1920,6 +1986,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 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('\'', "''"));
|
||||
c = Command::new(crate::platform::tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", &ps]);
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ mod jobrun;
|
|||
mod jobbuild;
|
||||
mod prover;
|
||||
mod provedefault;
|
||||
mod segments;
|
||||
mod verifier;
|
||||
mod wslhost;
|
||||
mod sweep;
|
||||
mod ember;
|
||||
mod powertask;
|
||||
mod watchdog;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
|
|
@ -52,6 +55,12 @@ fn main() {
|
|||
println!("igneum-app {}", engine::VERSION);
|
||||
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 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");
|
||||
|
|
@ -93,6 +102,8 @@ fn main() {
|
|||
}
|
||||
let packaged = config::Packaged::load(&candidates).with_env_overrides();
|
||||
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
|
||||
let mut raw = [0u8; 16];
|
||||
|
|
@ -134,7 +145,7 @@ fn main() {
|
|||
let Ok(l) = line else { break };
|
||||
let t = l.trim();
|
||||
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),
|
||||
"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
|
||||
|
|
@ -145,7 +156,7 @@ fn main() {
|
|||
}
|
||||
}
|
||||
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"), "");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::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 CHECK_EVERY_S: u64 = 3600;
|
||||
const RETRY_AFTER_ERROR_S: u64 = 600;
|
||||
|
|
@ -117,6 +119,11 @@ pub struct Updater {
|
|||
deferred_until: Option<Instant>,
|
||||
/// this machine's minute of the hour for applying (manifest::slot_minute of the machine id)
|
||||
slot: u64,
|
||||
/// When this engine started (unix seconds): an update published more than an hour before it is a catch-up, not a
|
||||
/// rollout, and skips the hourly slot (the project lead's morning of 6 October 2026: PC 1 came up after the 0.3.11 publish and
|
||||
/// 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
|
||||
live_samples: Vec<(Instant, u64)>,
|
||||
live_next: Instant,
|
||||
|
|
@ -163,6 +170,8 @@ impl Updater {
|
|||
apply_launched: None,
|
||||
deferred_until: None,
|
||||
slot: manifest::slot_minute(&shared.runtime.id8()),
|
||||
started_unix: crate::platform::unix_now(),
|
||||
catch_up_logged: false,
|
||||
live_samples: Vec::new(),
|
||||
live_next: now,
|
||||
live_busy: false,
|
||||
|
|
@ -175,7 +184,11 @@ impl Updater {
|
|||
if crate::platform::start_at_login_is_on() {
|
||||
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();
|
||||
// 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 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 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 {
|
||||
|
|
@ -1264,6 +1282,7 @@ function EngineAlive() { return [bool](Get-Process -Id $EnginePid -ErrorAction S
|
|||
function Relaunch() {
|
||||
if (EngineAlive) { return }
|
||||
$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 }
|
||||
}
|
||||
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 {
|
||||
# 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
|
||||
# console: the Inno Setup installer is a GUI program (no console), /VERYSILENT shows nothing
|
||||
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -166,17 +166,28 @@ pub fn lock_permissions(path: &Path, dir: bool) {
|
|||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = dir;
|
||||
let user = std::env::var("USERNAME").unwrap_or_default();
|
||||
if !user.is_empty() {
|
||||
let _ = quiet(&mut Command::new(tool("icacls")))
|
||||
.arg(path)
|
||||
.args(["/inheritance:r", "/grant:r", &format!("{user}:F")])
|
||||
.output();
|
||||
let _ = quiet(&mut Command::new(tool("icacls"))).arg(path).args(icacls_lock_args(dir, &user)).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).
|
||||
pub fn open_url(url: &str) {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
|
@ -396,10 +407,7 @@ pub fn sync_clock() -> Result<String, String> {
|
|||
#[cfg(windows)]
|
||||
{
|
||||
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 = Command::new(tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
||||
quiet(&mut c);
|
||||
let mut c = elevated_command(&cmd, "/c net start w32time & w32tm /resync /force");
|
||||
let out = c.output().map_err(|e| e.to_string())?;
|
||||
if out.status.success() {
|
||||
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> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let escaped = cmdline.replace('\'', "''");
|
||||
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 = Command::new(tool("powershell"));
|
||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
||||
quiet(&mut c);
|
||||
let mut c = elevated_command(&cmd, &format!("/c {cmdline}"));
|
||||
let out = c.output().map_err(|e| e.to_string())?;
|
||||
if out.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
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")]
|
||||
|
|
@ -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.
|
||||
pub fn quiet(cmd: &mut Command) -> &mut Command {
|
||||
#[cfg(windows)]
|
||||
|
|
@ -461,8 +511,47 @@ pub fn quiet(cmd: &mut Command) -> &mut Command {
|
|||
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)]
|
||||
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]
|
||||
fn token_redaction() {
|
||||
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 (the project lead, 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,
|
||||
Miner(usize), // card index
|
||||
Telemetry, // nvidia-smi -l 5
|
||||
AmdTelemetry, // igneum-gpu-telemetry -l 5 (ADLX or sysfs), 5 October 2026
|
||||
}
|
||||
|
||||
impl Source {
|
||||
|
|
@ -22,6 +23,7 @@ impl Source {
|
|||
Source::Watch => "watch".into(),
|
||||
Source::Miner(i) => format!("miner{}", i + 1),
|
||||
Source::Telemetry => "gpu".into(),
|
||||
Source::AmdTelemetry => "gpu-amd".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ pub struct Work {
|
|||
pub shard_wei: u128,
|
||||
/// The first of this machine's keys that is assigned (the label that signs).
|
||||
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.
|
||||
|
|
@ -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),
|
||||
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(),
|
||||
daa: hexu(&w["daaScore"]) as u64,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
|
|
@ -344,6 +347,13 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
let ram_mb = crate::detect::total_ram_mb();
|
||||
let mut last_probe = Instant::now() - Duration::from_secs(600);
|
||||
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 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
|
||||
|
|
@ -450,7 +460,7 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
});
|
||||
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),
|
||||
Err(e) => {
|
||||
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;
|
||||
set(&shared, |p| {
|
||||
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 {
|
||||
set(&shared, |p| {
|
||||
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;
|
||||
};
|
||||
|
|
@ -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
|
||||
/// 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
|
||||
|
|
|
|||
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));
|
||||
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" => {
|
||||
let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?;
|
||||
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 }))
|
||||
}
|
||||
"/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 }))
|
||||
}
|
||||
_ => Err("unknown api".into()),
|
||||
|
|
|
|||
|
|
@ -97,6 +97,11 @@ pub struct CardState {
|
|||
pub temp_gpu: f64,
|
||||
pub temp_mem: 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)
|
||||
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
|
||||
|
|
@ -108,6 +113,18 @@ pub struct CardState {
|
|||
pub sweep_mhs: f64,
|
||||
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
|
||||
// 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
|
||||
pub variant: String,
|
||||
pub race_mhs: f64,
|
||||
|
|
@ -172,6 +189,9 @@ pub struct ProvingState {
|
|||
pub submitted: u32,
|
||||
pub paid: 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 current: String,
|
||||
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
|
||||
pub aggregated: u32,
|
||||
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)]
|
||||
|
|
@ -244,8 +273,16 @@ pub struct SettingsState {
|
|||
pub remote_jobs: bool,
|
||||
/// the prover service (src/prover.rs)
|
||||
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,
|
||||
/// 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)
|
||||
pub dev_fee: bool,
|
||||
/// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`)
|
||||
|
|
@ -401,3 +438,22 @@ impl Rings {
|
|||
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
|
||||
/// `<dir>/cmd.txt` twice a second: a line `<seq> <watts>` runs `nvidia-smi -i <device> -pl <watts>`, `quit` ends it.
|
||||
/// After 20 minutes without a new command it restores `<restore watts>` and exits by itself, so an engine that died
|
||||
/// mid-sweep leaves the card on its old limit. It writes what it ran to `<dir>/helper.log`.
|
||||
/// 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; each line is `<seq> pl <watts>` (`nvidia-smi -i <device> -pl <watts>`; the
|
||||
/// 0.3.9 form `<seq> <watts>` still works), `<seq> lgc <mhz>` (`-lgc 0,<mhz>`, the core clock cap; the memory clock
|
||||
/// 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 {
|
||||
r#"param([string]$Dir, [string]$Smi, [string]$Device, [string]$Restore)
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
|
@ -359,15 +361,27 @@ while ($true) {
|
|||
$last = $c
|
||||
$idle = Get-Date
|
||||
if ($c -eq 'quit') { "$(Get-Date -Format o) quit" | Out-File -FilePath $log -Append -Encoding utf8; break }
|
||||
$w = ($c -split ' ')[-1]
|
||||
if ($w -match '^\d+$') {
|
||||
$out = (& $Smi -i $Device -pl $w 2>&1 | Out-String).Trim()
|
||||
"$(Get-Date -Format o) -pl $w : $out" | Out-File -FilePath $log -Append -Encoding utf8
|
||||
foreach ($line in ($c -split "`n")) {
|
||||
$p = ($line.Trim() -split ' ')
|
||||
if ($p.Count -lt 2) { continue }
|
||||
$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) {
|
||||
$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
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
|
@ -388,11 +402,20 @@ while true; do
|
|||
if [ -n "$c" ] && [ "$c" != "$last" ]; then
|
||||
last="$c"; idle=$(date +%s)
|
||||
if [ "$c" = "quit" ]; then echo "$(date -u +%FT%TZ) quit" >> "$dir/helper.log"; break; fi
|
||||
w="${c##* }"
|
||||
case "$w" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) -pl $w : $("$smi" -i "$dev" -pl "$w" 2>&1)" >> "$dir/helper.log";; esac
|
||||
printf '%s\n' "$c" | while IFS= read -r line; do
|
||||
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
|
||||
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
|
||||
sleep 0.5
|
||||
done
|
||||
|
|
@ -405,8 +428,9 @@ pub fn unsupported_reason(vendor: &str, power_default_w: f64, device: &str) -> O
|
|||
match vendor {
|
||||
"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"),
|
||||
"apple" => Some("not available on Apple silicon: there is no power cap to set, and powermetrics needs administrator rights for the draw"),
|
||||
"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)"),
|
||||
// Ember Tune (src/ember.rs, 5 October 2026): AMD is tuned through igneum-gpu-telemetry, Apple measures only;
|
||||
// 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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -580,14 +604,16 @@ mod tests {
|
|||
fn unsupported_reasons() {
|
||||
assert!(unsupported_reason("nvidia", 575.0, "0").is_none());
|
||||
assert!(unsupported_reason("nvidia", 0.0, "0").unwrap().contains("power limits"));
|
||||
assert!(unsupported_reason("apple", 0.0, "").unwrap().contains("powermetrics"));
|
||||
assert!(unsupported_reason("amd", 0.0, "1").unwrap().contains("AMD"));
|
||||
assert!(unsupported_reason("apple", 0.0, "").is_none(), "measure only, said by the tune");
|
||||
assert!(unsupported_reason("amd", 0.0, "1").is_none(), "tuned through igneum-gpu-telemetry");
|
||||
assert!(unsupported_reason("other", 0.0, "1").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn helper_scripts_carry_the_protocol() {
|
||||
for s in [helper_script_windows(), helper_script_unix()] {
|
||||
assert!(s.contains("cmd.txt") && s.contains("quit") && s.contains("-pl") && s.contains("20 min"));
|
||||
assert!(s.contains("-lgc") && s.contains("-rgc"), "the clock cap and its reset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
/// 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 {
|
||||
// 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);
|
||||
c.args(["-d", distro]);
|
||||
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; }
|
||||
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
|
||||
function shownCards(cards) { return (cards || []).filter(function (c) { return !c.gone; }); }
|
||||
// The list is ordered by performance (the project lead, 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
|
||||
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
|
||||
|
|
@ -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 };
|
||||
})();
|
||||
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 () {
|
||||
'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-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-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-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); });
|
||||
$('s-log-open').addEventListener('click', function () { setDrawer(true); });
|
||||
|
|
@ -591,8 +634,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('s-cards').addEventListener('click', function (e) {
|
||||
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'); }
|
||||
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.sweepStop) api('api/sweep/stop', {}).then(function () { toast('Sweep stopped; cap restored'); });
|
||||
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('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.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; }
|
||||
det.hidden = true;
|
||||
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 (!cards.length) {
|
||||
$('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-paid', String(pv.paid || 0));
|
||||
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);
|
||||
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 || '');
|
||||
|
|
@ -1097,7 +1145,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
var s = state;
|
||||
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-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));
|
||||
if (document.activeElement !== $('s-name')) $('s-name').value = s.display_name || '';
|
||||
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 (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>' +
|
||||
'<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);
|
||||
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>';
|
||||
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') {
|
||||
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>';
|
||||
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) {
|
||||
var cards = shownCards(s.mining.cards), box = $('s-cards');
|
||||
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;
|
||||
setCardsSig = sig;
|
||||
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">paid</div><div class="v" id="pv-paid">0</div><div class="s" id="pv-paid-sub">shards paid out</div></div>
|
||||
</div>
|
||||
<p class="note" id="pv-seg-note" hidden></p>
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Verifier</h3><div class="eyebrow">the node's check</div></div>
|
||||
|
|
@ -366,8 +367,10 @@
|
|||
<div class="card">
|
||||
<div class="card-head"><h3>Graphics cards</h3><div class="eyebrow" id="s-cards-eyebrow"></div></div>
|
||||
<div class="set-cards" id="s-cards"><div class="empty">No card yet.</div></div>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span>Find each NVIDIA card's best efficiency</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>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span>Ember Tune: tune every card for hashes per watt</span></label>
|
||||
<p class="help">Once after install, then weekly and after a driver or program change: the power limit steps from 100% down to 50%, then the core clock from its maximum down to 60%, 75 s a step on the live program; the memory clock is never touched. The card keeps the point with the most hashes per watt within 1% of its top rate. A step with a rejected hash, a hot GPU or a dragged memory clock is reverted. A card whose model the fleet already knows starts at that point and confirms it in two steps. Every result goes back to the fleet without anything that identifies you. A cap you set by hand is left alone.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span>Power control: let the app set NVIDIA limits</span></label>
|
||||
<p class="help">Windows asks for administrator rights once; the NVIDIA cap and the tune need them. Off, the app never asks and NVIDIA cards measure only. AMD cards need no rights. <span id="s-power-note"></span></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>This machine</h3></div>
|
||||
|
|
|
|||
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
|
||||
assert.deepEqual(V.shownCards([card(), card({ key: 'x', gone: true })]).map((c) => c.key), ['nvidia:0:RTX 5090']);
|
||||
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(bad), false);
|
||||
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.
|
||||
#ifndef IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.11"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,11,0
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.13"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,13,0
|
||||
#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/`.
|
||||
|
||||
## 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`. the project lead: "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, the project lead), 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`; the project lead 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 the project lead.** 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 the project lead's word, engine 25113f52..., PC 1 on 0.3.11):** the project lead 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)
|
||||
|
||||
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 (the project lead, 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.
|
||||
|
||||
**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)
|
||||
|
||||
the project lead, 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 |
|
||||
|---|---|---|---|---|
|
||||
| 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 (the project lead) | 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 | 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") |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| 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
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
|
|
@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
|
@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`
|
|||
| reproduced externally | 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
|
||||
|
||||
|
|
@ -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 |
|
||||
| 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 the project lead 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| 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 the project lead | the project lead, 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
|
||||
|
||||
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.
|
||||
|
||||
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. the project lead 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 @@ the project lead, 5 October 2026 (night): "not an information overload". Four le
|
|||
|
||||
## 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 the project lead.)
|
||||
Built for graphics cards. A custom chip gains under 2x, and the model is public. (the project lead'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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -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.]
|
||||
|
||||
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 the project lead'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) |
|
||||
|---|---|---|---|---|---|---|
|
||||
|
|
|
|||
|
|
@ -464,3 +464,7 @@ chip ops per hash, 78.2 MH/s, 0.57x bare, 0.69x at 1.2x, 0.86x at 1.5x: under 1x
|
|||
| The integrated tier's build with the day program | approximate (5.5); the gfx1036 measurement is a PC 2 OpenCL job, not run today (the one PC 2 job carries the 5090) |
|
||||
| A 64-lane interpreter for pools (two units per batch) | unimplemented; it would cut the dispatch share for pool verifiers only |
|
||||
| The NVRTC cost of memhard.h inside the per-epoch hash kernel compile and the variant race | the PC 2 job's nvrtc line; the fix, if large, is one module per day for the item function |
|
||||
|
||||
## 5.4b The RX 9070 XT rows (PC 1 job run-ca3-pc1-amd-derive-20261006, 6 October 2026, 17:23 to 17:29Z)
|
||||
|
||||
Beside the miners (ratios stand, absolutes are loaded-card figures). Both dr736 packs bit-exact on AMD OpenCL (gfx1201, AMD-APP 3683.0): fingerprints 9553f6d5c667205a (devnet epoch 0) and 50e3eaa779da4f1e (genesis), two passes each, self-tests PASS. OpenCL compile 2,070 and 2,092 ms per pack against 41 ms for mx8 (+2.0 s per pack; a once-a-day item module is required on AMD as on NVIDIA). Daily 1 GiB build 168 to 189 ms against 205 to 273 for mx8 in the same session. Rate ratio to mx8 0.982 and 1.006. The 9070 XT row of the owed list is closed; the chip model and the go / no-go are unchanged by it.
|
||||
|
|
|
|||
533
docs/plans/counter-asic-3-gate/class-v4-signal-failed-case.json
Normal file
533
docs/plans/counter-asic-3-gate/class-v4-signal-failed-case.json
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
{
|
||||
"pass": false,
|
||||
"expect": "flip",
|
||||
"signals": [
|
||||
4,
|
||||
4,
|
||||
3
|
||||
],
|
||||
"checks": {
|
||||
"zero_rejected_by_miners": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true,
|
||||
"block_counts_agree": true,
|
||||
"miners_agree_on_every_program": true,
|
||||
"window_line_on_every_node": true,
|
||||
"every_node_signals_its_byte": true,
|
||||
"chain_carries_the_bytes": false,
|
||||
"template_switched_to_v4": false,
|
||||
"switched_at_the_first_full_window_epoch": false,
|
||||
"switched_before_the_floor": false,
|
||||
"signal_line_on_every_node_same_epoch": false,
|
||||
"signal_share_at_or_above_threshold": false,
|
||||
"blocks_on_both_sides": false,
|
||||
"v4_ids_equal_the_cli_v4_id": false,
|
||||
"v4_ids_differ_from_the_same_seed_v3_id": false
|
||||
},
|
||||
"window": 120,
|
||||
"floor": "never",
|
||||
"v3_activation": 60,
|
||||
"epoch_blocks": 60,
|
||||
"lead": 10,
|
||||
"first_full_window_epoch": 3,
|
||||
"floor_epoch": null,
|
||||
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
|
||||
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
|
||||
"template_switch": null,
|
||||
"run_ended_at_s": 391.2,
|
||||
"final_daa": 420,
|
||||
"max_epoch_seen": 7,
|
||||
"epochs": {
|
||||
"0": {
|
||||
"class": 2,
|
||||
"firstSeenDaa": 0,
|
||||
"at": 4.7,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"1": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 60,
|
||||
"at": 29.7,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 7288,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"2": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 120,
|
||||
"at": 107.8,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6890,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"3": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 180,
|
||||
"at": 162.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6833,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"4": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 240,
|
||||
"at": 222,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6666,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"5": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 300,
|
||||
"at": 279,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6250,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"6": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 360,
|
||||
"at": 338.1,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6666,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"7": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 420,
|
||||
"at": 391.2,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6666,
|
||||
"signal_epoch": null
|
||||
}
|
||||
},
|
||||
"blocks": {
|
||||
"total": 423,
|
||||
"before_boundary": 423,
|
||||
"after_boundary": 0,
|
||||
"version_bytes": {
|
||||
"0": 1,
|
||||
"3": 139,
|
||||
"4": 283
|
||||
},
|
||||
"signal_share_bps_on_chain": 6690
|
||||
},
|
||||
"programs": [
|
||||
{
|
||||
"epoch": 0,
|
||||
"class": "v2",
|
||||
"program_id": "8f8806638d59850f",
|
||||
"seed": "234e082d653dc69d",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 1,
|
||||
"class": "v3",
|
||||
"program_id": "4b212c0be9e6fe63",
|
||||
"seed": "135da54d125df437",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 2,
|
||||
"class": "v3",
|
||||
"program_id": "ee3ac1e30bf65e70",
|
||||
"seed": "c59a2e57f4b8d7a5",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 3,
|
||||
"class": "v3",
|
||||
"program_id": "9f029268a9045087",
|
||||
"seed": "da24c710fa33c22f",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"class": "v3",
|
||||
"program_id": "3b36a0088571d624",
|
||||
"seed": "c99ee1f5201c6237",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"class": "v3",
|
||||
"program_id": "0045b23258d669cc",
|
||||
"seed": "6890cf1861d53035",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 6,
|
||||
"class": "v3",
|
||||
"program_id": "58f1b31878fa2f07",
|
||||
"seed": "10d336969f4efd63",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 7,
|
||||
"class": "v3",
|
||||
"program_id": "2a0d9087b89d7a9c",
|
||||
"seed": "7290dc7ee9b9c29a",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
}
|
||||
],
|
||||
"program_id_rows": [],
|
||||
"accepted_per_miner": [
|
||||
138,
|
||||
145,
|
||||
139
|
||||
],
|
||||
"rejected_by_miners": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rejected_by_nodes": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sinks": [
|
||||
"ca58bef3d51cfa4a",
|
||||
"ca58bef3d51cfa4a",
|
||||
"ca58bef3d51cfa4a"
|
||||
],
|
||||
"block_counts": [
|
||||
422,
|
||||
422,
|
||||
422
|
||||
],
|
||||
"signal_lines": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"floor_lines": [
|
||||
"Program class v4 from the override file: never",
|
||||
"Program class v4 from the override file: never",
|
||||
"Program class v4 from the override file: never"
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 4.7,
|
||||
"daa": 0,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"0/234e082d",
|
||||
"0/234e082d",
|
||||
"0/234e082d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 19.7,
|
||||
"daa": 42,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 7317,
|
||||
"nodes": [
|
||||
"42/b548603c",
|
||||
"42/b548603c",
|
||||
"42/b548603c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 34.7,
|
||||
"daa": 65,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7031,
|
||||
"nodes": [
|
||||
"65/1b29b2e8",
|
||||
"65/1b29b2e8",
|
||||
"65/1b29b2e8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 49.7,
|
||||
"daa": 67,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7121,
|
||||
"nodes": [
|
||||
"67/36bc6a1c",
|
||||
"67/36bc6a1c",
|
||||
"67/36bc6a1c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 64.7,
|
||||
"daa": 76,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7066,
|
||||
"nodes": [
|
||||
"76/e4a48be8",
|
||||
"76/e4a48be8",
|
||||
"76/e4a48be8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 79.8,
|
||||
"daa": 91,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7111,
|
||||
"nodes": [
|
||||
"91/d77402dd",
|
||||
"91/d77402dd",
|
||||
"91/d77402dd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 94.8,
|
||||
"daa": 111,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 6636,
|
||||
"nodes": [
|
||||
"111/f0e19e53",
|
||||
"111/f0e19e53",
|
||||
"111/f0e19e53"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 109.8,
|
||||
"daa": 122,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6890,
|
||||
"nodes": [
|
||||
"122/5adb1052",
|
||||
"122/5adb1052",
|
||||
"122/5adb1052"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 124.8,
|
||||
"daa": 135,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"135/a49b1340",
|
||||
"135/a49b1340",
|
||||
"135/a49b1340"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 139.9,
|
||||
"daa": 152,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6500,
|
||||
"nodes": [
|
||||
"152/e1c73ab2",
|
||||
"152/e1c73ab2",
|
||||
"152/e1c73ab2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 154.9,
|
||||
"daa": 169,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"169/e0fc0185",
|
||||
"169/e0fc0185",
|
||||
"169/e0fc0185"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 169.9,
|
||||
"daa": 186,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6833,
|
||||
"nodes": [
|
||||
"186/2cba3315",
|
||||
"186/2cba3315",
|
||||
"186/2cba3315"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 184.9,
|
||||
"daa": 207,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"207/a1e09949",
|
||||
"207/a1e09949",
|
||||
"207/a1e09949"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 199.9,
|
||||
"daa": 223,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"223/2313964e",
|
||||
"223/2313964e",
|
||||
"223/2313964e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 215,
|
||||
"daa": 230,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6916,
|
||||
"nodes": [
|
||||
"230/c99ee1f5",
|
||||
"230/c99ee1f5",
|
||||
"230/c99ee1f5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 230,
|
||||
"daa": 246,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"246/3500100b",
|
||||
"246/3500100b",
|
||||
"246/3500100b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 245,
|
||||
"daa": 262,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 6500,
|
||||
"nodes": [
|
||||
"262/aecdd009",
|
||||
"262/aecdd009",
|
||||
"262/aecdd009"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 260,
|
||||
"daa": 274,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 6666,
|
||||
"nodes": [
|
||||
"274/c93ff1f0",
|
||||
"274/c93ff1f0",
|
||||
"274/c93ff1f0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 275,
|
||||
"daa": 293,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"293/c1937fd0",
|
||||
"293/c1937fd0",
|
||||
"293/c1937fd0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 290.1,
|
||||
"daa": 311,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 6500,
|
||||
"nodes": [
|
||||
"311/47520bf7",
|
||||
"311/47520bf7",
|
||||
"311/47520bf7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 305.1,
|
||||
"daa": 328,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 6916,
|
||||
"nodes": [
|
||||
"328/ed9acfc1",
|
||||
"328/ed9acfc1",
|
||||
"328/ed9acfc1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 320.1,
|
||||
"daa": 343,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"343/6553cec0",
|
||||
"343/6553cec0",
|
||||
"343/6553cec0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 335.1,
|
||||
"daa": 357,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 6666,
|
||||
"nodes": [
|
||||
"357/f444a87b",
|
||||
"357/f444a87b",
|
||||
"357/f444a87b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 350.1,
|
||||
"daa": 368,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"368/af2319fb",
|
||||
"368/af2319fb",
|
||||
"368/af2319fb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 365.2,
|
||||
"daa": 381,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6666,
|
||||
"nodes": [
|
||||
"381/69556871",
|
||||
"381/69556871",
|
||||
"381/69556871"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 380.2,
|
||||
"daa": 404,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"404/7c17470a",
|
||||
"404/7c17470a",
|
||||
"404/7c17470a"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
433
docs/plans/counter-asic-3-gate/class-v4-signal-flip.json
Normal file
433
docs/plans/counter-asic-3-gate/class-v4-signal-flip.json
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
{
|
||||
"pass": true,
|
||||
"expect": "flip",
|
||||
"signals": [
|
||||
4,
|
||||
4,
|
||||
4
|
||||
],
|
||||
"checks": {
|
||||
"zero_rejected_by_miners": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true,
|
||||
"block_counts_agree": true,
|
||||
"miners_agree_on_every_program": true,
|
||||
"window_line_on_every_node": true,
|
||||
"every_node_signals_its_byte": true,
|
||||
"chain_carries_the_bytes": true,
|
||||
"template_switched_to_v4": true,
|
||||
"switched_at_the_first_full_window_epoch": true,
|
||||
"switched_before_the_floor": true,
|
||||
"signal_line_on_every_node_same_epoch": true,
|
||||
"signal_share_at_or_above_threshold": true,
|
||||
"blocks_on_both_sides": true,
|
||||
"v4_ids_equal_the_cli_v4_id": true,
|
||||
"v4_ids_differ_from_the_same_seed_v3_id": true
|
||||
},
|
||||
"window": 120,
|
||||
"floor": "never",
|
||||
"v3_activation": 60,
|
||||
"epoch_blocks": 60,
|
||||
"lead": 10,
|
||||
"first_full_window_epoch": 3,
|
||||
"floor_epoch": null,
|
||||
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
|
||||
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
|
||||
"template_switch": {
|
||||
"epoch": 3,
|
||||
"daa": 180,
|
||||
"at": 156.9
|
||||
},
|
||||
"run_ended_at_s": 271,
|
||||
"final_daa": 301,
|
||||
"max_epoch_seen": 5,
|
||||
"epochs": {
|
||||
"0": {
|
||||
"class": 2,
|
||||
"firstSeenDaa": 0,
|
||||
"at": 4.6,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"1": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 60,
|
||||
"at": 24.7,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 10000,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"2": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 121,
|
||||
"at": 98.8,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 10000,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"3": {
|
||||
"class": 4,
|
||||
"firstSeenDaa": 180,
|
||||
"at": 156.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 10000,
|
||||
"signal_epoch": 3
|
||||
},
|
||||
"4": {
|
||||
"class": 4,
|
||||
"firstSeenDaa": 240,
|
||||
"at": 213.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 10000,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"5": {
|
||||
"class": 4,
|
||||
"firstSeenDaa": 301,
|
||||
"at": 271,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 10000,
|
||||
"signal_epoch": null
|
||||
}
|
||||
},
|
||||
"blocks": {
|
||||
"total": 305,
|
||||
"before_boundary": 181,
|
||||
"after_boundary": 124,
|
||||
"version_bytes": {
|
||||
"0": 1,
|
||||
"4": 304
|
||||
},
|
||||
"signal_share_bps_on_chain": 9967
|
||||
},
|
||||
"programs": [
|
||||
{
|
||||
"epoch": 0,
|
||||
"class": "v2",
|
||||
"program_id": "8f8806638d59850f",
|
||||
"seed": "234e082d653dc69d",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 1,
|
||||
"class": "v3",
|
||||
"program_id": "96c2b5e891a1cec6",
|
||||
"seed": "dc3460d3c2c7e252",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 2,
|
||||
"class": "v3",
|
||||
"program_id": "9210599b352bb3d9",
|
||||
"seed": "c80aefcfef313159",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 3,
|
||||
"class": "v4",
|
||||
"program_id": "e48e6be7c6699824",
|
||||
"seed": "13629115abf4b06b",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"class": "v4",
|
||||
"program_id": "996db27539593c5c",
|
||||
"seed": "d8820f2c76bb1011",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"class": "v4",
|
||||
"program_id": "ebf1b4ee74ef5b04",
|
||||
"seed": "8b8a1e7ccb10053c",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
}
|
||||
],
|
||||
"program_id_rows": [
|
||||
{
|
||||
"epoch": 3,
|
||||
"seed": "13629115abf4b06b",
|
||||
"miners_id": "e48e6be7c6699824",
|
||||
"miners": 3,
|
||||
"cli_v3": "6847355c88e8215f",
|
||||
"cli_v4": "e48e6be7c6699824"
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"seed": "d8820f2c76bb1011",
|
||||
"miners_id": "996db27539593c5c",
|
||||
"miners": 3,
|
||||
"cli_v3": "34725526fbe79603",
|
||||
"cli_v4": "996db27539593c5c"
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"seed": "8b8a1e7ccb10053c",
|
||||
"miners_id": "ebf1b4ee74ef5b04",
|
||||
"miners": 3,
|
||||
"cli_v3": "ed109252a3333d97",
|
||||
"cli_v4": "ebf1b4ee74ef5b04"
|
||||
}
|
||||
],
|
||||
"accepted_per_miner": [
|
||||
105,
|
||||
102,
|
||||
97
|
||||
],
|
||||
"rejected_by_miners": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rejected_by_nodes": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sinks": [
|
||||
"322e4a57824ab7fd",
|
||||
"322e4a57824ab7fd",
|
||||
"322e4a57824ab7fd"
|
||||
],
|
||||
"block_counts": [
|
||||
304,
|
||||
304,
|
||||
304
|
||||
],
|
||||
"signal_lines": [
|
||||
"Program class v4 by miner signal: epoch 3 (share 10000 bps over 120 DAA ending at seed block 13629115abf4b06bd9b07dfee66c7aab817cef268a34c02f8da6884d52f26017, threshold 9500 bps, 120 of 120 blue blocks)",
|
||||
"Program class v4 by miner signal: epoch 3 (share 10000 bps over 120 DAA ending at seed block 13629115abf4b06bd9b07dfee66c7aab817cef268a34c02f8da6884d52f26017, threshold 9500 bps, 120 of 120 blue blocks)",
|
||||
"Program class v4 by miner signal: epoch 3 (share 10000 bps over 120 DAA ending at seed block 13629115abf4b06bd9b07dfee66c7aab817cef268a34c02f8da6884d52f26017, threshold 9500 bps, 120 of 120 blue blocks)"
|
||||
],
|
||||
"floor_lines": [
|
||||
"Program class v4 from the override file: never",
|
||||
"Program class v4 from the override file: never",
|
||||
"Program class v4 from the override file: never"
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 4.6,
|
||||
"daa": 0,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"0/234e082d",
|
||||
"0/234e082d",
|
||||
"0/234e082d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 19.7,
|
||||
"daa": 45,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"45/90a214d6",
|
||||
"45/90a214d6",
|
||||
"45/90a214d6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 34.7,
|
||||
"daa": 64,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"64/ccc78501",
|
||||
"64/ccc78501",
|
||||
"64/ccc78501"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 49.7,
|
||||
"daa": 67,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"67/30734219",
|
||||
"67/30734219",
|
||||
"67/30734219"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 64.7,
|
||||
"daa": 77,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"77/fb4c94d8",
|
||||
"77/fb4c94d8",
|
||||
"77/fb4c94d8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 79.7,
|
||||
"daa": 92,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"92/fa61d646",
|
||||
"92/fa61d646",
|
||||
"92/fa61d646"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 94.8,
|
||||
"daa": 113,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"113/a5072844",
|
||||
"113/a5072844",
|
||||
"113/a5072844"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 109.8,
|
||||
"daa": 131,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"131/d88a9f22",
|
||||
"131/d88a9f22",
|
||||
"131/d88a9f22"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 124.8,
|
||||
"daa": 157,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"157/65814d6e",
|
||||
"157/65814d6e",
|
||||
"157/65814d6e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 139.8,
|
||||
"daa": 165,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"165/ed5654bb",
|
||||
"165/ed5654bb",
|
||||
"165/ed5654bb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 154.9,
|
||||
"daa": 177,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"177/d0f60a64",
|
||||
"177/d0f60a64",
|
||||
"177/d0f60a64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 169.9,
|
||||
"daa": 192,
|
||||
"epoch": 3,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"192/dce1a9f6",
|
||||
"192/dce1a9f6",
|
||||
"192/dce1a9f6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 184.9,
|
||||
"daa": 206,
|
||||
"epoch": 3,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"206/617ab9be",
|
||||
"206/617ab9be",
|
||||
"206/617ab9be"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 199.9,
|
||||
"daa": 222,
|
||||
"epoch": 3,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"222/e04f63eb",
|
||||
"222/e04f63eb",
|
||||
"222/e04f63eb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 214.9,
|
||||
"daa": 242,
|
||||
"epoch": 4,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"242/a214e080",
|
||||
"242/a214e080",
|
||||
"242/a214e080"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 230,
|
||||
"daa": 253,
|
||||
"epoch": 4,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"253/c5f37595",
|
||||
"253/c5f37595",
|
||||
"253/c5f37595"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 245,
|
||||
"daa": 269,
|
||||
"epoch": 4,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"269/0ac500c5",
|
||||
"269/0ac500c5",
|
||||
"269/0ac500c5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 260,
|
||||
"daa": 285,
|
||||
"epoch": 4,
|
||||
"class": 4,
|
||||
"bps": 10000,
|
||||
"nodes": [
|
||||
"285/f5f748e0",
|
||||
"285/f5f748e0",
|
||||
"285/f5f748e0"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
583
docs/plans/counter-asic-3-gate/class-v4-signal-floor.json
Normal file
583
docs/plans/counter-asic-3-gate/class-v4-signal-floor.json
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
{
|
||||
"pass": true,
|
||||
"expect": "floor",
|
||||
"signals": [
|
||||
3,
|
||||
3,
|
||||
3
|
||||
],
|
||||
"checks": {
|
||||
"zero_rejected_by_miners": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true,
|
||||
"block_counts_agree": true,
|
||||
"miners_agree_on_every_program": true,
|
||||
"window_line_on_every_node": true,
|
||||
"every_node_signals_its_byte": true,
|
||||
"chain_carries_the_bytes": true,
|
||||
"template_switched_to_v4": true,
|
||||
"switched_at_the_floor_epoch": true,
|
||||
"no_signal_line_on_any_node": true,
|
||||
"floor_line_names_the_floor_epoch": true,
|
||||
"blocks_on_both_sides": true,
|
||||
"v4_ids_equal_the_cli_v4_id": true
|
||||
},
|
||||
"window": 120,
|
||||
"floor": 300,
|
||||
"v3_activation": 60,
|
||||
"epoch_blocks": 60,
|
||||
"lead": 10,
|
||||
"first_full_window_epoch": 3,
|
||||
"floor_epoch": 5,
|
||||
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
|
||||
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
|
||||
"template_switch": {
|
||||
"epoch": 5,
|
||||
"daa": 300,
|
||||
"at": 286
|
||||
},
|
||||
"run_ended_at_s": 410.2,
|
||||
"final_daa": 421,
|
||||
"max_epoch_seen": 7,
|
||||
"epochs": {
|
||||
"0": {
|
||||
"class": 2,
|
||||
"firstSeenDaa": 0,
|
||||
"at": 4.6,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"1": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 60,
|
||||
"at": 32.7,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"2": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 120,
|
||||
"at": 106.8,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"3": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 180,
|
||||
"at": 166.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"4": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 242,
|
||||
"at": 222,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"5": {
|
||||
"class": 4,
|
||||
"firstSeenDaa": 300,
|
||||
"at": 286,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"6": {
|
||||
"class": 4,
|
||||
"firstSeenDaa": 360,
|
||||
"at": 346.1,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"7": {
|
||||
"class": 4,
|
||||
"firstSeenDaa": 421,
|
||||
"at": 410.2,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
}
|
||||
},
|
||||
"blocks": {
|
||||
"total": 425,
|
||||
"before_boundary": 301,
|
||||
"after_boundary": 124,
|
||||
"version_bytes": {
|
||||
"0": 1,
|
||||
"3": 424
|
||||
},
|
||||
"signal_share_bps_on_chain": 0
|
||||
},
|
||||
"programs": [
|
||||
{
|
||||
"epoch": 0,
|
||||
"class": "v2",
|
||||
"program_id": "8f8806638d59850f",
|
||||
"seed": "234e082d653dc69d",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 1,
|
||||
"class": "v3",
|
||||
"program_id": "ab6cbb1e54ed5cce",
|
||||
"seed": "64e53c286d8dd9fb",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 2,
|
||||
"class": "v3",
|
||||
"program_id": "cf71a8c93f39aba3",
|
||||
"seed": "bf5fb4c810cbf7b9",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 3,
|
||||
"class": "v3",
|
||||
"program_id": "586eb24a6a1e956e",
|
||||
"seed": "ca912ff9e0ce19eb",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"class": "v3",
|
||||
"program_id": "179f30b619d3ae21",
|
||||
"seed": "fd39032d68eaf42c",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"class": "v4",
|
||||
"program_id": "99f8a29e69d425fd",
|
||||
"seed": "55b4bb2409b9dac0",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 6,
|
||||
"class": "v4",
|
||||
"program_id": "13f24ad402624682",
|
||||
"seed": "f6e6e2710786cb70",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 7,
|
||||
"class": "v4",
|
||||
"program_id": "ae4fe8f735e1e5da",
|
||||
"seed": "f3106b9f844b01c3",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
}
|
||||
],
|
||||
"program_id_rows": [
|
||||
{
|
||||
"epoch": 5,
|
||||
"seed": "55b4bb2409b9dac0",
|
||||
"miners_id": "99f8a29e69d425fd",
|
||||
"miners": 3,
|
||||
"cli_v3": "48b8b87ff3a7e9ae",
|
||||
"cli_v4": "99f8a29e69d425fd"
|
||||
},
|
||||
{
|
||||
"epoch": 6,
|
||||
"seed": "f6e6e2710786cb70",
|
||||
"miners_id": "13f24ad402624682",
|
||||
"miners": 3,
|
||||
"cli_v3": "912bbdb803462a49",
|
||||
"cli_v4": "13f24ad402624682"
|
||||
},
|
||||
{
|
||||
"epoch": 7,
|
||||
"seed": "f3106b9f844b01c3",
|
||||
"miners_id": "ae4fe8f735e1e5da",
|
||||
"miners": 3,
|
||||
"cli_v3": "3df23fd83c687af9",
|
||||
"cli_v4": "ae4fe8f735e1e5da"
|
||||
}
|
||||
],
|
||||
"accepted_per_miner": [
|
||||
130,
|
||||
134,
|
||||
160
|
||||
],
|
||||
"rejected_by_miners": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rejected_by_nodes": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sinks": [
|
||||
"aa8755bf4c1cc9e2",
|
||||
"aa8755bf4c1cc9e2",
|
||||
"aa8755bf4c1cc9e2"
|
||||
],
|
||||
"block_counts": [
|
||||
424,
|
||||
424,
|
||||
424
|
||||
],
|
||||
"signal_lines": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"floor_lines": [
|
||||
"Program class v4 from the override file: active from epoch 5 (DAA score 300 rounded up to the epoch boundary at 300, epochs of 60 DAA)",
|
||||
"Program class v4 from the override file: active from epoch 5 (DAA score 300 rounded up to the epoch boundary at 300, epochs of 60 DAA)",
|
||||
"Program class v4 from the override file: active from epoch 5 (DAA score 300 rounded up to the epoch boundary at 300, epochs of 60 DAA)"
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 4.6,
|
||||
"daa": 0,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"0/234e082d",
|
||||
"0/234e082d",
|
||||
"0/234e082d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 19.7,
|
||||
"daa": 40,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"40/bd7dc09a",
|
||||
"40/bd7dc09a",
|
||||
"40/bd7dc09a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 34.7,
|
||||
"daa": 60,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"60/0d0245d7",
|
||||
"60/0d0245d7",
|
||||
"60/0d0245d7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 49.7,
|
||||
"daa": 66,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"66/b31ac361",
|
||||
"66/b31ac361",
|
||||
"66/b31ac361"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 64.7,
|
||||
"daa": 75,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"75/63b041c6",
|
||||
"75/63b041c6",
|
||||
"75/63b041c6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 79.7,
|
||||
"daa": 94,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"94/3ba72d05",
|
||||
"94/3ba72d05",
|
||||
"94/3ba72d05"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 94.8,
|
||||
"daa": 109,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"109/08984ac2",
|
||||
"109/08984ac2",
|
||||
"109/08984ac2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 109.8,
|
||||
"daa": 123,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"123/735ce53a",
|
||||
"123/735ce53a",
|
||||
"123/735ce53a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 124.8,
|
||||
"daa": 134,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"134/b86e0b6c",
|
||||
"134/b86e0b6c",
|
||||
"134/b86e0b6c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 139.8,
|
||||
"daa": 154,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"154/e0278b3f",
|
||||
"154/e0278b3f",
|
||||
"154/e0278b3f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 154.9,
|
||||
"daa": 167,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"167/55672ada",
|
||||
"167/55672ada",
|
||||
"167/55672ada"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 169.9,
|
||||
"daa": 180,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"180/2549aa1c",
|
||||
"180/2549aa1c",
|
||||
"180/2549aa1c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 184.9,
|
||||
"daa": 199,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"199/8deada07",
|
||||
"199/8deada07",
|
||||
"199/8deada07"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 199.9,
|
||||
"daa": 210,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"210/b3c3984d",
|
||||
"210/b3c3984d",
|
||||
"210/b3c3984d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 214.9,
|
||||
"daa": 228,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"228/e863e711",
|
||||
"228/e863e711",
|
||||
"228/e863e711"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 230,
|
||||
"daa": 249,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"249/30016453",
|
||||
"249/30016453",
|
||||
"249/30016453"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 245,
|
||||
"daa": 264,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"264/7da6fa57",
|
||||
"264/7da6fa57",
|
||||
"264/7da6fa57"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 260,
|
||||
"daa": 285,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"285/132339cf",
|
||||
"285/132339cf",
|
||||
"285/132339cf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 275,
|
||||
"daa": 295,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"295/f284c363",
|
||||
"295/f284c363",
|
||||
"295/f284c363"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 290,
|
||||
"daa": 304,
|
||||
"epoch": 5,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"304/f7813a3d",
|
||||
"304/f7813a3d",
|
||||
"304/f7813a3d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 305.1,
|
||||
"daa": 324,
|
||||
"epoch": 5,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"324/2e0050f2",
|
||||
"324/2e0050f2",
|
||||
"324/2e0050f2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 320.1,
|
||||
"daa": 337,
|
||||
"epoch": 5,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"337/de4a3d18",
|
||||
"337/de4a3d18",
|
||||
"337/de4a3d18"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 335.1,
|
||||
"daa": 350,
|
||||
"epoch": 5,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"350/f6e6e271",
|
||||
"350/f6e6e271",
|
||||
"350/f6e6e271"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 350.1,
|
||||
"daa": 362,
|
||||
"epoch": 6,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"362/78e4df38",
|
||||
"362/78e4df38",
|
||||
"362/78e4df38"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 365.1,
|
||||
"daa": 380,
|
||||
"epoch": 6,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"380/ad2ac8c4",
|
||||
"380/ad2ac8c4",
|
||||
"380/ad2ac8c4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 380.2,
|
||||
"daa": 392,
|
||||
"epoch": 6,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"392/fd59c498",
|
||||
"392/fd59c498",
|
||||
"392/fd59c498"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 395.2,
|
||||
"daa": 404,
|
||||
"epoch": 6,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"404/179e8135",
|
||||
"404/179e8135",
|
||||
"404/179e8135"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 410.2,
|
||||
"daa": 421,
|
||||
"epoch": 7,
|
||||
"class": 4,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"421/cf7d8a0f",
|
||||
"421/cf7d8a0f",
|
||||
"421/cf7d8a0f"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
554
docs/plans/counter-asic-3-gate/class-v4-signal-no-flip.json
Normal file
554
docs/plans/counter-asic-3-gate/class-v4-signal-no-flip.json
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
{
|
||||
"pass": true,
|
||||
"expect": "no-flip",
|
||||
"signals": [
|
||||
4,
|
||||
4,
|
||||
3
|
||||
],
|
||||
"checks": {
|
||||
"zero_rejected_by_miners": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true,
|
||||
"block_counts_agree": true,
|
||||
"miners_agree_on_every_program": true,
|
||||
"window_line_on_every_node": true,
|
||||
"every_node_signals_its_byte": true,
|
||||
"chain_carries_the_bytes": true,
|
||||
"template_never_v4": true,
|
||||
"no_signal_line_on_any_node": true,
|
||||
"ran_the_epochs": true,
|
||||
"v3_programs_seen": true,
|
||||
"signal_share_under_threshold_on_chain": true
|
||||
},
|
||||
"window": 120,
|
||||
"floor": "never",
|
||||
"v3_activation": 60,
|
||||
"epoch_blocks": 60,
|
||||
"lead": 10,
|
||||
"first_full_window_epoch": 3,
|
||||
"floor_epoch": null,
|
||||
"node": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd",
|
||||
"miner": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-ca3v4/target-ca3v4/release/igneum-miner",
|
||||
"template_switch": null,
|
||||
"run_ended_at_s": 417.2,
|
||||
"final_daa": 421,
|
||||
"max_epoch_seen": 7,
|
||||
"epochs": {
|
||||
"0": {
|
||||
"class": 2,
|
||||
"firstSeenDaa": 0,
|
||||
"at": 4.6,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 0,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"1": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 60,
|
||||
"at": 23.7,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 7068,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"2": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 121,
|
||||
"at": 98.8,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6890,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"3": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 180,
|
||||
"at": 160.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6333,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"4": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 241,
|
||||
"at": 226,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6750,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"5": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 300,
|
||||
"at": 284,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 7583,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"6": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 361,
|
||||
"at": 343.1,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6833,
|
||||
"signal_epoch": null
|
||||
},
|
||||
"7": {
|
||||
"class": 3,
|
||||
"firstSeenDaa": 421,
|
||||
"at": 417.2,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"bps": 6166,
|
||||
"signal_epoch": null
|
||||
}
|
||||
},
|
||||
"blocks": {
|
||||
"total": 425,
|
||||
"before_boundary": 425,
|
||||
"after_boundary": 0,
|
||||
"version_bytes": {
|
||||
"0": 1,
|
||||
"3": 138,
|
||||
"4": 286
|
||||
},
|
||||
"signal_share_bps_on_chain": 6729
|
||||
},
|
||||
"programs": [
|
||||
{
|
||||
"epoch": 0,
|
||||
"class": "v2",
|
||||
"program_id": "8f8806638d59850f",
|
||||
"seed": "234e082d653dc69d",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 1,
|
||||
"class": "v3",
|
||||
"program_id": "be8294c0126f1faf",
|
||||
"seed": "b605d5200d048815",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 2,
|
||||
"class": "v3",
|
||||
"program_id": "ed3e4ffbf62a7424",
|
||||
"seed": "c34f4c0e895a7365",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 3,
|
||||
"class": "v3",
|
||||
"program_id": "32b5ea953b6266b9",
|
||||
"seed": "1df8b7b8f7f317c7",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"class": "v3",
|
||||
"program_id": "4a936017bde53483",
|
||||
"seed": "b99035c2fd14dd49",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"class": "v3",
|
||||
"program_id": "9eb843a8976bad11",
|
||||
"seed": "38b40c1bf554fe9c",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 6,
|
||||
"class": "v3",
|
||||
"program_id": "dd0155047fe53f92",
|
||||
"seed": "e09a1634afda0783",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 7,
|
||||
"class": "v3",
|
||||
"program_id": "e21684fe7648d6b4",
|
||||
"seed": "414f83716dfd9e60",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
}
|
||||
],
|
||||
"program_id_rows": [],
|
||||
"accepted_per_miner": [
|
||||
141,
|
||||
145,
|
||||
138
|
||||
],
|
||||
"rejected_by_miners": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rejected_by_nodes": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sinks": [
|
||||
"d845fdd016a25425",
|
||||
"d845fdd016a25425",
|
||||
"d845fdd016a25425"
|
||||
],
|
||||
"block_counts": [
|
||||
424,
|
||||
424,
|
||||
424
|
||||
],
|
||||
"signal_lines": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"floor_lines": [
|
||||
"Program class v4 from the override file: never",
|
||||
"Program class v4 from the override file: never",
|
||||
"Program class v4 from the override file: never"
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 4.6,
|
||||
"daa": 0,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 0,
|
||||
"nodes": [
|
||||
"0/234e082d",
|
||||
"0/234e082d",
|
||||
"0/234e082d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 19.7,
|
||||
"daa": 49,
|
||||
"epoch": 0,
|
||||
"class": 2,
|
||||
"bps": 7659,
|
||||
"nodes": [
|
||||
"49/21e82fed",
|
||||
"49/21e82fed",
|
||||
"49/21e82fed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 34.7,
|
||||
"daa": 64,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7258,
|
||||
"nodes": [
|
||||
"64/5e7ac558",
|
||||
"64/5e7ac558",
|
||||
"64/5e7ac558"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 49.7,
|
||||
"daa": 75,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7397,
|
||||
"nodes": [
|
||||
"75/eec9a8c7",
|
||||
"75/eec9a8c7",
|
||||
"75/eec9a8c7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 64.7,
|
||||
"daa": 87,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 7176,
|
||||
"nodes": [
|
||||
"87/df1f9872",
|
||||
"87/df1f9872",
|
||||
"87/df1f9872"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 79.8,
|
||||
"daa": 106,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 6923,
|
||||
"nodes": [
|
||||
"106/738281d2",
|
||||
"106/738281d2",
|
||||
"106/738281d2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 94.8,
|
||||
"daa": 118,
|
||||
"epoch": 1,
|
||||
"class": 3,
|
||||
"bps": 6896,
|
||||
"nodes": [
|
||||
"118/98818bdc",
|
||||
"118/98818bdc",
|
||||
"118/98818bdc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 109.8,
|
||||
"daa": 128,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"128/c1b1a275",
|
||||
"128/c1b1a275",
|
||||
"128/c1b1a275"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 124.8,
|
||||
"daa": 150,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6250,
|
||||
"nodes": [
|
||||
"150/11045465",
|
||||
"150/11045465",
|
||||
"150/11045465"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 139.8,
|
||||
"daa": 161,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6166,
|
||||
"nodes": [
|
||||
"161/161c74c3",
|
||||
"161/161c74c3",
|
||||
"161/161c74c3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 154.9,
|
||||
"daa": 171,
|
||||
"epoch": 2,
|
||||
"class": 3,
|
||||
"bps": 6333,
|
||||
"nodes": [
|
||||
"171/2419390d",
|
||||
"171/2419390d",
|
||||
"171/2419390d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 169.9,
|
||||
"daa": 191,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6083,
|
||||
"nodes": [
|
||||
"191/ae9f7aa0",
|
||||
"191/ae9f7aa0",
|
||||
"191/ae9f7aa0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 184.9,
|
||||
"daa": 206,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"206/557726ec",
|
||||
"206/557726ec",
|
||||
"206/557726ec"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 199.9,
|
||||
"daa": 221,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6833,
|
||||
"nodes": [
|
||||
"221/2458f010",
|
||||
"221/2458f010",
|
||||
"221/2458f010"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 214.9,
|
||||
"daa": 230,
|
||||
"epoch": 3,
|
||||
"class": 3,
|
||||
"bps": 6554,
|
||||
"nodes": [
|
||||
"230/b99035c2",
|
||||
"230/b99035c2",
|
||||
"230/b99035c2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 230,
|
||||
"daa": 246,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 6916,
|
||||
"nodes": [
|
||||
"246/8f951189",
|
||||
"246/8f951189",
|
||||
"246/8f951189"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 245,
|
||||
"daa": 265,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 7416,
|
||||
"nodes": [
|
||||
"265/6baad73f",
|
||||
"265/6baad73f",
|
||||
"265/6baad73f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 260,
|
||||
"daa": 278,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 7416,
|
||||
"nodes": [
|
||||
"278/cb17784e",
|
||||
"278/cb17784e",
|
||||
"278/cb17784e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 275,
|
||||
"daa": 289,
|
||||
"epoch": 4,
|
||||
"class": 3,
|
||||
"bps": 7416,
|
||||
"nodes": [
|
||||
"289/f8628f1e",
|
||||
"289/f8628f1e",
|
||||
"289/f8628f1e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 290.1,
|
||||
"daa": 308,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 7666,
|
||||
"nodes": [
|
||||
"308/77b769ef",
|
||||
"308/77b769ef",
|
||||
"308/77b769ef"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 305.1,
|
||||
"daa": 319,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 7250,
|
||||
"nodes": [
|
||||
"319/ac915fd6",
|
||||
"319/ac915fd6",
|
||||
"319/ac915fd6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 320.1,
|
||||
"daa": 335,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"335/eef7dabd",
|
||||
"335/eef7dabd",
|
||||
"335/eef7dabd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 335.1,
|
||||
"daa": 353,
|
||||
"epoch": 5,
|
||||
"class": 3,
|
||||
"bps": 6916,
|
||||
"nodes": [
|
||||
"353/4bf46535",
|
||||
"353/4bf46535",
|
||||
"353/4bf46535"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 350.1,
|
||||
"daa": 370,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6750,
|
||||
"nodes": [
|
||||
"370/1af27f0a",
|
||||
"370/1af27f0a",
|
||||
"370/1af27f0a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 365.2,
|
||||
"daa": 385,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"385/b23c95d5",
|
||||
"385/b23c95d5",
|
||||
"385/b23c95d5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 380.2,
|
||||
"daa": 392,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6416,
|
||||
"nodes": [
|
||||
"392/678e7203",
|
||||
"392/678e7203",
|
||||
"392/678e7203"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 395.2,
|
||||
"daa": 400,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6583,
|
||||
"nodes": [
|
||||
"400/c5ca53eb",
|
||||
"400/c5ca53eb",
|
||||
"400/c5ca53eb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 410.2,
|
||||
"daa": 413,
|
||||
"epoch": 6,
|
||||
"class": 3,
|
||||
"bps": 6000,
|
||||
"nodes": [
|
||||
"413/7b4f6674",
|
||||
"413/7b4f6674",
|
||||
"413/7b4f6674"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,4 +9,12 @@
|
|||
| G5 | the PC-built Windows workers and the Mac workers from the same commit | every sha256 listed; the two Windows workers carry the resource block; no packaging, no DMG, no manifest | GREEN on the program-id fix 7c22d0d (packfile.h and main.swift moved, so the workers were rebuilt from it; the earlier pair from a522d04 is superseded): igneum-worker-cuda.exe 3bc8ad8f69ad97d6a9491a734ef16e3cf10b3884d442fb1e5a490c751ae578c5 (1,536,512 bytes), igneum-worker-opencl.exe 16ef015470d7a45c5af5488dcc68a29c5e95f94487eb05e26f1bf418e6dd511f (478,208), `verify-exe.py`: both carry the coin icon and the version block; the Mac worker igneum-bench f9ca4b075a9a9c25afd8feeccd1339fbb6bb4c3728ffeddab9f549cd3c1725a4 (665,128, the binary of the G4 re-run); the node and app binaries of job build-20261006-155958 stand (the fork did not change). The a522d04 row for the record: the build job builds the node and the app only (`push-build-inputs.sh` writes two build units, `jobbuild.rs` has no worker unit), so the Windows workers were cross-built on the Mac from the committed tree a522d04 with `proto-cuda/nvrtc/build-windows.sh` under the build lock (mingw-w64, static, windres resource block), as 0.3.11's G5 row was: igneum-worker-cuda.exe e563126ed1d8ab8f6803ead89acca5b6fcb03203e6b87b70e0aec85e8e1046e4 (1,536,512 bytes), igneum-worker-opencl.exe 7fce1249d443aaf73a29b7474c1ef4084f3529b5bec3a2dc6bb97e624561a5e8 (478,208), `verify-exe.py`: both carry the coin icon and the version block (a first build of the same sources before the commit gave 26400edf... and 438e608a...: mingw stamps a link time, so the pair is not bit-reproducible; the sizes match the 0.3.11 pair, 1,536,512 and 478,208, and the sha256s differ from 0.3.11's 2b3b8c92... and edc4a75d...: the generator-4 rule is in them). The Mac worker from the same tree: igneum-bench 30c70754097dafbc3144a0c0f192526a0638089e5dde49ce46abb7a948dceade (665,128 bytes, built 16:47Z from `main.swift` as committed in a522d04, the binary G4b mined with). The same build job's (build-20261006-155958) node and app outputs from fork 5f7e0543, every sha256 verified against the PC's lines on fetch: igneumd.exe 140be25e486fe1634500b6e66eedfaca9fa6561d652f3e219b5a7a2bc377e766 (51,752,448), igneum-miner.exe ceb1e691ad57f58d09d5a0ff4cb5658aea0604d23ceb90b6a847a2c36a0bf81c (11,118,080), igneum-app.exe bfa3b871f17b69fd0c0c01a6f9fcac8964efb54dc10fd25db13422ba8f96d51f (3,065,344, PE and resource block ok); Linux igneumd d44b735869b129401d68f18786b4a5f16c22d00b682bd41a8e5022a01f7d7527 (49,603,112), igneum-miner a7985111d4972f4b67a9409b831a8c7af746508ebb3b19fc927aaddcfa2e2413, igneum-app 222f30c00bb9725f3127452f3d52403b380e25abaf9fba5c925839574cf1dabc; kept under the job's --out dir with --no-place. No packaging, no DMG, no manifest |
|
||||
| G6 | the node change on a fork branch from the current release tip with suites green on PC 2, with the igneum-pow feature | the build job id and its SUMMARY line; the v4 engine test named in the output | GREEN. PC 2 job `build-20261006-155958` (fork ca3-v4-node 5f7e0543, main a522d04; published 15:59:58Z after the clear file and under the mkdir lock, taken 15:58:50Z, released 16:08:06Z after the closing report; the installed app untouched, the prover left on), `node tools/build-job.mjs run --target 1ccfe586 --node vendor/igneum-node-ca3v4 --node-tests "kaspa-consensus kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows" --app-tests igneum-app --no-place`: `done exit 0 after 392 s: node ca3-v4-node 5f7e0543 app 0.3.11: every stage ok; 9 files uploaded (46 MB); 6 min of 40`; the test stage 50 s, exit 0 on both units: kaspa-consensus 98 passed (3 ignored; `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list ... ok`, so 2.0's flake did not recur and no re-run was needed), kaspa-consensus-core 109 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`), igneum-exec (its per-crate line is not in the relay's upload of the report, which keeps the stage's tail; the unit exited 0, so it passed), kaspa-pow 15 (`program_class_v3_seeds_hash_their_own_program_over_their_own_cache ... ok` and `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, both `#[cfg(feature = "igneum-pow")]`), igneum-miner (the same: not in the upload, in the exit-0 unit), kaspa-p2p-flows 33, igneum-app 112 + 26 + 8. The feature: `build-job.mjs` and `push-build-inputs.sh` carry no `--features` for the test units (the manifest's `tests` entries are `dir` and `packages` only; the build units have `kaspad/igneum-pow`), and the PC's `jobbuild.rs` runs `cargo test --release -p ... ` in ONE invocation per unit, where `igneum-miner` depends on `kaspa-pow` with `features = ["igneum-pow"]`, so cargo's feature unification turns the feature on for kaspa-pow's test target in that invocation: the two engine tests above ran on PC 2 with the feature, named in the report, which is the evidence main asked for. The same two suites also pass on the Mac with the flag explicit (`cargo test --release -p kaspa-pow --features igneum-pow`: 15; `-p kaspa-consensus-core`: 109 + 7). What the tool lacks: a `features` key on a test unit; not added here (a change to the PC's installed app would be needed to honour it) |
|
||||
|
||||
## The cut preconditions (the coordinator's second brief, 6 October 2026 evening)
|
||||
|
||||
| # | Gate | Evidence required | State |
|
||||
|---|---|---|---|
|
||||
| P1 | the rehearsal plan and the v4 override object for the rented fleet | `docs/plans/counter-asic-3-rehearsal.md`; the objects with their digests | WRITTEN (not run: the fleet agent runs it): the publish object (the live 13 fields plus the floor `N6` and the window 86,400; digest `ac8e60ce205852bdda6b554f8cbfbd9dbb040187f487cbd8affe8103633dfd56` at the worked example N6 = 219,600) and the rehearsal object (every earlier switch at 0, window 3,600, floor 14,400; digest `bc2142b178ff367ae84ff0699ff523760d8878f883375da21864ed8d3ad39237`), both read from the signalling node's start lines on throwaway private networks; `override-v4-publish-example.json` and `override-v4-rehearsal.json` in this directory |
|
||||
| P2 | miner-signalled class activation, implemented behind the override, the fast-time gate with three cases and the failed case | `docs/plans/counter-asic-3-node.md` section 6; `infra/fast-time/class-v4-signal.mjs` | GREEN on the Mac: two of three signalling never flips (7 epochs, 6,166 to 7,583 bps), all three flips at epoch 3 (the first full window, 10,000 bps, the same line on 3 of 3 nodes, the id assertion), nobody signalling flips at the floor epoch 5 and not before; the known-failed case fails on eight checks. Rows and summary files: node doc section 6.4; `class-v4-signal-{no-flip,flip,floor,failed-case}.json` |
|
||||
| G6 (signalling) | the fork change on PC 2 with the igneum-pow feature | the job ids and their lines | GREEN on fork 0562a7f2 (main f39a8eb), three PC 2 jobs under one clearance ("PC 2 open for G6", 17:27Z), the mkdir lock taken 17:28:15Z and released 17:38:58Z, then 17:39:41Z to 17:55:31Z; prover on, app untouched. Job 1 `build-20261006-173017` (the six crates and the app): every build stage ok, the app tests 112 + 26 + 8, but kaspa-consensus 97 passed and 1 FAILED on 2.0's known flake `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list` (`UnexpectedDifficulty(.., 487111630, 487128802)` in `mine_on_all`, the 0.3.10 cut's section 11 case, which also passed on the Mac and in this morning's job 155958), and cargo stopped the unit there. Treated as 2.0 did: job 2 `build-20261006-174027` (kaspa-consensus alone, 17:40 to 17:46Z): `RESULT test node [kaspa-consensus] exit 0 19 s`, `done exit 0 after 345 s ... every stage ok`. Job 3 `build-20261006-174823` (the five crates and the app, 17:48 to 17:55Z): `RESULT test node [kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows] exit 0 35 s` and `RESULT test app/igneum-app [igneum-app] exit 0 5 s`: kaspa-consensus-core 110 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`, the signal window test inside it), igneum-exec 18, kaspa-pow 15 (`program_class_signal_rule ... ok` is consensus-core's; kaspa-pow's `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, the feature on), igneum-miner 18, kaspa-p2p-flows 33, igneum-app 112 + 26 + 8; the job's own closing line reads `failed ... upload incomplete` because the relay's blob service refused one of the nine uploads (`igneum-app.exe.zst FAILED: blob PUT: no url in the reply: service_unavailable`, the shipper's 17:25Z fault) AFTER both test stages had closed with exit 0, so the STAGE and RESULT lines are the evidence, as the shipper's warning said. Where the runs went: these three on PC 2 under the clearance given before the build-server rule arrived (18:0xZ); no further Linux build or suite is needed by this brief; the next one from this lane goes to igneum-build-1 through tools/build-remote.sh |
|
||||
|
||||
Summary files in this directory: `class-v4-20261006-1553Z-cpu.json` (G4 run 1), `class-v4-20261006-never-failed-case.json` (G4 run 2, the failed case), `class-v4-20261006-metal.json` (G4b), `class-v4-20261006-id-rerun.json` (G4 on the program-id fix, with the id assertion), `class-v4-20261006-id-failed-case.json` (the id assertion's failed case).
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"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,"program_class_v4_activation_daa":219600,"program_class_v4_signal_window_daa":86400}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"difficulty_v2_activation_daa":0,"proving_v0_activation_daa":0,"fees_v1_activation_daa":0,"finality_v3_activation_daa":0,"program_class_v3_activation_daa":0,"proving_v1_activation_daa":0,"proving_v1_segment_blocks":8,"proving_v1_unproven_daa":600,"proving_v1_aggregator_share_bps":1000,"proving_v1_fresh_rule_daa":0,"exec_restart_number":18446744073709551615,"exec_restart_hash":"","exec_restart_trust_daa":18446744073709551615,"program_class_v4_activation_daa":14400,"program_class_v4_signal_window_daa":3600}
|
||||
|
|
@ -88,3 +88,45 @@ The hash lane found the seven gate packs under `proto-cuda/packs-ca3-v4` carryin
|
|||
- Wire: `RpcPowEpochInfo` gained one Borsh field (wRPC, versioned by `GetBlockTemplateResponse`) and one optional proto field; a 0.3.13 miner against this node reads the v4 height as never (the field absent) and would key epochs on the v3 switch alone, so every miner moves in the same binary sweep as the node (the rollout order of 2.0).
|
||||
- The build job has no `features` key for test units (G6 row): the feature was on through unification; adding the key needs the PC's installed app to honour it, a 0.3.14 app item.
|
||||
- The 48 GiB APFS clone of the 0.3.13 target dir (`vendor/igneum-node-ca3v4/target-ca3v4`, untracked) can be deleted after the cut.
|
||||
|
||||
## 6. PROPOSED: miner-signalled class activation (precondition 2 of the cut)
|
||||
|
||||
Status: PROPOSED spec text for spec 01 (a new section 1.12.2 beside the epoch rule) and spec 02; implemented behind the override on the fork branch `ca3-v4-node` (`consensus/src/processes/class_signal.rs`, the pure rule in `consensus/core/src/igneum.rs`), gated by the fast-time runs of section 6.4. Not in `docs/spec` until the project lead adopts it. The 2.0 fixed height stays, as the floor.
|
||||
|
||||
### 6.1 The rule
|
||||
|
||||
1. **The carrier: the block header's `version` field, 2 bytes little-endian.** The low byte is the block version as before (2 on every Igneum network; `block_version_of`). The high byte is the producer's OBJECT VERSION, the highest program class the node that built the template runs: 4 for this binary (`CLASS_SIGNAL_V4`), 0 on every block made before it (`signalled_version(2, 4) = 0x0402`; `class_signal_of`). Why the header and not the coinbase extra data: the tally is read in header validation and by a node that holds headers only (a pruning-proof sync, a headers-first IBD), the header is what GHOSTDAG orders and what the finality rule already reads for its weight (the vote key hash is a header field, `finality.rs`), and the bytes are already there: no new field, no new hash preimage, no proto change for the header. A node before this binary rejects any header whose version is not exactly 2 (`check_header_version`), which is why the signalling binary ships in the same one-sweep rollout as the digest flip it already needs (section 2); from this binary on, only the low byte is checked, so a later object (5, 6) can be signalled to it without another header rule.
|
||||
2. **The weight: blue blocks, the finality rule's convention.** For epoch `e` the anchor is its seed block `S_e`, the last selected-chain block whose DAA score is below `L e - lead` (the block the epoch seed is already taken from, `HeaderProcessor::epoch_seed`; so an epoch's seed and its class are decided at the same block of the same chain). The window is the `W` DAA below and including `S_e`: the selected chain is walked down from `S_e`, every chain block's mergeset blues counted once (the `compute_weights` walk of `finality.rs`, bounded by the merge depth), a blue block counted when its DAA score lies in `(daa(S_e) - W, daa(S_e)]`, and signalling when its object byte is at least 4. Share = signalling / total, in basis points.
|
||||
3. **The decision, per epoch, monotone.** Epoch `e` is class v4 when (a) `L e >= N6`, the floor (the fixed height, rounded up to the epoch boundary exactly as the v3 switch is: `program_class_for_epoch_at`), or (b) epoch `e - 1` was v4, or (c) the window ending at `S_e` is FULL (`daa(S_e) >= W`) and its share is at least 9,500 bps. A signal moves the class one step: the rule answers v4 only where the floor rule answers v3 (below the v3 switch the answer is v2 whatever is signalled). The decision is memoised per seed block, so a fork of the chain has its own entries and one tally is paid per epoch per process. The epoch-boundary rounding of the v2 to v3 switch is unchanged: a class never changes inside an epoch, every block's class is its epoch's.
|
||||
4. **The window `W`: 86,400 DAA (one day of blocks at 1 block/s), `program_class_v4_signal_window_daa` in the override file, in the digest right after the floor; 0 = signalling off, the floor alone (2.0's rule, byte for byte).** Why a day: the share must mean "the fleet that mines, not the fleet that happens to be up this hour", and a day covers every box's daily pattern (the rented boxes come and go by the hour); it is long enough that 95 percent cannot be reached by a burst and short enough that the flip lands within a day of the last upgrade; the finality rule's 30-day window answers a different question (who may vote) and would hold the class for a month after the fleet was ready. On the fast-time profile `W` is 120 (two epochs of 60).
|
||||
5. **The threshold: 9,500 bps (95 percent), a constant (`CLASS_SIGNAL_THRESHOLD_BPS`), not a file field**, so no file can lower it; 95 percent of the blue blocks of a day is 95 percent of the hash rate of that day, the coordinator's figure; a box that cannot mine v4 (an old worker, section 2's wire note) is at most 5 percent of the hash rate at the flip, and the floor catches the rest.
|
||||
6. **The floor `N6`: `program_class_v4_activation_daa`, set at the publish as DAA + 14,400 rounded up to the epoch boundary (the 2.0 rule for N4), checked `N6 - DAA >= 10,800`.** A stalled signal (a fleet that never reaches 95 percent) cannot hold the class forever: at `N6` v4 holds regardless. `never` is allowed in the file and means no floor (the signal alone decides; not for the devnet publish).
|
||||
7. **What a node reports.** `PowEpochInfo` and the template's `powEpoch` carry `programClassV4SignalWindowDaa`, `programClassV4SignalBps` (the share of the window ending at the SINK, the live tally the next decision is heading for), `programClassSignal` (this node's byte) and `programClassV4SignalEpoch` (the epoch v4 was decided by signal on this chain, when it has been); gRPC fields 20 to 23. The daemon prints `Program class v4 signal window from the override file: W DAA ending at each epoch's seed block, threshold 9500 bps of blue blocks; the fixed height is the floor` beside the floor line, and `Program class v4 by miner signal: epoch E (share X bps over W DAA ending at seed block S, threshold 9500 bps, N of M blue blocks)` once per flip.
|
||||
8. **The miner.** Nothing: the node builds the template header (the miner varies the nonce), so the signal is the node's binary; the miner takes the class of the epoch and the next from the template as before (`next_program_class` is the next epoch's decision once its seed block is known, else this epoch's class; a boundary that decides otherwise costs one refused pair and one prepare, the 2.0 era-boundary shape). `IGNEUM_CLASS_SIGNAL=<n>` on devnet and simnet only lowers a node's byte (the fast-time gate's non-signalling node); it is not read on mainnet or the testnet.
|
||||
|
||||
### 6.2 The devnet object changes shape
|
||||
|
||||
Two fields join the live object: `program_class_v4_activation_daa` is now the FLOOR (its name and its rounding unchanged: the 2.0 fixed-height form), and `program_class_v4_signal_window_daa` is the window (86,400 on the devnet; 0 turns signalling off). Both enter the digest (unconditionally, right after the v3 field), so the digest moves on the binary rollout once more; the pinned devnet digest of the fork's test is re-pinned to the value of this binary (section 6.4). The publish object and the rehearsal object are in `docs/plans/counter-asic-3-rehearsal.md` section 2. Defaults: devnet and mainnet 86,400, testnet and simnet 0 (the testnet is v4 from genesis by its floor of 0; the simnet keeps 2.0's rule).
|
||||
|
||||
### 6.3 What it does not cover (owed)
|
||||
|
||||
| Item | Why | What is done about it |
|
||||
|---|---|---|
|
||||
| A class-signal witness in the pruning-proof format | a node that synced from a proof holds no headers below its pruning point, so an epoch whose window reaches below it cannot be tallied; the node then takes the floor rule for that epoch and logs it (`class_signal.rs`), which can disagree with a full-history node for the epochs between a signal flip and the floor | the same class as the era witness of 2.0 section 7 (`MissingEraSeed`); the floor bounds the exposure to at most `N6 - flip`; the witness (the per-epoch decision beside the epoch seed in the proof) is the next node item |
|
||||
| The first tally after a restart | one walk of `W` chain blocks' mergesets per epoch per process, memoised; a day of blocks is about 86,400 header reads, under a second on the Mac's store | measured in the fast-time runs below at `W` = 120 only; the devnet figure is owed from the rehearsal |
|
||||
| A byte above 4 | accepted and counted as a v4 signal (a later object contains v4); a v5 rule would count bytes at or above 5 | nothing now |
|
||||
|
||||
### 6.4 The fast-time gate (three cases and the failed case)
|
||||
|
||||
`infra/fast-time/class-v4-signal.mjs`: three nodes on `override-60x.json` (v3 from DAA 60, window 120, the floor at `--floor`), each node's byte set by `IGNEUM_CLASS_SIGNAL` (`--signal a,b,c`), one real CPU miner each, the id assertion of G4 on every v4 epoch.
|
||||
|
||||
| Case | Run | SUMMARY | Summary file |
|
||||
|---|---|---|---|
|
||||
| Two of three signal (`--signal 4,4,3 --expect no-flip --epochs 7`) | 17:26:4x to 17:34:57Z (load average about 9) | PASS: no v4 epoch over epochs 0 to 7; the share at the sink read 6,166 to 7,583 bps epoch by epoch (two CPU miners of three, the third's blocks byte 3); on the chain 286 blocks byte 4, 138 byte 3, genesis byte 0 (6,729 bps); no node printed the signal line; 0 rejected; one sink `d845fdd016a25425` at 424/424/424; the floor line on 3 of 3 (`never`) | `class-v4-signal-no-flip.json` |
|
||||
| All three signal (`--signal 4,4,4 --expect flip`) | 17:34:5x to 17:39:33Z | PASS: the share 10,000 bps from epoch 1; the class flipped at epoch 3 (DAA 180), the first epoch whose seed block has a full 120-DAA window below it, before the floor (`never`); 3 of 3 nodes printed `Program class v4 by miner signal: epoch 3 (share 10000 bps over 120 DAA ending at seed block 13629115abf4b06bd9b07dfee66c7aab817cef268a34c02f8da6884d52f26017, threshold 9500 bps, 120 of 120 blue blocks)` with the same epoch and share; 181 / 124 blocks across DAA 180; 0 rejected; one sink `322e4a57824ab7fd` at 304/304/304; the id assertion on epochs 3, 4, 5: the three miners' v4 id equals the CLI's class v4 id and differs from the same-seed v3 id (e3 `e48e6be7c6699824` against `6847355c88e8215f`) | `class-v4-signal-flip.json` |
|
||||
| Nobody signals, the floor at 300 (`--signal 3,3,3 --floor 300 --expect floor`) | 17:39:3x to 17:46:28Z | PASS: the share 0 bps throughout (424 blocks byte 3); no signal line on any node; the class flipped at epoch 5 (DAA 300), the floor, and not before; the floor line `active from epoch 5` on 3 of 3; 301 / 124 blocks; 0 rejected; one sink `aa8755bf4c1cc9e2` at 424/424/424; the id assertion on epochs 5 to 7 (e5 `99f8a29e69d425fd` against `48b8b87ff3a7e9ae`) | `class-v4-signal-floor.json` |
|
||||
| The known-failed case (`--signal 4,4,3 --expect flip --epochs 7`) | 17:20:34 to 17:27:04Z | FAIL as it must: no flip at 6,250 to 7,288 bps, and the harness reports `FAILED CHECK` on eight checks (`template_switched_to_v4`, `switched_at_the_first_full_window_epoch`, `switched_before_the_floor`, `signal_line_on_every_node_same_epoch`, `signal_share_at_or_above_threshold`, `blocks_on_both_sides`, the two id checks), exit 1 | `class-v4-signal-failed-case.json` |
|
||||
|
||||
The first pass of the three good cases (17:01 to 17:20Z) had the same chain behaviour and failed on one harness check of its own (`chain_carries_the_bytes` demanded every byte on the chain be a node's and genesis carries byte 0); the check was corrected to allow the one genesis block and the three cases re-run above. Every number here is a count, a line or an id; the Mac's load average (about 9, other agents' builds) moves the wall times only.
|
||||
|
||||
Fork suites on the signalling change (0562a7f2): kaspa-consensus-core 110 + 7 (the signal-rule test, the window in the params, digest and fast-time file tests; the pinned devnet digest re-pinned from `3c505021...` to `7f2e49beabc253f327c5ac6bb457a674ea7f527af2971c95d3bdf65ef8bcf977`), kaspa-consensus lib 98 (the template test now reads the low byte and the signal byte), kaspa-pow with the feature 15; PC 2: section G6 of `node-gates.md`.
|
||||
|
|
|
|||
78
docs/plans/counter-asic-3-rehearsal.md
Normal file
78
docs/plans/counter-asic-3-rehearsal.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Counter ASIC 3.0: the class v4 rehearsal on the rented fleet (precondition 1 of the cut)
|
||||
|
||||
6 October 2026, worker "ca3-v4-node", on the coordinator's word of about 17:40 UTC. A precondition: no live-devnet date for class v4 is named until this rehearsal has PASSED on the rented fleet. The fleet agent owns the boxes (memory `gpu-rental.md`: the Vast and RunPod accounts, the fleet SSH key, the 6 October fleet plan and budget); this file is the plan it runs and the objects it runs with. The node lane touches no box. Nothing here is published to the live devnet; the rehearsal chain is a fleet-only network that the live devnet's nodes cannot join and that cannot join them.
|
||||
|
||||
## 1. What is rehearsed
|
||||
|
||||
The class v4 activation on a chain of real boxes, in the shape the live devnet will see, with both activation rules of the v4 seam (`docs/plans/counter-asic-3-node.md`): the miner-signalled flip (section 6 there, PROPOSED) and the fixed-height floor behind it; the stale-box case, where one box runs the old binary against the new object and must be refused at the handshake without forking the chain; and the live form of the G4 checks.
|
||||
|
||||
| Fact | Value | Why |
|
||||
|---|---|---|
|
||||
| Network | `--devnet-suffix=400` (`igneum-devnet-400`), every box; the live devnet has no suffix and node 1, the observer and the seeds refuse any other network name at the handshake | fleet-only by construction; the chain starts at its own genesis state (DAA 0) because no box carries a devnet-400 database |
|
||||
| Binary | one commit of ca3-v4-node (main) and ca3-v4-node (fork), the build job's Linux `igneumd`, `igneum-miner`, `igneum-app` from PC 2 (the G6 job of the signalling commit; the shas in `docs/plans/counter-asic-3-gate/node-gates.md`) on every box but the stale one; the stale box runs 0.3.13's Linux node (fork bb43e9a8, the 0.3.13 outputs) | the same objects as the cut's step 1 |
|
||||
| Boxes | 12 or more mining boxes (the 50-miner wave's shape at a tenth of the size; 1 CPU miner or 1 GPU worker each), 1 seed box (`--listen`, no miner), 1 stale box | 12 keeps the per-box share near 8 percent, so one box's absence moves the tally by 8 percent: the 95 percent threshold is exercised, not trivially met (with 3 boxes it is 67 or 100 percent, the fast-time shape) |
|
||||
| Override object | `rehearsal` below, the same file on every box (the stale box too) | the stale case is the digest refusal, so the file must be the same |
|
||||
| Duration | about 2 h 40 min of chain at 1 block/s: the flip by signal at DAA 7,200 (epoch 2), two epochs after it, then the floor at 14,400 is NOT reached (the run ends at DAA 10,800) | the floor is the backstop; the rehearsal proves the signal path, the fast-time gate proved the floor path |
|
||||
|
||||
## 2. The override objects
|
||||
|
||||
Both objects below are JSON text to be written verbatim; a `never` height is `18446744073709551615`, which no JSON tool that goes through a double may rewrite (the fast-time harness's rule). The digests are what a node of the signalling commit prints at start (`Consensus params digest`); the fleet agent compares every box's line against them and the stale box's against its own.
|
||||
|
||||
### 2a. The live devnet publish object (NOT published by this plan; the shape the cut will use)
|
||||
|
||||
The live file today (`/tmp/igneum-devnet/override-v3.json`, 13 fields, read 16:55Z) plus the two v4 fields. `N6` is the floor: DAA at the publish + 14,400 rounded UP to a multiple of 3,600 (the 2.0 rule for N4), checked at publish (`N6 - DAA >= 10,800`). At the live DAA of 202,919 (16:57:43Z) that would be 219,600 (epoch 61); the number is set at the publish, not here.
|
||||
|
||||
```
|
||||
{"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,"program_class_v4_activation_daa":N6,"program_class_v4_signal_window_daa":86400}
|
||||
```
|
||||
|
||||
What the two fields do on the live devnet: every node of the signalling binary stamps object byte 4 into its templates from its first block, so the signal share climbs as the fleet updates; the class flips at the first epoch boundary whose window (the 86,400 DAA, one day, below that epoch's seed block) has 95 percent of its blue blocks signalling, which is about a day after the LAST box of 95 percent of the hash rate has updated; the floor `N6` flips it regardless at the latest. Digest of this object: `ac8e60ce205852bdda6b554f8cbfbd9dbb040187f487cbd8affe8103633dfd56` (read from the signalling node's start line, 18:05Z; the node also prints `Program class v4 from the override file: active from epoch 61 (DAA score 219600 rounded up to the epoch boundary at 219600, epochs of 3600 DAA)` and the window line) (with `N6` = 219,600 as the worked example; any other `N6` moves it).
|
||||
|
||||
### 2b. The rehearsal object (the fleet chain)
|
||||
|
||||
A fresh chain, every earlier switch at 0 (the testnet's shape: the chain is born on calibrated difficulty v1, proving v1, fees v1, finality v3, class v3), the v4 signal window one epoch, the floor four hours out:
|
||||
|
||||
```
|
||||
{"difficulty_v2_activation_daa":0,"proving_v0_activation_daa":0,"fees_v1_activation_daa":0,"finality_v3_activation_daa":0,"program_class_v3_activation_daa":0,"proving_v1_activation_daa":0,"proving_v1_segment_blocks":8,"proving_v1_unproven_daa":600,"proving_v1_aggregator_share_bps":1000,"proving_v1_fresh_rule_daa":0,"exec_restart_number":18446744073709551615,"exec_restart_hash":"","exec_restart_trust_daa":18446744073709551615,"program_class_v4_activation_daa":14400,"program_class_v4_signal_window_daa":3600}
|
||||
```
|
||||
|
||||
The arithmetic: epochs of 3,600 DAA, lead 600. Epoch `e`'s seed block is the last chain block below `3600 e - 600`; its window is full when that block's DAA is at least 3,600: epoch 1's seed block sits at DAA 2,999 (not full), epoch 2's at 6,599 (full). With every mining box signalling 4, the tally at epoch 2's seed block is 100 percent of the blue blocks in DAA 2,999 to 6,599, so the class flips at epoch 2, DAA 7,200, about 2 hours after genesis; the floor (epoch 4, DAA 14,400) is 2 hours later and is not reached by the run. Digest: `bc2142b178ff367ae84ff0699ff523760d8878f883375da21864ed8d3ad39237` (the signalling node's start line on this object, 18:05Z, with `Program class v4 from the override file: active from epoch 4 (DAA score 14400 ...)` and `Program class v4 signal window from the override file: 3600 DAA ...`); the devnet digest with no file at all is `7f2e49beabc253f327c5ac6bb457a674ea7f527af2971c95d3bdf65ef8bcf977` on this binary (the fork's pinned test), `c562d70e...` on 0.3.11 to 0.3.13.
|
||||
|
||||
## 3. The steps the fleet agent runs
|
||||
|
||||
| Step | What | Done when |
|
||||
|---|---|---|
|
||||
| 1 | Fetch the signalling commit's Linux binaries from the G6 build job (the shas in node-gates.md), verify every sha256, place `igneumd` and `igneum-miner` on every box; the 0.3.13 Linux `igneumd` on the stale box | every sha matches |
|
||||
| 2 | Write the rehearsal object (2b) as `override.json` on every box, byte for byte (sha256 the file on each box and compare) | one sha on every box |
|
||||
| 3 | Start the seed box: `igneumd --devnet --devnet-suffix=400 --nodnsseed --disable-upnp --listen=0.0.0.0:16411 --rpclisten=127.0.0.1:16410 --rpclisten-json=127.0.0.1:16412 --override-params-file=override.json --utxoindex --enable-unsynced-mining --yes --appdir=<fresh dir>` (ports of the box's choosing, never the live devnet's 26610/26611); read its first lines: `Consensus params digest` equals 2b's, `Program class v4 from the override file: active from epoch 4 (DAA score 14400 ...)`, `Program class v4 signal window from the override file: 3600 DAA ...` | the three lines |
|
||||
| 4 | Start every mining box the same way with `--connect=<seed>:16411`, then its miner: `igneum-miner mine grpc://127.0.0.1:16410 1 100000000 <label> --engine igneum-pow --no-vote --payout-label <label>` (a CPU miner; a GPU box uses `--worker <path> --prepare-packs packs/prepare --exit-on-seed-change`, the app's shape) | every box's node prints the same digest and the two switch lines; every miner prints `epoch seed ... class v3 program id ...` for epoch 0 |
|
||||
| 5 | Start the stale box last, the same command on the 0.3.13 node, `--connect=<seed>:16411` | its log shows the handshake refusal (a digest mismatch line or `0 peers` after 60 s with connection attempts in the log) and its chain stays at its own genesis (block count 1 or its own lonely blocks if it mines; it must NOT mine: no miner on it) |
|
||||
| 6 | Every 15 minutes, on every mining box: `getBlockDagInfo` (block count, sink, virtual DAA) and one `getBlockTemplate` (`powEpoch.programClass`, `nextProgramClass`, `programClassV4SignalBps`, `programClassV4SignalEpoch`); keep the lines | the shares read 10,000 bps on every box from the first template; sinks agree across boxes at each sample |
|
||||
| 7 | At DAA 7,200 (about 2 h): every box's node prints `Program class v4 by miner signal: epoch 2 (share 10000 bps over 3600 DAA ending at seed block <hash>, threshold 9500 bps, N of N blue blocks)` with the SAME seed block hash and the same N on every box; the templates read class 4 from epoch 2; every miner prints `epoch seed <S2> ... class v4 program id <id>` | the same `<S2>`, the same `<id>` on every box |
|
||||
| 8 | Run to DAA 10,800 (epoch 3, one epoch after the flip; two epochs after is 14,400, the floor, so the run stops at 10,800 plus 600) | the final sample |
|
||||
| 9 | Collect every miner's `program and 256 MiB cache ready` lines (seed, class, id) and the template's `eraSeed` for epochs 2 and 3, and send them to the node lane; the Mac computes `igneum-pow show --epoch-hex <seed> --program-class v3|v4 --era-hex <era>` for each (the id assertion of the G4 harness, no binary needed on a box) | every box's v4 id equals the CLI's v4 id and differs from the CLI's v3 id of the same seed and era |
|
||||
| 10 | Stop every node and miner; destroy the instances by the fleet plan's rule | the report in section 4 is in |
|
||||
|
||||
Never: no live-devnet port, no live override file, no manifest, no `update-now`; the boxes' app installs (if any) are not touched (the rule of 5 October: a job never quits or restarts an app it did not start).
|
||||
|
||||
## 4. What the fleet agent reports back (one table, one JSON)
|
||||
|
||||
`docs/plans/counter-asic-3-gate/rehearsal-<time>.json` with the fields below and a row per box in `docs/plans/counter-asic-3-rehearsal.md` section 5 (this file, appended by the fleet agent):
|
||||
|
||||
| Field | From | Pass rule |
|
||||
|---|---|---|
|
||||
| `digest` per box | the node's first lines | one value on every signalling box, equal to 2b's; the stale box prints 0.3.13's digest of the same file (a different value, since its binary lacks the two fields) |
|
||||
| `switch_lines` per box | the node's first lines | the floor line names epoch 4 and the window line names 3,600 DAA, on every signalling box |
|
||||
| `signal_line` per box | step 7 | present on every signalling box, epoch 2, the same seed block hash, share 10,000 bps (at least 9,500) |
|
||||
| `blocks_before`, `blocks_after` | `getBlocks` from any box, split at DAA 7,200 | both over 0 |
|
||||
| `rejected` | every miner's `rejected=` STATUS count and every node's `PoW rejected` lines | 0 on every box |
|
||||
| `sinks` at the end | `getBlockDagInfo` | one value on every signalling box |
|
||||
| `program_ids` per epoch per box | the miners' lines | one id per epoch across boxes; epochs 2 and 3 class v4; the Mac's CLI check (step 9) holds |
|
||||
| `stale_box` | its log and `getBlockDagInfo` | refused (no peer), block count 1, no block of its ever appears on any signalling box's chain (its own genesis-state chain and the fleet's never merge) |
|
||||
| `shares` per sample | step 6 | 10,000 bps on every box at every sample |
|
||||
|
||||
PASS = every rule above holds. Any other outcome is RED, the file says which rule failed, and the cut waits.
|
||||
|
||||
## 5. Results
|
||||
|
||||
(appended by the fleet agent after the run)
|
||||
|
|
@ -10,7 +10,9 @@ The test every result is judged against (the project lead, 6 October): a chip ma
|
|||
|---|---|---|
|
||||
| Mac M5 Max | mining paused; Metal worker free | measurements under `with-lock.sh measure` only |
|
||||
| PC 2 (1ccfe586, RTX 5090) | HELD for the 0.3.14 shipper's Rust suite from 17:5x UK (main): nothing of 3.0 goes to PC 2 until the shipper reports the suite done (the P2 G6 run waits on it). Earlier: RELEASED at 08:43:24Z after the three jobs (derive 08:26 to 08:29Z, shadow 08:29 to 08:41Z, family 08:41 to 08:43Z, all exit 0; prover ON, app untouched); then the prover-floor agent, then the 0.3.12 release engineer's build. Before that: CLEAR at 08:24:27Z (the clear file carries the proving agent's constraints: prover left ON, no quit or restart, /opt/igneum, /opt/igneum-segal and settings.json untouched); the three 3.0 jobs run one at a time through the mkdir lock (items 8, 2, 6); "PC 2 released" to the proving agent after the last; the prover-floor agent next. Before that: the proving agent's jobs `segments-pc2-pv1` runs b and c (run b claimed nothing on a PowerShell key bug; run c from about 07:53Z); "PC 2 clear" expected about 08:35Z; then three 3.0 jobs one at a time (items 2, 6, 8); the prover-floor agent queues after "PC 2 released" | nothing published until "PC 2 clear"; one job at a time (`/tmp/igneum-devnet/pc2-ca3.lock`); released by message when done |
|
||||
| PC 1 (ae432dc7, RTX 5090 + RTX 4070 + RX 9070 XT) | the project lead's desk; the AMD queue opens on "Ember closed" (reset, family, G2, derive, the 4070, watts, about 25 min), then the Ember agent's 6-minute window, then the 0.3.14 update on the project lead's click (after the last job, not between jobs: an update wipes the jobs folder and the kits) | not used today; every AMD row OWED |
|
||||
| PC 1 (ae432dc7, RTX 5090 + RTX 4070 + RX 9070 XT) | FREE at 18:01:05Z after the queue (reset, family runs a to e, G2, derive, the 4070 ladder, the watts job that failed and left the 9070 XT off, the restore and the flag job); handed to main for the project lead's 0.3.14 click and the Ember window. Earlier: the project lead's desk; the AMD queue opens on "Ember closed" (reset, family, G2, derive, the 4070, watts, about 25 min), then the Ember agent's 6-minute window, then the 0.3.14 update on the project lead's click (after the last job, not between jobs: an update wipes the jobs folder and the kits) | not used today; every AMD row OWED |
|
||||
|
||||
Standing rule from main (17:5x UTC; CLAUDE.md on master 630b537, `docs/plans/build-server.md`): from the next build, every Linux and Windows cargo build and every Linux test suite from the 3.0 lanes runs on igneum-build-1 through `tools/build-remote.sh` and `tools/cross-remote.sh` from the worktree's crate directory (artefacts in `target-remote/`, `IGNEUM_AGENT=ca3-<lane>`, one slot, a 2 h cap; the clean node build 87 s there against 9 to 18 min on the Mac). PC 2 keeps only GPU and Windows-runtime jobs (G6's engine test on the card stays a PC job; the crate suites move). Passed to the node lane, the hash lane and the PC 1 worker.
|
||||
|
||||
## 2. The items
|
||||
|
||||
|
|
@ -52,6 +54,8 @@ Verifier headroom under the 10 ms gate (bdc07d3, the budget item 8's shadow ops
|
|||
|
||||
The 5090 rows (job run-ca3-derive-pc2-20261006, 08:26:43 to 08:28:49Z, exit 0; the card was LOADED: the miner stayed up because the job posted the settings key without the device index, see corrections; ratios valid, absolutes not): bit-exact on CUDA for both dr736 packs (64 samples, 96 lanes, fingerprints 50e3eaa779da4f1e and 9553f6d5c667205a equal to the Mac's); 1 GiB build 42 / 32 ms against x8 40 and v2 46; rates 61 to 62 MH/s on every pack (the v2 control 62.3 against 136 unloaded); NVRTC compile 1,266 ms against x8's 164 ms, +1.1 s per pack because memhard.h's item function sits inside every hash-kernel and race-variant compile, so a once-a-day derivation module is a requirement of the class, not an option. Prover left ON, app untouched.
|
||||
|
||||
The RX 9070 XT rows (PC 1 job run-ca3-pc1-amd-derive-20261006, 17:23:56 to 17:29:13Z, exit 0, beside the miners so the absolutes are loaded-card figures and the ratios stand): bit-exact on AMD OpenCL for both dr736 packs (fingerprints 9553f6d5c667205a and 50e3eaa779da4f1e equal to the Mac's and the 5090's, two passes each, self-tests PASS); the OpenCL compile 2,070 to 2,092 ms per dr736 pack against 41 ms for mx8 (+2.0 s per pack, the AMD twin of NVRTC's +1.1 s: the once-a-day derivation module is a requirement on every vendor, and on a one-click AMD miner the shadow-free x8 pack compiles in 0.04 s where the derivation pack takes 2.1); the 1 GiB daily build 168 to 189 ms against 205 to 273 for mx8 in the same loaded session (a build the same size or smaller, inside the noise); the rate ratio to mx8 0.982 and 1.006 (no hash-rate cost). With these the derivation's bit-exactness is on all three vendors and its hash-rate cost is zero on all three.
|
||||
|
||||
Consequence: the derivation costs no hash rate on any card and 7 ms a day of build on the Mac (29 against 22 ms; the 5090 and the 9070 XT rows owed, the loaded-iGPU tier is the one to watch); it costs the verifier, and the verifier budget is shared with item 8's shadow ops under the one 10 ms gate, so the class v4 candidate is the pairing that fits, not either lever alone (both workers told).
|
||||
|
||||
### Item 8, the Mac rows (interim, ca3-shadow a050a54; knob d4b7300; `docs/analysis/latency-shadow-2026-10-06.md`; Metal packbench, IOReport GPU + DRAM watts without root; the 5090 rows queued on PC 2; the 9070 XT OWED)
|
||||
|
|
@ -86,6 +90,19 @@ Chip side (approximate: the f = 1 stored-dataset chip of item 1 plus an ALU core
|
|||
|
||||
Consequences per tier at N = 100,000: an Apple miner loses 1.5 percent of rate and pays 16 W more (0.56x per watt, per pound unchanged); a 5090 loses 0.2 percent and goes from 350 to 431 W (0.81x per watt), so a 5090 rig pays about 23 percent more electricity for the same blocks; a pool user sees nothing; a small NVIDIA card (4060 class) binds near 100,000 by its ALU budget (model, owed); AMD holds by its budget (owed); every verifier tier is untouched (+0.17 ms per warp).
|
||||
|
||||
### Item 8, the RTX 4070 rows (PC 1 job run-ca3-pc1-4070-shadow-20261006, 17:36:05 to 17:48:10Z, exit 0; the 4070 alone through api/cards, the installed CUDA worker's --bench, nvidia-smi at 1 Hz with 12 of 12 idle samples carrying power; the card at its Ember tune point: a 1,860 MHz core lock and the 160 W cap, memory 10,251 MHz; the 5090 and 9070 XT mining beside it; every pack's fingerprint equal to the Mac's)
|
||||
|
||||
| N ops per hash | Pack | 4070 MH/s (delta) | Watts (min to max) | Microjoules per hash | SM MHz | Reading |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 930 (class v3) | mx8-devnet-epoch0, twice | 30.95 | 79.3 to 79.8 | 2.56 to 2.58 | 1,860 | the control: 2.5x the M5 Max's energy per hash, a third more than the 5090's at the hash |
|
||||
| 49,700 | sh256x13 | 31.08 (+0.4%) | 93.5 | 3.01 | 1,860 | |
|
||||
| 49,700, 64-instruction block | sh64x52 | 32.09 (+3.7%) | 92.6 | 2.89 | 1,860 | the 64-block gain seen on the Mac and the 5090 |
|
||||
| 102,100 (the candidate) | sh256x27 | 31.08 (+0.4%) | 109.0 | 3.51 | 1,860 | latency-bound; +30 W for no rate |
|
||||
| 199,600 | sh256x53 | 31.07 (+0.4%) | 138.2 | 4.45 | 1,860 | still latency-bound |
|
||||
| 330,700 | sh256x88 | 27.14 (-12.3%) | 159.9 (the cap) | 5.89 | 1,846 | the 160 W cap binds; compute-bound under it |
|
||||
|
||||
Reading: the model's "a small NVIDIA card binds near 100,000" is replaced by the measurement: the 4070 holds its rate to about 200,000 ops per hash at its tune point and binds only at 330,700 when its power cap does. Consequence per tier (main's reading, 17:5x UTC): class v4's 100,000 ops per hash costs the 12 GB NVIDIA tier nothing in rate, and the shadow has 2x headroom on that card; the 4070 owner pays 30 W more (79 to 109, 0.73x per watt); the N no card we own loses 5 percent at stays set by the M5 Max (about 130,000), not by the small card. One line to check: the restore printed "no entry was enabled before this job" and waited for no worker, while the card had been mining at 28.8 MH/s before it; the card's state after the job is read from the intake below.
|
||||
|
||||
### Item 8, the RX 9070 XT rows (PC 1 job run-ca3-pc1-amd-g1-shadow-20261006, 15:46:27 to 15:56:54Z, exit 0; the 9070 XT alone through api/cards in every key form (amd:3:gfx1201 is the installed key today), confirmed by the process list, restored in finally and mining again 15 s later; the 5090 and 4070 mining beside it, the 5090 probably clock-locked at about 2,781 MHz from the aborted Ember step; AMD OpenCL 3683.0, device-event time, 2^24 per dispatch)
|
||||
|
||||
| N ops per hash | Pack | 9070 XT MH/s (window s) | OpenCL build ms / dataset ms | Watts | Fingerprint = the Mac's |
|
||||
|
|
@ -115,6 +132,8 @@ Reading: the AMD card never leaves the latency bound on this ladder (its ALU bud
|
|||
| dot4 (comparison) | 1.60 unsigned / 4.73 signed, emulated | 1.16 | 1.06 (5 October) | |
|
||||
| mm8 (comparison, R8) | OWED (Metal 4 matmul2d) | 2.43 (`mma.m8n8k16.u8`, bit-exact) | OWED | the licensable block |
|
||||
|
||||
The RX 9070 XT column (PC 1 job run-ca3-pc1-amd-family-20261006-e, 17:31:57 to 17:34:47Z, exit 0; the card ALONE: every gfx1201 entry posted off, its worker pid 19788 gone, three runs at 17:32:11 / 15 / 19Z with `card_state=alone`, every entry restored with its own flag and identities and the card mining again 20 s later under pid 13040; the gfx1036 and old-platform columns run after the restore beside the miners; AMD OpenCL 3683.0, gfx1201, 32 CUs; the range below is over the three runs' best-of-3): alu 1,074 to 1,186 G steps/s (ratio 1.00); rotr 0.99 to 1.13; shflx (xor shuffle, ds_bpermute) 0.82 to 1.21; shl 1.00 to 1.02; shr 0.91 to 1.02; bfe 1.00 to 1.09 NATIVE (amd_bfe); andn 0.91 to 1.01; perm 1.73 to 1.93 EMULATED (no byte-permute path in AMD's OpenCL C); popc 0.91 to 1.01; clz 1.19 to 1.30; sel 0.90 to 1.01; shfla (lane + delta, ds_bpermute) 0.75 to 0.84 NATIVE; dot4 1.00 to 1.11 NATIVE (sudot4); mm8 1.68 to 1.83 NATIVE (the WMMA iu8 builtin reaches gfx12; exact UNVERIFIED: the fragment layout is not in any source at hand, so the CPU reference was not attempted rather than guessed). The AMD column of item 6 is CLOSED. Reading: on RDNA 4 every 32-bit datapath family sits within 0.75x to 1.30x of the alu chain, the shuffles cheaper than it (an LDS operation overlapping the dependent chain), so the ds_bpermute number that could have moved R3 leaves the order as proposed; the two dear rows on AMD are the same two as on Apple and NVIDIA, the byte permute and mm8.
|
||||
|
||||
Reading: against the live rotr every candidate is 0.95x to 1.23x on NVIDIA; on Apple only perm (emulated) and shfla cost more than rotr; the 8x emulation bound of 1.13.2 holds everywhere by 4x or more; the 5 percent hash-rate bound at W_new = 4 is argued from the step cost (under 1 percent of ALU time on a read-bound hash), not measured, since no reserve family is live. Proposed order R1 perm, R2 popc and clz, R3 shfla, R4 bfe, R5 shifts, R6 sel, R7 andn, R8 mm8, W_new = 4 each, family n at era n (mm8's era-4 unlock kept as a named exception or moved to era 8: the project lead's call); the full proposed 1.13.2 text with edge vectors per family is `docs/plans/counter-asic-3-reserve.md` section 6. Consequence per tier: an Apple miner pays the emulated perm at 1.13x a step and shfla at 1.91x, under 1 percent of its hash rate at W_new = 4 (argued); an NVIDIA miner pays nothing measurable; an AMD miner's row is owed and its `ds_bpermute_b32` cost is the one number that could move R3; a chip pays a barrel shifter, a byte crossbar, a popcount tree and a 32-lane crossbar per lane, which is the point.
|
||||
|
||||
### Item 6, the Mac rows (interim, ca3-reserve 192a683; the 5090 job waits on PC 2)
|
||||
|
|
@ -147,6 +166,8 @@ The live observer runs the shared checkout, which autosync fast-forwards from or
|
|||
|---|---|---|
|
||||
| item 3 (43c3ead) | spec 01 section 1.13.1's era table and `docs/plans/mixer-x4.md` section 2 still said `mixer_mult = 4`; the code (`LoadClass::MX8`, `V3_CLASS`) and spec 1.8.5 say 8 | both lines corrected on ca3-coord, 6 October 2026 |
|
||||
| PC 1 job 2 (family, run-ca3-pc1-amd-family-20261006, 16:56 to 16:59Z) | the probe ran on device 0, the integrated gfx1036, not the 9070 XT (device 1): every row `dev=0 name=` empty, alu 40.59 G steps/s (a one-CU figure); the rows are RDNA 2 iGPU ratios (shifts 1.15, bfe 1.15 native, andn 1.16, popc 1.55, clz 1.75, sel 1.81, shfla and shflx 1.89 through ds_bpermute, perm 2.43 and dot4 2.57 emulated, mm8 none: AMD's OpenCL C compiles no byte-permute, dot4 or WMMA builtin) and the 9070 XT column stays owed | the probe picks the device by name (gfx1201 on the newest AMD platform) and prints it in every row; re-run in the next PC 1 slot |
|
||||
| the watts job (run-ca3-pc1-amd-watts-20261006, 17:49:46 to 17:52:54Z, FAILED exit 1) | the sampler proved itself (12 of 12 9070 XT watts lines), the 90 s app-state window ran (the card mining at 18.87 MH/s, 203 W, identities 8 before it), every gfx1201 entry was posted off, the card went quiet and the sh256x27 bench started; then the script exited with no APPROW, no LADDER, no `watts error=` and NO RESTORE line in any upload (no `enabled=True` post, no `card_workers_after`): a `finally` that did not run or did not print, so the 9070 XT may have been left OFF | CONFIRMED and restored: api/state at 17:55:10Z read the card enabled false, state off, 23 W idle; the identities job run d posted it back and at 17:57:10Z it mined under pid 3436 at 18.9 MH/s, 195 W, identities 8; the entry amd:1:gfx1201 was set back to enabled at 17:59:05Z (job run-ca3-pc1-amd-flag-amd1-20261006) with the card mining through it (18.86 MH/s, 200 W); PC 1 free at 18:01:05Z; the AMD watts row stays OWED; the cause and a fixed script (an unconditional finally with its own first line, no `exit` inside the try, the device dumps out of the report) with the script worker before any re-run |
|
||||
| the 4070 ladder's restore line | "no entry was enabled before this job" and no wait for the worker, while the card had mined at 28.8 MH/s before it | the intake shows the 4070's worker restarted 15 s after the restore and racing at 30.9 MH/s: the card came back; the script's settings read, not the card, was wrong |
|
||||
| PC 1 family run d (run-ca3-pc1-amd-family-20261006-d, 17:17 to 17:20Z, exit 0; run b had failed because the script's probe parameter was named `$args`, PowerShell's automatic variable, so the splat was empty: renamed, with a rule added to `tools/ci/ps-drive-ref-check.sh` that fires on the old signature and stays quiet on the new) | the probe chose the 9070 XT by name (gfx1201, 32 CUs); every variant built on it: mm8 NATIVE through the WMMA builtin on gfx12 (exact unverified), dot4 native (sudot4), bfe native, the shuffles native (ds_bpermute, ds_swizzle), the byte permute EMULATED (no amd_perm path in AMD's OpenCL C). The step costs are unusable: the card was mining beside the probe, the alu chain read 226 then 195 G steps/s and the ratios swung from 5.9 to 11.8x (run 1) to 0.17 to 1.3x (run 2), the loaded card's scheduler | the 9070 XT column is taken with the card alone (run e, job 1's switch path), after the G2 job |
|
||||
| the identities job, first run (run-ca3-pc1-amd-identities-20261006, 17:02:10Z, failed in 0 s) | my own script: `"... under $appDir: nothing posted"` is a PowerShell 5.1 parse error (`$appDir:` reads as a drive-qualified variable), so the script never started; the script worker checks this shape by hand, CI did not | `${appDir}:`; the class guard `tools/ci/ps-drive-ref-check.sh` added to ci.yml (67 .ps1 files clean; a backtick-escaped `$` in a bash-generating here-string is ignored); the job republished as -b |
|
||||
| the identities job, run c (run-ca3-pc1-amd-identities-20261006-c, 17:05:53 to 17:07:53Z, done) | the 9070 XT's live entry amd:gfx1201 read identities 2 at 18.91 MH/s and 203 W before; the one POST (the app's cards-array shape) and a 120 s settle; after: identities 8, mining under a new worker pid, 18.9 MH/s (avg 18.77), 199 W. The identities count moves no rate (18.92 at 2 was the number to beat). Run b of the same job had failed on the flat body shape (400) | closed: PC 1's 9070 XT runs as it did before job 1 |
|
||||
|
|
@ -189,7 +210,7 @@ No publish, no manifest, nothing on the live devnet; PC 2 one job at a time with
|
|||
| Gate | What | State | Evidence (full lines in `docs/plans/counter-asic-3-gate/hash-gates.md` and `node-gates.md`, JSON per run beside them) |
|
||||
|---|---|---|---|
|
||||
| G1 | bit-exact v4 on every vendor against the Mac reference (2^24 fingerprint, self-test) | GREEN on all three vendors | Apple (Metal and Apple OpenCL, 15:49 to 15:50Z) and NVIDIA (the 5090, PC 2 job, 15:52Z): eight packs, three harnesses, one fingerprint per pack; AMD (PC 1 job run-ca3-pc1-amd-g1-shadow-20261006, 15:46 to 15:56Z): 7 of 7 packs equal to the Mac's on gfx1201 (sh256x27 3d2e8245cc084d07) |
|
||||
| G2 | the CPU verifier exact on 1,024 random hashes per card | GREEN on NVIDIA and Apple; AMD OWED (job run-ca3-pc1-amd-g2-20261006 ready on the chain-seed packs v4-devnet-epoch0 and mx8-devnet-epoch0 with the verifier's digests 435b976a... and 2a1824a2..., its own 198 KB kit; the first kit's sh256x27 is a string-seed pack the worker's job protocol refuses, so it cannot be served; queued after the Ember run) | 24 runs of 1,024 of 1,024 (eight packs on the 5090, Metal, Apple OpenCL), re-hashed on the Mac with `igneum-pow hash-bound --count 1024` on the same packs; tooling item: the found lines go to a file with a count and digest (in hand) |
|
||||
| G2 | the CPU verifier exact on 1,024 random hashes per card | GREEN on all three vendors | AMD (PC 1 job run-ca3-pc1-amd-g2-20261006, 17:22Z, the kit worker's serve mode on gfx1201 through the `class=v3` and `class=v4` tokens, beside the miners): 1,024 of 1,024 found and distinct on the control mx8-devnet-epoch0 (digest 2a1824a2...) and on the generator-4 candidate v4-devnet-epoch0 (435b976a...), both re-hashed on the Mac through `tools/ca3-v4/g2-recheck.sh --digest`: MATCH 1,024 of 1,024 each. The first kit's sh256x27 is a string-seed pack the worker's job protocol refuses, so G2 ran the chain-seed packs the 5090 and the Mac ran | 24 runs of 1,024 of 1,024 (eight packs on the 5090, Metal, Apple OpenCL), re-hashed on the Mac with `igneum-pow hash-bound --count 1024` on the same packs; tooling item: the found lines go to a file with a count and digest (in hand) |
|
||||
| G3 | the soundness suite green on the class | GREEN | the crate suite 96 of 96 (97 on the merged tree at 17:17Z, cargo 1.99); the Metal fuzz 200 of 200 and 50 of 50; Apple OpenCL 20 of 20 and 5 of 5; 4 of 4 CPU tests on the class and on the era-composed class |
|
||||
| Verifier benchmark | ms per warp on one M5 Max core, v2 / x8 / v4 in one session, gate 10 ms | GREEN | 2.33 ms on the candidate (x8 2.06), about 5.8 ms on a 2019-class core by the 2.5x rule (approximate); the 2019-class core itself still unmeasured (O-1.14) |
|
||||
| G4 | the fast-time 3-node network across a v4 activation, plus the known-failed case | GREEN (and GREEN again on the id fix with the id assertion and its failed case, 491131a) | run 1 (15:54 to 15:59Z, class-v4.mjs, activation 150 rounded to epoch 3 at DAA 180, the v3 switch at 60): 3 of 3 switch lines, templates class 2 / 3 / 4 by epoch, 181 blocks before and 128 after DAA 180, program ids agree on all three miners and no v2 or v3 id reappears under v4, rejected 0/0/0, one sink at 308/308/308, one digest; the first v4 epoch's cache ready in 2 ms (the v3 day cache reused across the boundary); run 2 the known-failed case (the switch at never: no v4 epoch reported) |
|
||||
|
|
@ -199,18 +220,18 @@ No publish, no manifest, nothing on the live devnet; PC 2 one job at a time with
|
|||
|
||||
BLOCKER before any cut (found by the hash lane, 17:0x UTC): `program_id(3, seed, attempt)` is class-independent, so all seven v4 packs carry the same program id as the v3 control of their seed (73bcbfe8ccf988f1), and a stale worker across the activation would see no id mismatch (2.0's G4 id check cannot fire on it; G4's per-epoch ids differ only because each epoch has its own seed). The fix is on the v4 seam, assigned to the node lane: a generator-4 stamp in the id so a v3 and a v4 program of one seed differ, v2 and v3 ids byte-identical, the packs re-exported (fingerprints unchanged, ids moved), G4's id assertion and the fuzz re-run. FIX MERGED (ca3-v4-node 7c22d0d, 17:3x UTC): the trap was the CLI's --era path (stamp_era stamped generator 3 on any class), not the chain seam (the fork already stamped generator 4 and its kaspa-pow test asserts the same-seed v3 and v4 ids differ); now generate_era and the CLI stamp the generator from the class, the shadow block marks class v4 in packcheck.rs, packfile.h and main.swift (a generator-3 pack with a shadow block is refused as "a v4 program stamped v3", a generator-4 pack without one refused; the ladder packs stay loadable); v2 and v3 ids byte-identical; the crate suite 97 of 97 on the merged tree; the seven gate packs re-exported with generator 4, class "v4" and program id c120d7963abdcd96 (the v3 control keeps 73bcbfe8ccf988f1), only the generator, id, class and comment lines changed, the kernels and vectors byte-identical. The hash lane's re-run on the fix is GREEN (ca3-v4-hash ca61dec, merged; `hash-gates.md` "Follow-up 1"): the crate suite 97 of 97; the seven packs re-exported here equal the tree's (0 differing files); the fingerprints unchanged on the rebuilt Metal and Apple OpenCL harnesses (all eight); Mac G2 through the `class=v4` token 1,024 of 1,024 on all eight packs, both harnesses; the fuzz 200 programs and 800 units, stats, edge and determinism 4 of 4 on the class and 4 of 4 era-composed, the same-seed v3 and v4 ids now differ (`assert_ne`). The node lane's re-run on the fix is GREEN (ca3-v4-node 491131a, merged): G4 re-run 16:32 to 16:38Z on igneumd and igneum-miner rebuilt on the fixed crate, SUMMARY PASS, 181 / 125 blocks across DAA 180, rejected 0/0/0 and 0/0/0, one sink at 305/305/305, and the id assertion: every v4 epoch's id on all three miners equals the CLI's class v4 id for that seed and era and differs from the same-seed v3 id (e3 30544487d1289d6e against 1ae1c9f0eda0cef5, e4 53e36801cc6fafcf against af81f6e844959460, e5 876e155e3fb59983 against b4f25c4678496a78); the assertion's own failed case (`--id-check-against v4`) reports exactly `FAILED CHECK v4_ids_differ_from_the_same_seed_v3_id`, exit 1. The seven re-exported packs' Metal fingerprints by packbench equal the table (only the ids moved). G5 re-done from 7c22d0d because packfile.h and main.swift moved: igneum-worker-cuda.exe 3bc8ad8f..., igneum-worker-opencl.exe 16ef0154... (resource block verified), igneum-bench f9ca4b07...; kaspa-pow with the feature 15 of 15 on the rebuilt fork. THE BLOCKER IS CLOSED: the gate table is green on the hash and on the cut, with AMD G2 and the AMD watts the owed rows (queued on PC 1). Two conditions ride with the fix: every kit sent to a PC carries the re-exported packs (the AMD G2 kit is being rebuilt from the merged tree), and the mixer.rs harness change rides with any merge of 7c22d0d (it does, on ca3-coord). The PC 1 AMD rows taken on the old-id packs stand: the id is not an input to the hash.
|
||||
|
||||
The per-tier cost line of the candidate (item 8, measured): the 5090 -0.2 percent of rate at 350 to 431 W (a rig about 23 percent more electricity), the M5 Max -1.5 percent at 21 to 37 W, pool users nothing, the verifier +0.17 ms per warp; the 9070 XT and the 4070 rows land with the PC 1 jobs. Owed before the cut, besides the blocker: AMD G2 (1,024 nonces on the 9070 XT, queued on PC 1 after the Ember run), the AMD watts (the sampler fix is in; the re-run queued), the 2019-class core (O-1.14; the US laptop's CPU could answer it with a Windows igneum-pow build, a proposal), the G2 found-lines file and digest and the 200 KB report cap (tooling, in hand on ca3-v4-hash).
|
||||
The per-tier cost line of the candidate (item 8, measured): the 5090 -0.2 percent of rate at 350 to 431 W (a rig about 23 percent more electricity), the M5 Max -1.5 percent at 21 to 37 W, pool users nothing, the verifier +0.17 ms per warp; the 9070 XT and the 4070 rows land with the PC 1 jobs. Owed before the cut, besides the blocker: the AMD watts (the sampler fix is in; the re-run queued), the 2019-class core (O-1.14; the US laptop's CPU could answer it with a Windows igneum-pow build, a proposal), the G2 found-lines file and digest and the 200 KB report cap (tooling, in hand on ca3-v4-hash).
|
||||
|
||||
Two preconditions on the cut, from main (17:0x UTC, both from today's incident), assigned to the node lane:
|
||||
|
||||
| # | Precondition | What passes it | State |
|
||||
|---|---|---|---|
|
||||
| P1 | The cut rehearses first on the rented fleet as a staging network (the fleet agent owns the boxes; the node lane supplies the v4 override object and the rehearsal plan `docs/plans/counter-asic-3-rehearsal.md`) | a fleet-only chain crosses the v4 activation with every box switched: zero rejected blocks, blocks on both sides, the G4 id assertion live on every box, one sink, one digest; the stale-box case refused at the handshake with no fork | OPEN; no live-devnet date is named until it has passed |
|
||||
| P2 | No fixed-height activation on the live devnet again: miner signalling for class changes as the v4 seam's activation rule (the block carries the miner's object version; the class flips at the first epoch boundary after 95 percent of mining weight over a window signals the new object, with a floor height after which it flips regardless), PROPOSED spec text in `counter-asic-3-node.md`, implemented behind the override, with the fast-time harness test (67 percent does not flip; 100 percent flips at the next boundary; nobody signals and the floor flips it; each with its failed case) and G6 again on PC 2 | the three harness runs PASS with their failed cases; G6 green on the fork change | OPEN |
|
||||
| P1 | The cut rehearses first on the rented fleet as a staging network (the fleet agent owns the boxes; the node lane supplies the v4 override object and the rehearsal plan `docs/plans/counter-asic-3-rehearsal.md`) | a fleet-only chain crosses the v4 activation with every box switched: zero rejected blocks, blocks on both sides, the G4 id assertion live on every box, one sink, one digest; the stale-box case refused at the handshake with no fork | PLAN WRITTEN (ca3-v4-node f39a8eb, merged): fleet-only chain igneum-devnet-400 from its own genesis state, 12 or more mining boxes, a seed box, one stale 0.3.13 box; the signal flip at DAA 7,200 (the first full 3,600-DAA window) with the floor at 14,400 not reached; ten steps, the report fields and pass rules; the id assertion from the boxes' miner lines. Two objects as files in `docs/plans/counter-asic-3-gate/`: the publish object (the live 13 fields plus `program_class_v4_activation_daa` = N6, the floor, tip + 14,400 rounded up, 219,600 at the 16:57Z DAA of 202,919, and `program_class_v4_signal_window_daa` = 86,400; digest ac8e60ce...) and the rehearsal object (every earlier switch at 0, window 3,600, floor 14,400; digest bc2142b1...); the no-file devnet digest on this binary 7f2e49be... (3c505021 superseded: the window field joined the digest). THE RUN ITSELF IS OPEN: the fleet agent runs it; no live-devnet date until it passes |
|
||||
| P2 | No fixed-height activation on the live devnet again: miner signalling for class changes as the v4 seam's activation rule (the block carries the miner's object version; the class flips at the first epoch boundary after 95 percent of mining weight over a window signals the new object, with a floor height after which it flips regardless), PROPOSED spec text in `counter-asic-3-node.md`, implemented behind the override, with the fast-time harness test (67 percent does not flip; 100 percent flips at the next boundary; nobody signals and the floor flips it; each with its failed case) and G6 again on PC 2 | the three harness runs PASS with their failed cases; G6 green on the fork change | GREEN (ca3-v4-node 0635c04 and 3892035, fork 0562a7f2 on 5f7e0543; PROPOSED text in `counter-asic-3-node.md` section 6: the header version's high byte carries the node's object version, the low byte stays the block version so later objects pass the version check; weight = blue blocks by the finality walk over the window ending at each epoch's seed block; 95 percent = 9,500 bps; window 86,400 DAA in the file and the digest, 0 = off; monotone, memoised per seed block; the fixed height stays as the floor; `IGNEUM_CLASS_SIGNAL` lowers a node's byte on devnet and simnet only; RPC fields 20 to 23). The fast-time gate `infra/fast-time/class-v4-signal.mjs` (three CPU miners, window 120, v3 from 60): two of three signalling PASS with no v4 epoch over epochs 0 to 7 (share 6,166 to 7,583 bps, 0 rejected, one sink 424/424/424); all three PASS with the flip at epoch 3, the first full window, 10,000 bps, 3 of 3 switch lines, 181 / 124 blocks, 0 rejected, one sink, the id assertion on epochs 3 to 5 (e3 e48e6be7c6699824 against the v3 6847355c88e8215f); nobody with the floor at 300 PASS, 0 bps, the flip at epoch 5 by the floor and not before; the failed case (two of three told to expect a flip) FAIL on eight checks. G6 on the signalling fork: three PC 2 jobs under the lock, prover on, app untouched (build-20261006-173017 hit 2.0's flake, 97 of 98; build-20261006-174027 kaspa-consensus alone exit 0; build-20261006-174823 the five crates exit 0 in 35 s with consensus-core 110 + 7, igneum-exec 18, kaspa-pow 15 with the v4 engine and the signal-rule tests, igneum-miner 18, p2p-flows 33, and the app 112 + 26 + 8 exit 0; its closing line "upload incomplete" is the relay blob fault after both test stages closed exit 0). Owed and named: a class-signal witness in the pruning-proof format (a proof-synced node falls back to the floor rule for epochs whose window reaches below its pruning point and logs it), the same class as the era witness |
|
||||
|
||||
Facts for the cut from the node lane (docs/plans/counter-asic-3-node.md): the devnet digest moves (c562d70e... to 3c505021...), so the cut is a one-sweep binary rollout and a 0.3.13 node is refused at the handshake afterwards (intended, fleet-wide); a 0.3.13 miner reads the v4 height as never (an optional proto field), so miners and nodes move together; `infra/fast-time/override-60x.json` as committed carried the proving-v1 block twice and lacked four 0.3.12 and 0.3.13 fields (the node refused the file; fixed, with a new CI check `override-json-check.sh`); the 48 GiB target clone `vendor/igneum-node-ca3v4/target-ca3v4` can go after the cut.
|
||||
|
||||
THE ONE LINE FOR [user]: class v4 (`mx8+sh256x27`, 100,000 ops per hash in the latency shadow) has passed every gate on the fixed tree (the program-id blocker closed with its own failed case) and is ready for a whole-fleet one-sweep cut (the digest moves) once two gates pass: P1, the rehearsal on the rented fleet as a staging network, and P2, miner-signalled activation (95 percent of mining weight over a window, with a floor height) in place of a fixed height; no date until 0.3.14 has run a clean day; its cost is the 5090 at 431 W instead of 350 for 0.2 percent less rate (a rig pays about 23 percent more electricity), the M5 Max at 37 W instead of 21 for 1.5 percent, the 9070 XT no rate at all (its watts pending), the verifier +0.27 ms per warp; what it buys is the stored-dataset chip's per-joule edge over the 5090 falling from 5.6x to 2.1x at a chip core equal to the GPU's; proposed for a day when no other cut is in flight, not tonight (0.3.14 and the fleet night come first).
|
||||
THE ONE LINE FOR [user]: class v4 (`mx8+sh256x27`, 100,000 ops per hash in the latency shadow) has passed every gate on the fixed tree (the program-id blocker closed with its own failed case) and is ready for a whole-fleet one-sweep cut (the digest moves to ac8e60ce... at the floor N6) once P1 passes: P2, miner-signalled activation (95 percent of blue-block weight over a one-day window, the floor height after which it flips regardless), is designed, implemented and GREEN on the fast-time gate with its failed case and on G6; P1, the rehearsal on the rented fleet as a staging network, has its plan and objects written and waits for the fleet agent's run; no date until 0.3.14 has run a clean day; its cost is the 5090 at 431 W instead of 350 for 0.2 percent less rate (a rig pays about 23 percent more electricity), the M5 Max at 37 W instead of 21 for 1.5 percent, the 9070 XT no rate at all (its watts pending), the verifier +0.27 ms per warp; what it buys is the stored-dataset chip's per-joule edge over the 5090 falling from 5.6x to 2.1x at a chip core equal to the GPU's; proposed for a day when no other cut is in flight, not tonight (0.3.14 and the fleet night come first).
|
||||
|
||||
## 6. Decisions for the project lead
|
||||
|
||||
|
|
@ -239,6 +260,22 @@ Either replaces "under 2x" on the site and in the litepaper once the project lea
|
|||
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).
|
||||
|
||||
### 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 [user] (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)
|
||||
|
||||
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).
|
||||
|
||||
## 7. Unverified and owed
|
||||
|
||||
| Item | Owed | Why |
|
||||
|
|
@ -250,6 +287,10 @@ Either replaces "under 2x" on the site and in the litepaper once the project lea
|
|||
| 4 + 7 | the live observer restart with both hooks, and the first live detector and vendor-share rows | needs a push to master (the project lead's word) |
|
||||
| 2 | the 2019-class core (O-1.14), which decides dr736 against dr368; the once-a-day NVRTC module for the item function (required before any activation); cryptanalysis of random ARX programs; the loaded-iGPU tier's build with the day program; the 5090 absolutes re-run with the card quiet (ratios stand) | unmeasured; unimplemented |
|
||||
| 8 | the 9070 XT and 4060-class rows (where a small card binds); the 5090 clock rows (an elevated job); the 5090 at a 575 W cap (model only); the Mac package watts (IOReport gives GPU + DRAM, Ember's 38 W approximate); the chip side's k, lane area and 28 nm scaling; the Metal fuzz, edge and stats runs and gates G2, G4 to G6 on the class; the acceptance rule's reading of the block | PC 1 not released; no elevated job; the gates are the next step on the project lead's word |
|
||||
| 6 | mm8 as a chain on Apple (Metal 4 matmul2d; the Mac's Swift toolchain has no tensor API); the 5 percent rule per family with the family live (argued only); the RDNA ISA guides unread (mnemonics from LLVM's tables) | |
|
||||
| 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 | |
|
||||
| all | every AMD RDNA 4 number in this run | PC 1 is the project lead'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 |
|
||||
| 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 |
|
||||
| 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: the project lead, 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 |
|
||||
| 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 the project lead 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 |
|
||||
|
|
@ -18,7 +18,7 @@ Placed nowhere, with the reasons in the history document's section 4.3: per-hash
|
|||
|
||||
## Decisions for the project lead 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
|
||||
|
||||
|
|
|
|||
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. the project lead: "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, the project lead 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; the project lead, 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 the project lead 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 the project lead 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 the project lead'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 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)
|
||||
|
||||
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 |
|
||||
| 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 |
|
||||
| 12 GB (RTX 3060, 4070) | nothing: the floor is 13.9 GB, and the shipped server refuses the card outright | nothing | off; the project lead'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) |
|
||||
| 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) | 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 the project lead 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. the project lead'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 |
|
||||
| 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.
|
||||
|
||||
## Segment-aligned proving (6 October 2026, the project lead: "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: the project lead'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)
|
||||
|
||||
the project lead, 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 the project lead'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 the project lead 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; the project lead
|
||||
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-labs 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). the project lead'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 the project lead'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) | the project lead'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 the project lead 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 the project lead, 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 (the project lead'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 the project lead'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
|
||||
the project lead 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 the project lead'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 the project lead'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-labs 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 (the project lead, 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. the project lead 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 the project lead'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 (the project lead 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 (the project lead'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 the project lead's order), PC 2 0.3.12 at 115 MH/s, PC 1 0.3.12 with the project lead 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 the project lead
|
||||
|
||||
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 the project lead's word (16:4x UTC). For r/CryptoCurrency or r/gpumining, posted by the project lead 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.
|
||||
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
|
||||
# node 1, under caffeinate as it runs today
|
||||
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 &
|
||||
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
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ Time parameters, divided by 60 (devnet value, 60x value):
|
|||
| `proving_v1_unproven_daa` | 600 | 10 | a DAA clock (the unproven allowance), divided by 60; the proving agent confirms the value |
|
||||
| `program_class_v3_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v3 (Counter ASIC 2.0, 5 Oct 2026) from the first EPOCH whose start is at or above this DAA score (rounded up to an epoch boundary: at 60 DAA per epoch, 150 means epoch 3 at DAA 180); `infra/fast-time/class-v3.mjs` sets it a few epochs ahead in its merged file |
|
||||
| `program_class_v4_activation_daa` | never | never | a height, not a clock: the lottery hash draws programs from class v4 (Counter ASIC 3.0, 6 Oct 2026: class v3 plus the latency-shadow block) from the first EPOCH whose start is at or above this DAA score, rounded up like the v3 switch; `infra/fast-time/class-v4.mjs` sets it a few epochs ahead in its merged file |
|
||||
| `program_class_v4_signal_window_daa` | 86,400 | 120 | a DAA window (one day of blocks), divided by 60 and rounded to two epochs: the class v4 signal tally (PROPOSED, `docs/plans/counter-asic-3-node.md` section 6) over the blue blocks below each epoch's seed block; 0 = off; `infra/fast-time/class-v4-signal.mjs` is its gate |
|
||||
| `proving_v1_fresh_rule_daa`, `exec_restart_number`, `exec_restart_trust_daa`, `exec_restart_hash` | never, never, never, "" | the same | heights and a hash, not clocks (the 0.3.12 and 0.3.13 switches); present so the fork's every-field test (`fast_time_60x_file_is_the_devnet_at_60x`) holds; added 6 Oct 2026 with the class v4 field |
|
||||
|
||||
Unchanged, and why:
|
||||
|
|
|
|||
274
infra/fast-time/class-v4-signal.mjs
Normal file
274
infra/fast-time/class-v4-signal.mjs
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
#!/usr/bin/env node
|
||||
// Counter ASIC 3.0, miner-signalled class activation (PROPOSED, docs/plans/counter-asic-3-node.md section 6): the
|
||||
// fast-time 3-node network where each node signals an object version of its own (IGNEUM_CLASS_SIGNAL, devnet only),
|
||||
// so the class v4 decision is made by the miners' blue-block share over the signal window, not by a fixed height.
|
||||
// Ports 29690 and up, network igneum-devnet-969, data /tmp/igneum-fast-time-v4s; the class-v4.mjs shape otherwise:
|
||||
// override-60x.json with CPU genesis bits, v3 from --v3-activation (default 60, epoch 1), the v4 floor at --floor
|
||||
// (default never), the window at --window (default 120 DAA, two epochs; the first epoch whose seed block has DAA >= 120
|
||||
// is epoch 3, seed at DAA 169), one real CPU miner per node.
|
||||
//
|
||||
// The three cases and the known-failed case:
|
||||
// --signal 4,4,3 --expect no-flip two of three miners signal: 67 percent, the class must stay v3 (run 7 epochs)
|
||||
// --signal 4,4,4 --expect flip all three: it flips at epoch 3, the first boundary with a full window
|
||||
// --signal 3,3,3 --floor 300 --expect floor nobody signals: it flips at the floor (epoch 5, DAA 300) and not before
|
||||
// --signal 4,4,3 --expect flip the known-failed case: the harness must report FAIL (no flip happened)
|
||||
//
|
||||
// node infra/fast-time/class-v4-signal.mjs --signal a,b,c --expect flip|no-flip|floor [--floor <daa>|never]
|
||||
// [--window 120] [--v3-activation 60] [--secs 480] [--epochs 7]
|
||||
// IGNEUMD, IGNEUM_MINER, IGNEUM_POW name the binaries (defaults: the ca3 fork worktree's target-ca3v4/release and
|
||||
// igneum-pow/target/release/igneum-pow).
|
||||
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
import { devAddress } from '../../tools/harness/lib/address.mjs';
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
||||
const BIN = process.env.IGNEUM_CA3_BIN || `${ROOT}vendor/igneum-node-ca3v4/target-ca3v4/release`;
|
||||
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
||||
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
||||
const IGNEUM_POW = process.env.IGNEUM_POW || `${ROOT}igneum-pow/target/release/igneum-pow`;
|
||||
const TMP = '/tmp/igneum-fast-time-v4s';
|
||||
const BASE = 29690, SUFFIX = 969;
|
||||
const NEVER = '18446744073709551615';
|
||||
const args = process.argv.slice(2);
|
||||
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? +args[i + 1] : dflt; };
|
||||
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
||||
const actflag = (name, dflt) => { const v = sflag(name); if (v == null) return dflt; return v === 'never' ? null : +v; };
|
||||
const GENESIS_BITS = flag('genesis-bits', 0x1f010000);
|
||||
const SECS = flag('secs', 480);
|
||||
const EPOCHS = flag('epochs', 7);
|
||||
const FLOOR = actflag('floor', null);
|
||||
const V3_ACTIVATION = actflag('v3-activation', 60);
|
||||
const WINDOW = flag('window', 120);
|
||||
const SIGNAL = (sflag('signal') || '4,4,4').split(',').map(Number);
|
||||
const EXPECT = sflag('expect') || 'flip';
|
||||
if (!['flip', 'no-flip', 'floor'].includes(EXPECT) || SIGNAL.length !== 3) { console.error('usage: --signal a,b,c --expect flip|no-flip|floor'); process.exit(2); }
|
||||
const started = [];
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
const baseText = readFileSync(FILE, 'utf8');
|
||||
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
|
||||
const EPOCH = field('pow_epoch_blocks');
|
||||
const LEAD = field('pow_epoch_lead');
|
||||
const DAY_MS = field('pow_day_ms');
|
||||
const FIRST_V3_EPOCH = V3_ACTIVATION == null ? null : Math.ceil(V3_ACTIVATION / EPOCH);
|
||||
const FLOOR_EPOCH = FLOOR == null ? null : Math.ceil(FLOOR / EPOCH);
|
||||
// the first epoch whose seed block (the last chain block below L*e - lead) can have DAA >= WINDOW: L*e - lead - 1 >= WINDOW
|
||||
let FIRST_FULL_EPOCH = 0;
|
||||
while (FIRST_FULL_EPOCH * EPOCH - LEAD - 1 < WINDOW) FIRST_FULL_EPOCH++;
|
||||
export function mergeOverrideText(text, fields) {
|
||||
let out = text;
|
||||
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
||||
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
|
||||
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
||||
}
|
||||
const asText = (v) => v == null ? NEVER : String(v);
|
||||
const override = `${TMP}/override.json`;
|
||||
writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: GENESIS_BITS, skip_proof_of_work: false, program_class_v3_activation_daa: asText(V3_ACTIVATION), program_class_v4_activation_daa: asText(FLOOR), program_class_v4_signal_window_daa: String(WINDOW) }));
|
||||
log(`signals ${SIGNAL.join('/')}, expect ${EXPECT}; v3 from ${V3_ACTIVATION ?? 'never'} (epoch ${FIRST_V3_EPOCH ?? 'none'}), v4 floor ${FLOOR ?? 'never'} (epoch ${FLOOR_EPOCH ?? 'none'}), window ${WINDOW} DAA (the first epoch with a full window is ${FIRST_FULL_EPOCH}); ${EPOCH} DAA per epoch, lead ${LEAD}; run ${SECS} s or ${EPOCHS} epochs`);
|
||||
|
||||
class Node {
|
||||
constructor(i, connect = []) {
|
||||
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
||||
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
||||
}
|
||||
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex',
|
||||
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`,
|
||||
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect.length) a.push(`--connect=${this.connect.join(',')}`); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
// the node's own object byte: what its templates signal
|
||||
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_CLASS_SIGNAL: String(SIGNAL[this.i]) } });
|
||||
started.push(this.proc);
|
||||
await sleep(1200);
|
||||
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
|
||||
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}, signals ${SIGNAL[this.i]}`);
|
||||
return this;
|
||||
}
|
||||
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||
}
|
||||
function miner(bin, argv, name, env = {}) {
|
||||
const out = openSync(`${TMP}/${name}.log`, 'a');
|
||||
const p = spawn(bin, argv, { stdio: ['ignore', out, out], env: { ...process.env, ...env } });
|
||||
started.push(p);
|
||||
return p;
|
||||
}
|
||||
async function stopAll() {
|
||||
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
||||
await sleep(1500);
|
||||
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
||||
}
|
||||
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
||||
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||
const minerLog = (i) => { try { return readFileSync(`${TMP}/cpu${i}.log`, 'utf8').split('\n'); } catch { return []; } };
|
||||
const SIGNAL_LINE = /Program class v4 by miner signal: epoch (\d+) \(share (\d+) bps/;
|
||||
const FLOOR_LINE = /Program class v4 from the override file/;
|
||||
const WINDOW_LINE = /Program class v4 signal window from the override file/;
|
||||
const OWN_LINE = /Program class signal from IGNEUM_CLASS_SIGNAL: this node signals object version (\d+)/;
|
||||
|
||||
const t0 = Date.now();
|
||||
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
||||
const n0 = await new Node(0).start();
|
||||
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`]).start();
|
||||
const nodes = [n0, n1, n2];
|
||||
for (const n of nodes) log(`n${n.i}: ${n.grepLog(WINDOW_LINE).map(l => l.replace(/^.*?(Program class v4 signal window)/, '$1'))[0] || '(no window line)'} | ${n.grepLog(OWN_LINE).map(l => l.replace(/^.*?(this node signals)/, '$1'))[0] || '(no signal line)'}`);
|
||||
log(`n0 digest: ${n0.grepLog(/Consensus params digest/).map(l => l.replace(/^.*?digest: /, '').slice(0, 16)).join(' ')}`);
|
||||
nodes.forEach((n, i) => miner(CPU_MINER, ['mine', n.grpc, '1', String(SECS), `cpu${i}`, '--engine', 'igneum-pow', '--payout-label', `cpu${i}`, '--status-secs', '30', '--no-vote'], `cpu${i}`, { IGNEUM_POW_DAY_MS: String(DAY_MS) }));
|
||||
const pay = devAddress('fast-time-v4s');
|
||||
|
||||
const epochs = new Map();
|
||||
let firstV4 = null, lastEpoch = -1, lastReport = 0, lastDaa = 0, endAt = null;
|
||||
const samples = [];
|
||||
while (Date.now() - t0 < SECS * 1000) {
|
||||
await sleep(1000);
|
||||
let daa = null, epoch = null, cls = null, nextCls = null, eraSeed = null, bps = null, win = null, sig = null, sigEpoch = null;
|
||||
try {
|
||||
const t = await n0.rpc.call('getBlockTemplate', { payAddress: pay, extraData: [] });
|
||||
const pe = t.powEpoch || t.pow_epoch || {};
|
||||
daa = pe.virtualDaaScore ?? t.block?.header?.daaScore; epoch = pe.epochIndex; cls = pe.programClass; nextCls = pe.nextProgramClass;
|
||||
eraSeed = pe.eraSeed; bps = pe.programClassV4SignalBps; win = pe.programClassV4SignalWindowDaa; sig = pe.programClassSignal; sigEpoch = pe.programClassV4SignalEpoch;
|
||||
} catch (e) { log(`template: ${e.message}`); }
|
||||
if (epoch != null && epoch !== lastEpoch) {
|
||||
epochs.set(epoch, { class: cls, firstSeenDaa: daa, at: +since(), eraSeed: eraSeed == null ? null : String(eraSeed), bps, signal_epoch: sigEpoch ?? null });
|
||||
log(`epoch ${lastEpoch} -> ${epoch} at daa ${daa}, ${since()} s: template class ${cls}, next ${nextCls}, signal share at the sink ${bps} bps (window ${win}, this node signals ${sig}, decided by signal at epoch ${sigEpoch ?? 'none'})`);
|
||||
if (firstV4 == null && cls === 4) { firstV4 = { epoch, daa, at: +since() }; log(`CLASS SWITCH: the template is class v4 from epoch ${epoch} (daa ${daa}) at ${since()} s wall`); }
|
||||
lastEpoch = epoch;
|
||||
}
|
||||
lastDaa = daa ?? lastDaa;
|
||||
if (Date.now() - lastReport > 15000) {
|
||||
lastReport = Date.now();
|
||||
const counts = await Promise.all(nodes.map(async n => { try { const d = await n.rpc.call('getBlockDagInfo'); return `${d.blockCount}/${String(d.sink).slice(0, 8)}`; } catch { return '?'; } }));
|
||||
log(`t=${since()} s daa ${daa} epoch ${epoch} class ${cls} signal ${bps} bps blocks/sink per node ${counts.join(' ')}`);
|
||||
samples.push({ t: +since(), daa, epoch, class: cls, bps, nodes: counts });
|
||||
}
|
||||
// the end: two epochs after a flip, or --epochs epochs when no flip is expected
|
||||
if (firstV4 != null && daa != null && daa >= (firstV4.epoch + 2) * EPOCH) { endAt = +since(); break; }
|
||||
if (firstV4 == null && daa != null && daa >= EPOCHS * EPOCH) { endAt = +since(); break; }
|
||||
}
|
||||
await sleep(3000);
|
||||
|
||||
const dag = await Promise.all(nodes.map(async n => { try { return await n.rpc.call('getBlockDagInfo'); } catch (e) { return { error: e.message }; } }));
|
||||
const genesis = dag[0].pruningPointHash;
|
||||
async function allBlocks(n) {
|
||||
const out = []; let low = genesis; const seen = new Set();
|
||||
for (let round = 0; round < 500; round++) {
|
||||
const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false });
|
||||
const blocks = r.blocks || [];
|
||||
let added = 0;
|
||||
for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, chain: !!b.verboseData?.isChainBlock }); added++; }
|
||||
if (!blocks.length || added === 0) break;
|
||||
low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let blocks = [];
|
||||
try { blocks = await allBlocks(n0); } catch (e) { log(`getBlocks: ${e.message}`); }
|
||||
const BOUNDARY = firstV4 ? firstV4.epoch * EPOCH : Infinity;
|
||||
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
|
||||
// the signal bytes on the chain: the share of blocks whose version high byte is 4
|
||||
const versionBytes = blocks.reduce((m, b) => { const v = b.version >> 8; m[v] = (m[v] || 0) + 1; return m; }, {});
|
||||
const signalShareOnChain = blocks.length ? Math.round(10000 * (blocks.filter(b => (b.version >> 8) >= 4).length) / blocks.length) : 0;
|
||||
|
||||
const programs = new Map();
|
||||
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {
|
||||
const m = /epoch seed ([0-9a-f]{64}) day (\d+) \(daa (\d+)\): program and 256 MiB cache ready in ([\d.]+) ms; class (v\d) program id ([0-9a-f]{16})/.exec(l);
|
||||
if (!m) continue;
|
||||
const k = m[1]; const e = programs.get(k) || { seed: k.slice(0, 16), epoch: Math.floor(+m[3] / EPOCH), class: m[5], id: m[6], miners: new Set() };
|
||||
if (e.id !== m[6] || e.class !== m[5]) e.disagree = true;
|
||||
e.miners.add(i); programs.set(k, e);
|
||||
}
|
||||
const programRows = [...programs.values()].sort((a, b) => a.epoch - b.epoch).map(p => ({ epoch: p.epoch, class: p.class, program_id: p.id, seed: p.seed, miners: p.miners.size, disagree: !!p.disagree }));
|
||||
function cliId(seedHex, eraHex, cls) {
|
||||
if (!existsSync(IGNEUM_POW)) return null;
|
||||
const r = spawnSync(IGNEUM_POW, ['show', '--epoch-hex', seedHex, '--program-class', cls, '--era-hex', eraHex], { encoding: 'utf8' });
|
||||
const m = /program id ([0-9a-f]{16})/.exec(r.stdout || '');
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
const idRows = [];
|
||||
for (const [k, e] of programs) {
|
||||
if (e.class !== 'v4') continue;
|
||||
const era = epochs.get(e.epoch)?.eraSeed;
|
||||
idRows.push({ epoch: e.epoch, seed: e.seed, miners_id: e.id, miners: e.miners.size, cli_v3: era ? cliId(k, era, 'v3') : null, cli_v4: era ? cliId(k, era, 'v4') : null });
|
||||
}
|
||||
const accepted = [0, 1, 2].map(i => minerLog(i).filter(l => /ACCEPTED block/.test(l)).length);
|
||||
const rejectedMiner = [0, 1, 2].map(i => minerLog(i).filter(l => /rejected nonce=|submit error/.test(l)));
|
||||
const rejectedNode = nodes.map(n => n.grepLog(/PoW rejected|Rejected block|rejected block/i));
|
||||
const signalLines = nodes.map(n => n.grepLog(SIGNAL_LINE).map(l => l.replace(/^.*?(Program class v4 by miner signal)/, '$1'))[0] || null);
|
||||
const signalEpochs = signalLines.map(l => { const m = l && SIGNAL_LINE.exec(l); return m ? +m[1] : null; });
|
||||
const signalShares = signalLines.map(l => { const m = l && SIGNAL_LINE.exec(l); return m ? +m[2] : null; });
|
||||
const floorLines = nodes.map(n => n.grepLog(FLOOR_LINE).map(l => l.replace(/^.*?(Program class v4 from)/, '$1'))[0] || null);
|
||||
const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));
|
||||
const counts = dag.map(d => d.blockCount ?? '?');
|
||||
const maxEpochSeen = Math.max(-1, ...epochs.keys());
|
||||
const classesSeen = [...epochs.values()].map(e => e.class);
|
||||
|
||||
const common = {
|
||||
zero_rejected_by_miners: rejectedMiner.every(r => r.length === 0),
|
||||
zero_rejected_by_nodes: rejectedNode.every(r => r.length === 0),
|
||||
sinks_agree: new Set(sinks).size === 1,
|
||||
block_counts_agree: new Set(counts.map(String)).size === 1,
|
||||
miners_agree_on_every_program: programRows.every(p => !p.disagree),
|
||||
window_line_on_every_node: nodes.every(n => n.grepLog(WINDOW_LINE).length > 0),
|
||||
every_node_signals_its_byte: nodes.every((n, i) => n.grepLog(OWN_LINE).some(l => +OWN_LINE.exec(l)[1] === SIGNAL[i])),
|
||||
// every block's byte is one of the three nodes' (genesis, made before any node, is the one byte-0 block)
|
||||
chain_carries_the_bytes: blocks.length > 0 && Object.keys(versionBytes).every(v => SIGNAL.includes(+v) || (+v === 0 && versionBytes[v] === 1)),
|
||||
};
|
||||
let checks;
|
||||
if (EXPECT === 'flip') {
|
||||
checks = {
|
||||
...common,
|
||||
template_switched_to_v4: firstV4 != null,
|
||||
switched_at_the_first_full_window_epoch: firstV4 != null && firstV4.epoch === FIRST_FULL_EPOCH,
|
||||
switched_before_the_floor: firstV4 != null && (FLOOR_EPOCH == null || firstV4.epoch < FLOOR_EPOCH),
|
||||
signal_line_on_every_node_same_epoch: signalEpochs.every(e => e != null) && new Set(signalEpochs).size === 1 && signalEpochs[0] === (firstV4 && firstV4.epoch),
|
||||
signal_share_at_or_above_threshold: signalShares.every(s => s != null && s >= 9500),
|
||||
blocks_on_both_sides: before.length > 0 && after.length > 0,
|
||||
v4_ids_equal_the_cli_v4_id: idRows.length > 0 && idRows.every(r => r.cli_v4 != null && r.cli_v4 === r.miners_id && r.miners === 3),
|
||||
v4_ids_differ_from_the_same_seed_v3_id: idRows.length > 0 && idRows.every(r => r.cli_v3 != null && r.cli_v3 !== r.miners_id),
|
||||
};
|
||||
} else if (EXPECT === 'no-flip') {
|
||||
checks = {
|
||||
...common,
|
||||
template_never_v4: firstV4 == null && !classesSeen.includes(4),
|
||||
no_signal_line_on_any_node: signalLines.every(l => l == null),
|
||||
ran_the_epochs: maxEpochSeen >= EPOCHS - 1,
|
||||
v3_programs_seen: programRows.some(p => p.class === 'v3'),
|
||||
signal_share_under_threshold_on_chain: signalShareOnChain < 9500,
|
||||
};
|
||||
} else {
|
||||
checks = {
|
||||
...common,
|
||||
template_switched_to_v4: firstV4 != null,
|
||||
switched_at_the_floor_epoch: firstV4 != null && firstV4.epoch === FLOOR_EPOCH,
|
||||
no_signal_line_on_any_node: signalLines.every(l => l == null),
|
||||
floor_line_names_the_floor_epoch: floorLines.every(l => l && l.includes(`active from epoch ${FLOOR_EPOCH} `)),
|
||||
blocks_on_both_sides: before.length > 0 && after.length > 0,
|
||||
v4_ids_equal_the_cli_v4_id: idRows.length > 0 && idRows.every(r => r.cli_v4 != null && r.cli_v4 === r.miners_id && r.miners === 3),
|
||||
};
|
||||
}
|
||||
const pass = Object.values(checks).every(Boolean);
|
||||
const summary = {
|
||||
pass, expect: EXPECT, signals: SIGNAL, checks, window: WINDOW, floor: FLOOR ?? 'never', v3_activation: V3_ACTIVATION ?? 'never', epoch_blocks: EPOCH, lead: LEAD, first_full_window_epoch: FIRST_FULL_EPOCH, floor_epoch: FLOOR_EPOCH,
|
||||
node: IGNEUMD, miner: CPU_MINER, template_switch: firstV4, run_ended_at_s: endAt, final_daa: lastDaa, max_epoch_seen: maxEpochSeen,
|
||||
epochs: Object.fromEntries([...epochs.entries()].map(([k, v]) => [k, v])),
|
||||
blocks: { total: blocks.length, before_boundary: before.length, after_boundary: after.length, version_bytes: versionBytes, signal_share_bps_on_chain: signalShareOnChain },
|
||||
programs: programRows, program_id_rows: idRows, accepted_per_miner: accepted,
|
||||
rejected_by_miners: rejectedMiner.map(r => r.length), rejected_by_nodes: rejectedNode.map(r => r.length),
|
||||
sinks, block_counts: counts, signal_lines: signalLines, floor_lines: floorLines, samples,
|
||||
};
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify(summary, null, 2));
|
||||
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'} (expect ${EXPECT}, signals ${SIGNAL.join('/')}): ${firstV4 ? `v4 from epoch ${firstV4.epoch} at DAA ${firstV4.daa}` : 'no v4 epoch'}; epochs seen ${[...epochs.entries()].map(([e, v]) => `e${e}:${v.class}:${v.bps}bps`).join(' ')}; chain bytes ${JSON.stringify(versionBytes)} (${signalShareOnChain} bps signal v4); blocks ${before.length} / ${after.length}; rejected miners ${rejectedMiner.map(r => r.length).join('/')} nodes ${rejectedNode.map(r => r.length).join('/')}; sinks ${sinks.join(' ')} (${checks.sinks_agree ? 'agree' : 'DIFFER'}); counts ${counts.join('/')}; signal lines ${signalLines.filter(Boolean).length}/3 (epochs ${signalEpochs.join('/')}, shares ${signalShares.join('/')}); floor lines ${floorLines.filter(Boolean).length}/3`);
|
||||
for (const r of idRows) log(`PROGRAM ID epoch ${r.epoch} seed ${r.seed}: miners ${r.miners_id} (${r.miners} of 3) cli v4 ${r.cli_v4} cli v3 ${r.cli_v3}`);
|
||||
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
|
||||
log(`summary: ${TMP}/summary.json`);
|
||||
await stopAll();
|
||||
process.exit(pass ? 0 : 1);
|
||||
|
|
@ -56,6 +56,7 @@
|
|||
"finality_v3_activation_daa": 18446744073709551615,
|
||||
"program_class_v3_activation_daa": 18446744073709551615,
|
||||
"program_class_v4_activation_daa": 18446744073709551615,
|
||||
"program_class_v4_signal_window_daa": 120,
|
||||
"proving_v1_fresh_rule_daa": 18446744073709551615,
|
||||
"exec_restart_number": 18446744073709551615,
|
||||
"exec_restart_hash": "",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
||||
| 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 |
|
||||
| Pool URL | `grpc://<your node>:26610` (your own igneumd, solo mining), or `local` to run the bundled node on the rig |
|
||||
| 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
|
||||
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 |
|
||||
|---|---|
|
||||
| `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-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-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` (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 |
|
||||
|
||||
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
|
||||
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.
|
||||
- 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.
|
||||
|
||||
## 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);
|
||||
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.
|
||||
- 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:
|
||||
# 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.
|
||||
# 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)
|
||||
# VOTE=1 sign finality checkpoints (0 = mine without voting)
|
||||
# PEERS=a:26611,b:26611 peers for the bundled node when CUSTOM_URL=local
|
||||
# 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)
|
||||
[[ -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
|
||||
|
||||
# defaults, then the user's KEY=VALUE lines
|
||||
DEV_FEE=1; IDENTITIES=8; WORKER=auto; VOTE=1; PEERS=""; EXTRA=""
|
||||
while read -r kv; do
|
||||
[[ -z "$kv" || "$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' "$CUSTOM_USER_CONFIG" | tr ' ' '\n' | sed 's/^"//; s/"$//')
|
||||
DEV_FEE=1; IDENTITIES=auto; WORKER=auto; VOTE=1; PEERS=""; EXTRA=""; OVERRIDE=""
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
if [[ "$line" == OVERRIDE=* ]]; then # the whole line: JSON may carry spaces; quotes around it are stripped
|
||||
val="${line#OVERRIDE=}"; val="${val#\'}"; val="${val%\'}"; val="${val#\"}"; val="${val%\"}"
|
||||
OVERRIDE="$val"; continue
|
||||
fi
|
||||
while read -r kv; do
|
||||
[[ -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
|
||||
[[ "$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" == "local" ]] && url="local"
|
||||
[[ "$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")"
|
||||
cat > "$CUSTOM_CONFIG_FILENAME" <<CONF
|
||||
# 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
|
||||
DEV_FEE=$DEV_FEE
|
||||
IDENTITIES=$IDENTITIES
|
||||
WORKER=$WORKER
|
||||
${ident_block}WORKER=$WORKER
|
||||
VOTE=$VOTE
|
||||
PEERS=$PEERS
|
||||
EXTRA=$EXTRA
|
||||
EXTRA=$(sq "$EXTRA")
|
||||
OVERRIDE=$(sq "$OVERRIDE")
|
||||
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=()
|
||||
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)
|
||||
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[@]}" \
|
||||
--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+=($!)
|
||||
fi
|
||||
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
|
||||
sleep 2
|
||||
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
|
||||
nv=0; amd=0
|
||||
|
|
@ -59,7 +77,8 @@ run_gpu() {
|
|||
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"
|
||||
--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)
|
||||
[[ "$DEV_FEE" != "1" ]] && args+=(--dev-fee "$DEV_FEE")
|
||||
[[ -n "$EXTRA" ]] && args+=($EXTRA)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ case "$1" in
|
|||
esac
|
||||
FAKE
|
||||
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"
|
||||
printf '#!/bin/sh\n[ "$1" = NVIDIA ] && echo 2 || echo 0\n' > "$T/bin/gpu-detect"
|
||||
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; }
|
||||
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"
|
||||
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)"
|
||||
( cd "$M" && CUSTOM_CONFIG_FILENAME="$M/igneum.conf" CUSTOM_LOG_BASENAME="$T/log/igneum" ./h-run.sh > "$T/log/run.out" 2>&1 ) &
|
||||
disown
|
||||
|
|
@ -56,9 +85,21 @@ sleep 8
|
|||
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; }
|
||||
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; }
|
||||
[[ -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)"
|
||||
( . "$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)"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STAMP</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.11</string>
|
||||
<string>0.3.13</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<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:
|
||||
# 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}'
|
||||
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
|
||||
# exists, else <base>; IGNEUM_CONFIG_DIR (tests) replaces ~/.config/igneum
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#define ArtDir "..\..\brand\icons"
|
||||
#endif
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "0.3.11"
|
||||
#define AppVersion "0.3.13"
|
||||
#endif
|
||||
#define AppName "Igneum Miner"
|
||||
#define Publisher "Igneum"
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ done
|
|||
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
|
||||
found_workers=0
|
||||
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; }
|
||||
done
|
||||
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"
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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_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
|
||||
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)"
|
||||
|
|
@ -108,9 +109,9 @@ rsync -a --exclude 'target' --exclude 'target-*' --exclude '.DS_Store' "$ROOT/ig
|
|||
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/"
|
||||
|
||||
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
|
||||
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 []
|
||||
tests = []
|
||||
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()})
|
||||
m = {
|
||||
"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"},
|
||||
"app_version": av if with_app == "1" else "",
|
||||
"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
|
||||
NVRTC_DIR="$ROOT/proto-cuda/nvrtc"
|
||||
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"
|
||||
DLSITE="${IGNEUM_DLSITE:-}"
|
||||
[ -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)"
|
||||
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 "$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)
|
||||
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
|
||||
[ -f "$ICONS/igneum.ico" ] || { echo "== no $ICONS/igneum.ico, making the icons"; python3 "$ICONS/make-icons.py"; }
|
||||
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"
|
||||
done
|
||||
|
||||
|
|
@ -37,11 +37,16 @@ echo "== igneum-worker-opencl.exe"
|
|||
-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"
|
||||
"$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"
|
||||
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")"
|
||||
x86_64-w64-mingw32-objdump -p "$exe" | sed -n 's/^[[:space:]]*DLL Name: //p' | tr '\n' ' '
|
||||
echo
|
||||
done
|
||||
# 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).
|
||||
// the project lead's rule (4 October 2026): every shipped exe carries the coin icon and a version block, like the Mac app and DMG.
|
||||
#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
|
||||
# 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.
|
||||
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"
|
||||
# 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
|
||||
echo "$sha" > ".cross-build-sha-$CARGO_TARGET_DIR"
|
||||
for exe in igneumd igneum-miner; do
|
||||
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' ' ')"
|
||||
[ "$exe" = igneumd ] && "$ROOT/tools/ci/commit-string-check.sh" "$f" "$sha" # only kaspad depends on kaspa-build-info
|
||||
done
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ proto-opencl/
|
|||
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_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.bat Windows (MSVC cl.exe + OpenCL.lib)
|
||||
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 the project lead 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
|
||||
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();
|
||||
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);
|
||||
// `--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
|
||||
|
|
@ -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 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 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 {
|
||||
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
|
||||
/// 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.
|
||||
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() {
|
||||
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);
|
||||
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 chain_t = Instant::now();
|
||||
let mut prev: Option<proof_system::Sp1SegmentProof> = None;
|
||||
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 out_dir = out_dir_of(out_path);
|
||||
let mut shard_files: Vec<String> = Vec::new();
|
||||
for (number, _hash, shards, _) in &built {
|
||||
let block_t = Instant::now();
|
||||
let mut proofs: Vec<Sp1ShardProof> = Vec::with_capacity(shards.len());
|
||||
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 {
|
||||
let i = s.output.shard_index;
|
||||
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_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);
|
||||
}
|
||||
shards_total += proofs.len();
|
||||
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 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;
|
||||
let claim = SegmentClaim::from_block(&seg.output);
|
||||
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 cumulative = chain_t.elapsed().as_secs_f64();
|
||||
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,
|
||||
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" },
|
||||
seg.output.chain_len,
|
||||
seg.output.agg_vk,
|
||||
|
|
@ -702,19 +742,21 @@ fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_
|
|||
if !ok {
|
||||
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 {
|
||||
bail!("block {number}: chain_len {} is not {expected_len}", seg.output.chain_len);
|
||||
}
|
||||
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,
|
||||
"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);
|
||||
}
|
||||
let seg = prev.unwrap();
|
||||
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)?;
|
||||
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 {}",
|
||||
|
|
@ -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("aggregate_prove_seconds_total".into(), agg_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);
|
||||
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 {
|
||||
let number = shards[0].output.number;
|
||||
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 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 ok = sp1.verify_segment(&seg, &claim);
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
let seg = prev.unwrap();
|
||||
|
|
|
|||
|
|
@ -259,6 +259,16 @@ impl Sp1ProofSystem {
|
|||
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> {
|
||||
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.
|
||||
fn aggregate(&self, prev: Option<&Sp1SegmentProof>, shards: &[Sp1ShardProof]) -> Result<Sp1SegmentProof> {
|
||||
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 input = AggInput {
|
||||
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")) };
|
||||
stdin.write_proof(*proof, self.agg_vk.vk.clone());
|
||||
}
|
||||
self.record("aggregate-stdin", t_stdin.elapsed());
|
||||
let t = Instant::now();
|
||||
let proof = self.client.prove(&self.agg_pk, stdin).compressed().run()?;
|
||||
self.record("aggregate", t.elapsed());
|
||||
|
|
|
|||
|
|
@ -34,9 +34,13 @@ if [ "${SKIP_GATE:-0}" != "1" ]; then
|
|||
fi
|
||||
H="$ROOT/proving/igneum-prove/target/release/igneum-prove-host"
|
||||
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
|
||||
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
|
||||
fi
|
||||
"$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 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 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 sync {items:[...]} bulk upsert by key
|
||||
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 { 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 CACHE_MS = 10_000;
|
||||
|
|
@ -213,6 +216,13 @@ async function chain(sql) {
|
|||
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) {
|
||||
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`),
|
||||
|
|
@ -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 === '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 === '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') {
|
||||
const limit = Math.min(300, Math.max(1, Number(q.limit) || 100));
|
||||
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 (the project lead 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'
|
||||
# the project lead, 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 "$_")) }
|
||||
return
|
||||
}
|
||||
$urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url'
|
||||
if (Test-Path $urlFile) {
|
||||
$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) }
|
||||
}
|
||||
}
|
||||
# (5 October 2026 rule: a job never quits the installed app it did not start; the api/quit that stood here is gone.
|
||||
# Stopping the app is the signed `restart` job kind's work; without the stop script this waits for the app to stop.)
|
||||
$until = (Get-Date).AddSeconds(50)
|
||||
while ((Get-Date) -lt $until) {
|
||||
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).
|
||||
# UNTESTED on a PC as of 4 Oct 2026 (parse-checked only).
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$resultTag = 'SWEEP'
|
||||
$budgetMinutes = 40
|
||||
$started = Get-Date
|
||||
$deadline = $started.AddMinutes($budgetMinutes)
|
||||
|
|
@ -36,15 +37,8 @@ foreach ($f in @('settings.json', 'machine-id', 'wallet.json')) {
|
|||
$src = Join-Path $appDir $f
|
||||
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
|
||||
$sj = Join-Path $sApp 'settings.json'
|
||||
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 }
|
||||
# the copies are verbatim: the engine switches jobs, updates and proving off itself under --sweep (Settings::for_measurement, 0.3.13)
|
||||
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 }
|
||||
Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# the cap state before, for the report
|
||||
|
|
@ -59,29 +53,29 @@ if (Test-Path $smi) {
|
|||
$env:IGNEUM_APP_DATA = $root
|
||||
$env:IGNEUM_APP_LOGS = $sLogs
|
||||
$env:IGNEUM_APP_STATUS_SECS = '10'
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $exe
|
||||
$psi.Arguments = '--sweep'
|
||||
$psi.WorkingDirectory = Split-Path $exe
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.CreateNoWindow = $true
|
||||
$p = New-Object System.Diagnostics.Process
|
||||
$p.StartInfo = $psi
|
||||
$lines = New-Object System.Collections.ArrayList
|
||||
$h = { if ($EventArgs.Data) { [void]$Event.MessageData.Add($EventArgs.Data) } }
|
||||
Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -Action $h -MessageData $lines | Out-Null
|
||||
Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -Action $h -MessageData $lines | Out-Null
|
||||
[void]$p.Start()
|
||||
$p.BeginOutputReadLine(); $p.BeginErrorReadLine()
|
||||
Say ("sweep engine started, pid " + $p.Id + ", data " + $root)
|
||||
$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
|
||||
while (-not $p.HasExited) {
|
||||
Start-Sleep -Seconds 5
|
||||
while ($seen -lt $lines.Count) {
|
||||
$l = [string]$lines[$seen]; $seen++
|
||||
$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++ } }
|
||||
elseif ($l -match '^(URL|STATE) ') { }
|
||||
else { Say $l }
|
||||
|
|
@ -91,12 +85,14 @@ while (-not $p.HasExited) {
|
|||
$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 { } }
|
||||
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'
|
||||
}
|
||||
}
|
||||
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")
|
||||
EndTree $p.Id 'end of run'
|
||||
if (Test-Path $smi) {
|
||||
$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", ' | '))
|
||||
|
|
|
|||
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="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<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="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<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="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<div class="eyebrow">Run</div>
|
||||
|
|
|
|||
|
|
@ -312,6 +312,42 @@ function stampDownloads(html, file, dl) {
|
|||
const downloads = await loadDownloads();
|
||||
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', ''],
|
||||
// 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']];
|
||||
|
|
@ -324,6 +360,7 @@ for (const [file, active] of PAGES) {
|
|||
html = inject(html, 'footer', FOOT, file);
|
||||
html = stampProduct(html, file);
|
||||
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);
|
||||
writeFileSync(p, html);
|
||||
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>' +
|
||||
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([
|
||||
'<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>',
|
||||
|
|
@ -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>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>`,
|
||||
'<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'));
|
||||
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,
|
||||
'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' }));
|
||||
|
|
|
|||
|
|
@ -175,52 +175,53 @@ code{font-family:var(--f-mono);font-size:.92em;background:var(--obsidian);paddin
|
|||
<!-- nav:end -->
|
||||
<main id="main" class="wrap">
|
||||
<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>
|
||||
</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>
|
||||
<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">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">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 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>
|
||||
<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>
|
||||
<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="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">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">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">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">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 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">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">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">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="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">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="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">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="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 (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 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 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; 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="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="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="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">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">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">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 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>
|
||||
<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>
|
||||
|
|
@ -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>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>
|
||||
<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>
|
||||
<!-- footer:start -->
|
||||
<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="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<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="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<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="/bench">Engineering log</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
</nav>
|
||||
<nav class="foot-col" aria-label="Run">
|
||||
<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
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue