Compare commits
142 commits
master
...
release-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e2b0b539 | ||
|
|
95aa5f5dac | ||
|
|
85d3ebc7fb | ||
|
|
b5daa3a2b4 | ||
|
|
8df47f6c21 | ||
|
|
f06bf2af49 | ||
|
|
d26b28c430 | ||
|
|
6c20670f22 | ||
|
|
59bbf46689 | ||
|
|
1c99053aad | ||
|
|
4f6cfbfba7 | ||
|
|
49daffbf9d | ||
|
|
4c3d68b63e | ||
|
|
fa99216dd3 | ||
|
|
16ec189a0b | ||
|
|
c88504b47b | ||
|
|
d5d8e76a7a | ||
|
|
740c35783e | ||
|
|
c1e3204481 | ||
|
|
39b8df93cd | ||
|
|
447cd019b9 | ||
|
|
4e4b1fab15 | ||
|
|
98c1f05eff | ||
|
|
172696bb26 | ||
|
|
4793cca547 | ||
|
|
24adda1025 | ||
|
|
6dd3c3686b | ||
|
|
5e3e6a1664 | ||
|
|
214333d5e6 | ||
|
|
116e767c0d | ||
|
|
d695948d2e | ||
|
|
a9c3167802 | ||
|
|
d969588aac | ||
|
|
011443a76f | ||
|
|
a621a04b29 | ||
|
|
93c530d0a1 | ||
|
|
d0c98db2fa | ||
|
|
0750b2bb35 | ||
|
|
568395b78b | ||
|
|
741424dfce | ||
|
|
9d162e4302 | ||
|
|
ced26f4e4e | ||
|
|
690e7dc12a | ||
|
|
b8010abf82 | ||
|
|
6084a7ea64 | ||
|
|
708b1e0fc8 | ||
|
|
e39d966840 | ||
|
|
770eca9daf | ||
|
|
057b489107 | ||
|
|
0bc7681875 | ||
|
|
85c712f01a | ||
|
|
d33fa30f89 | ||
|
|
1e8bcf7cb1 | ||
|
|
b3cd2d21f4 | ||
|
|
c4d29c58fd | ||
|
|
6660722bd7 | ||
|
|
edf5e8831e | ||
|
|
b0af074fb4 | ||
|
|
9c9a09ff09 | ||
|
|
ea76eac0f8 | ||
|
|
27677b596c | ||
|
|
30511ab700 | ||
|
|
60e78a56ba | ||
|
|
10112d1df7 | ||
|
|
9601749fb2 | ||
|
|
3559537130 | ||
|
|
e976b344f3 | ||
|
|
ea7c30d799 | ||
|
|
c07d259323 | ||
|
|
de058a4af5 | ||
|
|
0969bedb2e | ||
|
|
e49c7a058a | ||
|
|
1e9ddb457e | ||
|
|
3ef7f901c6 | ||
|
|
0be32c92ce | ||
|
|
fddb02d016 | ||
|
|
359dd11d00 | ||
|
|
ebe27aae69 | ||
|
|
3be454f128 | ||
|
|
93b05c9b7b | ||
|
|
54cc0bd6aa | ||
|
|
12f4fdbde3 | ||
|
|
f35816ddfb | ||
|
|
ff320d9fe3 | ||
|
|
5baaa32273 | ||
|
|
3fd96e4882 | ||
|
|
547b3323d2 | ||
|
|
3dd010bca8 | ||
|
|
f242646a01 | ||
|
|
91c3ed747d | ||
|
|
62736cecb5 | ||
|
|
19b802a17b | ||
|
|
418801fac2 | ||
|
|
0aef24f747 | ||
|
|
7ee76db53c | ||
|
|
4bf858f3db | ||
|
|
8321127385 | ||
|
|
f0660d59c3 | ||
|
|
6f6d058c9f | ||
|
|
9201a07faf | ||
|
|
7996240f6f | ||
|
|
2c2859253d | ||
|
|
e8244a7853 | ||
|
|
17e1eb4d12 | ||
|
|
f7d5db67ad | ||
|
|
98b686059a | ||
|
|
54bcdbe510 | ||
|
|
64e70bb27d | ||
|
|
8aa8f0a048 | ||
|
|
05a2d0599a | ||
|
|
b371bf2bf4 | ||
|
|
0ac8d097b2 | ||
|
|
5b4cb56a27 | ||
|
|
fa20b72fea | ||
|
|
4af9ab4d0a | ||
|
|
409cb0f76c | ||
|
|
7a69c03135 | ||
|
|
7ce5965e46 | ||
|
|
18b154de9c | ||
|
|
7943f83c17 | ||
|
|
4f6b7810d5 | ||
|
|
fc94277775 | ||
|
|
02627f3d9f | ||
|
|
441d1eaf88 | ||
|
|
53d99b17f9 | ||
|
|
3cc1b9ef11 | ||
|
|
0b48c028f3 | ||
|
|
95707125c5 | ||
|
|
f57c5293bc | ||
|
|
3d0a6458da | ||
|
|
2b244d732e | ||
|
|
0659fab5f2 | ||
|
|
0477f8857d | ||
|
|
6e9bd00dbb | ||
|
|
98bdeeb88b | ||
|
|
5143cd2ed6 | ||
|
|
9a45ea3785 | ||
|
|
75a6364384 | ||
|
|
bd3fb7a809 | ||
|
|
ecfffa019a | ||
|
|
14dc093549 | ||
|
|
1065b81d05 |
4
.gitignore
vendored
|
|
@ -32,6 +32,10 @@ vendor/igneum-node-ship/
|
|||
brand/trademark/pbip-pack/
|
||||
brand/trademark/*.zip
|
||||
|
||||
# cargo target dirs of the shipper's local test runs and the build box's fetched artefacts (6 October 2026: a stray add -A staged 450 of them)
|
||||
app/igneum-app/target-tests/
|
||||
proving/igneum-prove/target-remote/
|
||||
**/target-remote/
|
||||
# the Discord bot and reddit bot dry-run renders
|
||||
tools/community/out/
|
||||
# the GPU workers built on igneum-build-1 (tools/workers-remote.sh); binaries, never committed
|
||||
|
|
|
|||
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.14"
|
||||
version = "0.3.17"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.14"
|
||||
version = "0.3.17"
|
||||
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,14,0
|
||||
PRODUCTVERSION 0,3,14,0
|
||||
FILEVERSION 0,3,17,0
|
||||
PRODUCTVERSION 0,3,17,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.14"
|
||||
VALUE "FileVersion", "0.3.17"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.14"
|
||||
VALUE "ProductVersion", "0.3.17"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ pub struct CardPref {
|
|||
pub sweep_class: String,
|
||||
#[serde(default)]
|
||||
pub sweep_source: String,
|
||||
/// the chosen clock sat on the ladder's floor (the lowest step measured, not the optimum)
|
||||
#[serde(default)]
|
||||
pub sweep_floor: bool,
|
||||
/// Miner UI 4 (6 October 2026): this card's goal (efficiency | balanced | rate); empty = the global tune_goal
|
||||
#[serde(default)]
|
||||
pub tune_goal: String,
|
||||
/// the untuned point the last FULL plan measured first (its step 0), kept across confirm plans so the row can
|
||||
/// read "saves 84 W, 0.15% of rate"; 0 = never measured
|
||||
#[serde(default)]
|
||||
pub sweep_before_watts: f64,
|
||||
#[serde(default)]
|
||||
pub sweep_before_mhs: f64,
|
||||
/// Ember 2: the memory clock the last tune chose (0 = the driver's default)
|
||||
#[serde(default)]
|
||||
pub sweep_mem_mhz: u32,
|
||||
|
|
@ -357,8 +369,9 @@ impl Runtime {
|
|||
let p2p_port = env("IGNEUM_APP_P2P_PORT").and_then(|v| v.parse().ok()).unwrap_or(26611);
|
||||
let peers = match std::env::var("IGNEUM_APP_PEERS") {
|
||||
Ok(v) => v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
|
||||
// the public seed node first, then the project lead's Mac on the house LAN (devnet only)
|
||||
Err(_) if network == "devnet" => vec!["188.245.5.161:26611".to_string(), "192.168.68.64:26611".to_string()],
|
||||
// the public seed node first, then the build box's hand nodes (main's decision (b), 6 October 2026, 19:1x UTC:
|
||||
// the hands move off the project lead's Mac to igneum-build-1), then the project lead's Mac on the house LAN (devnet only)
|
||||
Err(_) if network == "devnet" => vec!["188.245.5.161:26611".to_string(), "188.40.146.49:26611".to_string(), "192.168.68.64:26611".to_string()],
|
||||
Err(_) => vec![],
|
||||
};
|
||||
let unsynced_mining = env("IGNEUM_APP_UNSYNCED").map(|v| v == "1").unwrap_or(false);
|
||||
|
|
|
|||
|
|
@ -906,6 +906,115 @@ pub fn tuned_line(mhs: f64, watts: f64, eff: f64) -> String {
|
|||
format!("Tuned: {mhs:.1} MH/s at {watts:.0} W ({eff:.3} MH/W)")
|
||||
}
|
||||
|
||||
/// HH:MM UTC of a unix time (the day is not shown: "since 18:39 UTC").
|
||||
pub fn hhmm_utc(unix: f64) -> String {
|
||||
let s = unix.max(0.0) as u64 % 86_400;
|
||||
format!("{:02}:{:02}", s / 3600, (s % 3600) / 60)
|
||||
}
|
||||
|
||||
/// Horizon polish Q83: the one sentence every surface shows while finality is paused. The cause is the node's own
|
||||
/// (`reason`, with who holds it) when it carries one, else the plain two-thirds line; never the word "final".
|
||||
pub fn finality_paused_line(since_unix: f64, reason: &str, held_by: &str) -> String {
|
||||
let cause = if reason.trim().is_empty() {
|
||||
"under two thirds of the weight is signing".to_string()
|
||||
} else if held_by.trim().is_empty() {
|
||||
reason.trim().to_string()
|
||||
} else {
|
||||
format!("{} (held by {})", reason.trim(), held_by.trim())
|
||||
};
|
||||
format!("Finality paused since {} UTC: {cause}", hhmm_utc(since_unix))
|
||||
}
|
||||
|
||||
/// The node's structured finality fields on igneum_getProvingStatus (the node lane, 0.3.16): "finalityReason"
|
||||
/// (empty when live), "finalityProvisional", "heldBy" {tableIndex, stayersShareBps, expiresDaa} or null,
|
||||
/// "pausedSinceMs" or null. Returns (reason, held-by words, provisional, paused_since unix seconds).
|
||||
pub struct FinalityStatus {
|
||||
/// the node's own word on whether finality is live (b2e21447: finalityActive); None on an older node
|
||||
pub active: Option<bool>,
|
||||
pub reason: String,
|
||||
pub held_by: String,
|
||||
pub provisional: bool,
|
||||
pub paused_since: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn parse_finality_status(v: &serde_json::Value) -> Option<FinalityStatus> {
|
||||
// the node's words begin "paused: ..." (b2e21447); the app's sentence already says "Finality paused since", so
|
||||
// that prefix goes; a reason that already names who holds it ("held by weight table 7, ...") is not repeated
|
||||
let reason = v.get("finalityReason")?.as_str().unwrap_or("").trim().trim_start_matches("paused:").trim().to_string();
|
||||
let active = v.get("finalityActive").and_then(|b| b.as_bool());
|
||||
let provisional = v.get("finalityProvisional").and_then(|b| b.as_bool()).unwrap_or(false);
|
||||
let held_by = match v.get("heldBy") {
|
||||
Some(h) if h.is_object() => {
|
||||
let idx = h.get("tableIndex").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
let bps = h.get("stayersShareBps").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
let exp = h.get("expiresDaa").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
format!("weight table {idx}, {}.{:02}% still signing, expires at DAA {exp}", bps / 100, bps % 100)
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
let held_by = if reason.contains("held by") { String::new() } else { held_by };
|
||||
let paused_since = v.get("pausedSinceMs").and_then(|x| x.as_u64()).filter(|ms| *ms > 0).map(|ms| ms as f64 / 1000.0);
|
||||
Some(FinalityStatus { active, reason, held_by, provisional, paused_since })
|
||||
}
|
||||
|
||||
/// The node's pause line, when it carries one: `... finality_reason=<words or "quoted words"> held_by=<id>`.
|
||||
/// Returns (reason, held_by); None when the line is not one.
|
||||
pub fn parse_finality_line(text: &str) -> Option<(String, String)> {
|
||||
let i = text.find("finality_reason=")?;
|
||||
let rest = &text[i + "finality_reason=".len()..];
|
||||
let (reason, after) = if let Some(q) = rest.strip_prefix('"') {
|
||||
let end = q.find('"')?;
|
||||
(q[..end].to_string(), &q[end + 1..])
|
||||
} else {
|
||||
let end = rest.find(' ').unwrap_or(rest.len());
|
||||
(rest[..end].replace('_', " "), &rest[end..])
|
||||
};
|
||||
let held_by = after.find("held_by=").map(|j| after[j + 8..].split_whitespace().next().unwrap_or("").to_string()).unwrap_or_default();
|
||||
Some((reason, held_by))
|
||||
}
|
||||
|
||||
/// Miner UI 4 (6 October 2026): the fleet's draw now: the sum over cards that mine and have a draw reading under
|
||||
/// `max_age_s` old (a stale reading is not a draw).
|
||||
pub fn fleet_watts<'a>(cards: impl Iterator<Item = (&'a str, f64, f64)>, now: f64, max_age_s: f64) -> f64 {
|
||||
cards.filter(|(state, w, at)| *state == "mining" && *w > 0.0 && now - *at < max_age_s).map(|(_, w, _)| w).sum()
|
||||
}
|
||||
|
||||
/// A hex quantity (0x-prefixed, as eth_getBalance answers) as a decimal string; None when it is not one.
|
||||
pub fn wei_from_hex(hex: &str) -> Option<String> {
|
||||
let h = hex.trim().strip_prefix("0x").unwrap_or(hex.trim());
|
||||
if h.is_empty() {
|
||||
return Some("0".into());
|
||||
}
|
||||
u128::from_str_radix(h, 16).ok().map(|v| v.to_string())
|
||||
}
|
||||
|
||||
/// Miner UI 4 (6 October 2026): a card's own goal when set, else the global one.
|
||||
pub fn goal_for(card_goal: &str, global: &str) -> Goal {
|
||||
Goal::parse(if card_goal.trim().is_empty() { global } else { card_goal })
|
||||
}
|
||||
|
||||
/// (D) May a step be SET on this card? Measure-only (no control: Apple, NVIDIA without Power control, an AMD card
|
||||
/// whose probe gave no tune line) sets nothing: the run notices the missing acknowledgement and measures. An AMD
|
||||
/// card also needs its ADLX ordinal. 6 October 2026, PC 1 17:57Z: a measure-only 9070 XT was sent a set, refused.
|
||||
pub fn may_set(vendor: &str, tune_control: bool, amd_ordinal: i64) -> bool {
|
||||
match vendor {
|
||||
"nvidia" => tune_control,
|
||||
"amd" => tune_control && amd_ordinal >= 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// (E) The row's words while a back-off from a failed tune holds: when the retry comes and why it stopped.
|
||||
pub fn retry_note(remaining_s: u64, last_note: &str) -> String {
|
||||
let mins = (remaining_s + 59) / 60;
|
||||
let why = last_note.trim_start_matches("tuning stopped: ").trim_start_matches("tuning: ").trim();
|
||||
if why.is_empty() {
|
||||
format!("tuning: retry in {mins} min")
|
||||
} else {
|
||||
format!("tuning: retry in {mins} min ({why})")
|
||||
}
|
||||
}
|
||||
|
||||
/// The row's line for a baseline (measure-only) result: the card was measured as it runs, nothing was set.
|
||||
/// (6 October 2026: PC 1's rows read "Tuned: 114.2 MH/s at 305 W" for a baseline, which is not a tune.)
|
||||
pub fn measured_line(mhs: f64, watts: f64, eff: f64) -> String {
|
||||
|
|
@ -1007,6 +1116,74 @@ mod tests {
|
|||
assert!(Run::applied(&step, Readback { limit_w: 90.0, acked: false }, 100.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fleet_draw_sums_mining_cards_with_a_fresh_reading_and_the_balance_reads_in_wei() {
|
||||
let now = 1_791_300_000.0;
|
||||
let cards = vec![("mining", 226.8, now - 5.0), ("mining", 75.6, now - 10.0), ("mining", 201.0, now - 120.0), ("off", 30.0, now - 1.0), ("mining", 0.0, now)];
|
||||
let w = fleet_watts(cards.iter().map(|(s, w, at)| (*s, *w, *at)), now, 60.0);
|
||||
assert!((w - 302.4).abs() < 1e-9, "the stale 9070 XT reading and the card that is off do not count: {w}");
|
||||
assert!((pounds_per_day(302.4, 28.5) - 2.068416).abs() < 1e-6);
|
||||
assert_eq!(wei_from_hex("0x1a"), Some("26".into()));
|
||||
assert_eq!(wei_from_hex("0x0"), Some("0".into()));
|
||||
assert_eq!(wei_from_hex("0x"), Some("0".into()));
|
||||
assert_eq!(wei_from_hex("0xde0b6b3a7640000"), Some("1000000000000000000".into()), "one IGN");
|
||||
assert_eq!(wei_from_hex("soon"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_finality_pause_line_names_the_time_and_the_cause_and_never_says_final() {
|
||||
// 6 October 2026 18:39:00Z
|
||||
let since = 1_791_311_940.0;
|
||||
assert_eq!(hhmm_utc(since), "18:39");
|
||||
let plain = finality_paused_line(since, "", "");
|
||||
assert_eq!(plain, "Finality paused since 18:39 UTC: under two thirds of the weight is signing");
|
||||
assert!(!plain.to_ascii_lowercase().contains("final "), "no 'final' word while paused: {plain}");
|
||||
assert_eq!(finality_paused_line(since, "a checkpoint vote is split", "ae432dc7"), "Finality paused since 18:39 UTC: a checkpoint vote is split (held by ae432dc7)");
|
||||
assert_eq!(parse_finality_line("1791311940 finality paused finality_reason=\"vote split at index 412\" held_by=ae432dc7"), Some(("vote split at index 412".into(), "ae432dc7".into())));
|
||||
assert_eq!(parse_finality_line("x finality_reason=under_two_thirds"), Some(("under two thirds".into(), String::new())));
|
||||
assert_eq!(parse_finality_line("status: accepted 3 blocks"), None);
|
||||
// the structured carrier on igneum_getProvingStatus (the node lane's field names)
|
||||
// the node's own words (b2e21447): "paused: under two thirds ... ({p}%; the table frozen at lock {j} has {q}% signing)"
|
||||
let v: serde_json::Value = serde_json::from_str(r#"{"v1":{"active":true},"finalityActive":false,"finalityReason":"paused: under two thirds of the weight is signing (61%; the table frozen at lock 7 has 58% signing)","finalityProvisional":false,"heldBy":null,"pausedSinceMs":1791311940000}"#).unwrap();
|
||||
let f = parse_finality_status(&v).unwrap();
|
||||
assert_eq!((f.active, f.provisional, f.paused_since), (Some(false), false, Some(1_791_311_940.0)));
|
||||
assert_eq!(finality_paused_line(f.paused_since.unwrap(), &f.reason, &f.held_by), "Finality paused since 18:39 UTC: under two thirds of the weight is signing (61%; the table frozen at lock 7 has 58% signing)");
|
||||
// the held-by form: the reason names the table itself, so heldBy is not repeated
|
||||
let v2: serde_json::Value = serde_json::from_str(r#"{"finalityActive":false,"finalityReason":"paused: held by weight table 7, 61% of it signing, expires at DAA 205000","finalityProvisional":true,"heldBy":{"tableIndex":7,"stayersShareBps":6150,"expiresDaa":205000},"pausedSinceMs":1791311940000}"#).unwrap();
|
||||
let g = parse_finality_status(&v2).unwrap();
|
||||
assert!(g.provisional && g.held_by.is_empty());
|
||||
assert_eq!(finality_paused_line(g.paused_since.unwrap(), &g.reason, &g.held_by), "Finality paused since 18:39 UTC: held by weight table 7, 61% of it signing, expires at DAA 205000");
|
||||
// a reason without the table words keeps the heldBy suffix
|
||||
let v3: serde_json::Value = serde_json::from_str(r#"{"finalityActive":false,"finalityReason":"paused: no checkpoint determined above the latest lock","heldBy":{"tableIndex":7,"stayersShareBps":6150,"expiresDaa":205000}}"#).unwrap();
|
||||
assert_eq!(parse_finality_status(&v3).unwrap().held_by, "weight table 7, 61.50% still signing, expires at DAA 205000");
|
||||
// live finality: an empty reason, null heldBy, null pausedSinceMs
|
||||
let live: serde_json::Value = serde_json::from_str(r#"{"finalityReason":"","finalityProvisional":false,"heldBy":null,"pausedSinceMs":null}"#).unwrap();
|
||||
let l = parse_finality_status(&live).unwrap();
|
||||
assert!(l.reason.is_empty() && l.held_by.is_empty() && !l.provisional && l.paused_since.is_none() && l.active.is_none());
|
||||
// a node before 0.3.16 carries none of it
|
||||
assert!(parse_finality_status(&serde_json::json!({"v1":{"active":true}})).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_goal_overrides_the_global_goal_and_empty_follows_it() {
|
||||
assert_eq!(goal_for("", "balanced"), Goal::Balanced);
|
||||
assert_eq!(goal_for("rate", "balanced"), Goal::MaxRate);
|
||||
assert_eq!(goal_for("efficiency", "rate"), Goal::Efficiency);
|
||||
assert_eq!(goal_for(" ", "efficiency"), Goal::Efficiency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_measure_only_card_is_never_set_and_a_back_off_says_when_the_retry_comes() {
|
||||
assert!(!may_set("amd", false, 1), "the probe gave no tune line: measure only");
|
||||
assert!(may_set("amd", true, 1));
|
||||
assert!(!may_set("amd", true, -1), "no ADLX ordinal");
|
||||
assert!(!may_set("nvidia", false, -1), "Power control off: measure only");
|
||||
assert!(may_set("nvidia", true, -1));
|
||||
assert!(!may_set("apple", true, -1));
|
||||
assert_eq!(retry_note(3599, "tuning stopped: the card refused the setting (see the log)"), "tuning: retry in 60 min (the card refused the setting (see the log))");
|
||||
assert_eq!(retry_note(30, ""), "tuning: retry in 1 min");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baseline_result_reads_measured_and_a_tune_reads_tuned() {
|
||||
assert_eq!(result_line(PlanKind::Baseline, 114.2, 305.0, 0.3744), "Measured: 114.2 MH/s at 305 W (0.374 MH/W)");
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ pub struct CardChoice {
|
|||
|
||||
pub enum Cmd {
|
||||
Detect,
|
||||
/// the payout address's balance (eth_getBalance, hex) or why it could not be read
|
||||
BalanceRead(Result<String, String>),
|
||||
/// the node's finality fields from igneum_getProvingStatus (None: the node carries none, or did not answer)
|
||||
FinalityStatus(Option<crate::ember::FinalityStatus>),
|
||||
ApplyCards(Vec<CardChoice>),
|
||||
/// one enumeration of the cards finished (the first, or a re-detection: src/hotplug.rs)
|
||||
Detected(crate::detect::Detection),
|
||||
|
|
@ -48,6 +52,10 @@ pub enum Cmd {
|
|||
WorkerBuilt(usize, Result<PathBuf, String>),
|
||||
/// local minus the latest block's timestamp, seconds (from the node's EVM RPC)
|
||||
ClockSample(f64),
|
||||
/// another node holds the app's ports: what it answered (src/extnode.rs)
|
||||
ExternalNode(crate::extnode::Check),
|
||||
/// the node's own description over RPC (igneum_getNodeInfo), every 30 s
|
||||
NodeInfo(crate::extnode::Check),
|
||||
/// local minus an HTTPS Date header, seconds; None when the check failed
|
||||
ClockHttps(Option<f64>),
|
||||
ClockCheck,
|
||||
|
|
@ -77,6 +85,8 @@ pub enum Cmd {
|
|||
TuneProgress(Value),
|
||||
/// Ember 2: Settings > goal, electricity price, the hill-climb switch
|
||||
TuneGoal(Option<String>, Option<f64>, Option<bool>),
|
||||
/// one card's goal override (key, goal; "" = back to the global goal)
|
||||
TuneCardGoal(String, String),
|
||||
/// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the
|
||||
/// prover found a host that was not there when the node started
|
||||
RestartNode(String),
|
||||
|
|
@ -118,6 +128,7 @@ impl Shared {
|
|||
st.setup_done = settings.setup_done;
|
||||
st.phase = if settings.setup_done { "dashboard".into() } else { "welcome".into() };
|
||||
st.node.state = "stopped".into();
|
||||
st.node.tip_age_s = -1.0;
|
||||
st.mining.state = "idle".into();
|
||||
st.mining.paused = settings.paused;
|
||||
st.mining.accepted_total = settings.accepted_total;
|
||||
|
|
@ -402,6 +413,10 @@ fn address_state(s: &Settings, wallet_path: &std::path::Path) -> crate::state::A
|
|||
source: s.address_source.clone(),
|
||||
key_saved: s.key_saved,
|
||||
wallet_file: if s.address_source == "generated" { wallet_path.display().to_string() } else { String::new() },
|
||||
balance_wei: None,
|
||||
balance_age_s: -1.0,
|
||||
balance_note: String::new(),
|
||||
price_gbp_per_ign: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -532,6 +547,11 @@ pub struct Engine {
|
|||
lines_tx: Sender<Line>,
|
||||
node: Option<Proc>,
|
||||
node_external: bool,
|
||||
/// the ports are held by a node this app refused (other rules or network): re-checked, the app takes the ports when it leaves
|
||||
node_refused: bool,
|
||||
/// when the other node's port stopped answering (engine seconds), None while it answers
|
||||
external_gone_since: Option<f64>,
|
||||
node_info_at: Instant,
|
||||
node_restart_at: Option<Instant>,
|
||||
/// resume rule (5 October 2026): when due, every enabled card must be mining or its name goes to the log
|
||||
resume_check_at: Option<Instant>,
|
||||
|
|
@ -608,6 +628,10 @@ pub struct Engine {
|
|||
tune_acked: Option<u64>,
|
||||
/// cards whose confirm check found a better neighbour: the full plan runs next
|
||||
tune_full_due: std::collections::HashSet<usize>,
|
||||
/// Miner UI 4: the balance read (every 30 s while the node runs; 60 s after a failure)
|
||||
balance_next: Option<Instant>,
|
||||
balance_busy: bool,
|
||||
balance_at: f64,
|
||||
/// a sweep waiting for the cap-mode probe: (card index, forced)
|
||||
sweep_pending: Option<(usize, bool)>,
|
||||
/// how caps are set: Some(true) directly, Some(false) through the elevated helper, None = not probed yet
|
||||
|
|
@ -651,6 +675,9 @@ impl Engine {
|
|||
lines_tx,
|
||||
node: None,
|
||||
node_external: false,
|
||||
node_refused: false,
|
||||
external_gone_since: None,
|
||||
node_info_at: Instant::now() - Duration::from_secs(25),
|
||||
node_restart_at: None,
|
||||
resume_check_at: None,
|
||||
node_started_at: now,
|
||||
|
|
@ -712,6 +739,9 @@ impl Engine {
|
|||
sweep_helper_is_task: false,
|
||||
tune_acked: None,
|
||||
tune_full_due: std::collections::HashSet::new(),
|
||||
balance_next: None,
|
||||
balance_busy: false,
|
||||
balance_at: 0.0,
|
||||
sweep_pending: None,
|
||||
sweep_direct: None,
|
||||
sweep_helper: false,
|
||||
|
|
@ -939,6 +969,24 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
}
|
||||
Cmd::ExternalNode(check) => self.external_node_decided(check),
|
||||
Cmd::NodeInfo(info) => {
|
||||
if info.pow_engine.as_deref() == Some("stub") {
|
||||
let first = self.st().node.state != "stub";
|
||||
let mut st = self.st();
|
||||
st.node.state = "stub".into();
|
||||
st.node.synced = false;
|
||||
st.node.message = "this node was built without the mining engine (stub) and refuses every real block".into();
|
||||
drop(st);
|
||||
if first { self.shared.event("error", "the node runs the stub engine, not igneum-pow: it refuses every real block. Reinstall the app (ledger N5)"); }
|
||||
}
|
||||
let mut st = self.st();
|
||||
if let Some(d) = info.digest.filter(|d| d.len() >= 16) {
|
||||
if st.node.consensus_digest != d { st.node.consensus_digest = d; }
|
||||
st.node.digest_source = "rpc".into();
|
||||
}
|
||||
if let Some(v) = info.version { if !v.is_empty() && st.node.version.is_empty() { st.node.version = v; } }
|
||||
}
|
||||
Cmd::ClockSample(d) => {
|
||||
self.clock_samples.push(d);
|
||||
if self.clock_samples.len() > 9 {
|
||||
|
|
@ -1147,6 +1195,71 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
Cmd::TuneProgress(v) => self.tune_progress(&v),
|
||||
Cmd::FinalityStatus(f) => {
|
||||
let mut st = self.st();
|
||||
match f {
|
||||
Some(f) => {
|
||||
st.finality.node_active = f.active;
|
||||
st.finality.reason = f.reason;
|
||||
st.finality.held_by = f.held_by;
|
||||
st.finality.provisional = f.provisional;
|
||||
st.finality.cause_source = "node".into();
|
||||
if let Some(since) = f.paused_since {
|
||||
st.finality.paused_since = since;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// a node without the fields: the log line (parse_finality_line) may still have set a cause
|
||||
st.finality.node_active = None;
|
||||
if st.finality.cause_source != "node-line" {
|
||||
st.finality.cause_source.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::BalanceRead(r) => {
|
||||
self.balance_busy = false;
|
||||
match r {
|
||||
Ok(hex) => match crate::ember::wei_from_hex(&hex) {
|
||||
Some(wei) => {
|
||||
let mut st = self.st();
|
||||
st.address.balance_wei = Some(wei);
|
||||
st.address.balance_note.clear();
|
||||
drop(st);
|
||||
self.balance_at = crate::platform::unix_now_f();
|
||||
self.balance_next = Some(Instant::now() + Duration::from_secs(30));
|
||||
}
|
||||
None => {
|
||||
self.st().address.balance_note = format!("the node answered {hex} for the balance, not a quantity");
|
||||
self.balance_next = Some(Instant::now() + Duration::from_secs(60));
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
self.st().address.balance_note = e;
|
||||
self.balance_next = Some(Instant::now() + Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::TuneCardGoal(key, goal) => {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
let (enabled, identities) = self.st().mining.cards.iter().find(|c| c.key == key).map(|c| (c.enabled, c.identities)).unwrap_or((true, 1));
|
||||
let e = s.cards.entry(key.clone()).or_insert_with(|| crate::config::CardPref { enabled, identities, ..Default::default() });
|
||||
e.tune_goal = goal.clone();
|
||||
}
|
||||
self.shared.save_settings();
|
||||
let name = {
|
||||
let mut st = self.st();
|
||||
let c = st.mining.cards.iter_mut().find(|c| c.key == key);
|
||||
c.map(|c| {
|
||||
c.tune_goal = goal.clone();
|
||||
c.name.clone()
|
||||
})
|
||||
};
|
||||
if let Some(name) = name {
|
||||
self.shared.event("info", &format!("{name}: tune goal {}", if goal.is_empty() { "follows the global setting".to_string() } else { goal.clone() }));
|
||||
}
|
||||
}
|
||||
Cmd::TuneGoal(goal, price, climb) => {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
|
|
@ -1359,14 +1472,22 @@ impl Engine {
|
|||
let _ = std::fs::create_dir_all(&self.shared.runtime.log_dir);
|
||||
self.apply_power_limits("start");
|
||||
if port_open(self.shared.runtime.rpc_port) {
|
||||
self.node_external = true;
|
||||
self.shared.event("info", &format!("a node already answers on 127.0.0.1:{}; using it (it is not stopped by this app)", self.shared.runtime.rpc_port));
|
||||
// another node holds the app's ports: check whose rules it runs before reading it (publish 2's read-back,
|
||||
// 6 October 2026: the Mac read the hand node silently as its own); the answer comes back as Cmd::ExternalNode
|
||||
let port = self.shared.runtime.rpc_port;
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.node.state = "syncing".into();
|
||||
st.node.message = "external node".into();
|
||||
st.proving.verifier_reason = "external node: the app did not start it, so it set no verifier".into();
|
||||
st.proving.verifier_note = crate::verifier::note("unknown", "", "", true);
|
||||
st.node.state = "starting".into();
|
||||
st.node.source = String::new();
|
||||
st.node.message = format!("another node holds port {port} on this machine; checking its rules");
|
||||
}
|
||||
self.shared.log(&format!("port {port} is held by another node; asking it for its chain id and rules before using it"));
|
||||
let shared = self.shared.clone();
|
||||
let evm_port = self.shared.runtime.evm_port();
|
||||
std::thread::spawn(move || { shared.send(Cmd::ExternalNode(crate::extnode::probe(evm_port))); });
|
||||
} else {
|
||||
crate::extnode::prune_logs(&self.shared.runtime.log_dir, "node-", 10);
|
||||
self.set_node_mode("app", "own", "the ports were free at launch, so the app started its own node");
|
||||
self.start_node();
|
||||
}
|
||||
self.plan_miners();
|
||||
|
|
@ -1400,6 +1521,74 @@ impl Engine {
|
|||
/// The packager's consensus parameters (igneum-app.json `node_override_params`, for example the difficulty v2
|
||||
/// activation height) written to <app data>/override-params.json for --override-params-file. None when the
|
||||
/// package pins nothing, so the node runs on the network's defaults as before.
|
||||
/// The check of another node on the app's ports came back (src/extnode.rs): use it and say so, or refuse it
|
||||
fn external_node_decided(&mut self, check: crate::extnode::Check) {
|
||||
let port = self.shared.runtime.p2p_port;
|
||||
let ours = self.node_override_file().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|t| serde_json::from_str::<Value>(&t).ok());
|
||||
let network = self.shared.runtime.network.clone();
|
||||
let d = crate::extnode::decide_on(port, ours.as_ref(), None, Some(network.as_str()), &check);
|
||||
self.node_external = d.use_it;
|
||||
self.node_refused = !d.use_it;
|
||||
self.external_gone_since = None;
|
||||
self.set_node_mode(if d.use_it { "external" } else { "none" }, if d.use_it { "external" } else { "none" }, &d.line);
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.node.rules_check = d.verdict.into();
|
||||
st.node.port_note = d.line.clone();
|
||||
}
|
||||
if d.use_it {
|
||||
self.shared.event(if d.verdict == "match" { "info" } else { "error" }, &d.line);
|
||||
let mut st = self.st();
|
||||
st.node.state = "syncing".into();
|
||||
st.node.message = "another node on this machine".into();
|
||||
if let Some(dg) = check.digest.filter(|x| x.len() >= 16) { st.node.consensus_digest = dg; st.node.digest_source = "rpc".into(); }
|
||||
if let Some(v) = check.version { if !v.is_empty() { st.node.version = v; } }
|
||||
st.proving.verifier_reason = "external node: the app did not start it, so it set no verifier".into();
|
||||
st.proving.verifier_note = crate::verifier::note("unknown", "", "", true);
|
||||
} else {
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.node.state = "stopped".into();
|
||||
st.node.synced = false;
|
||||
st.node.message = d.line.clone();
|
||||
}
|
||||
self.shared.event("error", &d.line);
|
||||
self.shared.event("error", "mining waits: stop the other node or start this app with other ports, then open the app again");
|
||||
}
|
||||
}
|
||||
|
||||
/// Record which node the app reads (api/state node.source, node.mode, node.mode_reason).
|
||||
fn set_node_mode(&self, source: &str, mode: &str, reason: &str) {
|
||||
let mut st = self.st();
|
||||
st.node.source = source.into();
|
||||
st.node.mode = mode.into();
|
||||
st.node.mode_reason = reason.into();
|
||||
}
|
||||
|
||||
/// The other node left its ports for TAKEOVER_WAIT_S: the app starts its own node on them and says so.
|
||||
fn take_over_ports(&mut self) {
|
||||
let port = self.shared.runtime.rpc_port;
|
||||
let was = if self.node_external { "the node this app was reading" } else { "the node this app refused" };
|
||||
self.node_external = false;
|
||||
self.node_refused = false;
|
||||
self.external_gone_since = None;
|
||||
let reason = format!("{was} left port {port} for {} s, so the app started its own node", crate::extnode::TAKEOVER_WAIT_S as u64);
|
||||
self.shared.event("info", &format!("The other node went away; this app starts its own node on port {port}"));
|
||||
self.set_node_mode("app", "own", &reason);
|
||||
{
|
||||
let mut st = self.st();
|
||||
st.node.rules_check = String::new();
|
||||
st.node.port_note = reason.clone();
|
||||
st.node.synced = false;
|
||||
st.node.consensus_digest = String::new();
|
||||
st.node.digest_source = String::new();
|
||||
st.node.version = String::new();
|
||||
}
|
||||
self.node_last_reading = None;
|
||||
crate::extnode::prune_logs(&self.shared.runtime.log_dir, "node-", 10);
|
||||
self.start_node();
|
||||
}
|
||||
|
||||
fn node_override_file(&self) -> Option<std::path::PathBuf> {
|
||||
if self.node_override_unusable {
|
||||
return None;
|
||||
|
|
@ -1447,6 +1636,9 @@ impl Engine {
|
|||
let mut st = self.st();
|
||||
st.node.pid = p.pid();
|
||||
st.node.state = "starting".into();
|
||||
st.node.stall_exits = 0;
|
||||
st.node.tip_age_s = -1.0;
|
||||
st.node.sync_cause = String::new();
|
||||
st.node.starts = self.node_starts;
|
||||
st.node.message = "opening the database".into();
|
||||
st.node.restart_in_s = 0;
|
||||
|
|
@ -1823,7 +2015,7 @@ impl Engine {
|
|||
std::thread::spawn(move || {
|
||||
let r = crate::powertask::start().and_then(|_| {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mut seq = crate::platform::unix_now() % 1_000_000;
|
||||
let mut seq = crate::powertask::wire_seq();
|
||||
let mut text = String::new();
|
||||
for (dev, w) in &pairs {
|
||||
seq += 1;
|
||||
|
|
@ -2219,9 +2411,24 @@ impl Engine {
|
|||
if c.tune_control && c.vendor == "nvidia" && !power_control && !forced {
|
||||
// measure-only until Power control is on; the baseline plan covers it below
|
||||
}
|
||||
if self.sweep_retry.get(&c.index).map(|t| now < *t).unwrap_or(false) {
|
||||
// (E) 6 October 2026, PC 1 18:18Z: three "Tune now" requests sat behind the hour's back-off from the
|
||||
// 17:55 failures and the rows said "queued" with no start; an explicit request now clears the back-off,
|
||||
// and a held card's row says when the retry comes
|
||||
if let Some(t) = self.sweep_retry.get(&c.index).copied() {
|
||||
if now < t {
|
||||
if forced {
|
||||
self.sweep_retry.remove(&c.index);
|
||||
} else {
|
||||
if c.sweep_pct == 0 || c.sweep_note.starts_with("tuning stopped") {
|
||||
let note = crate::ember::retry_note((t - now).as_secs(), &c.sweep_note);
|
||||
if let Some(cc) = self.st().mining.cards.get_mut(c.index) {
|
||||
cc.sweep_note = note;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let stable = self.miners.iter().find(|m| m.card == c.index).map(|m| m.proc.as_ref().map(|p| p.started.elapsed() >= Duration::from_secs(crate::ember::STABLE_S)).unwrap_or(false) && m.last_status.is_some()).unwrap_or(false);
|
||||
let wait = if c.state != "mining" {
|
||||
Some(format!("tuning: waits for the card to mine ({})", c.state))
|
||||
|
|
@ -2399,7 +2606,8 @@ impl Engine {
|
|||
let full_due = self.tune_full_due.remove(&idx);
|
||||
let (goal, climb_on) = {
|
||||
let s = self.shared.settings.lock().unwrap();
|
||||
(crate::ember::Goal::parse(&s.tune_goal), s.tune_climb)
|
||||
// the card's own goal when set, else the global one
|
||||
(crate::ember::goal_for(s.cards.get(&c.key).map(|p| p.tune_goal.as_str()).unwrap_or(""), &s.tune_goal), s.tune_climb)
|
||||
};
|
||||
let plan = if let Some(why) = control.as_ref() {
|
||||
if let Some(cc) = self.st().mining.cards.get_mut(idx) {
|
||||
|
|
@ -2485,7 +2693,7 @@ impl Engine {
|
|||
// under a scratch IGNEUM_APP_DATA), no prompt
|
||||
let hdir = crate::powertask::helper_dir();
|
||||
let _ = std::fs::create_dir_all(&hdir);
|
||||
let _ = std::fs::write(hdir.join("cmd.txt"), format!("{} dev {}\n", crate::platform::unix_now() % 1_000_000, c.device));
|
||||
let _ = std::fs::write(hdir.join("cmd.txt"), format!("{} dev {}\n", crate::powertask::wire_seq(), c.device));
|
||||
crate::powertask::start()?;
|
||||
self.shared.log("tune helper: the Igneum Power Helper task (no prompt)");
|
||||
self.sweep_helper = true;
|
||||
|
|
@ -2566,17 +2774,51 @@ impl Engine {
|
|||
return;
|
||||
}
|
||||
let dev = device.to_string();
|
||||
let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n{seq}3 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }, if mem > 0 { format!("lmc {mem}") } else { "rmc".to_string() });
|
||||
let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), cmd);
|
||||
// the helper polls twice a second and nvidia-smi answers within a second or two
|
||||
// (F) the wire numbers come from the one monotonic space every writer uses (powertask::wire_seq);
|
||||
// `seq` (the run's request index) stays the engine's own id for the acknowledgement
|
||||
let wire = crate::powertask::wire_seq();
|
||||
let cmd = format!("{} dev {dev}\n{} pl {w}\n{} {}\n{} {}\n", wire, wire + 1, wire + 2, if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }, wire + 3, if mem > 0 { format!("lmc {mem}") } else { "rmc".to_string() });
|
||||
let dir = self.helper_cmd_dir();
|
||||
let is_task = self.sweep_helper_is_task;
|
||||
// 6 October 2026, PC 1: (B) the task's helper had exited while the engine's flag still said it ran,
|
||||
// so commands went to nobody and a blind 4-second sleep called them acknowledged; (C) the first
|
||||
// request of the first tune was written in the same second the helper started, and the helper
|
||||
// skipped it as stale. Now, in the task case: the helper's heartbeat says whether it runs; the task
|
||||
// is started and a fresh heartbeat awaited BEFORE the command is written; and the acknowledgement
|
||||
// is the helper's own log line for this sequence (`<seq>3 nvidia-smi ...`), read for up to 15 s;
|
||||
// a missing line is a failure with that reason
|
||||
let want = format!(" {} nvidia-smi", wire + 3);
|
||||
let log_file = dir.join("helper.log");
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_secs(4));
|
||||
shared.send(Cmd::TuneSet(seq, Ok("helper".into())));
|
||||
if is_task {
|
||||
if let Err(e) = crate::powertask::ensure_running(&dir, Duration::from_secs(12)) {
|
||||
shared.send(Cmd::TuneSet(seq, Err(e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let _ = std::fs::write(dir.join("cmd.txt"), cmd);
|
||||
let until = Instant::now() + Duration::from_secs(if is_task { 15 } else { 6 });
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
let text = std::fs::read_to_string(&log_file).unwrap_or_default();
|
||||
if let Some(line) = text.lines().rev().find(|l| l.contains(&want)) {
|
||||
shared.send(Cmd::TuneSet(seq, Ok(format!("helper: {}", line.trim()))));
|
||||
return;
|
||||
}
|
||||
if Instant::now() >= until {
|
||||
shared.send(Cmd::TuneSet(seq, Err(format!("the helper did not run sequence {seq} within {} s (no line in {})", if is_task { 15 } else { 6 }, log_file.display()))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
"amd" => {
|
||||
let Some(exe) = self.bins.telemetry.clone() else { return };
|
||||
if c.amd_ordinal < 0 {
|
||||
// (D) 6 October 2026, PC 1 17:57Z: a measure-only card (the probe gave no tune line) was still sent a
|
||||
// set, which the helper refused and the tune stopped; measure only means nothing is set, the run
|
||||
// notices the missing acknowledgement and measures
|
||||
if !crate::ember::may_set("amd", c.tune_control, c.amd_ordinal) {
|
||||
return;
|
||||
}
|
||||
let n = c.amd_ordinal.to_string();
|
||||
|
|
@ -2789,9 +3031,21 @@ impl Engine {
|
|||
e.sweep_mhs = row.mhs;
|
||||
e.sweep_clock_mhz = row.point.clock_mhz;
|
||||
e.sweep_mem_mhz = row.point.mem_mhz;
|
||||
// Miner UI 4: the row's sub-line reads "(the ladder's floor)" when the chosen clock is the lowest step
|
||||
let floor_point = row.point.clock_mhz > 0 && row.point.clock_mhz <= run.plan.limits.clock_floor();
|
||||
e.sweep_floor = floor_point;
|
||||
c.tune_floor = floor_point;
|
||||
e.sweep_driver = c.driver.clone();
|
||||
e.sweep_class = c.program_class.clone();
|
||||
e.sweep_source = kind.name().into();
|
||||
if kind == crate::ember::PlanKind::Full {
|
||||
if let Some(b) = run.rows.first() {
|
||||
e.sweep_before_watts = b.watts;
|
||||
e.sweep_before_mhs = b.mhs;
|
||||
c.tune_before_watts = b.watts;
|
||||
c.tune_before_mhs = b.mhs;
|
||||
}
|
||||
}
|
||||
if !c.pinned && kind != crate::ember::PlanKind::Baseline {
|
||||
e.power_pct = row.point.power_pct;
|
||||
}
|
||||
|
|
@ -2907,6 +3161,12 @@ impl Engine {
|
|||
self.last_awake = now;
|
||||
crate::platform::keep_awake_tick();
|
||||
}
|
||||
if self.running && (self.node_external || self.node.is_some()) && now.duration_since(self.node_info_at) >= Duration::from_secs(30) {
|
||||
self.node_info_at = now;
|
||||
let shared = self.shared.clone();
|
||||
let evm_port = self.shared.runtime.evm_port();
|
||||
std::thread::spawn(move || { let c = crate::extnode::probe(evm_port); if c.digest.is_some() || c.version.is_some() { shared.send(Cmd::NodeInfo(c)); } });
|
||||
}
|
||||
if self.running {
|
||||
self.tick_node(now);
|
||||
self.tick_watch(now);
|
||||
|
|
@ -2927,6 +3187,7 @@ impl Engine {
|
|||
self.tick_sweep(now);
|
||||
}
|
||||
self.derive(now);
|
||||
self.tick_balance(now);
|
||||
if now.duration_since(self.last_status) >= Duration::from_secs(self.shared.runtime.status_secs as u64) {
|
||||
self.last_status = now;
|
||||
if self.running {
|
||||
|
|
@ -2975,9 +3236,14 @@ impl Engine {
|
|||
let st = self.st();
|
||||
crate::ota::Ctx {
|
||||
node_synced: st.node.synced && st.clock.severity != "block",
|
||||
// Horizon frontier lane: finality paused = a synced node with no checkpoint lock for FINALITY_PAUSE_S
|
||||
// (the last LOCK line's age; or, when none was ever seen this run, the engine's own uptime)
|
||||
finality_paused: st.finality.paused,
|
||||
boundary_eta_s: if st.node.daa > 0 && st.program.boundary_daa > 0 { Some(st.program.eta_s) } else { None },
|
||||
// a remote job in progress counts as busy: no update applies under it (src/jobrun.rs)
|
||||
miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting") || self.jobs.active(),
|
||||
miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting"),
|
||||
// a remote job in progress holds the update, urgent or not (src/manifest.rs safe_to_apply; PC 1, 6 October 2026)
|
||||
job_active: self.jobs.active(),
|
||||
daa: st.node.daa,
|
||||
}
|
||||
};
|
||||
|
|
@ -3155,12 +3421,24 @@ impl Engine {
|
|||
}
|
||||
|
||||
fn tick_node(&mut self, now: Instant) {
|
||||
if self.node_external {
|
||||
if !port_open(self.shared.runtime.rpc_port) && now.duration_since(self.last_sync_check) >= Duration::from_secs(5) {
|
||||
if self.node_external || self.node_refused {
|
||||
// re-decided every 5 s: the other node leaving hands the ports to this app after the wait (7 October 2026)
|
||||
if now.duration_since(self.last_sync_check) >= Duration::from_secs(5) {
|
||||
self.last_sync_check = now;
|
||||
let port = self.shared.runtime.rpc_port;
|
||||
let first = self.external_gone_since.is_none();
|
||||
match crate::extnode::step(port_open(port), self.secs(now), &mut self.external_gone_since) {
|
||||
crate::extnode::Step::Stay => {}
|
||||
crate::extnode::Step::Gone { for_s } => {
|
||||
let left = (crate::extnode::TAKEOVER_WAIT_S - for_s).max(0.0) as u64;
|
||||
if first && self.node_external { self.shared.event("info", &format!("The other node on port {port} went away; if it stays away, this app starts its own node in {left} s")); }
|
||||
let mut st = self.st();
|
||||
st.node.state = "stopped".into();
|
||||
st.node.message = "the external node went away".into();
|
||||
st.node.synced = false;
|
||||
st.node.message = format!("the other node on port {port} went away; this app starts its own node in {left} s unless it comes back");
|
||||
}
|
||||
crate::extnode::Step::TakeOver => self.take_over_ports(),
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -3306,6 +3584,30 @@ impl Engine {
|
|||
} else if verdict != crate::watchdog::Action::None {
|
||||
// exit 43: the miner gave up on its worker; once more, then the card is faulted
|
||||
self.watchdog_verdict(i, verdict, &tail);
|
||||
} else if is_stall_exit(code, &tail) {
|
||||
// N4: the miner found no new template for its stall span and stopped hashing and voting. Once:
|
||||
// the miner restarts and the node gets another look. Twice since the node started: the node
|
||||
// itself restarts and re-dials its peers (the seed list), because the miner alone cannot fix a
|
||||
// node that fell off the network.
|
||||
let n = { let mut st = self.st(); st.node.stall_exits += 1; st.node.stall_exits };
|
||||
let label = self.miners[i].label.clone();
|
||||
for l in &tail {
|
||||
self.shared.log(&format!(" miner: {l}"));
|
||||
}
|
||||
if n >= 2 {
|
||||
self.shared.event("error", &format!("{label}: no new block to work on, twice; the node restarts and re-dials its peers"));
|
||||
self.st().node.stall_exits = 0;
|
||||
self.restart_node("the miner saw no new block twice", Duration::from_secs(2));
|
||||
} else {
|
||||
let delay = jitter_secs(crate::platform::unix_now() + i as u64);
|
||||
self.miners[i].restart_at = Some(now + Duration::from_secs(delay));
|
||||
self.shared.event("error", &format!("{label}: no new block to work on for the stall span; the miner restarts in {delay} s (a second stall restarts the node)"));
|
||||
if let Some(c) = self.st().mining.cards.get_mut(card_idx) {
|
||||
c.state = "restarting".into();
|
||||
c.hash_now = 0.0;
|
||||
c.message = "no new block to work on; waiting for the node".into();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.miners[i].restarts += 1;
|
||||
let delay = jitter_secs(crate::platform::unix_now() + i as u64);
|
||||
|
|
@ -3461,6 +3763,32 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
|
||||
/// Miner UI 4: the payout address's balance through the node's own RPC, every 30 s while the node answers.
|
||||
fn tick_balance(&mut self, now: Instant) {
|
||||
if self.balance_busy || self.balance_next.map(|t| now < t).unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
let (addr, node_up) = {
|
||||
let st = self.st();
|
||||
(st.address.value.clone(), matches!(st.node.state.as_str(), "syncing" | "synced"))
|
||||
};
|
||||
if addr.len() != 42 || !node_up {
|
||||
self.balance_next = Some(now + Duration::from_secs(10));
|
||||
return;
|
||||
}
|
||||
self.balance_busy = true;
|
||||
self.balance_next = Some(now + Duration::from_secs(30));
|
||||
let shared = self.shared.clone();
|
||||
std::thread::spawn(move || {
|
||||
let r = crate::prover::evm_rpc(&shared, "eth_getBalance", serde_json::json!([addr, "latest"]), Duration::from_secs(8)).and_then(|v| v.as_str().map(|s| s.to_string()).ok_or_else(|| "eth_getBalance: not a string".to_string()));
|
||||
shared.send(Cmd::BalanceRead(r));
|
||||
// the same 30-s cadence carries the node's finality fields (the node lane, 0.3.16); a node without them
|
||||
// answers without the keys and the engine's own rule stands
|
||||
let f = crate::prover::evm_rpc(&shared, "igneum_getProvingStatus", serde_json::json!([]), Duration::from_secs(8)).ok().and_then(|v| crate::ember::parse_finality_status(&v));
|
||||
shared.send(Cmd::FinalityStatus(f));
|
||||
});
|
||||
}
|
||||
|
||||
/// Derived fields: ages, the hash total, the program countdown, the finality age, the mining state word.
|
||||
fn derive(&mut self, now: Instant) {
|
||||
let unix = crate::platform::unix_now_f();
|
||||
|
|
@ -3475,6 +3803,10 @@ impl Engine {
|
|||
c.eff_mhw = if c.state == "mining" && c.power_w > 1.0 && c.hash_now > 0.0 && unix - c.telemetry_at < 60.0 { c.hash_now / c.power_w } else { 0.0 };
|
||||
}
|
||||
st.mining.hash_total = total;
|
||||
// Miner UI 4: the fleet's draw and its £ a day (settings.power_price_pence; 0 when no price is set)
|
||||
st.mining.watts_total = crate::ember::fleet_watts(st.mining.cards.iter().map(|c| (c.state.as_str(), c.power_w, c.telemetry_at)), unix, 60.0);
|
||||
st.mining.pounds_per_day = if st.settings.power_price_pence > 0.0 { crate::ember::pounds_per_day(st.mining.watts_total, st.settings.power_price_pence) } else { 0.0 };
|
||||
st.address.balance_age_s = if self.balance_at > 0.0 { unix - self.balance_at } else { -1.0 };
|
||||
crate::hotplug::age(&mut st.mining.cards, unix);
|
||||
let cut = unix - 3600.0;
|
||||
st.mining.found.retain(|t| *t > cut);
|
||||
|
|
@ -3496,6 +3828,22 @@ impl Engine {
|
|||
st.finality.age_s = unix - st.finality.last_lock_at;
|
||||
st.finality.message = String::new();
|
||||
}
|
||||
// Horizon polish Q83/Q84: finality paused = a synced node with no checkpoint lock for FINALITY_PAUSE_S (the
|
||||
// last lock's age, else the engine's uptime); since the last lock (else the start); one sentence everywhere
|
||||
let gap = if st.finality.last_lock > 0 { st.finality.age_s } else { st.uptime_s as f64 };
|
||||
// the node's own word wins when it carries one (0.3.16, finalityActive); else the engine's 15-minute rule
|
||||
let paused = match st.finality.node_active {
|
||||
Some(active) => !active,
|
||||
None => st.node.synced && gap > crate::manifest::FINALITY_PAUSE_S,
|
||||
};
|
||||
if paused && !st.finality.paused && st.finality.cause_source != "node" {
|
||||
st.finality.paused_since = if st.finality.last_lock > 0 { st.finality.last_lock_at } else { unix - st.uptime_s as f64 };
|
||||
}
|
||||
st.finality.paused = paused;
|
||||
st.finality.line = if paused { crate::ember::finality_paused_line(st.finality.paused_since, &st.finality.reason, &st.finality.held_by) } else { String::new() };
|
||||
if paused {
|
||||
st.finality.message = st.finality.line.clone();
|
||||
}
|
||||
if self.running {
|
||||
let mining_now = st.mining.cards.iter().any(|c| c.state == "mining");
|
||||
let any_slot = !self.miners.is_empty();
|
||||
|
|
@ -3545,6 +3893,7 @@ impl Engine {
|
|||
if st.node.consensus_digest != d {
|
||||
st.node.consensus_digest = d;
|
||||
}
|
||||
if st.node.digest_source.is_empty() { st.node.digest_source = "log".into(); }
|
||||
return;
|
||||
}
|
||||
if !text.contains("too far into the future") {
|
||||
|
|
@ -3650,6 +3999,8 @@ impl Engine {
|
|||
let blue = kv_u64(text, "blue").unwrap_or(0);
|
||||
let difficulty = kv_f64(text, "difficulty").unwrap_or(0.0);
|
||||
let flag = kv(text, "synced").map(|s| s == "true");
|
||||
// N4: the sink header's age from the node (ca3-v4-0316 appends tip_age_s after synced=); -1 on an older node
|
||||
let tip_age_s = kv(text, "tip_age_s").and_then(|s| s.parse::<f64>().ok()).unwrap_or(-1.0);
|
||||
let was_synced = self.st().node.synced;
|
||||
// Derived from every reading, never a one-shot transition (PC 2, 4 October 2026: the node caught up after a
|
||||
// clock fix and mined at 118 MH/s while the card still said "syncing", because getInfo's flag stayed false).
|
||||
|
|
@ -3662,7 +4013,7 @@ impl Engine {
|
|||
let stable_s = self.sync_stable_since.map(|t| now.duration_since(t).as_secs_f64()).unwrap_or(0.0);
|
||||
let accepted_recent = self.last_accepted.map(|t| now.duration_since(t) <= Duration::from_secs(60)).unwrap_or(false);
|
||||
let private = self.shared.runtime.unsynced_mining && self.shared.runtime.peers.is_empty();
|
||||
let synced = sync_decision(&Reading { blocks, headers, peers, flag }, caught_up_moving, stable_s, accepted_recent, private);
|
||||
let (synced, cause) = sync_decision_v2(&Reading { blocks, headers, peers, flag }, caught_up_moving, stable_s, accepted_recent, private, tip_age_s);
|
||||
self.sync_prev = Some(blocks);
|
||||
// the first clock source: local time against the latest block the peers produced, once blocks arrive
|
||||
if blocks > 0 && (peers > 0 || self.shared.runtime.peers.is_empty()) && now.duration_since(self.clock_last_sample) >= Duration::from_secs(9) {
|
||||
|
|
@ -3685,11 +4036,17 @@ impl Engine {
|
|||
st.node.blue = blue;
|
||||
st.node.difficulty = difficulty;
|
||||
st.node.synced = synced;
|
||||
st.node.state = if synced { "synced".into() } else { "syncing".into() };
|
||||
st.node.tip_age_s = tip_age_s;
|
||||
st.node.sync_cause = cause.to_string();
|
||||
// the state word: "behind" for a frozen tip, "no peers" for none, "syncing" for the rest (N4: never
|
||||
// "synced" on a tip that stopped moving)
|
||||
st.node.state = if synced { "synced".into() } else if cause == "frozen" { "behind".into() } else if cause == "no peers" { "no peers".into() } else { "syncing".into() };
|
||||
st.node.message = if synced {
|
||||
String::new()
|
||||
} else if cause == "frozen" {
|
||||
format!("no new block for {} s with {peers} peer{}; the chain may have moved on without this node", tip_age_s.round() as i64, if peers == 1 { "" } else { "s" })
|
||||
} else if peers == 0 {
|
||||
"waiting for a peer".into()
|
||||
"waiting for a peer on our chain".into()
|
||||
} else if headers > blocks + 2 {
|
||||
format!("{blocks} of {headers} blocks")
|
||||
} else {
|
||||
|
|
@ -3901,6 +4258,18 @@ impl Engine {
|
|||
st.finality.age_s = 0.0;
|
||||
}
|
||||
}
|
||||
} else if text.contains("finality_reason=") {
|
||||
// the node lane's pause line (0.3.16): the cause and who holds it, shown in the one sentence
|
||||
if let Some((reason, held_by)) = crate::ember::parse_finality_line(text) {
|
||||
let mut st = self.st();
|
||||
st.finality.reason = reason;
|
||||
st.finality.held_by = held_by;
|
||||
st.finality.cause_source = "node-line".into();
|
||||
}
|
||||
} else if text.contains("finality resumed") {
|
||||
let mut st = self.st();
|
||||
st.finality.reason.clear();
|
||||
st.finality.held_by.clear();
|
||||
} else if text.contains(" VOTE index=") {
|
||||
self.st().finality.votes += 1;
|
||||
} else if text.contains("worker could not prepare") || text.contains(" prepare-failed ") {
|
||||
|
|
@ -4227,6 +4596,38 @@ pub fn sync_decision(r: &Reading, moving: bool, stable_s: f64, accepted_recent:
|
|||
caught_up && (moving || stable_s >= 60.0)
|
||||
}
|
||||
|
||||
/// The tip age at which a node that still says "synced" is treated as frozen (ledger N4, 6 October 2026: a home
|
||||
/// miner's one same-digest peer relayed nothing for 53 minutes while the app read "mining, synced").
|
||||
pub const TIP_FROZEN_S: f64 = 120.0;
|
||||
|
||||
/// N4: what "synced" means on every surface. The old decision, AND the tip moved in the last 120 s (the watch
|
||||
/// line's tip_age_s, the sink header's age; -1 = the node does not report it yet, which gates nothing so an older node
|
||||
/// keeps today's words), AND at least one peer (the handshake refuses a peer with another digest, so `peers` already
|
||||
/// counts peers on our chain). Returns (synced, cause): the cause is "" when synced, else "frozen" (a peer, caught up,
|
||||
/// no new block for over 120 s), "no peers", "syncing", or "behind" (the node's own flag false while the count moves).
|
||||
pub fn sync_decision_v2(r: &Reading, moving: bool, stable_s: f64, accepted_recent: bool, private: bool, tip_age_s: f64) -> (bool, &'static str) {
|
||||
if private {
|
||||
return (true, "");
|
||||
}
|
||||
let frozen = tip_age_s > TIP_FROZEN_S && !accepted_recent;
|
||||
if r.peers == 0 && !accepted_recent {
|
||||
return (false, "no peers");
|
||||
}
|
||||
if frozen {
|
||||
return (false, "frozen");
|
||||
}
|
||||
if sync_decision(r, moving, stable_s, accepted_recent, private) {
|
||||
return (true, "");
|
||||
}
|
||||
(false, if r.headers > r.blocks + 2 { "syncing" } else { "behind" })
|
||||
}
|
||||
|
||||
/// The miner's stall exit (the node lane, ca3-v4-0316): code 45, or a tail line starting "STALLED '".
|
||||
pub const STALL_EXIT_CODE: i32 = 45;
|
||||
pub fn is_stall_exit(code: i32, tail: &[String]) -> bool {
|
||||
code == STALL_EXIT_CODE || tail.iter().any(|l| l.contains(" STALLED '") || l.starts_with("STALLED '"))
|
||||
}
|
||||
|
||||
/// "...block timestamp is 1791112663000 but maximum timestamp allowed is 1791112600000" -> at least 63 s behind.
|
||||
fn behind_from_warning(text: &str) -> Option<f64> {
|
||||
let nums: Vec<f64> = text.split(|c: char| !c.is_ascii_digit()).filter(|s| s.len() >= 12).filter_map(|s| s.parse::<f64>().ok()).collect();
|
||||
|
|
@ -4385,7 +4786,7 @@ mod tests {
|
|||
assert_eq!(super::power_control_after_prompt(&Err("the elevated step exited with code 2".into())), None);
|
||||
}
|
||||
|
||||
use super::{digest_from_line, parse_race, switches_of, sync_decision, Reading};
|
||||
use super::{is_stall_exit, sync_decision_v2, digest_from_line, parse_race, switches_of, sync_decision, Reading};
|
||||
|
||||
#[test]
|
||||
fn digest_comes_from_the_nodes_own_line_only() {
|
||||
|
|
@ -4457,6 +4858,44 @@ mod tests {
|
|||
assert!(sync_decision(&r(0, 0, 0, None), false, 0.0, false, true));
|
||||
}
|
||||
|
||||
/// Ledger N4 (6 October 2026): a home miner's node kept one same-digest peer that relayed nothing for 53 minutes;
|
||||
/// today's rule said "synced" the whole time. The known-failed case first, then the fix.
|
||||
#[test]
|
||||
fn n4_a_frozen_tip_is_never_synced() {
|
||||
// the frozen tip: caught up, one peer, the count stable for an hour, getInfo's flag true. Today: synced.
|
||||
let frozen = r(133_000, 133_000, 1, Some(true));
|
||||
assert!(sync_decision(&frozen, false, 3_180.0, false, false), "the old rule says synced on the frozen tip (the N4 failure)");
|
||||
// the fix: the tip age gates it
|
||||
let (ok, cause) = sync_decision_v2(&frozen, false, 3_180.0, false, false, 3_180.0);
|
||||
assert!(!ok);
|
||||
assert_eq!(cause, "frozen");
|
||||
// the same node moving: synced
|
||||
assert_eq!(sync_decision_v2(&frozen, true, 0.0, false, false, 4.0), (true, ""));
|
||||
// exactly at the bound is still synced; one second over is not
|
||||
assert_eq!(sync_decision_v2(&frozen, false, 60.0, false, false, 120.0), (true, ""));
|
||||
assert_eq!(sync_decision_v2(&frozen, false, 60.0, false, false, 121.0), (false, "frozen"));
|
||||
// an older node that does not report the age (-1) keeps today's words
|
||||
assert_eq!(sync_decision_v2(&frozen, false, 60.0, false, false, -1.0), (true, ""));
|
||||
// our own block accepted in the last minute proves the tip moves, whatever the reported age
|
||||
assert_eq!(sync_decision_v2(&frozen, false, 60.0, true, false, 900.0), (true, ""));
|
||||
// no peer at all: "no peers", not "frozen" and never synced
|
||||
assert_eq!(sync_decision_v2(&r(133_000, 133_000, 0, Some(true)), false, 600.0, false, false, 500.0), (false, "no peers"));
|
||||
// still downloading: "syncing"
|
||||
assert_eq!(sync_decision_v2(&r(100, 700, 1, Some(false)), true, 0.0, false, false, 2.0), (false, "syncing"));
|
||||
// caught up but the flag false and the count not yet stable: "behind"
|
||||
assert_eq!(sync_decision_v2(&r(700, 700, 1, Some(false)), false, 10.0, false, false, 5.0), (false, "behind"));
|
||||
// a private test node is always synced
|
||||
assert_eq!(sync_decision_v2(&r(0, 0, 0, None), false, 0.0, false, true, 9_999.0), (true, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn n4_the_stall_exit_is_code_45_or_the_stalled_line() {
|
||||
assert!(is_stall_exit(45, &[]));
|
||||
assert!(is_stall_exit(1, &["1791327000.123 STALLED 'nvidia-1ccfe586-1': no new template for 300 s (tip d3dc2f78, daa 197752); the node has fallen off the network or stopped moving; hashing and voting paused, exiting with code 45 so the launcher restarts (and after a second stall, restarts the node)".to_string()]));
|
||||
assert!(!is_stall_exit(1, &["worker fault: rejected nonce".to_string()]));
|
||||
assert!(!is_stall_exit(42, &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_warning() {
|
||||
let l = "2026-10-04 11:02:11.123+00:00 [WARN ] HandleRelayInvsFlow flow error: the block timestamp is too far into the future: block timestamp is 1791112663000 but maximum timestamp allowed is 1791112600000";
|
||||
|
|
|
|||
243
app/igneum-app/src/extnode.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
//! Another node on this machine (publish 2's read-back, 6 October 2026): the hand node on the Mac held 26610/26611,
|
||||
//! the app's default RPC and p2p ports, the app read its state as its own and the digest field read empty. Now the
|
||||
//! collision is loud on every surface and checked: the other node's chain id (eth_chainId on the EVM port) and its
|
||||
//! consensus overrides (`igneum_getNodeInfo`, the 0.3.17 node lane; "method not found" on an older node) against the
|
||||
//! override file the app would start its own node with. A match: "Another node holds port N on this machine; using
|
||||
//! it". A mismatch: "Node not started: port N is taken" and the app does not read that node. Unknown (an older node
|
||||
//! that cannot answer): used, and said so. The same RPC gives the digest, so api/state carries it for an external node
|
||||
//! too, read every 30 s instead of parsed from a stdout the app does not own.
|
||||
use serde_json::{json, Value};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
/// What the other node answered: None where it could not answer.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Check {
|
||||
pub chain_id: Option<u64>,
|
||||
pub digest: Option<String>,
|
||||
pub network: Option<String>,
|
||||
pub version: Option<String>,
|
||||
/// the node's consensus params as it resolved them (`params` in igneum_getNodeInfo, every field by its override-file name)
|
||||
pub overrides: Option<Value>,
|
||||
/// "igneum-pow" or "stub" (ledger N5: a node built without the mining engine refuses every real block)
|
||||
pub pow_engine: Option<String>,
|
||||
}
|
||||
|
||||
/// The decision for the port-collision path.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Decision {
|
||||
pub use_it: bool,
|
||||
/// match | mismatch | unknown
|
||||
pub verdict: &'static str,
|
||||
/// the event line, in a user's words
|
||||
pub line: String,
|
||||
}
|
||||
|
||||
/// Compare the other node with ours: `ours` is the override file the app would start its own node with (the
|
||||
/// `*_activation_daa` and friends), `our_chain` the chain id this app's network uses when known.
|
||||
pub fn decide(port: u16, ours: Option<&Value>, our_chain: Option<u64>, theirs: &Check) -> Decision { decide_on(port, ours, our_chain, None, theirs) }
|
||||
|
||||
/// `decide` with the network name too (devnet | simnet | testnet): the node lane's rule is that `network` must be equal.
|
||||
pub fn decide_on(port: u16, ours: Option<&Value>, our_chain: Option<u64>, our_network: Option<&str>, theirs: &Check) -> Decision {
|
||||
let mut checked = false;
|
||||
if let (Some(a), Some(b)) = (our_network.filter(|s| !s.is_empty()), theirs.network.as_deref().filter(|s| !s.is_empty())) {
|
||||
checked = true;
|
||||
if a != b {
|
||||
return Decision { use_it: false, verdict: "mismatch", line: format!("Node not started: port {port} is taken by a node on another network ({b}, ours {a})") };
|
||||
}
|
||||
}
|
||||
if let (Some(a), Some(b)) = (our_chain, theirs.chain_id) {
|
||||
checked = true;
|
||||
if a != b {
|
||||
return Decision { use_it: false, verdict: "mismatch", line: format!("Node not started: port {port} is taken by a node on another network (chain id {b}, ours {a})") };
|
||||
}
|
||||
}
|
||||
if theirs.pow_engine.as_deref() == Some("stub") {
|
||||
return Decision { use_it: false, verdict: "mismatch", line: format!("Node not started: port {port} is taken by a node built without the mining engine (stub), which refuses every real block") };
|
||||
}
|
||||
// every key the manifest sets, against the node's resolved params (the node lane: compare values, never recompute the hash)
|
||||
if let (Some(o), Some(t)) = (ours.and_then(|v| v.as_object()), theirs.overrides.as_ref().and_then(|v| v.as_object())) {
|
||||
checked = true;
|
||||
for (k, v) in o {
|
||||
if t.get(k) != Some(v) {
|
||||
let theirs_v = t.get(k).map(|x| x.to_string()).unwrap_or_else(|| "none".into());
|
||||
return Decision { use_it: false, verdict: "mismatch", line: format!("Node not started: port {port} is taken by a node on other rules ({k} {theirs_v}, ours {v})") };
|
||||
}
|
||||
}
|
||||
}
|
||||
if checked {
|
||||
Decision { use_it: true, verdict: "match", line: format!("Another node holds port {port} on this machine; using it (same network and rules; it is not stopped by this app)") }
|
||||
} else {
|
||||
Decision { use_it: true, verdict: "unknown", line: format!("Another node holds port {port} on this machine; using it. Its rules could not be checked (it predates 0.3.17), so the app reads it as it is") }
|
||||
}
|
||||
}
|
||||
|
||||
/// How long the app waits after another node leaves its ports before starting its own (the Mac, 7 October 2026: the
|
||||
/// hand node left at 00:18 UK and the app sat on "node stopped" all night, since the mode was decided once at launch).
|
||||
pub const TAKEOVER_WAIT_S: f64 = 60.0;
|
||||
|
||||
/// What the app does about ports another node held, re-checked while attached (external) or refused (none).
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Step {
|
||||
/// the other node still answers on the port
|
||||
Stay,
|
||||
/// the port has been closed for `for_s` seconds; the app waits out TAKEOVER_WAIT_S in case it comes back
|
||||
Gone { for_s: f64 },
|
||||
/// the port stayed closed for TAKEOVER_WAIT_S: start our own node on the freed ports
|
||||
TakeOver,
|
||||
}
|
||||
|
||||
/// `port_open` is whether the other node's RPC port answers now, `gone_since` the app's memory of when it stopped
|
||||
/// answering (None while it answers). A node that comes back inside the wait is kept; the wait starts over.
|
||||
pub fn step(port_open: bool, now_s: f64, gone_since: &mut Option<f64>) -> Step {
|
||||
if port_open {
|
||||
*gone_since = None;
|
||||
return Step::Stay;
|
||||
}
|
||||
let since = *gone_since.get_or_insert(now_s);
|
||||
let for_s = now_s - since;
|
||||
if for_s >= TAKEOVER_WAIT_S {
|
||||
*gone_since = None;
|
||||
Step::TakeOver
|
||||
} else {
|
||||
Step::Gone { for_s }
|
||||
}
|
||||
}
|
||||
|
||||
/// One JSON-RPC call to the node's EVM port through curl (the engine carries no HTTP client; update.rs does the same).
|
||||
pub fn rpc(evm_port: u16, method: &str, params: Value, limit: Duration) -> Option<Value> {
|
||||
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
|
||||
let out = crate::detect::run_timeout(
|
||||
Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", &format!("{}", limit.as_secs().max(1)), "-X", "POST", &format!("http://127.0.0.1:{evm_port}"), "-H", "Content-Type: application/json", "-d", &body]),
|
||||
None,
|
||||
limit + Duration::from_secs(2),
|
||||
)?;
|
||||
let v: Value = serde_json::from_str(&out).ok()?;
|
||||
v.get("result").cloned().filter(|r| !r.is_null())
|
||||
}
|
||||
|
||||
/// The other node's answers, with what each method gives: eth_chainId (every node), igneum_getNodeInfo (0.3.17).
|
||||
pub fn probe(evm_port: u16) -> Check {
|
||||
let mut c = Check::default();
|
||||
if let Some(Value::String(h)) = rpc(evm_port, "eth_chainId", json!([]), Duration::from_secs(4)) {
|
||||
c.chain_id = u64::from_str_radix(h.trim_start_matches("0x"), 16).ok();
|
||||
}
|
||||
if let Some(info) = rpc(evm_port, "igneum_getNodeInfo", json!([]), Duration::from_secs(4)) {
|
||||
c.digest = info.get("digest").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
c.network = info.get("network").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
c.version = info.get("version").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
c.overrides = info.get("params").cloned().filter(|v| v.is_object());
|
||||
c.pow_engine = info.get("powEngine").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Keep the newest `keep` files named `<prefix>...` in `dir`; the rest go (the node and miner logs grow one per start).
|
||||
pub fn prune_logs(dir: &Path, prefix: &str, keep: usize) -> usize {
|
||||
let Ok(rd) = std::fs::read_dir(dir) else { return 0 };
|
||||
let mut files: Vec<(std::time::SystemTime, std::path::PathBuf)> = rd
|
||||
.flatten()
|
||||
.filter(|e| e.file_name().to_string_lossy().starts_with(prefix) && e.path().extension().map(|x| x == "log").unwrap_or(false))
|
||||
.filter_map(|e| e.metadata().ok().and_then(|m| m.modified().ok()).map(|t| (t, e.path())))
|
||||
.collect();
|
||||
files.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
let mut removed = 0;
|
||||
for (_, p) in files.into_iter().skip(keep) {
|
||||
if std::fs::remove_file(&p).is_ok() {
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ours() -> Value { json!({ "difficulty_v2_activation_daa": 33000, "fees_v1_activation_daa": 210000, "exec_restart_number": 27276, "exec_restart_hash": "bb45cf0d" }) }
|
||||
|
||||
/// The Mac, 6 October 2026: the hand node held the ports; the app said nothing and read it as its own.
|
||||
#[test]
|
||||
fn a_node_on_our_rules_is_used_and_said_so() {
|
||||
let theirs = Check { chain_id: Some(7_777), overrides: Some(ours()), digest: Some("1f4b".into()), ..Default::default() };
|
||||
let d = decide(26611, Some(&ours()), Some(7_777), &theirs);
|
||||
assert!(d.use_it);
|
||||
assert_eq!(d.verdict, "match");
|
||||
assert_eq!(d.line, "Another node holds port 26611 on this machine; using it (same network and rules; it is not stopped by this app)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_node_on_another_network_or_other_rules_is_refused() {
|
||||
let other_chain = Check { chain_id: Some(7_778), ..Default::default() };
|
||||
let d = decide(26611, Some(&ours()), Some(7_777), &other_chain);
|
||||
assert!(!d.use_it);
|
||||
assert_eq!(d.verdict, "mismatch");
|
||||
assert_eq!(d.line, "Node not started: port 26611 is taken by a node on another network (chain id 7778, ours 7777)");
|
||||
let mut theirs = ours(); theirs["fees_v1_activation_daa"] = json!(200000);
|
||||
let d = decide(26611, Some(&ours()), None, &Check { overrides: Some(theirs), ..Default::default() });
|
||||
assert!(!d.use_it);
|
||||
assert_eq!(d.line, "Node not started: port 26611 is taken by a node on other rules (fees_v1_activation_daa 200000, ours 210000)");
|
||||
// a stub engine is refused whatever else matches
|
||||
assert!(!decide(26611, Some(&ours()), None, &Check { overrides: Some(ours()), pow_engine: Some("stub".into()), ..Default::default() }).use_it);
|
||||
// an override we have that the other node lacks is a mismatch too
|
||||
let d = decide(26611, Some(&ours()), None, &Check { overrides: Some(json!({ "difficulty_v2_activation_daa": 33000 })), ..Default::default() });
|
||||
assert!(!d.use_it);
|
||||
assert!(d.line.contains("exec_restart_hash none"), "{}", d.line);
|
||||
// the network name (node lane, igneum_getNodeInfo.network) must be equal
|
||||
let d = decide_on(26611, Some(&ours()), None, Some("testnet"), &Check { network: Some("devnet".into()), overrides: Some(ours()), ..Default::default() });
|
||||
assert!(!d.use_it);
|
||||
assert_eq!(d.line, "Node not started: port 26611 is taken by a node on another network (devnet, ours testnet)");
|
||||
assert!(decide_on(26611, Some(&ours()), None, Some("testnet"), &Check { network: Some("testnet".into()), overrides: Some(ours()), ..Default::default() }).use_it);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_older_node_that_cannot_answer_is_used_and_marked_unknown() {
|
||||
let d = decide(26611, Some(&ours()), None, &Check::default());
|
||||
assert!(d.use_it);
|
||||
assert_eq!(d.verdict, "unknown");
|
||||
assert!(d.line.starts_with("Another node holds port 26611 on this machine; using it. Its rules could not be checked"));
|
||||
// a chain id alone, matching, is a check
|
||||
assert_eq!(decide(26611, None, Some(1), &Check { chain_id: Some(1), ..Default::default() }).verdict, "match");
|
||||
}
|
||||
|
||||
/// The Mac, 7 October 2026 00:18 UK: the hand node left and the app never started its own (today's app never recovers).
|
||||
#[test]
|
||||
fn an_external_node_that_goes_away_hands_the_ports_to_the_app_after_the_wait() {
|
||||
let mut gone = None;
|
||||
assert_eq!(step(true, 0.0, &mut gone), Step::Stay);
|
||||
assert_eq!(gone, None);
|
||||
assert_eq!(step(false, 100.0, &mut gone), Step::Gone { for_s: 0.0 });
|
||||
assert_eq!(step(false, 130.0, &mut gone), Step::Gone { for_s: 30.0 });
|
||||
assert_eq!(step(false, 160.0, &mut gone), Step::TakeOver);
|
||||
assert_eq!(gone, None, "the memory resets once the app has its own node");
|
||||
// a node that comes back inside the wait is kept, and the wait starts over
|
||||
let mut gone = None;
|
||||
assert_eq!(step(false, 0.0, &mut gone), Step::Gone { for_s: 0.0 });
|
||||
assert_eq!(step(true, 30.0, &mut gone), Step::Stay);
|
||||
assert_eq!(step(false, 40.0, &mut gone), Step::Gone { for_s: 0.0 });
|
||||
assert_eq!(step(false, 99.0, &mut gone), Step::Gone { for_s: 59.0 });
|
||||
assert_eq!(step(false, 100.0, &mut gone), Step::TakeOver);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_keeps_the_newest_logs() {
|
||||
let dir = std::env::temp_dir().join(format!("igneum-prune-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
for i in 0..6 {
|
||||
let p = dir.join(format!("node-2026100{i}.log"));
|
||||
std::fs::write(&p, "x").unwrap();
|
||||
let t = std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000 + i * 60);
|
||||
let f = std::fs::File::options().write(true).open(&p).unwrap();
|
||||
f.set_modified(t).unwrap();
|
||||
}
|
||||
std::fs::write(dir.join("miner-1.log"), "y").unwrap();
|
||||
std::fs::write(dir.join("node-notes.txt"), "z").unwrap();
|
||||
assert_eq!(prune_logs(&dir, "node-", 4), 2);
|
||||
let mut left: Vec<String> = std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
||||
left.sort();
|
||||
assert_eq!(left, vec!["miner-1.log", "node-20261002.log", "node-20261003.log", "node-20261004.log", "node-20261005.log", "node-notes.txt"]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
@ -167,6 +167,10 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) {
|
|||
c.mem_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_mem_mhz };
|
||||
c.clock_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_clock_mhz };
|
||||
c.tune_source = p.sweep_source.clone();
|
||||
c.tune_goal = p.tune_goal.clone();
|
||||
c.tune_before_watts = p.sweep_before_watts;
|
||||
c.tune_before_mhs = p.sweep_before_mhs;
|
||||
c.tune_floor = p.sweep_floor;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
366
app/igneum-app/src/live.rs
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
//! GET /api/live for the dashboard's chain scene (ui/live-dag.js, the site's module) and the Cards tab's network
|
||||
//! numbers. Two sources, one contract:
|
||||
//!
|
||||
//! * the observer's /api/live (the same URL ota.rs polls for the identity count), read through curl, cached 2 s per
|
||||
//! window, with this machine's blocks marked as the lane "you" (`shape`): the scene's full feed (lanes, parents,
|
||||
//! colours, the selected chain, checkpoints, proof state). `state.source` = "observer".
|
||||
//! * the local node's `igneum_getRecentBlocks(seconds)` (igneumd 0.3.17, the node lane's eec34ac3): the last 600 s
|
||||
//! of chain and merged blocks with vote key hashes, blue scores and colours. The engine computes the observer's
|
||||
//! state fields from it (`shape_from_blocks`: miners_10m, blocks_10m, blocks_per_minute) and adds them to every
|
||||
//! observer reply as `state.node`, so the Cards tab reads network numbers that need no site; and when the observer
|
||||
//! is unreachable the node's rows stand in for the scene (`state.source` = "node", `partial: true`: no parents, no
|
||||
//! proof state), instead of `{ok:false}`. A node before 0.3.17 answers -32601 and the node source rests 10 minutes.
|
||||
//!
|
||||
//! Read-only; one observer call per 2 s whatever the window asks; one node call per 5 s at most.
|
||||
use crate::engine::Shared;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
static CACHE: Mutex<Option<(Instant, u32, Value)>> = Mutex::new(None);
|
||||
const TTL: Duration = Duration::from_secs(2);
|
||||
|
||||
struct NodeCache {
|
||||
at: Option<Instant>,
|
||||
blocks: Vec<RecentBlock>,
|
||||
/// the node said "method not found" (-32601): rest until then
|
||||
retry: Option<Instant>,
|
||||
}
|
||||
static NODE: Mutex<NodeCache> = Mutex::new(NodeCache { at: None, blocks: Vec::new(), retry: None });
|
||||
const NODE_TTL: Duration = Duration::from_secs(5);
|
||||
|
||||
/// The observer's reply with this machine's lane marked. `ids` are the 8-character vote key ids of this machine's
|
||||
/// cards. Blocks whose `miner` is one of them read `miner: "you"`; `state.you_blocks` counts them; `state.source` names
|
||||
/// where the data came from. Anything else passes through untouched, so the module's contract holds.
|
||||
pub fn shape(mut reply: Value, ids: &HashSet<String>) -> Value {
|
||||
let mut yours = 0u64;
|
||||
if let Some(blocks) = reply.get_mut("blocks").and_then(|b| b.as_array_mut()) {
|
||||
for b in blocks.iter_mut() {
|
||||
let mine = b.get("miner").and_then(|m| m.as_str()).map(|m| ids.contains(m)).unwrap_or(false);
|
||||
if mine {
|
||||
yours += 1;
|
||||
if let Some(o) = b.as_object_mut() {
|
||||
o.insert("miner".into(), json!("you"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(st) = reply.get_mut("state").and_then(|s| s.as_object_mut()) {
|
||||
st.insert("you_blocks".into(), json!(yours));
|
||||
st.insert("source".into(), json!("observer"));
|
||||
}
|
||||
reply
|
||||
}
|
||||
|
||||
/// The observer URL for a window: `<live_api>?window=<s>`, the window clamped to the module's 30 to 300 s.
|
||||
pub fn url_for(live_api: &str, window_s: u32) -> String {
|
||||
format!("{live_api}?window={}", window_s.clamp(30, 300))
|
||||
}
|
||||
|
||||
/// One block from `igneum_getRecentBlocks`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RecentBlock {
|
||||
pub hash: String,
|
||||
pub blue_score: u64,
|
||||
pub daa_score: u64,
|
||||
pub timestamp_ms: u64,
|
||||
pub vote_key_hash: String,
|
||||
pub is_chain_block: bool,
|
||||
pub color: String,
|
||||
/// "header" or "chain_block" (eec34ac3: a merged block whose own header was not at hand carries its merging
|
||||
/// chain block's time and blue score, marked so blocks_per_minute stays honest); "" on an older row = header
|
||||
pub timestamp_source: String,
|
||||
}
|
||||
|
||||
pub fn parse_recent(v: &Value) -> Vec<RecentBlock> {
|
||||
let s = |b: &Value, k: &str| b.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
|
||||
let n = |b: &Value, k: &str| b.get(k).and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
v.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|b| RecentBlock {
|
||||
hash: s(b, "hash"),
|
||||
blue_score: n(b, "blue_score"),
|
||||
daa_score: n(b, "daa_score"),
|
||||
timestamp_ms: n(b, "timestamp_ms"),
|
||||
vote_key_hash: s(b, "vote_key_hash"),
|
||||
is_chain_block: b.get("is_chain_block").and_then(|x| x.as_bool()).unwrap_or(false),
|
||||
color: s(b, "color"),
|
||||
timestamp_source: s(b, "timestamp_source"),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The lane id the UI matches against a card's `ids`: the first 8 hex of the vote key hash (engine.rs gives a card
|
||||
/// its ids the same way, `h.chars().take(8)`); "" when the block carries no key.
|
||||
pub fn lane(vote_key_hash: &str) -> String {
|
||||
vote_key_hash.trim_start_matches("0x").chars().take(8).collect()
|
||||
}
|
||||
|
||||
/// The site's `short`: the first 16 hex (site/api/live.mjs), so a hash reads the same from either source.
|
||||
fn short(h: &str) -> String {
|
||||
h.trim_start_matches("0x").chars().take(16).collect()
|
||||
}
|
||||
|
||||
/// The observer's `state` fields that come from the blocks: distinct vote keys in 10 minutes, blocks in 10
|
||||
/// minutes, blocks per minute over the last 10 minutes (oldest first), and the `blocks` (last `window_s`, in the
|
||||
/// scene's row shape as far as the node knows it: no parents) and `miners` arrays. `now_ms` is the reference time.
|
||||
pub fn shape_from_blocks(blocks: &[RecentBlock], now_ms: u64, window_s: u64) -> Value {
|
||||
let ten = now_ms.saturating_sub(600_000);
|
||||
let recent: Vec<&RecentBlock> = blocks.iter().filter(|b| b.timestamp_ms >= ten).collect();
|
||||
let mut miners: std::collections::BTreeMap<&str, u64> = Default::default();
|
||||
for b in &recent {
|
||||
if !b.vote_key_hash.is_empty() {
|
||||
*miners.entry(b.vote_key_hash.as_str()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
let mut per_min = vec![0u64; 10];
|
||||
for b in &recent {
|
||||
let idx = ((b.timestamp_ms - ten) / 60_000).min(9) as usize;
|
||||
per_min[idx] += 1;
|
||||
}
|
||||
let win = now_ms.saturating_sub(window_s * 1000);
|
||||
let rows: Vec<Value> = blocks
|
||||
.iter()
|
||||
.filter(|b| b.timestamp_ms >= win)
|
||||
.map(|b| {
|
||||
json!({
|
||||
"hash": short(&b.hash), "ts": b.timestamp_ms, "timestamp_ms": b.timestamp_ms, "blue_score": b.blue_score, "daa": b.daa_score,
|
||||
"parents": [], "chain": b.is_chain_block, "is_chain_block": b.is_chain_block,
|
||||
"color": if b.color.is_empty() { "pending" } else { b.color.as_str() },
|
||||
"miner": lane(&b.vote_key_hash), "vote_key_hash": short(&b.vote_key_hash),
|
||||
"timestamp_source": if b.timestamp_source.is_empty() { "header" } else { b.timestamp_source.as_str() },
|
||||
"locked": false, "final": false, "shards": [], "proven": false
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({
|
||||
"miners_10m": miners.len(),
|
||||
"blocks_10m": recent.len(),
|
||||
"blocks_per_minute": per_min,
|
||||
"blocks": rows,
|
||||
"miners": miners.iter().map(|(k, n)| json!({ "id": lane(k), "vote_key_hash": short(k), "blocks_10m": n })).collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The node's recent blocks, cached 5 s; empty when the node is down, carries no such method (-32601, rests 10
|
||||
/// minutes) or answered nothing.
|
||||
fn node_blocks(shared: &Arc<Shared>) -> Vec<RecentBlock> {
|
||||
{
|
||||
let c = NODE.lock().unwrap();
|
||||
if c.at.map(|t| t.elapsed() < NODE_TTL).unwrap_or(false) {
|
||||
return c.blocks.clone();
|
||||
}
|
||||
if c.retry.map(|t| Instant::now() < t).unwrap_or(false) {
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
let node_up = matches!(shared.state.lock().unwrap().node.state.as_str(), "syncing" | "synced");
|
||||
if !node_up {
|
||||
return Vec::new();
|
||||
}
|
||||
match crate::prover::evm_rpc(shared, "igneum_getRecentBlocks", json!([600]), Duration::from_secs(6)) {
|
||||
Ok(v) if v.is_array() => {
|
||||
let blocks = parse_recent(&v);
|
||||
let mut c = NODE.lock().unwrap();
|
||||
c.at = Some(Instant::now());
|
||||
c.blocks = blocks.clone();
|
||||
blocks
|
||||
}
|
||||
Ok(_) => Vec::new(),
|
||||
Err(e) => {
|
||||
// the node's default arm: {"code": -32601, "message": "method igneum_getRecentBlocks not found"}
|
||||
if e.contains("not found") || e.contains("-32601") {
|
||||
NODE.lock().unwrap().retry = Some(Instant::now() + Duration::from_secs(600));
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The node's `state` fields for the Cards tab (`state.node` on an observer reply, `state` on a node-only one).
|
||||
fn node_state(shared: &Arc<Shared>, blocks: &[RecentBlock], now_ms: u64) -> Value {
|
||||
let mut v = shape_from_blocks(blocks, now_ms, 90);
|
||||
let st = shared.state.lock().unwrap();
|
||||
let o = v.as_object_mut().unwrap();
|
||||
o.remove("blocks");
|
||||
o.remove("miners");
|
||||
o.insert("block_count".into(), json!(st.node.blocks));
|
||||
o.insert("header_count".into(), json!(st.node.headers));
|
||||
o.insert("daa".into(), json!(st.node.daa));
|
||||
o.insert("blue_score".into(), json!(st.node.blue));
|
||||
o.insert("difficulty".into(), json!(st.node.difficulty));
|
||||
o.insert("peers".into(), json!(st.node.peers));
|
||||
o.insert("synced".into(), json!(st.node.synced));
|
||||
o.insert("hashes_per_second_estimate".into(), Value::Null);
|
||||
o.insert("source".into(), json!("node"));
|
||||
v
|
||||
}
|
||||
|
||||
/// The reply for the dashboard: the observer's, cached 2 s per window, with the node's state fields added as
|
||||
/// `state.node` when the node answers; the node's rows alone (`partial: true`) when the observer is unreachable;
|
||||
/// `{ok:false, error}` when neither answers (the UI then draws its own blocks strip).
|
||||
pub fn fetch(shared: &Arc<Shared>, live_api: &str, window_s: u32, ids: &HashSet<String>) -> Value {
|
||||
let window_s = window_s.clamp(30, 300);
|
||||
let now_ms = (crate::platform::unix_now_f() * 1000.0) as u64;
|
||||
let blocks = node_blocks(shared);
|
||||
let node = if blocks.is_empty() { None } else { Some(node_state(shared, &blocks, now_ms)) };
|
||||
let cached = CACHE.lock().unwrap().as_ref().and_then(|(at, w, v)| if *w == window_s && at.elapsed() < TTL { Some(v.clone()) } else { None });
|
||||
let mut reply = match cached {
|
||||
Some(v) => v,
|
||||
None if live_api.is_empty() => json!({ "ok": false, "error": "no observer address in this build" }),
|
||||
None => {
|
||||
let url = url_for(live_api, window_s);
|
||||
let out = crate::detect::run_timeout(Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", "4", &url]), None, Duration::from_secs(6));
|
||||
let reply = match out.and_then(|t| serde_json::from_str::<Value>(&t).ok()) {
|
||||
Some(v) if v.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) && v.get("blocks").map(|b| b.is_array()).unwrap_or(false) => shape(v, ids),
|
||||
_ => json!({ "ok": false, "error": "the observer did not answer" }),
|
||||
};
|
||||
if reply.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) {
|
||||
*CACHE.lock().unwrap() = Some((Instant::now(), window_s, reply.clone()));
|
||||
}
|
||||
reply
|
||||
}
|
||||
};
|
||||
let observer_ok = reply.get("ok").and_then(|o| o.as_bool()).unwrap_or(false);
|
||||
match (observer_ok, node) {
|
||||
(true, Some(n)) => {
|
||||
if let Some(st) = reply.get_mut("state").and_then(|s| s.as_object_mut()) {
|
||||
st.insert("node".into(), n);
|
||||
}
|
||||
reply
|
||||
}
|
||||
(true, None) => reply,
|
||||
(false, Some(n)) => {
|
||||
// the node's rows stand in: the scene gets blocks without parents or proof state, and says so
|
||||
let mut v = shape_from_blocks(&blocks, now_ms, window_s as u64);
|
||||
let o = v.as_object_mut().unwrap();
|
||||
let rows = o.remove("blocks").unwrap_or(Value::Null);
|
||||
let miners = o.remove("miners").unwrap_or(Value::Null);
|
||||
let mut out = json!({ "ok": true, "partial": true, "now": crate::platform::unix_now_f(), "state": n, "blocks": rows, "miners": miners, "events": [], "finality": { "checkpoints": [] } });
|
||||
out = shape(out, ids);
|
||||
if let Some(st) = out.get_mut("state").and_then(|s| s.as_object_mut()) {
|
||||
st.insert("source".into(), json!("node"));
|
||||
}
|
||||
out
|
||||
}
|
||||
(false, None) => reply,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// a fixture in the observer's shape (site/api/live.mjs): three miners, this machine is 8fafda27, one of its blocks on
|
||||
// the selected chain and proven, one excluded; a locked and a pending checkpoint
|
||||
fn fixture() -> Value {
|
||||
json!({
|
||||
"ok": true, "now": 1791301670,
|
||||
"state": { "stale": false, "age_s": 1, "height": 198490 },
|
||||
"blocks": [
|
||||
{ "hash": "a1", "ts": 1791301600000i64, "blue_score": 194690, "daa": 198486, "parents": [], "chain": true, "color": "blue", "miner": "8fafda27", "locked": true, "final": true, "shards": [{ "state": "paid" }], "proven": true },
|
||||
{ "hash": "a2", "ts": 1791301601000i64, "blue_score": 194691, "daa": 198487, "parents": ["a1"], "chain": true, "color": "blue", "miner": "1b2c3d4e", "locked": false, "final": false, "shards": [{ "state": "proving" }], "proven": false },
|
||||
{ "hash": "a3", "ts": 1791301601500i64, "blue_score": null, "daa": 198487, "parents": ["a1"], "chain": false, "color": "red", "miner": "8fafda27", "locked": false, "final": false, "shards": [], "proven": false },
|
||||
{ "hash": "a4", "ts": 1791301602000i64, "blue_score": 194692, "daa": 198488, "parents": ["a2", "a3"], "chain": true, "color": "pending", "miner": "deadbeef", "locked": false, "final": false, "shards": [], "proven": false }
|
||||
],
|
||||
"finality": { "checkpoints": [
|
||||
{ "index": 6490, "blue_score": 194690, "daa": 198486, "state": "locked", "fraction_total": 0.71 },
|
||||
{ "index": 6491, "blue_score": 194720, "daa": 198516, "state": "pending", "fraction_total": 0.44 }
|
||||
] }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_machines_blocks_become_the_lane_you_and_nothing_else_moves() {
|
||||
let ids: HashSet<String> = ["8fafda27".to_string(), "9a8b7c6d".to_string()].into_iter().collect();
|
||||
let out = shape(fixture(), &ids);
|
||||
let blocks = out["blocks"].as_array().unwrap();
|
||||
assert_eq!(blocks.len(), 4);
|
||||
assert_eq!(blocks[0]["miner"], "you");
|
||||
assert_eq!(blocks[2]["miner"], "you");
|
||||
assert_eq!(blocks[1]["miner"], "1b2c3d4e");
|
||||
assert_eq!(blocks[3]["miner"], "deadbeef");
|
||||
// the rest of the contract passes through: parents, chain, colour, proof state, checkpoints
|
||||
assert_eq!(blocks[3]["parents"], json!(["a2", "a3"]));
|
||||
assert_eq!(blocks[0]["chain"], true);
|
||||
assert_eq!(blocks[2]["color"], "red");
|
||||
assert_eq!(blocks[0]["proven"], true);
|
||||
assert_eq!(blocks[1]["shards"][0]["state"], "proving");
|
||||
assert_eq!(out["finality"]["checkpoints"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(out["finality"]["checkpoints"][0]["state"], "locked");
|
||||
assert_eq!(out["state"]["you_blocks"], 2);
|
||||
assert_eq!(out["state"]["source"], "observer");
|
||||
assert_eq!(out["state"]["height"], 198490);
|
||||
assert_eq!(out["ok"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_ids_means_no_lane_is_yours_and_a_bare_reply_survives() {
|
||||
let out = shape(fixture(), &HashSet::new());
|
||||
assert!(out["blocks"].as_array().unwrap().iter().all(|b| b["miner"] != "you"));
|
||||
assert_eq!(out["state"]["you_blocks"], 0);
|
||||
let bare = shape(json!({ "ok": true, "blocks": [] }), &HashSet::new());
|
||||
assert_eq!(bare["blocks"].as_array().unwrap().len(), 0);
|
||||
assert!(bare.get("state").is_none(), "no state object is invented");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_window_is_clamped_to_the_modules_range() {
|
||||
assert_eq!(url_for("https://igneum.network/api/live", 120), "https://igneum.network/api/live?window=120");
|
||||
assert_eq!(url_for("https://igneum.network/api/live", 5), "https://igneum.network/api/live?window=30");
|
||||
assert_eq!(url_for("https://igneum.network/api/live", 9000), "https://igneum.network/api/live?window=300");
|
||||
}
|
||||
|
||||
fn b(ts: u64, key: &str, chain: bool) -> RecentBlock {
|
||||
RecentBlock { hash: format!("0x{:064x}", ts), blue_score: ts / 1000, daa_score: ts / 1000, timestamp_ms: ts, vote_key_hash: key.into(), is_chain_block: chain, color: if chain { "blue".into() } else { String::new() }, timestamp_source: String::new() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_state_fields_come_from_the_nodes_blocks_of_the_last_ten_minutes() {
|
||||
let now = 1_791_300_000_000u64;
|
||||
let blocks = vec![b(now - 5_000, "aa", true), b(now - 30_000, "bb", true), b(now - 95_000, "aa", false), b(now - 500_000, "cc", true), b(now - 700_000, "dd", true)];
|
||||
let v = shape_from_blocks(&blocks, now, 120);
|
||||
assert_eq!(v["miners_10m"], 3, "dd is older than 10 minutes");
|
||||
assert_eq!(v["blocks_10m"], 4);
|
||||
let pm = v["blocks_per_minute"].as_array().unwrap();
|
||||
assert_eq!(pm.len(), 10);
|
||||
assert_eq!(pm[9], 2, "the newest minute holds the 5 s and 30 s blocks");
|
||||
assert_eq!(pm[8], 1, "the 95 s block");
|
||||
assert_eq!(pm[1], 1, "the 500 s block");
|
||||
assert_eq!(v["blocks"].as_array().unwrap().len(), 3, "the 120 s window");
|
||||
assert_eq!(v["blocks"][2]["color"], "pending", "an unmerged block without a colour");
|
||||
assert_eq!(v["blocks"][0]["color"], "blue");
|
||||
assert_eq!(v["blocks"][0]["timestamp_source"], "header");
|
||||
assert_eq!(v["blocks"][0]["miner"], "aa", "the lane id is the first 8 hex of the vote key hash, as a card's ids");
|
||||
assert_eq!(v["blocks"][0]["chain"], true, "the scene's row shape");
|
||||
assert_eq!(v["blocks"][0]["parents"], json!([]), "the node method carries no parents");
|
||||
assert_eq!(v["miners"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(v["miners"][0]["id"], "aa", "the miners rows carry id, as the site's");
|
||||
// the lane "you" marks node rows too
|
||||
let ids: HashSet<String> = ["aa".to_string()].into_iter().collect();
|
||||
let marked = shape(json!({ "ok": true, "state": {}, "blocks": v["blocks"] }), &ids);
|
||||
assert_eq!(marked["blocks"][0]["miner"], "you");
|
||||
assert_eq!(marked["state"]["you_blocks"], 2);
|
||||
assert_eq!(lane("0x0123456789abcdef"), "01234567");
|
||||
assert_eq!(lane(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_blocks_parse_from_the_node_reply_and_an_empty_reply_is_empty() {
|
||||
let v: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"cd","is_chain_block":true,"color":"blue"},{"hash":"0xcd","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"ef","is_chain_block":false,"color":"red","timestamp_source":"chain_block"}]"#).unwrap();
|
||||
let p = parse_recent(&v);
|
||||
assert_eq!(p.len(), 2);
|
||||
assert_eq!((p[0].blue_score, p[0].is_chain_block, p[0].color.as_str()), (5, true, "blue"));
|
||||
assert_eq!((p[1].color.as_str(), p[1].timestamp_source.as_str()), ("red", "chain_block"));
|
||||
// a null vote_key_hash (a chain block with no mergeset entry, which the executor never produces) reads as ""
|
||||
let n: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":null,"is_chain_block":true,"color":"blue","timestamp_source":"header"}]"#).unwrap();
|
||||
assert_eq!(parse_recent(&n)[0].vote_key_hash, "");
|
||||
assert!(parse_recent(&json!(null)).is_empty());
|
||||
assert_eq!(short("0x1234567890abcdef00"), "1234567890abcdef", "the site's 16-hex short");
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ mod jobrun;
|
|||
mod jobbuild;
|
||||
mod prover;
|
||||
mod provedefault;
|
||||
mod provingdir;
|
||||
mod segments;
|
||||
mod verifier;
|
||||
mod wslhost;
|
||||
|
|
@ -37,6 +38,8 @@ mod sweep;
|
|||
mod ember;
|
||||
mod powertask;
|
||||
mod watchdog;
|
||||
mod live;
|
||||
mod extnode;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
use std::sync::mpsc::channel;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ pub struct Manifest {
|
|||
pub min_supported_version: String,
|
||||
pub notes: String,
|
||||
pub activation_height: Option<u64>,
|
||||
/// Horizon frontier lane (6 October 2026): the publisher's word that this version installs even while the
|
||||
/// network's finality is paused (nothing else does); false unless the signed manifest says so
|
||||
pub urgent: bool,
|
||||
pub deadline_note: String,
|
||||
/// consensus.override: the exact object the engine writes to <app data>/override.json for igneumd's
|
||||
/// --override-params-file (for example {"difficulty_v2_activation_daa": N}); signed with the rest of the manifest.
|
||||
|
|
@ -157,6 +160,8 @@ pub fn parse(text: &str) -> Result<Manifest, String> {
|
|||
min_supported_version: s(&v, "min_supported_version"),
|
||||
notes: s(&v, "notes"),
|
||||
activation_height: consensus.get("activation_height").and_then(|x| x.as_u64()),
|
||||
// the manifest's TOP-LEVEL "urgent": true (the publisher sets it; igneum-ota-sign passes the document through)
|
||||
urgent: v.get("urgent").and_then(|u| u.as_bool()).unwrap_or(false),
|
||||
deadline_note: s(&consensus, "deadline_note"),
|
||||
override_params: match consensus.get("override") {
|
||||
Some(o) if o.is_object() && !o.as_object().unwrap().is_empty() => Some(o.clone()),
|
||||
|
|
@ -300,6 +305,10 @@ pub struct Moment {
|
|||
pub boundary_eta_s: Option<i64>,
|
||||
/// A worker is starting, exporting a pack or being built: let it finish.
|
||||
pub miner_busy: bool,
|
||||
/// A remote job is running on this engine (src/jobrun.rs). It holds even an urgent install: a job is bounded by its
|
||||
/// cap, and an install under it kills its process tree (PC 1, 6 October 2026, 17:52:54Z: 0.3.14 went in during a
|
||||
/// measurement job because install_asked from a two-hour-old update-now still counted as urgent).
|
||||
pub job_active: bool,
|
||||
/// How long the update has been ready and waiting.
|
||||
pub ready_for_s: u64,
|
||||
/// A consensus activation is close, or this version is below min_supported_version: now beats later.
|
||||
|
|
@ -308,8 +317,16 @@ pub struct Moment {
|
|||
pub slot_ok: bool,
|
||||
/// How much of the network's identity count (/api/live, last 10 minutes) is gone right now, in percent.
|
||||
pub network_drop_pct: f64,
|
||||
/// Horizon frontier lane (6 October 2026): the network's finality is paused (the node is synced and no checkpoint
|
||||
/// has locked for FINALITY_PAUSE_S); a rollout never lands on a chain that cannot lock
|
||||
pub finality_paused: bool,
|
||||
/// the signed manifest's own urgent flag: the one thing that installs while finality is paused
|
||||
pub manifest_urgent: bool,
|
||||
}
|
||||
|
||||
/// No checkpoint lock for this long on a synced node = finality paused (the devnet locks every few minutes).
|
||||
pub const FINALITY_PAUSE_S: f64 = 15.0 * 60.0;
|
||||
|
||||
/// The percentage of identities the network may lose in 10 minutes before updates hold (we are the devnet).
|
||||
pub const NETWORK_DROP_HOLD_PCT: f64 = 30.0;
|
||||
|
||||
|
|
@ -320,6 +337,13 @@ pub fn slot_minute(id8: &str) -> u64 {
|
|||
|
||||
/// Ok when the update may be applied now; Err carries the reason to wait, in the words the dashboard shows.
|
||||
pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
||||
// the finality rule comes first: a fork-close or unsupported urgency does not pass it, only the manifest's flag
|
||||
if m.finality_paused && !m.manifest_urgent {
|
||||
return Err("waiting for finality: the network has not locked a checkpoint for 15 min; nothing installs on a chain that cannot lock".into());
|
||||
}
|
||||
if m.job_active {
|
||||
return Err("a remote job is running; installing when it closes".into());
|
||||
}
|
||||
if m.urgent {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -349,7 +373,10 @@ pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
|||
/// A consensus activation is within FORK_URGENT_BLOCKS of the node's DAA score (and the node has a score).
|
||||
pub fn fork_is_close(activation_height: Option<u64>, daa: u64) -> bool {
|
||||
match activation_height {
|
||||
Some(h) if daa > 0 => daa.saturating_add(FORK_URGENT_BLOCKS) >= h,
|
||||
// Horizon polish Q4 (6 October 2026): an activation at or below the DAA has PASSED, nothing is pending; the
|
||||
// old rule read it as close, so every update since the 0.3.14 manifest said "0 blocks away, installing now",
|
||||
// stripped Later and skipped every safe-moment guard (PC 1's 17:52:54Z install under a job came through it)
|
||||
Some(h) if daa > 0 && h > daa => daa.saturating_add(FORK_URGENT_BLOCKS) >= h,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -492,8 +519,15 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn safe_moments() {
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 };
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, job_active: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0, finality_paused: false, manifest_urgent: false };
|
||||
assert!(safe_to_apply(&base).is_ok());
|
||||
// the finality rule (Horizon frontier lane, 6 October 2026), the known-failed case: paused, no install; active, install;
|
||||
// paused with the manifest's urgent flag, install; paused with only a fork-close urgency, no install; patience never passes it
|
||||
assert!(safe_to_apply(&Moment { finality_paused: true, ..base.clone() }).unwrap_err().starts_with("waiting for finality"));
|
||||
assert!(safe_to_apply(&Moment { finality_paused: false, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { finality_paused: true, manifest_urgent: true, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { finality_paused: true, urgent: true, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { finality_paused: true, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_err());
|
||||
assert_eq!(safe_to_apply(&Moment { node_synced: false, ..base.clone() }).unwrap_err(), "waiting for the node to sync");
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(120), ..base.clone() }).unwrap_err().contains("boundary in 120 s"));
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(0), ..base.clone() }).is_err());
|
||||
|
|
@ -502,6 +536,11 @@ mod tests {
|
|||
assert!(safe_to_apply(&Moment { miner_busy: true, ..base.clone() }).unwrap_err().contains("worker"));
|
||||
// urgent beats every wait
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, boundary_eta_s: Some(5), miner_busy: true, urgent: true, ..base.clone() }).is_ok());
|
||||
// PC 1, 6 October 2026, 17:52:54Z: a scheduled update arriving mid-job is deferred to the job's close, urgent or not
|
||||
assert!(safe_to_apply(&Moment { job_active: true, ..base.clone() }).unwrap_err().contains("remote job"));
|
||||
assert!(safe_to_apply(&Moment { job_active: true, urgent: true, ..base.clone() }).unwrap_err().contains("remote job"));
|
||||
assert!(safe_to_apply(&Moment { job_active: true, urgent: true, slot_ok: false, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { job_active: false, urgent: true, slot_ok: false, ..base.clone() }).is_ok());
|
||||
// patience: an unsynced node for 6 h applies anyway
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S - 1, ..base.clone() }).is_err());
|
||||
|
|
@ -531,8 +570,11 @@ mod tests {
|
|||
assert!(!fork_is_close(Some(120_000), 0));
|
||||
assert!(!fork_is_close(Some(120_000), 118_199));
|
||||
assert!(fork_is_close(Some(120_000), 118_200));
|
||||
assert!(fork_is_close(Some(120_000), 120_000));
|
||||
assert!(fork_is_close(Some(120_000), 130_000));
|
||||
assert!(fork_is_close(Some(120_000), 119_999), "one block before the activation");
|
||||
// a passed activation is not close (Horizon polish Q4): at the height and after it, nothing is pending
|
||||
assert!(!fork_is_close(Some(120_000), 120_000));
|
||||
assert!(!fork_is_close(Some(120_000), 130_000));
|
||||
assert!(!fork_is_close(Some(33_000), 201_776), "the 0.3.14 manifest's case: difficulty v2 at 33,000 against PC 1's DAA");
|
||||
let m = parse(SAMPLE).unwrap();
|
||||
assert!(!unsupported(&m, "0.3.0"));
|
||||
assert!(unsupported(&m, "0.2.9"));
|
||||
|
|
|
|||
|
|
@ -68,8 +68,12 @@ pub enum Launch {
|
|||
|
||||
pub struct Ctx {
|
||||
pub node_synced: bool,
|
||||
/// the network's finality is paused (engine: synced and no lock for manifest::FINALITY_PAUSE_S)
|
||||
pub finality_paused: bool,
|
||||
pub boundary_eta_s: Option<i64>,
|
||||
pub miner_busy: bool,
|
||||
/// A remote job is running (src/jobrun.rs): the install holds, urgent or not.
|
||||
pub job_active: bool,
|
||||
pub daa: u64,
|
||||
}
|
||||
|
||||
|
|
@ -539,7 +543,8 @@ impl Updater {
|
|||
}
|
||||
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 };
|
||||
let manifest_urgent = self.manifest.as_ref().map(|m| m.urgent).unwrap_or(false);
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, job_active: ctx.job_active, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct , finality_paused: ctx.finality_paused, manifest_urgent };
|
||||
if !self.auto && !urgent && !self.install_asked {
|
||||
shared.state.lock().unwrap().update.wait = "waiting for Install now (automatic updates are off)".into();
|
||||
return None;
|
||||
|
|
@ -668,6 +673,12 @@ impl Updater {
|
|||
shared.log(&format!("update check: {} is published but has no {} build yet", m.version, manifest::platform_name()));
|
||||
} else {
|
||||
shared.log(&format!("update check: {} is current (manifest {})", self.current, m.version));
|
||||
// an asked install (update-now) covers this one check: nothing newer, so the ask is spent.
|
||||
// Left true it made the NEXT manifest urgent hours later (PC 1, 6 October 2026, 17:52:54Z).
|
||||
if self.install_asked {
|
||||
self.install_asked = false;
|
||||
shared.log("update check: Install now asked and nothing newer is published; the ask is spent (the next manifest takes the usual slot)");
|
||||
}
|
||||
}
|
||||
self.entry = None;
|
||||
self.file = None;
|
||||
|
|
@ -954,7 +965,7 @@ impl Updater {
|
|||
// ---- the threads ---------------------------------------------------------------------------------------------------
|
||||
|
||||
/// https://igneum.network/live -> https://igneum.network/api/live; "" when the build carries no live page.
|
||||
fn live_api_from(live_page: &str) -> String {
|
||||
pub fn live_api_from(live_page: &str) -> String {
|
||||
let Some(rest) = live_page.strip_prefix("https://") else { return String::new() };
|
||||
let host = rest.split('/').next().unwrap_or("");
|
||||
if host.is_empty() { String::new() } else { format!("https://{host}/api/live") }
|
||||
|
|
|
|||
|
|
@ -35,6 +35,46 @@ use std::time::{Duration, Instant};
|
|||
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;
|
||||
/// The heartbeat file the helper refreshes every poll; an engine judges "the helper runs" by it, never by a flag
|
||||
/// of its own (6 October 2026, PC 1: the flag said yes after the helper's idle exit, and commands went to nobody).
|
||||
pub const ALIVE_FILE: &str = "helper.alive";
|
||||
/// A heartbeat older than this is a dead helper.
|
||||
pub const ALIVE_MAX_S: u64 = 4;
|
||||
|
||||
/// Writes the heartbeat: the unix time, as text.
|
||||
pub fn beat(dir: &Path, now: u64) {
|
||||
let _ = std::fs::write(dir.join(ALIVE_FILE), now.to_string());
|
||||
}
|
||||
|
||||
/// Reads a heartbeat: is the helper alive at `now`?
|
||||
pub fn alive_at(text: &str, now: u64) -> bool {
|
||||
text.trim().parse::<u64>().map(|t| now.saturating_sub(t) <= ALIVE_MAX_S).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Is the helper that reads `dir` alive now?
|
||||
pub fn alive(dir: &Path) -> bool {
|
||||
std::fs::read_to_string(dir.join(ALIVE_FILE)).map(|t| alive_at(&t, crate::platform::unix_now())).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Starts the task unless the helper already runs, then waits for a fresh heartbeat (up to `wait`). The engine
|
||||
/// writes a command only after this says Ok: the helper skips what the file held before its start (a stale quit
|
||||
/// is not a command), so a command written before the start would be skipped with it (6 October 2026, 17:55:25Z
|
||||
/// on PC 1: the first request of the first tune).
|
||||
pub fn ensure_running(dir: &Path, wait: Duration) -> Result<(), String> {
|
||||
if alive(dir) {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
start()?;
|
||||
let until = Instant::now() + wait;
|
||||
while Instant::now() < until {
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
if alive(dir) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(format!("the Igneum Power Helper task gave no heartbeat within {} s of its start", wait.as_secs()))
|
||||
}
|
||||
|
||||
/// One parsed command from cmd.txt.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -79,6 +119,24 @@ pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The sequence number a command line carries on the wire. The helper runs a line only when its number is above
|
||||
/// every number it has seen (its `last_seq`, seeded from the file at its start), so every writer must draw from
|
||||
/// ONE monotonic space: the unix time modulo a million (six digits, what `parse_line` accepts), plus a small
|
||||
/// offset per line. (F) 6 October 2026, 22:19Z on PC 1: the tune path wrote its request index ("00 dev 1", "01 pl
|
||||
/// 160", ...) while the cap path had written 305327 and up, so the helper skipped every tune command as stale and
|
||||
/// both climbs stopped on "the helper did not run sequence 1 within 15 s". Wraps every 11.6 days; the helper's
|
||||
/// idle exit (20 minutes) re-seeds it from the file, so a wrap costs at most one tune step.
|
||||
pub fn wire_seq() -> u64 {
|
||||
crate::platform::unix_now() % 999_990
|
||||
}
|
||||
|
||||
/// The commands a helper acts on: the lines added after its start (`skip` = the line count at the start, 0 again
|
||||
/// when the file shrank). A stale `quit` or `remove` from an earlier engine is never a command.
|
||||
pub fn commands_after(text: &str, skip: usize) -> Vec<(u64, HelperCmd)> {
|
||||
let skip = if text.lines().count() < skip { 0 } else { skip };
|
||||
text.lines().skip(skip).filter_map(parse_line).collect()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
|
@ -195,20 +253,30 @@ pub fn run_helper(dir: &Path) -> i32 {
|
|||
}
|
||||
};
|
||||
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();
|
||||
beat(dir, crate::platform::unix_now());
|
||||
// a stale file from an earlier run is not a command: only lines ADDED after the start count. 6 October 2026,
|
||||
// PC 1 17:55:55Z: a helper that started after an engine had written `quit` read that line and exited in the
|
||||
// same second, and every later start did the same; so the lines present at the start are skipped whole
|
||||
// (quit and remove included), and a file that shrinks starts the count again
|
||||
let initial = std::fs::read_to_string(&cmd_file).unwrap_or_default();
|
||||
let mut last_seq: u64 = initial.lines().filter_map(parse_line).map(|(s, _)| s).max().unwrap_or(0);
|
||||
let mut skip = initial.lines().count();
|
||||
let mut last_text = initial;
|
||||
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 {
|
||||
if text.lines().count() < skip {
|
||||
skip = 0;
|
||||
}
|
||||
last_text = text.clone();
|
||||
for (seq, c) in text.lines().filter_map(parse_line) {
|
||||
for (seq, c) in commands_after(&text, skip) {
|
||||
match c {
|
||||
HelperCmd::Quit => {
|
||||
log("quit");
|
||||
let _ = std::fs::remove_file(dir.join(ALIVE_FILE));
|
||||
return 0;
|
||||
}
|
||||
HelperCmd::Remove => {
|
||||
|
|
@ -261,8 +329,10 @@ pub fn run_helper(dir: &Path) -> i32 {
|
|||
}
|
||||
if idle.elapsed() >= Duration::from_secs(IDLE_S) {
|
||||
log("idle 20 min: exit (the engine starts the task again when it needs it)");
|
||||
let _ = std::fs::remove_file(dir.join(ALIVE_FILE));
|
||||
return 0;
|
||||
}
|
||||
beat(dir, crate::platform::unix_now());
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
|
|
@ -296,6 +366,18 @@ mod tests {
|
|||
assert_eq!(parse_line("quit"), Some((0, HelperCmd::Quit)));
|
||||
assert_eq!(parse_line("remove"), Some((0, HelperCmd::Remove)));
|
||||
assert_eq!(parse_line("12 reregister"), Some((12, HelperCmd::Reregister)));
|
||||
// a stale quit present at the start is skipped; a quit added later counts; a rewritten (shorter) file counts whole
|
||||
assert_eq!(commands_after("5 dev 0\n6 pl 460\nquit\n", 3), vec![]);
|
||||
assert_eq!(commands_after("5 dev 0\n6 pl 460\nquit\n7 dev 1\n", 3), vec![(7, HelperCmd::Dev("1".into()))]);
|
||||
assert_eq!(commands_after("quit\n", 3), vec![(0, HelperCmd::Quit)]);
|
||||
// the heartbeat: fresh within ALIVE_MAX_S, dead after, dead when unreadable
|
||||
// (F) every wire number parses (six digits at most) and leaves room for the four lines of a tune step
|
||||
let w = wire_seq();
|
||||
assert!(w + 3 <= 999_999 && parse_line(&format!("{} rmc", w + 3)).is_some());
|
||||
assert!(alive_at("1791309325", 1791309325 + ALIVE_MAX_S));
|
||||
assert!(!alive_at("1791309325", 1791309325 + ALIVE_MAX_S + 1));
|
||||
assert!(!alive_at("", 1791309325));
|
||||
assert!(!alive_at("soon", 1791309325));
|
||||
assert_eq!(parse_line("12 reregister C:\\evil.exe"), None, "no path argument: the helper picks the install folder itself");
|
||||
assert_eq!(smi_args("0", &HelperCmd::Reregister), None);
|
||||
assert!(readback_command().contains("Actions[0].Execute"));
|
||||
|
|
|
|||
|
|
@ -135,6 +135,22 @@ struct Tools {
|
|||
/// The node's re-derivation boundary (0.3.14, 6 October 2026): the chain block its EVM restarted at after the
|
||||
/// bodies below the pruning point were gone (`igneum_getExecStatus.restartNumber`; on a 0.3.13 node the
|
||||
/// `startedFrom` text "restart at chain block N"), else 0. The prover never claims work below it (no bodies, no
|
||||
/// The exporter's failure line. When the node's export carries the account dump (0.3.14) and the exporter's output
|
||||
/// never mentions it, the exporter at `path` predates 0.3.14: it replays the dump's own segment from the restart
|
||||
/// state and fails there with "port state root differs" (PC 1, 6 October 2026 18:16Z, block 140,662: the installer
|
||||
/// had not replaced igneum-prove-export); the line names the stale binary instead of the misleading root error.
|
||||
fn exporter_failure(export_has_dump: bool, out: &str, path: &Path, block: Option<u64>) -> String {
|
||||
let last = out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed");
|
||||
let what = match block {
|
||||
Some(b) => format!("exporter, block {b}"),
|
||||
None => "exporter".to_string(),
|
||||
};
|
||||
if export_has_dump && !out.contains("account dump") {
|
||||
return format!("{what}: stale igneum-prove-export at {}: the node's export carries the account dump and this exporter does not read it (it predates 0.3.14); install this release's exporter there ({last})", path.display());
|
||||
}
|
||||
format!("{what}: {last}")
|
||||
}
|
||||
|
||||
/// state: unprovable on every node).
|
||||
fn exec_boundary(shared: &Shared) -> u64 {
|
||||
let st = match evm_rpc(shared, "igneum_getExecStatus", json!([]), Duration::from_secs(5)) {
|
||||
|
|
@ -151,7 +167,7 @@ fn exec_boundary(shared: &Shared) -> u64 {
|
|||
text.strip_prefix("restart at chain block ").and_then(|t| t.split_whitespace().next()).and_then(|t| t.parse().ok()).unwrap_or(0)
|
||||
}
|
||||
|
||||
fn evm_rpc(shared: &Shared, method: &str, params: Value, timeout: Duration) -> Result<Value, String> {
|
||||
pub(crate) fn evm_rpc(shared: &Shared, method: &str, params: Value, timeout: Duration) -> Result<Value, String> {
|
||||
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
|
||||
let tmp = std::env::temp_dir().join(format!("igneum-prover-{}-{}.json", std::process::id(), method));
|
||||
std::fs::write(&tmp, body).map_err(|e| e.to_string())?;
|
||||
|
|
@ -665,7 +681,7 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
let (seq_p, fix_p, res_p) = if t.wsl { (wsl_path(&seq), wsl_path(&fixture), wsl_path(&results)) } else { (seq.display().to_string(), fixture.display().to_string(), results.display().to_string()) };
|
||||
let (ok, out) = run_tool(&shared, t, &t.export, &[seq_p, w.number.to_string(), fix_p.clone()], &[], Duration::from_secs(600), &dir.join(format!("export-{}.log", w.number)));
|
||||
if !ok || !fixture.exists() {
|
||||
return Err(format!("exporter: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed")));
|
||||
return Err(exporter_failure(export["preState"].is_array(), &out, &t.export, None));
|
||||
}
|
||||
set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "CPU prover: about five minutes a shard, 30 GB of RAM, paid only when no card proves first".into() });
|
||||
let prover_env = if t.cuda { "cuda" } else { "cpu" };
|
||||
|
|
@ -854,7 +870,7 @@ fn prove_segment(shared: &Shared, t: &Tools, seg: &crate::segments::SegmentWork,
|
|||
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")));
|
||||
return Err(exporter_failure(export["preState"].is_array(), &out, &t.export, Some(b)));
|
||||
}
|
||||
fixtures.push(as_host_path(&fixture));
|
||||
}
|
||||
|
|
@ -1092,6 +1108,21 @@ pub fn setup(shared: &Shared) -> Result<Value, String> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_stale_exporter_is_named_when_the_export_carries_the_dump() {
|
||||
let p = Path::new("C:/x/igneum-prove-export.exe");
|
||||
let old_out = "Error: segment 140661: port state root 0xe45c differs from the node's 0xddd7; the port is not the node's executor, stop\n";
|
||||
let line = exporter_failure(true, old_out, p, Some(140662));
|
||||
assert!(line.contains("stale igneum-prove-export at C:/x/igneum-prove-export.exe"), "{line}");
|
||||
assert!(line.starts_with("exporter, block 140662:"));
|
||||
// the 0.3.14 exporter read the dump and failed later: the real line, not the stale one
|
||||
let new_out = "account dump: 79 accounts after chain block 140661, state root 0x1 (equals the node's)\nError: segment 140662: port state root 0x2 differs from the node's 0x3\n";
|
||||
let line = exporter_failure(true, new_out, p, None);
|
||||
assert_eq!(line, "exporter: Error: segment 140662: port state root 0x2 differs from the node's 0x3");
|
||||
// an export without a dump (an older node): never the stale line
|
||||
assert_eq!(exporter_failure(false, old_out, p, None), format!("exporter: {}", old_out.trim()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinned_ids_come_out_of_the_hosts_id_line() {
|
||||
let out = "RESULT id: pinned guests: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (2832504 bytes, sha256 0x150f4c05a2951fc5) aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896 (319744 bytes), pinned 2026-10-05T16:20:38Z on Darwin, SP1 5.0 circuit v5\n";
|
||||
|
|
|
|||
178
app/igneum-app/src/provingdir.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
//! The prover's working directory (`<app dir>/proving`) owns its disk (0.3.16, 6 October 2026): the fleet's segment
|
||||
//! exports (50 to 500 MB each with the old [0, last] export, never pruned) filled 60 GB on the hub and 3 to 46 GB on
|
||||
//! every standing box, and the hub's node died three times on "No space left on device" (the last at 21:42 UK).
|
||||
//! Rules: a size cap and an age cap on the directory (defaults that fit a 100 GB box for a week), enforced before
|
||||
//! every export; a segment's directory is deleted the moment its record is submitted or paid; no export starts
|
||||
//! below 10% free disk, and the skip is logged.
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// Defaults: 20 GB and 7 days. `IGNEUM_PROVING_DIR_MAX_GB` and `IGNEUM_PROVING_DIR_MAX_DAYS` change them.
|
||||
pub const DEFAULT_MAX_BYTES: u64 = 20 * 1024 * 1024 * 1024;
|
||||
pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(7 * 24 * 3600);
|
||||
/// No export below this share of free disk.
|
||||
pub const MIN_FREE_FRACTION: f64 = 0.10;
|
||||
|
||||
pub fn max_bytes() -> u64 {
|
||||
std::env::var("IGNEUM_PROVING_DIR_MAX_GB").ok().and_then(|v| v.parse::<u64>().ok()).map(|g| g * 1024 * 1024 * 1024).unwrap_or(DEFAULT_MAX_BYTES)
|
||||
}
|
||||
|
||||
pub fn max_age() -> Duration {
|
||||
std::env::var("IGNEUM_PROVING_DIR_MAX_DAYS").ok().and_then(|v| v.parse::<u64>().ok()).map(|d| Duration::from_secs(d * 24 * 3600)).unwrap_or(DEFAULT_MAX_AGE)
|
||||
}
|
||||
|
||||
/// One entry of the directory as the planner sees it: a top-level file or a segment directory, its total bytes
|
||||
/// and its age.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub path: PathBuf,
|
||||
pub bytes: u64,
|
||||
pub age: Duration,
|
||||
}
|
||||
|
||||
/// What to delete so the directory fits: everything older than `max_age`, then the oldest entries until the total
|
||||
/// is at or under `max_bytes`. Pure, so the cap is unit-tested.
|
||||
pub fn prune_plan(entries: &[Entry], max_bytes: u64, max_age: Duration) -> Vec<PathBuf> {
|
||||
let mut keep: Vec<&Entry> = Vec::new();
|
||||
let mut out: Vec<PathBuf> = Vec::new();
|
||||
for e in entries {
|
||||
if e.age > max_age {
|
||||
out.push(e.path.clone());
|
||||
} else {
|
||||
keep.push(e);
|
||||
}
|
||||
}
|
||||
let mut total: u64 = keep.iter().map(|e| e.bytes).sum();
|
||||
// oldest first
|
||||
keep.sort_by(|a, b| b.age.cmp(&a.age));
|
||||
for e in keep {
|
||||
if total <= max_bytes {
|
||||
break;
|
||||
}
|
||||
total = total.saturating_sub(e.bytes);
|
||||
out.push(e.path.clone());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn dir_bytes(p: &Path) -> u64 {
|
||||
let mut total = 0;
|
||||
if let Ok(rd) = std::fs::read_dir(p) {
|
||||
for e in rd.flatten() {
|
||||
let m = match e.metadata() {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
total += if m.is_dir() { dir_bytes(&e.path()) } else { m.len() };
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// The directory's entries, oldest by modification time.
|
||||
pub fn scan(dir: &Path) -> Vec<Entry> {
|
||||
let now = SystemTime::now();
|
||||
let mut out = Vec::new();
|
||||
if let Ok(rd) = std::fs::read_dir(dir) {
|
||||
for e in rd.flatten() {
|
||||
let Ok(m) = e.metadata() else { continue };
|
||||
let age = m.modified().ok().and_then(|t| now.duration_since(t).ok()).unwrap_or_default();
|
||||
let bytes = if m.is_dir() { dir_bytes(&e.path()) } else { m.len() };
|
||||
out.push(Entry { path: e.path(), bytes, age });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Enforces the caps on `dir`; returns (entries deleted, bytes freed).
|
||||
pub fn enforce(dir: &Path) -> (usize, u64) {
|
||||
let entries = scan(dir);
|
||||
let plan = prune_plan(&entries, max_bytes(), max_age());
|
||||
let mut freed = 0;
|
||||
for p in &plan {
|
||||
if let Some(e) = entries.iter().find(|e| &e.path == p) {
|
||||
freed += e.bytes;
|
||||
}
|
||||
let _ = if p.is_dir() { std::fs::remove_dir_all(p) } else { std::fs::remove_file(p) };
|
||||
}
|
||||
(plan.len(), freed)
|
||||
}
|
||||
|
||||
/// Free disk as a share of the volume `path` is on; None when the platform call fails.
|
||||
#[cfg(unix)]
|
||||
pub fn free_fraction(path: &Path) -> Option<f64> {
|
||||
use std::ffi::CString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let c = CString::new(path.as_os_str().as_bytes()).ok()?;
|
||||
let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statvfs(c.as_ptr(), &mut st) } != 0 {
|
||||
return None;
|
||||
}
|
||||
let total = st.f_blocks as f64 * st.f_frsize as f64;
|
||||
if total <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(st.f_bavail as f64 * st.f_frsize as f64 / total)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn free_fraction(path: &Path) -> Option<f64> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
#[link(name = "kernel32")]
|
||||
extern "system" {
|
||||
fn GetDiskFreeSpaceExW(dir: *const u16, avail: *mut u64, total: *mut u64, free: *mut u64) -> i32;
|
||||
}
|
||||
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
|
||||
let (mut avail, mut total, mut free) = (0u64, 0u64, 0u64);
|
||||
if unsafe { GetDiskFreeSpaceExW(wide.as_ptr(), &mut avail, &mut total, &mut free) } == 0 || total == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(avail as f64 / total as f64)
|
||||
}
|
||||
|
||||
/// The pre-export gate: the caps enforced, then the free-disk check. Err carries the line to log when the export
|
||||
/// must be skipped.
|
||||
pub fn before_export(dir: &Path) -> Result<(), String> {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
let (n, freed) = enforce(dir);
|
||||
if n > 0 {
|
||||
eprintln!("prover: proving dir cap: {n} entries deleted, {} MB freed", freed / (1024 * 1024));
|
||||
}
|
||||
match free_fraction(dir) {
|
||||
Some(f) if f < MIN_FREE_FRACTION => Err(format!("no export: {:.1}% of the disk is free, under the {:.0}% floor; the proving dir is {} MB after its cap; free space or lower IGNEUM_PROVING_DIR_MAX_GB", f * 100.0, MIN_FREE_FRACTION * 100.0, dir_bytes(dir) / (1024 * 1024))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A segment's directory goes the moment its record is submitted or paid.
|
||||
pub fn remove_segment(dir: &Path, first: u64) {
|
||||
let _ = std::fs::remove_dir_all(dir.join(format!("seg-{first}")));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn e(name: &str, mb: u64, days: u64) -> Entry {
|
||||
Entry { path: PathBuf::from(name), bytes: mb * 1024 * 1024, age: Duration::from_secs(days * 24 * 3600) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cap_deletes_the_old_then_the_oldest_until_it_fits() {
|
||||
let entries = vec![e("seg-1", 400, 9), e("seg-2", 300, 3), e("seg-3", 300, 2), e("seg-4", 200, 1), e("block-5.json", 1, 0)];
|
||||
// the age cap takes seg-1; the size cap (600 MB) then takes seg-2 (oldest kept), leaving 501 MB
|
||||
let plan = prune_plan(&entries, 600 * 1024 * 1024, Duration::from_secs(7 * 24 * 3600));
|
||||
assert_eq!(plan, vec![PathBuf::from("seg-1"), PathBuf::from("seg-2")]);
|
||||
// under both caps: nothing
|
||||
assert!(prune_plan(&entries[1..], 2 * 1024 * 1024 * 1024, Duration::from_secs(30 * 24 * 3600)).is_empty());
|
||||
// a 100 GB box for a week: the default caps leave 80 GB to the node and the system
|
||||
assert_eq!(DEFAULT_MAX_BYTES, 20 * 1024 * 1024 * 1024);
|
||||
assert_eq!(DEFAULT_MAX_AGE, Duration::from_secs(7 * 24 * 3600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_free_check_answers_on_this_machine() {
|
||||
let f = free_fraction(Path::new(".")).expect("statvfs");
|
||||
assert!((0.0..=1.0).contains(&f));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ const INDEX: &str = include_str!("../ui/index.html");
|
|||
const CSS: &str = include_str!("../ui/app.css");
|
||||
const JS: &str = include_str!("../ui/app.js");
|
||||
const MARK: &str = include_str!("../ui/mark.svg");
|
||||
const DAG_JS: &str = include_str!("../ui/live-dag.js");
|
||||
const COIN: &[u8] = include_bytes!("../../../brand/igneum-coin-1024.png");
|
||||
const FONT_MONO_400: &[u8] = include_bytes!("../ui/fonts/IBMPlexMono-400.woff2");
|
||||
const FONT_MONO_500: &[u8] = include_bytes!("../ui/fonts/IBMPlexMono-500.woff2");
|
||||
|
|
@ -138,6 +139,15 @@ fn json_resp(stream: &mut TcpStream, status: u16, v: Value) {
|
|||
respond(stream, status, "application/json; charset=utf-8", v.to_string().as_bytes(), false);
|
||||
}
|
||||
|
||||
/// Where a screenshot lands: `<data root>/shots/igneum-<name>-<unix>.png`, the name reduced to [a-z0-9-] (at most
|
||||
/// 24 characters, "shot" when empty) so a caller cannot steer the host outside the folder.
|
||||
pub fn shot_path(data_root: &std::path::Path, name: &str, unix: u64) -> std::path::PathBuf {
|
||||
let safe: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').map(|c| c.to_ascii_lowercase()).take(24).collect();
|
||||
let safe = if safe.is_empty() { "shot".to_string() } else { safe };
|
||||
data_root.join("shots").join(format!("igneum-{safe}-{unix}.png"))
|
||||
}
|
||||
|
||||
|
||||
fn query_param(q: &str, key: &str) -> Option<String> {
|
||||
q.split('&').find_map(|kv| {
|
||||
let (k, v) = kv.split_once('=')?;
|
||||
|
|
@ -165,6 +175,7 @@ fn handle(mut stream: TcpStream, shared: Arc<Shared>) {
|
|||
("GET", "/app.css") => respond(&mut stream, 200, "text/css; charset=utf-8", CSS.as_bytes(), false),
|
||||
("GET", "/app.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", JS.as_bytes(), false),
|
||||
("GET", "/mark.svg") => respond(&mut stream, 200, "image/svg+xml", MARK.as_bytes(), true),
|
||||
("GET", "/live-dag.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", DAG_JS.as_bytes(), false),
|
||||
("GET", "/coin.png") => respond(&mut stream, 200, "image/png", COIN, true),
|
||||
("GET", "/fonts/IBMPlexMono-400.woff2") => respond(&mut stream, 200, "font/woff2", FONT_MONO_400, true),
|
||||
("GET", "/fonts/IBMPlexMono-500.woff2") => respond(&mut stream, 200, "font/woff2", FONT_MONO_500, true),
|
||||
|
|
@ -178,6 +189,13 @@ fn handle(mut stream: TcpStream, shared: Arc<Shared>) {
|
|||
let v = shared.state_json();
|
||||
json_resp(&mut stream, 200, v);
|
||||
}
|
||||
// the chain scene's feed (src/live.rs): the observer's /api/live with this machine's lane as "you", cached 2 s
|
||||
("GET", "/api/live") => {
|
||||
let window = query_param(&req.query, "window").and_then(|s| s.parse().ok()).unwrap_or(120u32);
|
||||
let live_api = std::env::var("IGNEUM_APP_LIVE_API").unwrap_or_else(|_| crate::ota::live_api_from(&shared.packaged.live_page));
|
||||
let ids: std::collections::HashSet<String> = shared.state.lock().unwrap().mining.cards.iter().flat_map(|c| c.ids.iter().cloned()).collect();
|
||||
json_resp(&mut stream, 200, crate::live::fetch(&shared, &live_api, window, &ids));
|
||||
}
|
||||
("GET", "/api/log") => {
|
||||
let after = query_param(&req.query, "after").and_then(|s| s.parse().ok()).unwrap_or(0u64);
|
||||
let limit = query_param(&req.query, "limit").and_then(|s| s.parse().ok()).unwrap_or(400usize).min(2000);
|
||||
|
|
@ -256,6 +274,14 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
let goal = body.get("goal").and_then(|v| v.as_str()).map(|g| crate::ember::Goal::parse(g).name().to_string());
|
||||
let price = body.get("price_pence").and_then(|v| v.as_f64()).filter(|p| (0.0..=500.0).contains(p));
|
||||
let climb = body.get("climb").and_then(|v| v.as_bool());
|
||||
// with "key": that card's goal override ("" or "global" clears it); without: the global goal
|
||||
let key = body.get("key").and_then(|v| v.as_str()).map(|k| k.to_string());
|
||||
if let Some(k) = key {
|
||||
let g = body.get("goal").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let g = if g.is_empty() || g == "global" { String::new() } else { crate::ember::Goal::parse(g).name().to_string() };
|
||||
shared.send(Cmd::TuneCardGoal(k, g));
|
||||
return Ok(json!({ "ok": true }));
|
||||
}
|
||||
shared.send(Cmd::TuneGoal(goal, price, climb));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
|
|
@ -351,6 +377,17 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
shared.send(Cmd::ClockCheck);
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
// Miner UI 4 (b): a screenshot from the machine itself. The host (app/mac, app/windows) owns the window, so the
|
||||
// engine asks it over the host protocol ("SHOT <path>", beside URL, STATE and EXIT) and the host writes a PNG of
|
||||
// what it shows to that path under <data root>/shots/, where a collect job's glob "shots/*.png" picks it up.
|
||||
// Read-only: nothing about mining changes. A caller without a host (a bare engine) gets the path and no file.
|
||||
"/api/shot" => {
|
||||
let name = body.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let path = shot_path(&crate::platform::data_root(), name, crate::platform::unix_now());
|
||||
if let Some(dir) = path.parent() { let _ = std::fs::create_dir_all(dir); }
|
||||
println!("SHOT {}", path.display());
|
||||
Ok(json!({ "ok": true, "path": path.display().to_string(), "note": "the window host writes the PNG within a few seconds; a bare engine without a host writes nothing" }))
|
||||
}
|
||||
"/api/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)"));
|
||||
|
|
@ -359,3 +396,17 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
_ => Err("unknown api".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn a_shot_lands_under_the_data_root_with_a_safe_name() {
|
||||
let root = std::path::Path::new("/tmp/igneum-data");
|
||||
assert_eq!(shot_path(root, "overview", 1791327000), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-overview-1791327000.png"));
|
||||
assert_eq!(shot_path(root, "../../etc/passwd", 1), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-etcpasswd-1.png"));
|
||||
assert_eq!(shot_path(root, "", 2), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-shot-2.png"));
|
||||
assert_eq!(shot_path(root, "Cards Light!", 3), std::path::PathBuf::from("/tmp/igneum-data/shots/igneum-cardslight-3.png"));
|
||||
assert!(shot_path(root, &"x".repeat(80), 4).file_name().unwrap().to_string_lossy().len() < 48);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,29 @@ pub struct NodeState {
|
|||
pub version: String,
|
||||
pub last_reading_age_s: f64,
|
||||
pub message: String,
|
||||
/// N4 (6 October 2026, the frozen tip): seconds since the sink block's header time, from the watch line's
|
||||
/// tip_age_s (the node lane, ca3-v4-0316); -1 until the node reports it. "synced" needs it at or under 120.
|
||||
pub tip_age_s: f64,
|
||||
/// whose node this is (publish 2's read-back, 6 October 2026): "app" (started by this app), "external" (another
|
||||
/// node on this machine holds the ports; used after a check), "none" (the ports are taken by a node on other rules
|
||||
/// or another network, so no node is read), "" before the first start
|
||||
pub source: String,
|
||||
/// for an external node: match | mismatch | unknown (an older node that cannot answer the check)
|
||||
pub rules_check: String,
|
||||
/// the one line that says what happened at the ports, kept for the Node details
|
||||
pub port_note: String,
|
||||
/// where consensus_digest came from: "rpc" (igneum_getNodeInfo), "log" (the node's own stdout line), ""
|
||||
pub digest_source: String,
|
||||
/// which node this app reads: "own" (it started one), "external" (another node on this machine holds the ports),
|
||||
/// "none" (the ports are taken by a node it refuses), "" before the first decision. Re-decided when the other
|
||||
/// node goes away (7 October 2026): after 60 s the app starts its own on the freed ports.
|
||||
pub mode: String,
|
||||
/// why the mode is what it is, one line
|
||||
pub mode_reason: String,
|
||||
/// why the node is not "synced", in plain words ("" when synced): "behind" | "no peers" | "syncing" | "frozen"
|
||||
pub sync_cause: String,
|
||||
/// the miner's stall exits (code 45, "STALLED") since the node last started; the second one restarts the node
|
||||
pub stall_exits: u32,
|
||||
/// a consensus switch the signed manifest announced (difficulty v2 activation DAA); 0 = none
|
||||
pub consensus_switch_daa: u64,
|
||||
pub override_restart_wait: String,
|
||||
|
|
@ -126,6 +149,10 @@ pub struct CardState {
|
|||
pub tune_clock_mhz: u32, // the clock cap the last tune chose (0 = unlocked)
|
||||
pub tune_source: String, // full | confirm | baseline
|
||||
pub tune_line: String, // "Tuned: 122.3 MH/s at 290 W (0.422 MH/W)" once tuned
|
||||
pub tune_goal: String, // this card's goal override (efficiency | balanced | rate); "" = the global goal
|
||||
pub tune_before_watts: f64, // the untuned point of the last full plan (step 0); 0 = never measured
|
||||
pub tune_before_mhs: f64,
|
||||
pub tune_floor: bool, // the chosen clock is the ladder's floor (the row's sub-line says so)
|
||||
// a tune in progress on this card, by this engine or by a measurement engine posting /api/tune-progress
|
||||
// (the project lead, 6 October 2026: "don't we need to show in the app that tuning is in progress?")
|
||||
pub tune_step: u32,
|
||||
|
|
@ -163,6 +190,10 @@ pub struct MiningState {
|
|||
/// dev-fee blocks (the miner's `dev-fee block` lines): this run, and lifetime
|
||||
pub fee_session: u64,
|
||||
pub fee_total: u64,
|
||||
/// Miner UI 4 (6 October 2026): the sum of the mining cards' draw (a reading under 60 s old), and that draw as
|
||||
/// £ a day at settings.power_price_pence (0 when no price is set)
|
||||
pub watts_total: f64,
|
||||
pub pounds_per_day: f64,
|
||||
}
|
||||
|
||||
/// The miner software's dev fee as the miner reports it at start (`dev fee 1% (1 block in 100) to 0x...`).
|
||||
|
|
@ -249,6 +280,23 @@ pub struct FinalityState {
|
|||
pub age_s: f64,
|
||||
pub votes: u64,
|
||||
pub message: String,
|
||||
/// Horizon polish Q83/Q84 (6 October 2026): the network's finality is paused (a synced node, no checkpoint lock
|
||||
/// for manifest::FINALITY_PAUSE_S); since when (the last lock's time, else the engine's start); the cause when
|
||||
/// the node carries it (its `finality_reason=` / `held_by=` line), else the plain two-thirds line; and the one
|
||||
/// sentence every surface shows: "Finality paused since 18:39 UTC: under two thirds of the weight is signing"
|
||||
pub paused: bool,
|
||||
pub paused_since: f64,
|
||||
pub reason: String,
|
||||
pub held_by: String,
|
||||
pub line: String,
|
||||
/// the node's word (igneum_getProvingStatus finalityProvisional, 0.3.16): locks are provisional right now
|
||||
pub provisional: bool,
|
||||
/// where the cause came from: "node" (the RPC's structured fields), "node-line" (its log line) or "" (the
|
||||
/// engine's own rule)
|
||||
pub cause_source: String,
|
||||
/// the node's own finalityActive (b2e21447), when it carries it: then the node decides `paused`, not the
|
||||
/// engine's 15-minute rule
|
||||
pub node_active: Option<bool>,
|
||||
}
|
||||
|
||||
/// Clock skew against the network. skew_s = local time minus network time (negative = this machine is behind).
|
||||
|
|
@ -271,6 +319,15 @@ pub struct AddressState {
|
|||
pub source: String,
|
||||
pub key_saved: bool,
|
||||
pub wallet_file: String,
|
||||
/// Miner UI 4: the payout address's balance in wei as a decimal string (eth_getBalance through the node's own
|
||||
/// RPC, read every 30 s while the node runs); null until the first read; balance_age_s = -1 until then
|
||||
pub balance_wei: Option<String>,
|
||||
pub balance_age_s: f64,
|
||||
pub balance_note: String, // the last read's error, in words; "" when the last read was good
|
||||
/// £ per IGN. null until a market exists. Its one source will be a SIGNED field of the OTA manifest (`price`:
|
||||
/// gbp_per_ign, as_of, source), checked like the manifest's tuning object; the app never computes or fetches a
|
||||
/// price on its own
|
||||
pub price_gbp_per_ign: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Default)]
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ input,textarea{font-variant-numeric:tabular-nums}
|
|||
.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500}
|
||||
.btn.ghost{border-color:transparent;color:var(--ink-2)}
|
||||
.btn.ghost:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.btn.danger:hover{color:var(--ember);border-color:var(--ember)}
|
||||
:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
@media (prefers-reduced-motion:reduce){.btn{transition:none}}
|
||||
|
|
@ -77,7 +77,7 @@ body.mac .rail-brand{padding-top:30px}
|
|||
.nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease}
|
||||
.nav:hover{background:var(--hover);color:var(--bone)}
|
||||
.nav:hover svg{color:var(--ink-2)}
|
||||
.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600}
|
||||
.nav.on{background:var(--hover);border-color:var(--line);color:var(--bone);font-weight:600}
|
||||
.nav.on svg{color:var(--ember)}
|
||||
.nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)}
|
||||
.nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)}
|
||||
|
|
@ -99,13 +99,15 @@ body.has-rail .top{left:var(--rail)}
|
|||
.brand{display:flex;align-items:center;gap:10px;min-width:0}
|
||||
.brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em}
|
||||
.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px}
|
||||
body.has-rail .top{align-items:baseline;padding-top:19px}
|
||||
.page-title{display:flex;align-items:baseline;gap:12px;min-width:0}
|
||||
.page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap}
|
||||
.page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0}
|
||||
.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center}
|
||||
.pill{display:inline-flex;align-items:baseline;gap:var(--s-2);font-family:var(--sans);font-size:var(--t-base);font-weight:600;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:5px 12px 5px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;justify-content:center;line-height:1.3}
|
||||
.pill .dot{position:relative;top:-1px}
|
||||
.pill.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.pill.warn{color:var(--ember)}
|
||||
.pill.warn{color:var(--ember);border-color:var(--ember-40)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px}
|
||||
.on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite}
|
||||
.warn .dot,.dot.bad{background:var(--ember);animation:none}
|
||||
|
|
@ -116,26 +118,30 @@ body.has-rail .top{left:var(--rail)}
|
|||
top moves once per change with a 150 ms transition (layoutStrip), never per poll. */
|
||||
.notices{position:fixed;top:var(--top);left:0;right:0;z-index:19}
|
||||
body.has-rail .notices{left:var(--rail)}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)}
|
||||
.notice.bad{background:var(--ember-12);border-bottom-color:var(--ember-40)}
|
||||
.notice.warn{background:var(--molten-10);border-bottom-color:var(--molten-40)}
|
||||
.notice.update-urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-3);padding:9px calc(var(--gutter) - 6px) 9px var(--gutter);background:var(--row);border-bottom:1px solid var(--line);box-shadow:inset 0 1px 0 var(--ember);font-size:var(--t-base);line-height:1.4;color:var(--ash)}
|
||||
.notice-dot{width:7px;height:7px;border-radius:50%;background:var(--ember);flex:0 0 7px;display:inline-block}
|
||||
.notice.bad .notice-text,.notice.update-urgent .notice-text{color:var(--bone)}
|
||||
.notice.update-urgent .notice-text{font-weight:600}
|
||||
.notice .notice-text b{color:var(--bone);font-weight:600}
|
||||
.notice-text{flex:1 1 320px;min-width:0}
|
||||
.notice-actions{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto}
|
||||
.notice-actions:empty{display:none}
|
||||
.notice-close{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);font-size:20px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-close:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice.update-urgent .notice-close{color:#fff}
|
||||
.notice-detail{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-3px}
|
||||
.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice-more{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-more:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice-more .chev{width:8px;height:8px;transform:rotate(45deg) translateY(-2px)}
|
||||
.notice-more.open .chev{transform:rotate(225deg) translateY(-2px)}
|
||||
.notice-detail{flex-basis:100%;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);white-space:normal;word-break:break-word;margin-top:-2px;user-select:text;-webkit-user-select:text}
|
||||
.notice .prog{flex-basis:100%;height:2px;background:var(--line);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear}
|
||||
|
||||
/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */
|
||||
.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none}
|
||||
body.has-rail .ask-wrap{left:var(--rail)}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--row);border:1px solid var(--line);border-radius:14px;padding:12px 16px;box-shadow:inset 0 1px 0 var(--ember),0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0}
|
||||
.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none}
|
||||
.ask.inline{box-shadow:inset 0 1px 0 var(--ember);background:var(--row);margin-top:var(--s-3);animation:none}
|
||||
|
||||
/* screens and pages */
|
||||
main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease}
|
||||
|
|
@ -214,7 +220,7 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.toggle-big .ts{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
||||
.toggle-big.stop{background:var(--graphite);border-color:var(--line-2);color:var(--bone)}
|
||||
.toggle-big.stop:hover{border-color:var(--ash);background:var(--row)}
|
||||
.toggle-big.stop .ring{background:var(--ember-12);color:var(--ember)}
|
||||
.toggle-big.stop .ring{background:var(--obsidian);border:1px solid var(--line-2);color:var(--ember)}
|
||||
.toggle-big.stop .ts{color:var(--molten);opacity:1}
|
||||
.totals{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s-3);background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;min-width:0}
|
||||
.tot{display:flex;flex-direction:column;min-width:0;padding-left:var(--s-4);border-left:1px solid var(--line)}
|
||||
|
|
@ -246,9 +252,10 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.feed>div:last-child{border-bottom:0}
|
||||
.feed>div>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.feed .t{color:var(--ash);flex:0 0 auto;font-size:var(--t-xs)}
|
||||
.feed .k{color:var(--molten);margin-right:6px}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{color:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{color:var(--ash)}
|
||||
.feed .k{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--molten);margin-right:10px;position:relative;top:-1px}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{background:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{background:var(--line-2)}
|
||||
.feed .k.proving{background:var(--nvidia)}
|
||||
|
||||
/* the card rows on Mine (the hero object): badge, name and state, the numbers, Tune, the switch, the chevron;
|
||||
the tune line under; the details under that */
|
||||
|
|
@ -349,8 +356,8 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
#s-jobs-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top}
|
||||
#s-jobs-history tr.failed td,#s-jobs-history tr.timeout td,#s-jobs-history tr.aborted td{color:var(--ember)}
|
||||
#s-jobs-history tr.running td{color:var(--molten)}
|
||||
.clock-card{border:1px solid var(--ember-40);background:var(--ember-12);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.clock-card{border:1px solid var(--line);background:var(--row);box-shadow:inset 0 1px 0 var(--ember);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{box-shadow:inset 0 1px 0 var(--molten)}
|
||||
.clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45}
|
||||
.clock-card .note{margin-top:0}
|
||||
|
||||
|
|
@ -372,7 +379,7 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
.options{display:flex;flex-direction:column;gap:var(--s-3);margin-top:6px}
|
||||
.option{display:flex;gap:var(--s-4);align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease}
|
||||
.option:hover{border-color:var(--line-2)}
|
||||
.option.on{border-color:var(--ember);background:var(--ember-12)}
|
||||
.option.on{border-color:var(--ember);background:var(--row)}
|
||||
.option input[type=radio]{position:absolute;opacity:0;width:0;height:0}
|
||||
.option .radio{width:20px;height:20px;border-radius:50%;border:2px solid var(--line-2);flex:0 0 20px;margin-top:2px;position:relative}
|
||||
.option.on .radio{border-color:var(--ember)}
|
||||
|
|
@ -506,8 +513,8 @@ body.drawer-drag main{pointer-events:none}
|
|||
.log .src.app{color:var(--ember)}
|
||||
.log .src.watch{color:var(--ash)}
|
||||
.log .e,.log .e .src{color:var(--ember)}
|
||||
.log .ln.hit{background:rgba(255,179,92,.18);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:rgba(255,179,92,.3);color:var(--bone);border-radius:2px;padding:0 1px}
|
||||
.log .ln.hit{background:var(--hover);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:transparent;color:var(--bone);text-decoration:underline;text-decoration-color:var(--ember);text-underline-offset:2px}
|
||||
.log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ash);font-size:var(--t-sm);padding:0 var(--gutter);text-align:center}
|
||||
.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px var(--shadow);z-index:2;gap:6px;padding:6px 12px}
|
||||
.log-jump:hover{border-color:var(--molten)}
|
||||
|
|
@ -590,6 +597,8 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
.step{padding:32px 0}
|
||||
.money-row .v{font-size:var(--t-num)}
|
||||
.drawer-head{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
.notice{padding-left:var(--s-4);padding-right:var(--s-2);gap:var(--s-2)}
|
||||
.notice-text{flex-basis:160px}
|
||||
.log,.log-ruler .t0,.log-ruler .t1{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
}
|
||||
/* short windows (the 600 px floor): tighter paddings, a smaller canvas, so the drawer always has room */
|
||||
|
|
@ -605,3 +614,81 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
.drawer-head{padding-bottom:6px}
|
||||
.toggle-big{min-height:96px}
|
||||
}
|
||||
|
||||
/* miner-ui-4 (6 October 2026): Overview and Cards. The Overview hero (the project lead's pick, C): the fleet rate at 84 px on a
|
||||
graphite-to-obsidian fade with W, £ a day and blocks beside it, Start/Stop as the full-width bar under it. */
|
||||
.hero-row{grid-template-columns:1fr}
|
||||
.totals{order:1;grid-template-columns:2fr 1fr 1fr 1fr;align-items:end;padding:28px 28px 24px;border-color:transparent;background:linear-gradient(180deg,var(--graphite),var(--obsidian))}
|
||||
.tot{border-left:0;padding-left:0}
|
||||
.tot:first-child .v{font-size:84px;line-height:.95;letter-spacing:-.03em}
|
||||
.tot:first-child .k{font-size:var(--t-sm);margin-top:10px}
|
||||
.tot:first-child .s{font-size:var(--t-md)}
|
||||
.tot .v{font-size:30px}
|
||||
.tot .v.ask-price{font-size:var(--t-md);padding:9px 0 8px}
|
||||
.toggle-big{order:2;min-height:64px;flex-direction:row;align-items:center;gap:14px;padding:12px 20px}
|
||||
.toggle-big .ring{margin:0}
|
||||
.toggle-big .ts{margin-left:auto}
|
||||
/* the chain scene: the site's live-dag.js in a 220 px box, the legend words under it, the blocks strip as the fallback */
|
||||
.dag-wrap{position:relative}
|
||||
#dag-live{display:block;width:100%;height:220px;border-radius:10px;background:var(--obsidian);border:1px solid var(--line)}
|
||||
#dag{height:120px;margin-bottom:0}
|
||||
.dag-tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;font-size:var(--t-xs);color:var(--ink-2);display:flex;flex-direction:column;gap:2px;max-width:320px;box-shadow:0 8px 24px var(--shadow);z-index:3}
|
||||
.dag-tip b{color:var(--bone);font-weight:500;word-break:break-all}
|
||||
.dag-chip{position:absolute;right:10px;top:10px;font-size:var(--t-xs);color:var(--ash);background:var(--graphite);border:1px solid var(--line-2);border-radius:999px;padding:3px 10px;pointer-events:none;z-index:2}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash);align-items:center}
|
||||
.legend span{display:inline-flex;align-items:center;gap:7px}
|
||||
.lg{width:10px;height:10px;border-radius:3px;display:inline-block;border:1.5px solid var(--ash);background:var(--row)}
|
||||
.lg.you{border-color:var(--molten);box-shadow:0 0 0 2px var(--molten-40)}
|
||||
.lg.pending{border-color:var(--ash)}
|
||||
.lg.included{border-color:var(--ember)}
|
||||
.lg.excluded{border-color:var(--ash);opacity:.45}
|
||||
.lg.proven{background:var(--ember);border-color:var(--ember)}
|
||||
.lg.locked{border-color:var(--ember);border-radius:50%;box-shadow:0 0 0 2px var(--ember-40)}
|
||||
/* Cards: the Tuning strip above the list */
|
||||
.tune-strip{padding:var(--s-4) var(--card-pad)}
|
||||
.ts-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap}
|
||||
.ts-row .goal-line{flex:0 1 auto}
|
||||
.ts-saving{font-size:var(--t-base);color:var(--molten);font-weight:500;margin-left:auto}
|
||||
.ts-saving:empty{display:none}
|
||||
.tune-strip .line-text{margin-top:var(--s-2);font-size:var(--t-base);color:var(--ash)}
|
||||
.tune-strip .switch{padding-bottom:0}
|
||||
/* the Ember layer on a row: the flame mark, the saving, the sparkline, the curve */
|
||||
.gl-tune{align-items:center}
|
||||
.flame{flex:0 0 16px;display:inline-block}
|
||||
.flame .fo{fill:none;stroke:var(--ash);stroke-width:7}
|
||||
.flame .ff{fill:var(--ember)}
|
||||
.flame.running .ff{fill:var(--molten)}
|
||||
.flame.measured .ff{fill:var(--ash)}
|
||||
.flame.idle .ff,.flame.paused .ff,.flame.stopped .ff{fill:none}
|
||||
.gl-tune.tuned .t{color:var(--ink-2)}
|
||||
.gl-tune .save,.gl-details .save{color:var(--molten);white-space:nowrap}
|
||||
.num .k.spark{display:inline-flex;align-items:center;gap:6px}
|
||||
.spark{display:inline-block;vertical-align:middle}
|
||||
.spark path{fill:none;stroke:var(--ash);stroke-width:1.2}
|
||||
.spark circle{fill:var(--ember)}
|
||||
.curve-wrap{display:flex;flex-direction:column;gap:6px;max-width:420px}
|
||||
.curve-svg{width:100%;height:auto;background:var(--obsidian);border:1px solid var(--line);border-radius:10px}
|
||||
.curve-svg .ln{fill:none;stroke:var(--ash);stroke-width:1.2}
|
||||
.curve-svg circle{fill:var(--ember)}
|
||||
.curve-svg circle.chosen{fill:none;stroke:var(--ember);stroke-width:2}
|
||||
.curve-svg circle.marked{fill:none;stroke:var(--ash);stroke-width:1.2}
|
||||
.curve-svg text{font-family:var(--mono);font-size:9px;fill:var(--ash)}
|
||||
.seg.small .seg-b{padding:4px 10px;font-size:var(--t-sm)}
|
||||
.gl-details .ctl .seg{justify-self:start}
|
||||
@media (max-width:1180px){
|
||||
.totals{grid-template-columns:1fr 1fr 1fr 1fr}
|
||||
.tot:first-child .v{font-size:64px}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.totals{grid-template-columns:1fr 1fr;row-gap:var(--s-4)}
|
||||
.tot:first-child{grid-column:1 / -1}
|
||||
.tot:nth-child(3){padding-left:0}
|
||||
}
|
||||
@media (max-width:720px){
|
||||
.tot:first-child .v{font-size:56px}
|
||||
.toggle-big .ts{display:none}
|
||||
#dag-live{height:170px}
|
||||
.ts-saving{margin-left:0}
|
||||
.legend{gap:6px 12px}
|
||||
.rail-foot .nav.small{padding:6px 4px}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,12 +49,27 @@ var Notices = (function () {
|
|||
}
|
||||
if (u.updated_from && s.uptime_s >= 0 && s.uptime_s < INSTALLED_S) {
|
||||
var now = s.version || ver;
|
||||
return notice('update-installed', 'update:installed:' + now, 'Updated to Igneum Miner ' + now + ' from ' + u.updated_from + '.');
|
||||
var at = s.now > 0 && s.uptime_s >= 0 ? ' at ' + hhmm(s.now - s.uptime_s) : '';
|
||||
return notice('update-installed', 'update:installed:' + now, 'Updated to ' + now + at + '.', { title: 'Igneum Miner ' + now + ', from ' + u.updated_from });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function jobTitle(j) { return j.title && j.title !== j.kind ? j.title : (j.kind === 'shard-benchmark' ? 'shard benchmark' : j.kind === 'build' ? 'build (mining continues)' : j.kind); }
|
||||
// the job in a user's words (miner-ui-3 polish, 6 October 2026): from its kind, else from its id's prefix
|
||||
function jobWord(kind, id) {
|
||||
var k = String(kind || ''), i = String(id || '');
|
||||
var t = k || i;
|
||||
if (/^update-now/.test(t) || /^update-now/.test(i)) return 'update check';
|
||||
if (/^restart/.test(t) || /^restart/.test(i)) return 'restart';
|
||||
if (/^collect/.test(t) || /^collect/.test(i)) return 'log collection';
|
||||
if (/shard-benchmark|^shard/.test(t) || /shard/.test(i)) return 'shard benchmark';
|
||||
if (/^build/.test(t) || /^build/.test(i)) return 'build';
|
||||
if (/ember|tune|sweep/.test(t) || /ember|tune|sweep/.test(i)) return 'tuning check';
|
||||
if (/script|playbook|run/.test(t)) return 'a script';
|
||||
return k || 'a task';
|
||||
}
|
||||
function hhmm(t) { var d = new Date(t * 1000); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); }
|
||||
function firstErrorLine(results, summary) {
|
||||
for (var i = 0; i < results.length; i++) if (/BUILD FAILED|error|failed|panic/i.test(results[i])) return firstLine(results[i]);
|
||||
return firstLine(summary);
|
||||
|
|
@ -65,18 +80,20 @@ var Notices = (function () {
|
|||
if (j.active) {
|
||||
var m = Math.max(0, Math.floor((now - j.started_at) / 60));
|
||||
var results = (j.results || []).filter(function (x) { return x.indexOf('RESULT') === 0; });
|
||||
return notice('job-running', 'job:run:' + j.id, 'Job: ' + jobTitle(j) + ' running, ' + m + ' min.' + (j.stage ? ' ' + endDot(cap(j.stage)) : ''), { detail: results.length ? results[results.length - 1] : '', title: j.message || '' });
|
||||
var tech = [jobTitle(j), results.length ? results[results.length - 1] : ''].filter(Boolean).join(' · ');
|
||||
return notice('job-running', 'job:run:' + j.id, 'A job from the team is running: ' + jobWord(j.kind, j.id) + ', ' + m + ' min.' + (j.stage ? ' ' + endDot(cap(j.stage)) : ''), { detail: tech, title: j.message || '' });
|
||||
}
|
||||
var l = j.last;
|
||||
if (!l || !l.id) return null;
|
||||
var mins = Math.max(0, Math.round((l.finished_at - l.started_at) / 60)), t = l.title || l.kind;
|
||||
var report = l.uploaded ? 'Report uploaded.' : 'Report not uploaded.';
|
||||
var report = l.uploaded ? '' : ' The report did not upload.', word = jobWord(l.kind, l.id), tech = t + (l.summary ? ': ' + l.summary : '');
|
||||
if (l.status === 'done') {
|
||||
if (l.finished_at && now - l.finished_at > JOB_DONE_S) return null;
|
||||
return notice('job-done', 'job:done:' + l.id, 'Job: ' + t + ' done after ' + mins + ' min. ' + report, { title: l.summary || '' });
|
||||
return notice('job-done', 'job:done:' + l.id, 'A job from the team ran: ' + word + ', ' + mins + ' min.' + report, { detail: tech, title: l.summary || '' });
|
||||
}
|
||||
var word = l.status === 'timeout' ? 'hit its time cap' : l.status === 'aborted' ? 'was stopped' : 'failed';
|
||||
return notice('job-failed', 'job:failed:' + l.id, 'Job: ' + t + ' ' + word + ' after ' + mins + ' min, exit ' + l.exit + '. ' + report, { tone: 'bad', detail: firstErrorLine(j.last_results || [], l.summary), title: l.summary || '' });
|
||||
var verb = l.status === 'timeout' ? 'ran out of time' : l.status === 'aborted' ? 'was stopped' : 'failed';
|
||||
var cause = firstErrorLine(j.last_results || [], l.summary);
|
||||
return notice('job-failed', 'job:failed:' + l.id, 'A job from the team ' + verb + ': ' + word + ', after ' + mins + ' min.' + report, { tone: 'bad', detail: [tech, cause !== l.summary ? cause : ''].filter(Boolean).join(' · ') + ' · exit ' + l.exit, title: l.summary || '' });
|
||||
}
|
||||
|
||||
// c = state.clock: the engine's words, Sync clock, the manual hint underneath
|
||||
|
|
@ -136,7 +153,7 @@ var Notices = (function () {
|
|||
}
|
||||
return best;
|
||||
}
|
||||
return { tuneNotice: tuneNotice, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord };
|
||||
return { tuneNotice: tuneNotice, jobWord: jobWord, hhmm: hhmm, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord };
|
||||
})();
|
||||
|
||||
/* ---------- the update card (pure; update-card.test.mjs loads this block) ----------
|
||||
|
|
@ -249,9 +266,10 @@ var UpdateCard = (function () {
|
|||
var View = (function () {
|
||||
'use strict';
|
||||
var PAGES = [
|
||||
{ id: 'mine', title: 'Mine', sub: 'your cards, the rate, what it costs' },
|
||||
{ id: 'earnings', title: 'Earnings', sub: 'what it made, where it goes' },
|
||||
{ id: 'prove', title: 'Prove', sub: 'turn blocks into proofs, paid in IGN' },
|
||||
{ id: 'overview', title: 'Overview', sub: 'your rate, what it costs, the chain as it grows' },
|
||||
{ id: 'cards', title: 'Cards', sub: 'every card, tuned by Ember' },
|
||||
{ id: 'earnings', title: 'Earnings', sub: 'what you made, where it goes' },
|
||||
{ id: 'prove', title: 'Prove', sub: 'prove blocks, earn IGN' },
|
||||
{ id: 'settings', title: 'Settings', sub: 'tuning, this machine, updates, logs' }
|
||||
];
|
||||
var DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
|
@ -293,14 +311,14 @@ var View = (function () {
|
|||
var on = !!cd.enabled && !removed && !unusable, st = removed ? 'removed' : unusable ? 'unusable' : (cd.state || 'off');
|
||||
// the rule (docs/plans/ember-tune.md, 6 October 2026): a card never shows 0 MH/s without a reason word
|
||||
var tuning = st === 'tuning', held = st === 'held';
|
||||
var word = removed ? 'removed' : unusable ? ('not usable (' + cd.problem + ')') : !on ? 'off' : st === 'mining' ? 'mining' : tuning ? tuneWord(cd) : held ? (cd.message || 'held for a remote job') : st === 'restarting' ? 'restart in ' + (cd.restart_in_s || 0) + ' s' : st === 'waiting' ? 'waiting for the node' : st === 'faulted' ? 'stopped after repeated faults' : st === 'failed' ? 'failed' : st === 'starting' ? 'starting' : st === 'ready' ? 'ready' : st;
|
||||
var word = removed ? 'removed' : unusable ? ('not usable (' + cd.problem + ')') : !on ? 'off' : st === 'mining' ? 'mining' : tuning ? tuneWord(cd) : held ? (cd.message || 'paused for a job from the team') : st === 'restarting' ? 'restart in ' + (cd.restart_in_s || 0) + ' s' : st === 'waiting' ? 'waiting for the node' : st === 'faulted' ? 'stopped after repeated errors' : st === 'failed' ? 'failed' : st === 'starting' ? 'starting' : st === 'ready' ? 'ready' : st;
|
||||
var tone = removed ? 'off' : unusable ? 'bad' : !on ? 'off' : (st === 'mining' || tuning) ? 'on' : (st === 'failed' || st === 'faulted' || st === 'restarting') ? 'bad' : '';
|
||||
var hash = on && (st === 'mining' || tuning) ? (cd.hash_now >= 100 ? cd.hash_now.toFixed(0) : (cd.hash_now || 0).toFixed(1)) : '';
|
||||
var temp = cd.temp_gpu > 0 ? Math.round(cd.temp_gpu) + ' °C' : '';
|
||||
var tempTone = cd.temp_mem > 95 || cd.temp_gpu > 92 ? 'hot' : cd.temp_mem > 90 || cd.temp_gpu > 86 ? 'warm' : '';
|
||||
var power = cd.power_w > 0 ? Math.round(cd.power_w) + ' W' : '';
|
||||
var cap = cd.power_default_w > 0 ? Math.round(cd.power_default_w * (cd.power_pct || 80) / 100) : 0;
|
||||
var sub = unusable ? (cd.reason || '') : removed ? 'unplugged; its worker stopped. The row goes in five minutes.' : cd.message || (!on && cd.reason ? cd.reason : '') || '';
|
||||
var sub = unusable ? (cd.reason || '') : removed ? 'unplugged. Its row goes in five minutes.' : cd.message || (!on && cd.reason ? cd.reason : '') || '';
|
||||
var meta = [];
|
||||
if (cd.vram_mb) { var gb = cd.vram_mb / 1024; meta.push((gb >= 10 ? Math.round(gb) : gb.toFixed(gb < 4 ? 1 : 0)) + ' GB' + (cd.kind === 'apple' ? ' unified' : '')); }
|
||||
if (on && cd.accepted) meta.push(cd.accepted + ' block' + (cd.accepted === 1 ? '' : 's'));
|
||||
|
|
@ -326,8 +344,9 @@ var View = (function () {
|
|||
// MH per watt: the engine's live figure, else rate over draw
|
||||
function effOf(cd) { if (cd.eff_mhw > 0) return cd.eff_mhw; return cd.power_w > 0 && cd.hash_now > 0 ? cd.hash_now / cd.power_w : 0; }
|
||||
function effText(e) { return e > 0 ? (e >= 10 ? e.toFixed(1) : e.toFixed(2)) + ' MH/W' : ''; }
|
||||
// the watts drawn now by the present cards
|
||||
function wattsTotal(cards) { return (cards || []).filter(present).reduce(function (a, c) { return a + (c.power_w > 0 ? c.power_w : 0); }, 0); }
|
||||
// the watts drawn now by the cards that are mining or tuning (an idle card's standby draw is not the fleet's cost;
|
||||
// the 0.3.17 engine's mining.watts_total counts the same way)
|
||||
function wattsTotal(cards) { return (cards || []).filter(function (c) { return present(c) && (c.state === 'mining' || c.state === 'tuning'); }).reduce(function (a, c) { return a + (c.power_w > 0 ? c.power_w : 0); }, 0); }
|
||||
// why a row has no power or temperature cell: one sentence, once, instead of three n/a
|
||||
function noReading(cd) {
|
||||
if (cd.power_w > 0 || cd.temp_gpu > 0) return '';
|
||||
|
|
@ -379,9 +398,9 @@ var View = (function () {
|
|||
var when = cd.sweep_at > 0 && now ? ago(now - cd.sweep_at) : '';
|
||||
var nc = nextCheck(cd.sweep_at, st.tune_period_s, now);
|
||||
if (cd.tune_line) {
|
||||
var nums = String(cd.tune_line).replace(/^Tuned:\s*/, '');
|
||||
var nums = String(cd.tune_line).replace(/^(Tuned|Measured):\s*/, '');
|
||||
if (cd.tune_source === 'baseline' || !cd.tune_control) return { kind: 'measured', text: 'Measured ' + nums + ' as it runs' + (when ? ', ' + when : ''), note: reasonOf(cd.sweep_note), button: 'tune', progress: -1 };
|
||||
var src = cd.tune_source === 'confirm' ? ' from the fleet prior, confirmed' : '';
|
||||
var src = cd.tune_source === 'confirm' ? ' from what other miners found, confirmed' : '';
|
||||
var pin = cd.pinned ? 'Your cap stays pinned at ' + (cd.power_pct || 0) + '% · ' : '';
|
||||
var pt = tunePoint(cd);
|
||||
var stopped = cd.sweep_note && /^tuning stopped/i.test(cd.sweep_note) ? ' · ' + cap(cd.sweep_note) : '';
|
||||
|
|
@ -402,10 +421,11 @@ var View = (function () {
|
|||
list.forEach(function (c) { if (c.sweep_at > 0 && (!last || c.sweep_at > last.sweep_at)) last = c; });
|
||||
if (last) return 'Last tune ' + ago(now - last.sweep_at) + ' (' + last.name + ') · ' + nextCheck(last.sweep_at, st.tune_period_s, now) + '.';
|
||||
if (!list.length) return 'No card here can be tuned.';
|
||||
return st.sweep === false ? 'Ember Tune is off. Tune and Tune all still work by hand.' : 'Not tuned yet: the first tune starts 2 min into steady mining.';
|
||||
return st.sweep === false ? 'Ember Tune is off. Tune and Tune all still work when you press them.' : 'Not tuned yet: the first tune starts 2 min into steady mining.';
|
||||
}
|
||||
// the goal control's consequence: goal = efficiency|balanced|rate, cards, pence
|
||||
var GOALS = { efficiency: 'the most hashes per watt', balanced: 'hashes per watt within 1% of the top rate', rate: 'the top rate, whatever the watts' };
|
||||
var GOAL_NAMES = { efficiency: 'Efficiency', balanced: 'Balanced', rate: 'Maximum' };
|
||||
function goalWords(goal, cards, pence) {
|
||||
goal = GOALS[goal] ? goal : 'balanced';
|
||||
var list = (cards || []).filter(present), w = 0;
|
||||
|
|
@ -427,11 +447,11 @@ var View = (function () {
|
|||
var tc = tuningCard(cards);
|
||||
if (tc) return { label: 'Tuning', sub: 'mining again in ' + tuneEta(tc.tune_eta_s) + ' · ' + tc.name, cls: 'stop', disabled: true, act: '' };
|
||||
var heldCards = cards.filter(function (c) { return c.state === 'held'; }).length;
|
||||
if (heldCards && !mining) return { label: 'Held', sub: 'a remote job holds the cards; mining restarts when it ends', cls: 'stop', disabled: true, act: '' };
|
||||
if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a GPU on below first' : 'no GPU this app can drive', cls: '', disabled: true, act: '' };
|
||||
if (heldCards && !mining) return { label: 'Held', sub: 'a job from the team is using the cards. Mining restarts when it ends', cls: 'stop', disabled: true, act: '' };
|
||||
if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a card on in Cards first' : 'no card this app can use', cls: '', disabled: true, act: '' };
|
||||
if (m.paused) return { label: 'Start mining', sub: 'paused · the node keeps running', cls: '', disabled: false, act: 'resume' };
|
||||
if (m.state === 'mining') return { label: 'Stop mining', sub: 'mining on ' + mining + ' of ' + on + ' card' + (on === 1 ? '' : 's'), cls: 'stop', disabled: false, act: 'pause' };
|
||||
if (m.state === 'waiting') return { label: 'Stop mining', sub: n && n.synced ? 'worker starting' : 'waiting for the node to sync', cls: 'stop', disabled: false, act: 'pause' };
|
||||
if (m.state === 'waiting') return { label: 'Stop mining', sub: n && n.synced ? 'card starting' : n && n.state === 'behind' ? 'waiting: the node is behind the chain' : n && n.state === 'no peers' ? 'waiting: no peers on our chain' : 'waiting for the node to sync', cls: 'stop', disabled: false, act: 'pause' };
|
||||
return { label: 'Stop mining', sub: m.state || 'starting', cls: 'stop', disabled: false, act: 'pause' };
|
||||
}
|
||||
// the row's state word while the miner is paused: the card is on, the miner is not
|
||||
|
|
@ -442,7 +462,10 @@ var View = (function () {
|
|||
var bad = clock && clock.severity === 'block';
|
||||
if (bad) return { word: n.state === 'synced' ? 'synced' : n.state, line: 'Your clock is off by ' + skewWord(clock.skew_s) + '. Mining waits until it is synced.', tone: 'bad' };
|
||||
switch (n.state) {
|
||||
case 'synced': return { word: 'synced', line: 'This node has every block the network has made.' + (n.last_reading_age_s >= 0 ? ' Read ' + Math.round(n.last_reading_age_s) + ' s ago.' : ''), tone: 'ok' };
|
||||
case 'synced': return { word: 'synced', line: 'This node has every block the network has made.' + (n.tip_age_s >= 0 ? ' Last block ' + Math.round(n.tip_age_s) + ' s ago.' : n.last_reading_age_s >= 0 ? ' Read ' + Math.round(n.last_reading_age_s) + ' s ago.' : ''), tone: 'ok' };
|
||||
// N4 (6 October 2026): a tip that stopped moving is never "synced"; the node says why
|
||||
case 'behind': return { word: 'behind', line: (n.tip_age_s >= 0 ? 'No new block for ' + tipAge(n.tip_age_s) + '. ' : '') + 'The chain may have moved on without this node. Mining pauses until it catches up; the node re-dials its peers by itself.', tone: 'bad' };
|
||||
case 'no peers': return { word: 'no peers', line: 'No other node on our chain is connected. The node keeps dialling the seed list; mining waits.', tone: 'bad' };
|
||||
case 'syncing':
|
||||
var pct = n.headers > n.blocks && n.headers > 0 ? Math.floor(n.blocks / n.headers * 100) : 0;
|
||||
var line = n.headers > n.blocks ? (eta || n.message || 'downloading blocks') + ' · ' + withCommas(n.blocks) + ' of ' + withCommas(n.headers) + ' (' + pct + '%)' : (n.message || 'caught up, waiting for the next block');
|
||||
|
|
@ -450,15 +473,19 @@ var View = (function () {
|
|||
case 'starting': return { word: 'starting', line: n.message || 'opening the database', tone: '' };
|
||||
case 'restarting': return { word: 'restarting', line: 'restart in ' + (n.restart_in_s || 0) + ' s' + (n.message ? ' · ' + n.message : ''), tone: 'bad' };
|
||||
case 'failed': return { word: 'failed', line: n.message || 'the node could not start', tone: 'bad' };
|
||||
case 'stopped': return { word: 'stopped', line: n.message || 'the node is not running', tone: 'bad' };
|
||||
case 'stopped': return { word: n.source === 'none' ? 'not started' : 'stopped', line: n.message || 'the node is not running', tone: 'bad' };
|
||||
case 'stub': return { word: 'stub engine', line: 'This node was built without the mining engine, so it refuses every real block. Reinstall the app.', tone: 'bad' };
|
||||
}
|
||||
return { word: n.state || 'starting', line: n.message || '', tone: '' };
|
||||
}
|
||||
// the one node line on Mine: "Node synced · 6 peers · 109,859 blocks"
|
||||
// "12 s" / "4 min" / "53 min" / "2 h" for a tip age
|
||||
function tipAge(s) { s = Math.max(0, Math.round(s)); return s < 90 ? s + ' s' : s < 5400 ? Math.round(s / 60) + ' min' : Math.round(s / 3600) + ' h'; }
|
||||
// the one node line on Mine: "Node synced · 6 peers · 109,859 blocks · last block 4 s ago"
|
||||
function nodeLine(n, clock, eta) {
|
||||
var w = nodeWords(n, clock, eta), parts = ['Node ' + w.word];
|
||||
if (n.peers > 0) parts.push(n.peers + ' peer' + (n.peers === 1 ? '' : 's'));
|
||||
if (n.blocks > 0) parts.push(withCommas(n.blocks) + ' blocks');
|
||||
if (n.tip_age_s >= 0 && (n.state === 'synced' || n.state === 'behind')) parts.push('last block ' + tipAge(n.tip_age_s) + ' ago');
|
||||
return { text: parts.join(' · '), sub: w.line, tone: w.tone };
|
||||
}
|
||||
function skewWord(sk) { sk = Math.round(Math.abs(sk || 0)); return sk < 120 ? sk + ' s' : sk < 7200 ? Math.floor(sk / 60) + ' min ' + (sk % 60) + ' s' : Math.floor(sk / 3600) + ' h ' + Math.floor(sk % 3600 / 60) + ' min'; }
|
||||
|
|
@ -472,25 +499,25 @@ var View = (function () {
|
|||
return null;
|
||||
}
|
||||
function switchLine(sw, daa) {
|
||||
if (!sw) return (daa > 0 ? 'Every planned switch is behind this node. ' : '') + 'A switch is a planned rule change. The node applies it by itself when the chain reaches that height.';
|
||||
if (!sw) return (daa > 0 ? 'Every planned rule change is behind this node. ' : '') + 'A rule change is planned ahead. The node applies it by itself when the chain reaches that height.';
|
||||
var blocks = daa > 0 ? withCommas(sw.away) + ' blocks away, about ' + (sw.away < 3600 ? Math.max(1, Math.round(sw.away / 60)) + ' min' : sw.away < 86400 ? Math.floor(sw.away / 3600) + ' h ' + Math.round(sw.away % 3600 / 60) + ' min' : Math.floor(sw.away / 86400) + ' d') + ' at one block a second.' : 'the node applies it by itself at that height.';
|
||||
return sw.name + ' at DAA ' + withCommas(sw.daa) + ': ' + blocks;
|
||||
return sw.name + ' at block ' + withCommas(sw.daa) + ': ' + blocks;
|
||||
}
|
||||
|
||||
// the Prove page: pv = state.proving, enabled = state.settings.prove, synced = state.node.synced
|
||||
function proveWords(pv, enabled, synced) {
|
||||
pv = pv || {};
|
||||
if (!enabled) return { word: 'off', sub: 'not proving', tone: 'off', note: 'Off. Switch it on and this machine proves the pieces the chain assigns to its keys.' };
|
||||
if (!enabled) return { word: 'off', sub: 'not proving', tone: 'off', note: 'Off. Switch it on and this machine proves the shards it is given.' };
|
||||
if (pv.status === 'setup' || (pv.enabled && !pv.available && pv.setup_hint)) return { word: 'needs setup', sub: 'one-time install', tone: 'bad', note: pv.setup_hint || pv.message || 'The prover is not installed yet.' };
|
||||
if (!synced || pv.status === 'waiting') return { word: 'waiting', sub: 'for the node to sync', tone: '', note: pv.message || 'Proving starts once the node has every block.' };
|
||||
if (pv.status === 'proving') return { word: 'proving', sub: pv.current || 'a shard', tone: 'on', note: pv.message || 'A proof takes minutes on a CPU and seconds on a CUDA card.' };
|
||||
if (pv.status === 'proving') return { word: 'proving', sub: pv.current || 'a shard', tone: 'on', note: pv.message || 'A proof takes minutes on a CPU and seconds on an NVIDIA card.' };
|
||||
if (pv.status === 'submitted') return { word: 'submitted', sub: 'waiting to be paid', tone: 'on', note: pv.message || 'The node checks the proof, then the chain pays it.' };
|
||||
if (pv.status === 'idle') return { word: 'idle', sub: 'nothing assigned', tone: 'ok', note: pv.message || 'On. Nothing is assigned to your keys right now; the next shard arrives within a minute.' };
|
||||
if (pv.status === 'idle') return { word: 'idle', sub: 'nothing assigned', tone: 'ok', note: pv.message || 'On. Nothing to prove right now. The next shard arrives within a minute.' };
|
||||
return { word: pv.status || 'on', sub: pv.backend ? pv.backend.toUpperCase() : '', tone: '', note: pv.message || '' };
|
||||
}
|
||||
function verifierWords(pv) {
|
||||
var vm = (pv && pv.verifier_mode) || 'unknown';
|
||||
var word = { command: 'verifying', trust: 'trusting (devnet only)', off: 'off: relay only', unknown: 'not read yet' }[vm] || vm;
|
||||
var word = { command: 'verifying', trust: 'trusting without verifying (devnet only)', off: 'off, passing proofs along', unknown: 'not read yet' }[vm] || vm;
|
||||
if (vm === 'command' && pv.pool_entries) word += ' · pool ' + pv.pool_verified + '/' + pv.pool_entries + (pv.pool_failed ? ', ' + pv.pool_failed + ' rejected' : '');
|
||||
return { word: word, tone: vm === 'command' ? 'ok' : (vm === 'off' || vm === 'trust') ? 'warn' : 'dim', needsSetup: vm === 'off' && /WSL2 prover is not installed/.test((pv && pv.verifier_reason) || '') };
|
||||
}
|
||||
|
|
@ -500,7 +527,7 @@ var View = (function () {
|
|||
var gb = cd.vram_mb ? Math.round(cd.vram_mb / 1024) : 0;
|
||||
if (cd.vendor === 'apple') return cd.name + ': proves on the CPU, slowly.';
|
||||
if (cd.vendor === 'nvidia') return gb >= PROVE_GB ? cd.name + ': proves while it mines (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' : cd.name + ': cannot prove a full shard (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).';
|
||||
return cd.name + ': cannot prove yet (the prover is CUDA only).';
|
||||
return cd.name + ': cannot prove yet (proving needs an NVIDIA card).';
|
||||
}
|
||||
// the counts in one sentence: "idle · nothing assigned · 0 assigned · 0 proven · 0 paid · 0.0000 IGN"
|
||||
function proveLine(pv, enabled, synced) {
|
||||
|
|
@ -525,10 +552,10 @@ var View = (function () {
|
|||
function jobsNote(j, now, jobTitle) {
|
||||
var parts = [];
|
||||
if (j.account) parts.push(j.account);
|
||||
if (!j.allowed) parts.push('Off: nothing runs here until the switch above allows remote jobs.');
|
||||
else if (!j.url_set) parts.push('No jobs address in this build.');
|
||||
if (!j.allowed) parts.push('Off. Nothing runs here until the switch above allows jobs from the team.');
|
||||
else if (!j.url_set) parts.push('This build has no jobs address.');
|
||||
else if (j.error) parts.push(j.error + '.');
|
||||
else if (j.active) parts.push('Running ' + jobTitle(j) + ' (' + j.id + ').');
|
||||
else if (j.active) parts.push('Running: ' + jobTitle(j) + '.');
|
||||
else parts.push('Nothing running' + (j.checked_at ? '; checked ' + rel((now || 0) - j.checked_at) : '') + (j.queued ? '; ' + j.queued + ' queued' : '') + ((j.history || []).length ? '; ' + j.history.length + ' job' + (j.history.length === 1 ? '' : 's') + ' ran here' : '') + '.');
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
|
@ -539,10 +566,213 @@ var View = (function () {
|
|||
if (text.length > max) { var cut = text.lastIndexOf(' ', max - 2); text = text.slice(0, cut > 40 ? cut : max - 2) + '…'; }
|
||||
return text;
|
||||
}
|
||||
// the engine's event line in a user's voice (src/engine.rs event strings, 6 October 2026): {text, tech}; tech is
|
||||
// the engine's own line when it differs, for the tooltip. Unknown lines keep their words with a JSON tail cut.
|
||||
var EVENT_MAP = [
|
||||
[/^updated to Igneum Miner (\S+) from (\S+)/, function (m) { return 'Updated to ' + m[1]; }],
|
||||
[/^Igneum Miner (\S+) is available: downloading/, function (m) { return 'Downloading ' + m[1]; }],
|
||||
[/^Igneum Miner (\S+) is ready/, function (m) { return m[1] + ' is ready to install'; }],
|
||||
[/^installing Igneum Miner (\S+)/, function (m) { return 'Installing ' + m[1]; }],
|
||||
[/^update to (\S+) failed/, function (m) { return 'The update to ' + m[1] + ' failed'; }],
|
||||
[/^the update to (\S+) did not take/, function (m) { return 'The update to ' + m[1] + ' did not take'; }],
|
||||
[/^Igneum Miner (\S+) did not stay up twice; restoring (\S+)/, function (m) { return m[1] + ' did not stay up; going back to ' + m[2]; }],
|
||||
[/^update downloaded/, function () { return 'Update downloaded'; }],
|
||||
[/^OTA: administrator approval not given/, function () { return 'The update waits for the next time someone is at this PC'; }],
|
||||
[/^job (\S+) \((.+)\) starts: /, function (m) { return 'A job from the team started: ' + Notices.jobWord(m[2], m[1]); }],
|
||||
[/^job (\S+) \((.+)\) (done|failed|timeout|aborted) after (\d+) min/, function (m) { return 'A job from the team ' + (m[3] === 'done' ? 'ran' : m[3] === 'timeout' ? 'ran out of time' : m[3] === 'aborted' ? 'was stopped' : 'failed') + ': ' + Notices.jobWord('', m[1]) + ', ' + m[4] + ' min'; }],
|
||||
[/^(\d+) remote jobs? received from Igneum/, function (m) { return m[1] + ' job' + (m[1] === '1' ? '' : 's') + ' from the team received'; }],
|
||||
[/^remote job: update check now/, function () { return 'The team asked for an update check'; }],
|
||||
[/^remote job: the app restarts/, function () { return 'The team asked the app to restart'; }],
|
||||
[/^remote job asked for a node restart, but the node is external/, function () { return 'The team asked for a node restart. This node is not the app’s, so nothing was done'; }],
|
||||
[/^remote jobs allowed/, function () { return 'Jobs from the team can run here'; }],
|
||||
[/^remote jobs switched off/, function () { return 'Jobs from the team are off'; }],
|
||||
[/^job finished; the miners restart/, function () { return 'The job finished; mining restarts'; }],
|
||||
[/^node synced: (\d+) blocks, (\d+) peer/, function (m) { return 'Node synced: ' + withCommas(+m[1]) + ' blocks, ' + m[2] + ' peer' + (m[2] === '1' ? '' : 's'); }],
|
||||
[/^a node already answers on/, function () { return 'Using the node already running on this machine'; }],
|
||||
[/^consensus parameters from the signed manifest/, function () { return 'Rule settings read from the team'; }],
|
||||
[/^this node build does not understand the consensus parameters/, function () { return 'This node is too old for the team’s rule settings. An app update carries the newer node'; }],
|
||||
[/^restarting the node with the new consensus parameters/, function () { return 'Restarting the node with the new rule settings'; }],
|
||||
[/^kernel tuning from the signed manifest/, function () { return 'Tuning starting points read from the team'; }],
|
||||
[/^block accepted by the node \((.+)\)/, function (m) { return 'Block found by ' + m[1]; }],
|
||||
[/^igneumd exited with code (\d+) after (\d+) s; restarting/, function (m) { return 'The node stopped (code ' + m[1] + '). Restarting'; }],
|
||||
[/^igneumd exited at once \(code (\d+)\)/, function (m) { return 'The node stopped at once (code ' + m[1] + ')'; }],
|
||||
[/^igneumd could not start/, function () { return 'The node could not start'; }],
|
||||
[/^miner (\S+) exited with code (\d+); restarting in (\d+) s/, function (m) { return 'A card stopped (code ' + m[2] + '). Restarting in ' + m[3] + ' s'; }],
|
||||
[/^no reading from the node for (\d+) s/, function (m) { return 'No reading from the node for ' + m[1] + ' s. Restarting it'; }],
|
||||
[/^no peer after 90 s/, function () { return 'No other node found after 90 s. Still looking'; }],
|
||||
[/^no payout address/, function () { return 'No rewards address yet: finish the setup'; }],
|
||||
[/^mining paused; the node keeps running/, function () { return 'Mining paused. The node keeps running'; }],
|
||||
[/^mining resumed/, function () { return 'Mining resumed'; }],
|
||||
[/^GPU power cap in force: (.+)/, function (m) { return 'Power cap in force: ' + m[1]; }],
|
||||
[/^GPU power cap NOT applied/, function () { return 'Power cap not set. The administrator prompt was refused (Retry on the card)'; }],
|
||||
[/^power control on:/, function () { return 'Power control on'; }],
|
||||
[/^rewards go to (\S+)/, function (m) { return 'Rewards go to ' + shortHex(m[1], 6, 4); }],
|
||||
[/^settings changed \((.+)\); the miners restart/, function (m) { return 'Settings changed (' + m[1] + '). Mining restarts'; }],
|
||||
[/^(.+): tune queued; it starts once/, function (m) { return m[1] + ': tune queued'; }],
|
||||
[/^(.+): tuning started: /, function (m) { return m[1] + ': tuning started'; }],
|
||||
[/^(.+): best point (.+?) \((.+?)\); your setting stays pinned/, function (m) { return m[1] + ': best point ' + m[2] + ' (' + m[3] + '). Your cap stays pinned'; }],
|
||||
[/^(.+): measured as it runs: (.+)/, function (m) { return m[1] + ': measured ' + m[2]; }],
|
||||
[/^(.+): no tune: (.+)/, function (m) { return m[1] + ': no tune (' + m[2] + ')'; }],
|
||||
[/^(.+): worker fault: (.+); the miner restarts the worker/, function (m) { return m[1] + ': an error. Restarting it'; }],
|
||||
[/^(.+): the worker could not compile the next program/, function (m) { return m[1] + ': the next program is built at the top of the hour instead'; }],
|
||||
[/^(.+) worker ready/, function (m) { return m[1] + ': ready'; }],
|
||||
[/^(.+) kernel race: (.+?) at ([\d.]+) MH\/s/, function (m) { return m[1] + ': fastest program chosen, ' + m[3] + ' MH/s'; }],
|
||||
[/^(.+): switched on, (\d+) identit/, function (m) { return m[1] + ': on'; }],
|
||||
[/^(.+): switched off/, function (m) { return m[1] + ': off'; }],
|
||||
[/^(.+): (\d+) identit(y|ies); its worker restarts/, function (m) { return m[1] + ': ' + m[2] + ' miner' + (m[2] === '1' ? '' : 's') + ' on it. The card restarts'; }],
|
||||
[/^next hourly program compiled and ready/, function () { return 'Next hourly program ready'; }],
|
||||
[/^the hourly program changed; the worker restarts/, function () { return 'New hourly program. The card restarts on it'; }],
|
||||
[/^hourly program changed: /, function () { return 'New hourly program'; }],
|
||||
[/^program pack out of date, rebuilding/, function () { return 'Rebuilding the mining programs'; }],
|
||||
[/^GPU worker built from source/, function () { return 'Mining program built'; }],
|
||||
[/^block (\d+) shard (\d+) paid (.+) IGN/, function (m) { return 'Proof paid: ' + m[3] + ' IGN'; }],
|
||||
[/^block (\d+) shard (\d+) proven and submitted in (\d+) s/, function (m) { return 'Proof sent in ' + m[3] + ' s'; }],
|
||||
[/^segment .+ paid (.+) IGN to the aggregator/, function (m) { return 'Segment paid: ' + m[1] + ' IGN'; }],
|
||||
[/^segment (\S+)\.\.(\S+) aggregated and submitted in (\d+) s/, function (m) { return 'Segment proven in ' + m[3] + ' s'; }],
|
||||
[/^segment (\S+)\.\.(\S+): (\d+) shards proven, aggregated and submitted in (\d+) s/, function (m) { return 'Segment proven: ' + m[3] + ' shards in ' + m[4] + ' s'; }],
|
||||
[/^segment (\S+)\.\.(\S+): (\d+) shards proven and submitted in (\d+) s; the segment record is held/, function (m) { return 'Segment proven: ' + m[3] + ' shards in ' + m[4] + ' s. Its record waits for the chain'; }],
|
||||
[/^segment (\S+)\.\.(\S+) record accepted on retry/, function () { return 'Segment record accepted'; }],
|
||||
[/^WSL2 prover setup started/, function () { return 'Prover setup started in its own window'; }],
|
||||
[/^Card removed: (.+); its worker stopped/, function (m) { return 'Card removed: ' + m[1]; }],
|
||||
[/^(.+): not usable \((.+?)\); its worker stopped/, function (m) { return m[1] + ': not usable (' + m[2] + ')'; }],
|
||||
[/^clock agrees with the network again/, function () { return 'Clock agrees with the network again'; }],
|
||||
[/^asking the system to set its clock/, function () { return 'Setting the clock from a time server'; }],
|
||||
[/^clock: (.+)/, function (m) { return 'Clock: ' + m[1]; }],
|
||||
[/^devnet only: the node trusts proof records/, function () { return 'Devnet trust mode: proofs are not checked (Settings)'; }],
|
||||
[/^a sweep is setting the caps right now/, function () { return 'A tune is setting the caps now. Retry when it is done'; }],
|
||||
[/^updates are off for this engine/, function () { return 'Updates are off for this copy (a measurement run)'; }],
|
||||
[/^(.+); the node restarts$/, function (m) { return cap(m[1]) + '; the node restarts'; }],
|
||||
[/^(.+); the node is external, so the app cannot restart it/, function (m) { return cap(m[1]) + '; the node is not the app’s, so it is not restarted'; }]
|
||||
];
|
||||
function plainEvent(text) {
|
||||
text = String(text || '');
|
||||
for (var i = 0; i < EVENT_MAP.length; i++) { var m = EVENT_MAP[i][0].exec(text); if (m) { var out = EVENT_MAP[i][1](m); return { text: out, tech: out === text ? '' : text }; } }
|
||||
var cut = eventLine(text);
|
||||
return { text: cap(cut), tech: cut === text ? '' : text };
|
||||
}
|
||||
// the pill in the top bar: a verb a user understands, and its tone (on = molten, bad = ember, '' = grey)
|
||||
function pill(s) {
|
||||
if (s.quitting) return { text: 'Stopping', tone: '' };
|
||||
if (!s.setup_done) return { text: s.detecting ? 'Detecting cards' : 'Setting up', tone: '' };
|
||||
var m = s.mining || {}, n = s.node || {};
|
||||
if (tuningCard(m.cards)) return { text: 'Tuning', tone: 'on' };
|
||||
if (m.state === 'mining') return { text: 'Mining · ' + (m.hash_total >= 100 ? m.hash_total.toFixed(0) : (m.hash_total || 0).toFixed(1)) + ' MH/s', tone: 'on' };
|
||||
if (m.state === 'paused') return { text: 'Paused', tone: '' };
|
||||
if (n.state === 'syncing') return { text: 'Syncing the node', tone: '' };
|
||||
if (n.state === 'starting') return { text: 'Starting the node', tone: '' };
|
||||
if (n.state === 'restarting') return { text: 'Node restarting', tone: 'bad' };
|
||||
if (n.state === 'behind') return { text: 'Node behind', tone: 'bad' };
|
||||
if (n.state === 'no peers') return { text: 'No peers', tone: 'bad' };
|
||||
if (n.state === 'failed') return { text: 'Node failed', tone: 'bad' };
|
||||
if (n.state === 'stopped') return { text: n.source === 'none' ? 'Node not started' : 'Node stopped', tone: 'bad' };
|
||||
if (n.state === 'stub') return { text: 'Node fault', tone: 'bad' };
|
||||
if (m.state === 'waiting') return { text: 'Starting the worker', tone: '' };
|
||||
if (m.state === 'idle') return { text: 'Not mining', tone: '' };
|
||||
return { text: cap(m.state || 'starting'), tone: '' };
|
||||
}
|
||||
// ---- Ember on the card row (miner-ui-4, 6 October 2026): the flame mark's fill, the before -> after watts, the saving,
|
||||
// the sparkline points and the plain label. cd = the card, s = state, now = unix s ----
|
||||
// the "before" point (Ember, 6 October 2026): card.tune_before_watts / tune_before_mhs, step 0 of the last FULL plan,
|
||||
// kept across confirm plans; until a build carries them, the full plan's step 0 in tune_curve (unlocked, 100%); a
|
||||
// confirm or baseline plan without the fields has no before, so no saving is claimed
|
||||
function beforeOf(cd) {
|
||||
if (cd.tune_before_watts > 0) return { watts: cd.tune_before_watts, mhs: cd.tune_before_mhs || 0 };
|
||||
if (cd.tune_before_watts !== undefined) return null;
|
||||
var r = (cd.tune_curve || [])[0];
|
||||
if (cd.tune_source === 'full' && r && r.watts > 0 && (r.power_pct || 0) >= 100 && !(r.clock_mhz > 0)) return { watts: r.watts, mhs: r.mhs || 0 };
|
||||
return null;
|
||||
}
|
||||
// {kind: none|idle|measured|running|tuned|stopped|paused, fill: 0..1, label, sub, before, after, savingW, savingPct, savingText, next, points}
|
||||
function ember(cd, s, now) {
|
||||
var tw = tuneWords(cd, s, now), out = { kind: tw.kind, fill: 0, label: '', sub: '', before: 0, after: 0, savingW: 0, savingPct: 0, savingText: '', next: '', points: [] };
|
||||
if (tw.kind === 'none') return out;
|
||||
var st = (s && s.settings) || {};
|
||||
out.next = nextCheck(cd.sweep_at, st.tune_period_s, now).replace(/^next check /, '');
|
||||
out.points = (cd.tune_curve || []).filter(function (r) { return r.watts > 0 && r.mhs > 0; }).map(function (r) { return { w: r.watts, mhs: r.mhs, eff: r.eff || (r.mhs / r.watts), marked: !!r.mark, chosen: cd.sweep_watts > 0 && Math.abs(r.watts - cd.sweep_watts) < 0.5 && Math.abs((r.mhs || 0) - (cd.sweep_mhs || 0)) < 0.05 }; });
|
||||
if (cd.tune_floor) out.floor = true;
|
||||
if (tw.kind === 'running') { out.fill = tw.progress >= 0 ? tw.progress : 0.1; out.label = cd.tune_steps > 0 ? 'Tuning: step ' + (cd.tune_step || 1) + ' of ' + cd.tune_steps : 'Tuning'; out.sub = cd.tune_eta_s > 0 ? tuneEta(cd.tune_eta_s) + ' left' : ''; return out; }
|
||||
if (tw.kind === 'paused') { out.label = 'Tuning paused by Igneum'; out.sub = ''; return out; }
|
||||
if (tw.kind === 'stopped') { out.label = tw.text; return out; }
|
||||
if (tw.kind === 'measured') { out.fill = 0.5; out.label = 'Measured, not tuned'; out.sub = /Power control/i.test(cd.sweep_note || '') ? 'Power control is off' : cd.vendor === 'apple' ? 'Apple silicon sets its own power' : (tw.note || '').replace(/^Measured only /, ''); return out; }
|
||||
if (tw.kind === 'tuned') {
|
||||
out.fill = 1; var when = cd.sweep_at > 0 && now ? ago(now - cd.sweep_at) : '';
|
||||
out.label = 'Tuned' + (when ? ' ' + when : '') + (cd.tune_source === 'confirm' ? ' from what other miners found' : '');
|
||||
out.sub = tunePoint(cd) + (cd.tune_floor ? ' (the lowest clock Ember will set)' : '') + (out.next ? ' · next check ' + out.next : '');
|
||||
var b = beforeOf(cd); out.after = cd.sweep_watts > 0 ? cd.sweep_watts : 0; out.before = b ? b.watts : 0;
|
||||
if (out.before > 0 && out.after > 0 && out.before > out.after) {
|
||||
out.savingW = Math.round(out.before - out.after);
|
||||
out.savingPct = b.mhs > 0 && cd.sweep_mhs > 0 ? Math.max(0, (b.mhs - cd.sweep_mhs) / b.mhs * 100) : 0;
|
||||
out.savingText = 'saves ' + out.savingW + ' W' + (b.mhs > 0 ? ', ' + (out.savingPct < 0.05 ? 'no' : out.savingPct.toFixed(2) + '% of') + ' rate' : '');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
out.label = st.sweep === false ? 'Not tuned: Ember Tune is off' : 'Not tuned yet'; out.sub = st.sweep === false ? 'Tune starts one by hand' : 'starts after 2 min of steady mining'; return out;
|
||||
}
|
||||
// the Tuning strip's fleet saving: watts and £ a day summed over the tuned cards
|
||||
function fleetSaving(cards, s, now, pence) {
|
||||
var w = 0, n = 0;
|
||||
(cards || []).forEach(function (c) { if (!present(c)) return; var e = ember(c, s, now); if (e.kind === 'tuned' && e.savingW > 0) { w += e.savingW; n++; } });
|
||||
if (!n) return '';
|
||||
var ppd = poundsPerDay(w, pence);
|
||||
return 'Ember saves ' + Math.round(w) + ' W' + (ppd !== null ? ', ' + money(ppd) + ' a day' : '') + ' across ' + n + ' tuned card' + (n === 1 ? '' : 's');
|
||||
}
|
||||
// ---- the engine's 0.3.16 fields (ember-tune fd03f35): mining.watts_total, mining.pounds_per_day, address.balance_wei
|
||||
// (string | null), address.balance_age_s (-1 until read), address.balance_note, address.price_gbp_per_ign
|
||||
// (null until a signed manifest carries a price), the chain scene's reply.source ("node" | "site") ----
|
||||
function ign(wei) { var n = Number(wei); return isNaN(n) ? null : n / 1e18; }
|
||||
// the Earnings balance row: {text, sub, pounds} ; the UI shows "not read yet" until the first read
|
||||
function balanceWords(a, now) {
|
||||
a = a || {};
|
||||
if (a.balance_wei === undefined) return { text: '', sub: '', pounds: null };
|
||||
if (a.balance_wei === null || a.balance_wei === '') return { text: 'not read yet', sub: a.balance_note || 'the node answers the balance once it is synced', pounds: null };
|
||||
var v = ign(a.balance_wei); if (v === null) return { text: 'not read yet', sub: a.balance_note || '', pounds: null };
|
||||
var age = a.balance_age_s >= 0 ? ' · read ' + ago(a.balance_age_s) : '';
|
||||
var price = a.price_gbp_per_ign;
|
||||
return { text: (v >= 1000 ? withCommas(v) : v.toFixed(4)) + ' IGN', sub: 'in the wallet' + age + (a.balance_note ? ' · ' + a.balance_note : ''), pounds: price > 0 ? v * price : null };
|
||||
}
|
||||
// the fleet watts and £ a day: the engine's figures when it carries them, else the UI's own sums
|
||||
function fleetWatts(m, cards) { return m && m.watts_total > 0 ? m.watts_total : wattsTotal(cards); }
|
||||
function fleetPounds(m, cards, pence) { if (m && m.pounds_per_day > 0) return m.pounds_per_day; return poundsPerDay(fleetWatts(m, cards), pence); }
|
||||
// is a lane key this machine's? The cards' ids are the first 8 hex of each vote key hash; a source may shorten the
|
||||
// hash to another length (the 0.3.16 engine keeps 12), so either prefix of the other counts
|
||||
function isMine(miner, cards) {
|
||||
var m = String(miner || '').replace(/^0x/, '').toLowerCase(); if (!m) return false;
|
||||
var head = m.split('..')[0]; // the 0.3.16 engine shortens a long hash to "head6..tail6"; the site keeps 8 hex
|
||||
return (cards || []).some(function (c) { return (c.ids || []).some(function (id) {
|
||||
id = String(id).replace(/^0x/, '').toLowerCase(); if (!id) return false;
|
||||
var n = Math.min(head.length, id.length); return n >= 6 && head.slice(0, n) === id.slice(0, n);
|
||||
}); });
|
||||
}
|
||||
// the chain scene's note from the reply's source
|
||||
function liveNote(reply) {
|
||||
if (!reply) return '';
|
||||
if (reply.pending) return 'connecting';
|
||||
var src = reply.source || ((reply.state || {}).source);
|
||||
if (src === 'node') return 'From your node. Your blocks are ringed.';
|
||||
if (src === 'site' || src === 'observer') return 'From Igneum’s observer. Your blocks are ringed.';
|
||||
return '';
|
||||
}
|
||||
// finality (ember-tune 1357d280, folded in for 0.3.17): f = state.finality. While the network's finality is paused
|
||||
// (under two thirds of the weight signing) the node line's sub-text and the lock row say so, and no surface calls a
|
||||
// lock "final" (main, Q83/Q84). {paused, age, note}
|
||||
function finalityWords(f) {
|
||||
f = f || {};
|
||||
if (f.paused) return { paused: true, age: 'paused', note: f.line || 'Finality paused: under two thirds of the weight is signing' };
|
||||
if (f.last_lock > 0) return { paused: false, age: rel(f.age_s), note: 'A lock is a point the miners have agreed can never be undone. Checkpoints lock at 2/3 of the voting weight.' };
|
||||
return { paused: false, age: 'n/a', note: f.message || 'Locks appear once the miner votes on checkpoints.' };
|
||||
}
|
||||
// whose node the app reads (publish 2's read-back, 6 October 2026): n.source app | external | none | ""
|
||||
function nodeSource(n) {
|
||||
n = n || {};
|
||||
if (n.source === 'app') return { text: 'this app’s own node', sub: n.mode_reason || n.port_note || '' };
|
||||
if (n.source === 'external') return { text: 'another node on this machine', sub: n.rules_check === 'match' ? 'same network and rules, checked' : n.rules_check === 'unknown' ? 'its rules could not be checked (it predates 0.3.17)' : (n.port_note || '') };
|
||||
if (n.source === 'none') return { text: 'no node', sub: n.port_note || n.mode_reason || 'the ports are taken by a node on other rules' };
|
||||
return { text: '', sub: '' };
|
||||
}
|
||||
// the confirmations' words (in place, never a dialog)
|
||||
function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? The miner restarts.'; }
|
||||
function trustAsk(on) { return on ? 'Include proof records the node never checked? Devnet only; the node restarts.' : 'Verify proof records again? The node restarts.'; }
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, goalWords: goalWords, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, addressAsk: addressAsk, trustAsk: trustAsk };
|
||||
function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; }
|
||||
function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; }
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk };
|
||||
})();
|
||||
/* ---------- 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
|
||||
|
|
@ -585,7 +815,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var state = null, phase = 'welcome', page = 'mine', forced = null, cardsSeen = false, keyShownThisRun = false;
|
||||
var params = new URLSearchParams(location.search);
|
||||
if (params.get('screen')) forced = params.get('screen');
|
||||
if (params.get('page')) page = params.get('page'); // ?page=mine|earnings|prove|settings (screenshots)
|
||||
if (params.get('page')) page = params.get('page'); // ?page=overview|cards|earnings|prove|settings (screenshots)
|
||||
var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated
|
||||
var forcedJob = params.get('job'); // a sample remote job for screenshots: running|done|failed
|
||||
if (params.get('host') === 'mac') document.body.classList.add('mac');
|
||||
|
|
@ -659,7 +889,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('page-title').hidden = !dash;
|
||||
if (!dash && drawerOpen()) setDrawer(false);
|
||||
if (p === 'cards' && !cardsSeen) { cardsSeen = true; api('api/detect', {}); }
|
||||
if (dash) showPage(page);
|
||||
if (dash) { showPage(page); mountDag(); }
|
||||
api('api/phase', { phase: p });
|
||||
}
|
||||
function showPage(id) {
|
||||
|
|
@ -674,7 +904,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
});
|
||||
setText('page-title-text', meta.title); setText('page-sub', meta.sub);
|
||||
$('main').scrollTop = 0;
|
||||
if (state) { renderPage(state); if (page === 'mine') size(); }
|
||||
if (state) { renderPage(state); if (page === 'overview') size(); }
|
||||
}
|
||||
$('rail-nav').addEventListener('click', function (e) { var b = e.target.closest('.nav[data-page]'); if (b) showPage(b.dataset.page); });
|
||||
// the rail answers the arrow keys and Home/End, so the sections are reachable without a mouse
|
||||
|
|
@ -713,7 +943,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (!r.ok) { $('addr-err').textContent = r.error || 'could not save'; $('addr-err').hidden = false; return; }
|
||||
if (mode === 'generate' && r.private_key) { showKeySheet(r); }
|
||||
else { api('api/start', {}).then(function () { show('dashboard'); }); }
|
||||
}).catch(function () { $('btn-start').disabled = false; toast('The engine did not answer'); });
|
||||
}).catch(function () { $('btn-start').disabled = false; toast('The app did not answer'); });
|
||||
});
|
||||
function showKeySheet(r) {
|
||||
$('key-address').textContent = r.display || r.address;
|
||||
|
|
@ -743,7 +973,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (!state) return;
|
||||
var t = View.toggle(state.mining, state.node, state);
|
||||
if (!t.act) return;
|
||||
api('api/' + t.act, {}).then(function () { toast(t.act === 'pause' ? 'Mining stops; the node keeps running' : 'Mining starts'); });
|
||||
api('api/' + t.act, {}).then(function () { toast(t.act === 'pause' ? 'Mining stops. The node keeps running' : 'Mining starts'); });
|
||||
});
|
||||
// a row's switch, slider or stepper sends the whole list to the engine (pending[key] holds a flipped switch until
|
||||
// the engine agrees, so the row does not flicker back)
|
||||
|
|
@ -752,7 +982,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
clearTimeout(cardsT);
|
||||
cardsT = setTimeout(function () {
|
||||
var choices = readCardRows($('d-cards'));
|
||||
if (choices.length) api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(msg || 'Applied; only the changed worker restarts'); else toast(r.error || 'could not change'); });
|
||||
if (choices.length) api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(msg || 'Applied. Only that card restarts'); else toast(r.error || 'could not change'); });
|
||||
}, 400);
|
||||
}
|
||||
$('d-cards').addEventListener('change', function (e) {
|
||||
|
|
@ -761,14 +991,14 @@ if (typeof document !== 'undefined') (function () {
|
|||
var key = e.target.closest('.gpu-line').dataset.key, on = e.target.checked;
|
||||
pending[key] = { on: on, at: Date.now() };
|
||||
var choices = shownCards(state.mining.cards).filter(View.present).map(function (c) { var it = { key: c.key, enabled: c.key === key ? on : c.enabled, identities: c.identities || 1 }; if (c.vendor === 'nvidia' && c.power_default_w > 0) it.power_pct = c.power_pct || 80; return it; });
|
||||
api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(on ? 'Worker starting on that card' : 'Worker stopped on that card'); else { delete pending[key]; toast(r.error || 'could not change'); } });
|
||||
} else if (e.target.matches('.ids input')) sendCards('Identities set; that card’s worker restarts');
|
||||
api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(on ? 'Starting that card' : 'That card is off'); else { delete pending[key]; toast(r.error || 'could not change'); } });
|
||||
} else if (e.target.matches('.ids input')) sendCards('Set. The card restarts');
|
||||
});
|
||||
$('d-cards').addEventListener('input', function (e) {
|
||||
if (e.target.matches('input[type=range]')) {
|
||||
var row = e.target.closest('.gpu-line'), cd = cardOf(row.dataset.key), pct = parseInt(e.target.value, 10);
|
||||
row.querySelector('.pv').textContent = pct + '% · ' + (cd && cd.power_default_w ? Math.round(cd.power_default_w * pct / 100) + ' W' : '');
|
||||
sendCards('Power cap set; pinned until you unpin it');
|
||||
sendCards('Power cap set. It stays until you choose Default again');
|
||||
}
|
||||
});
|
||||
$('d-cards').addEventListener('click', function (e) {
|
||||
|
|
@ -776,11 +1006,14 @@ if (typeof document !== 'undefined') (function () {
|
|||
var row = b.closest('.gpu-line'), key = row ? row.dataset.key : '';
|
||||
if (b.dataset.open !== undefined) { openCards[key] = !openCards[key]; applyOpen(row); }
|
||||
else if (b.dataset.tune !== undefined) api('api/sweep/start', { key: key }).then(function (r) { toast(r && r.ok === false ? (r.error || 'could not start the tune') : 'Tune queued for ' + (cardOf(key) || {}).name); });
|
||||
else if (b.dataset.stop !== undefined) api('api/sweep/stop', {}).then(function () { toast('Tune stopped; the card is back where it was'); });
|
||||
else if (b.dataset.stop !== undefined) api('api/sweep/stop', {}).then(function () { toast('Tune stopped. The card is back where it was'); });
|
||||
else 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.pin !== undefined) api('api/sweep/pin', { key: key, pinned: b.dataset.pin === '1' }).then(function () { toast(b.dataset.pin === '1' ? 'Cap pinned' : 'The tune chooses the cap again'); });
|
||||
else if (b.dataset.powerRetry !== undefined) api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); });
|
||||
else if (b.dataset.pin !== undefined) api('api/sweep/pin', { key: key, pinned: b.dataset.pin === '1' }).then(function () { toast(b.dataset.pin === '1' ? 'Cap pinned' : 'Default: Ember chooses the cap again'); });
|
||||
else if (b.dataset.powerRetry !== undefined) api('api/power/apply', {}).then(function () { toast('Allow the administrator prompt to set the cap'); });
|
||||
else if (b.dataset.price !== undefined) { showPage('settings'); $('s-price').focus(); }
|
||||
else if (b.closest('[data-card-goal]') && b.dataset.goal !== undefined) {
|
||||
api('api/tune/goal', { key: key, goal: b.dataset.goal || '' }).then(function (r) { if (r && r.ok) toast(b.dataset.goal ? 'Goal for this card: ' + View.GOALS[b.dataset.goal] : 'Default: this card follows the goal set for all cards'); else toast('A goal for one card needs the next Ember Tune update'); }).catch(function () { toast('A per-card goal needs the Ember Tune update'); });
|
||||
}
|
||||
});
|
||||
function applyOpen(row) { var open = !!openCards[row.dataset.key], det = row.querySelector('.gl-details'), chev = row.querySelector('[data-open]'); if (det) det.hidden = !open; if (chev) { chev.setAttribute('aria-expanded', open ? 'true' : 'false'); chev.classList.toggle('open', open); } row.classList.toggle('open', open); }
|
||||
function cardOf(key) { return state ? state.mining.cards.filter(function (c) { return c.key === key; })[0] : null; }
|
||||
|
|
@ -789,21 +1022,21 @@ if (typeof document !== 'undefined') (function () {
|
|||
var list = shownCards(state.mining.cards).filter(function (c) { return View.tunable(c) && c.enabled; });
|
||||
if (!list.length) { toast('No card to tune'); return; }
|
||||
var n = 0;
|
||||
list.reduce(function (p, c) { return p.then(function () { return api('api/sweep/start', { key: c.key }).then(function (r) { if (!r || r.ok !== false) n++; }); }); }, Promise.resolve()).then(function () { toast(n === 1 ? 'Tune queued for 1 card' : 'Tune queued for ' + n + ' cards, one at a time'); });
|
||||
list.reduce(function (p, c) { return p.then(function () { return api('api/sweep/start', { key: c.key }).then(function (r) { if (!r || r.ok !== false) n++; }); }); }, Promise.resolve()).then(function () { toast(n === 1 ? 'Tune queued for 1 card' : 'Tune queued for ' + n + ' cards, one after another'); });
|
||||
});
|
||||
// the goal (ember-tune: POST api/tune/goal); a build without the route says so and keeps the old value
|
||||
function setGoal(goal) {
|
||||
api('api/tune/goal', { goal: goal }).then(function (r) { if (r && r.ok) { store('igneum.goal', goal); toast('Goal: ' + View.GOALS[goal]); } else toast('The goal needs the Ember Tune update'); if (state) renderTuneCard(state); }).catch(function () { toast('The goal needs the Ember Tune update'); });
|
||||
api('api/tune/goal', { goal: goal }).then(function (r) { if (r && r.ok) { store('igneum.goal', goal); toast('Goal: ' + View.GOALS[goal]); } else toast('The goal needs the next Ember Tune update'); if (state) renderTuneCard(state); }).catch(function () { toast('The goal needs the Ember Tune update'); });
|
||||
}
|
||||
['goal-seg', 'goal-seg-2'].forEach(function (id) { $(id).addEventListener('click', function (e) { var b = e.target.closest('.seg-b'); if (b) setGoal(b.dataset.goal); }); });
|
||||
function powerControl(on) { 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'); }); }
|
||||
function powerControl(on) { 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'); }); }
|
||||
$('m-power-control').addEventListener('change', function () { powerControl(this.checked); });
|
||||
$('s-power-control').addEventListener('change', function () { powerControl(this.checked); });
|
||||
$('m-clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); });
|
||||
$('btn-open-log').addEventListener('click', function () { setDrawer(true); });
|
||||
|
||||
// ---------- Earnings ----------
|
||||
$('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); });
|
||||
$('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied. Mining restarts' : 'Applied'); }); });
|
||||
$('e-price-btn').addEventListener('click', function () { showPage('settings'); $('s-price').focus(); });
|
||||
$('t-money-cell').addEventListener('click', function () { if ($('t-money').classList.contains('ask-price')) { showPage('settings'); $('s-price').focus(); } });
|
||||
$('s-address-save').addEventListener('click', function () {
|
||||
|
|
@ -826,7 +1059,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('r-wallet').addEventListener('click', function () { api('api/open', { url: 'https://igneum.network/wallet' }); });
|
||||
|
||||
// ---------- Prove ----------
|
||||
$('s-prove').addEventListener('change', function () { var on = this.checked; api('api/prove', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Proving on; the first piece arrives within a minute' : 'Proving off'); else { toast(r.error || 'could not change'); $('s-prove').checked = !on; } }); });
|
||||
$('s-prove').addEventListener('change', function () { var on = this.checked; api('api/prove', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Proving on. The first shard arrives within a minute' : 'Proving off'); else { toast(r.error || 'could not change'); $('s-prove').checked = !on; } }); });
|
||||
$('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); });
|
||||
|
||||
// ---------- Settings ----------
|
||||
|
|
@ -838,11 +1071,11 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('s-vote').addEventListener('change', function () { api('api/settings', { vote: 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 ? 'Ember Tune on: once after install, then every 7 days' : 'Ember Tune 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 every 7 days' : 'Ember Tune off'); }); });
|
||||
$('s-climb').addEventListener('change', function () { var on = this.checked; api('api/tune/goal', { climb: on }).then(function (r) { if (!r || !r.ok) { toast('Needs the Ember Tune update'); $('s-climb').checked = !on; } }); });
|
||||
function savePrice() {
|
||||
var p = parseFloat($('s-price').value);
|
||||
if (!(p >= 0)) { toast('A price in pence per kWh, for example 28'); return; }
|
||||
if (!(p >= 0)) { toast('Type a price in pence per kWh, for example 28'); return; }
|
||||
store('igneum.price', p > 0 ? p : null);
|
||||
api('api/tune/goal', { price_pence: p }).then(function () { toast(p > 0 ? 'Price saved: ' + p + 'p per kWh' : 'Price cleared'); if (state) renderPage(state); }).catch(function () { toast(p > 0 ? 'Price saved on this window: ' + p + 'p per kWh' : 'Price cleared'); if (state) renderPage(state); });
|
||||
}
|
||||
|
|
@ -852,7 +1085,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('ask-trust-no').addEventListener('click', function () { $('ask-trust').hidden = true; });
|
||||
$('ask-trust-yes').addEventListener('click', function () {
|
||||
var on = $('ask-trust').dataset.on === '1'; $('ask-trust').hidden = true;
|
||||
api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) { $('s-trust').checked = on; toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); } else toast(r.error || 'could not change'); });
|
||||
api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) { $('s-trust').checked = on; toast(on ? 'Trust mode on (devnet only). The node restarts' : 'Trust mode off. The node restarts'); } else toast(r.error || 'could not change'); });
|
||||
});
|
||||
$('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); });
|
||||
|
|
@ -1121,7 +1354,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (found.length && Object.keys(born).length > found.length + 50) { var keep = {}; for (var k = 0; k < found.length; k++) if (born[String(found[k])]) keep[String(found[k])] = born[String(found[k])]; born = keep; }
|
||||
if (!shown) {
|
||||
x.fillStyle = css('--ash') || '#9A9A9E'; x.font = '500 12px IBM Plex Mono, monospace'; x.textAlign = 'center';
|
||||
var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'mining: a block this machine finds appears here' : state.mining.state === 'paused' ? 'paused: no blocks while mining is stopped' : state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync';
|
||||
var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'Mining. A block this machine finds appears here' : state.mining.state === 'paused' ? 'Paused. No blocks while mining is stopped' : state.node.synced ? 'waiting for the card' : 'waiting for the node to sync';
|
||||
x.fillText(msg, W / 2, rail - 10);
|
||||
}
|
||||
return animating && !reduce;
|
||||
|
|
@ -1131,7 +1364,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
drawRaf = 0; clearTimeout(drawTimer); drawTimer = null;
|
||||
if (hiddenNow()) return;
|
||||
var fast = false;
|
||||
if (phase === 'dashboard' && page === 'mine') { var a = performance.now(); fast = draw(now); perf.draw += performance.now() - a; perf.frames++; }
|
||||
if (phase === 'dashboard' && page === 'overview' && !$('dag').hidden) { var a = performance.now(); fast = draw(now); perf.draw += performance.now() - a; perf.frames++; }
|
||||
if (fast) drawRaf = requestAnimationFrame(frame);
|
||||
else drawTimer = setTimeout(function () { drawTimer = null; drawRaf = requestAnimationFrame(frame); }, reduce ? 1000 : 500);
|
||||
}
|
||||
|
|
@ -1143,6 +1376,21 @@ if (typeof document !== 'undefined') (function () {
|
|||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function () { size(); });
|
||||
requestDraw();
|
||||
|
||||
// ---------- the chain scene (live-dag.js, the site's module, copied in): the engine's own api/live, this machine's
|
||||
// lane as "you"; until the engine serves it (owed) the module reports failed and the blocks strip draws instead ----------
|
||||
var dag = null, dagMode = '';
|
||||
function mountDag() {
|
||||
if (dag || typeof IgneumDag === 'undefined' || !$('dag-live')) return;
|
||||
try {
|
||||
var mine = function (b) { if (!b) return false; if (b.miner === 'you') return true; if (!state) return false; return View.isMine(b.miner, state.mining.cards); };
|
||||
dag = IgneumDag.mount($('dag-live'), { compact: window.innerWidth < 720, window: 120, url: 'api/live', mine: mine, tooltip: $('dag-tip'), chip: $('dag-chip'), pauseButton: $('dag-pause'), onData: function (d) { var n = View.liveNote(d); if (n && dagMode !== 'failed') setText('dag-note', n); }, onState: function (st, why) {
|
||||
var live = st === 'live' || st === 'stale';
|
||||
if (dagMode !== st) { dagMode = st; setText('dag-state', st === 'live' ? 'live' : st === 'stale' ? 'behind' : st === 'failed' ? 'your blocks only' : 'connecting'); setText('dag-note', st === 'failed' ? 'The full chain view needs the next update. This is the last 10 minutes of your own blocks.' : st === 'stale' ? (why || '') : ''); }
|
||||
$('dag-live').hidden = !live; $('dag-tip').hidden = true; $('dag-pause').hidden = !live; $('dag').hidden = live;
|
||||
if (!live) { size(); requestDraw(); }
|
||||
} });
|
||||
} catch (e) { dag = null; $('dag-live').hidden = true; $('dag').hidden = false; size(); }
|
||||
}
|
||||
// ---------- render ----------
|
||||
function badgeHtml(cd) {
|
||||
if (cd.vendor === 'apple') return '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg>';
|
||||
|
|
@ -1192,7 +1440,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var sig = cards.map(function (c) { return c.key + ':' + (c.problem || '') + ':' + (c.removed_at > 0 ? 'r' : '') + ':' + (c.enabled ? 'on' : 'off'); }).join('|');
|
||||
if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); }
|
||||
if (!cards.length) {
|
||||
$('cards-sub').textContent = 'No graphics card this app can drive was found.';
|
||||
$('cards-sub').textContent = 'No graphics card this app can use was found.';
|
||||
$('cards-note').textContent = (s.detect_message || '') + ' The node can still run on its own; mining needs a supported card.';
|
||||
$('cards-note').hidden = false; $('cards-help').hidden = true; $('btn-cards-retry').hidden = false; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue without mining';
|
||||
return;
|
||||
|
|
@ -1229,13 +1477,16 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (cells.money) nums += numCell('money', cells.money, '', 'a day', '');
|
||||
else if (cells.needsPrice) nums += '<div class="num money"><button class="btn tiny ghost" data-price="1">Set a price</button><span class="k">£ a day</span></div>';
|
||||
if (r.temp) nums += numCell('temp', r.temp.replace(' °C', ''), '°C', 'GPU', r.tempTone);
|
||||
var tuneBtn = tw.button === 'stop' ? '<button class="btn small" data-stop="1">Stop</button>' : tw.button === 'tune' ? '<button class="btn small ghost" data-tune="1">Tune</button>' : '';
|
||||
var em = View.ember(cd, s, s.now);
|
||||
if (em.kind === 'tuned' && em.before > 0 && em.after > 0 && cells.power) nums = nums.replace(numCell('power', cells.power.replace(' W', ''), 'W', cells.eff, ''), numCell('power', Math.round(em.before) + ' → ' + cells.power.replace(' W', ''), 'W', cells.eff, ''));
|
||||
if (em.points.length > 1 && cells.power) nums = nums.replace('<span class="k">' + esc(cells.eff) + '</span>', '<span class="k spark">' + esc(cells.eff) + sparkHtml(em.points) + '</span>');
|
||||
var tuneBtn = tw.button === 'stop' ? '<button class="btn small" data-stop="1">Stop</button>' : tw.button === 'tune' ? '<button class="btn small ghost" data-tune="1">' + (em.kind === 'tuned' ? 'Retune' : 'Tune') + '</button>' : '';
|
||||
var sw = '<label class="switch" title="' + (r.on ? 'mining on this card' : 'not mining on this card') + '"><input type="checkbox" aria-label="Mine on ' + esc(r.name) + '"' + (r.on ? ' checked' : '') + (r.canToggle ? '' : ' disabled') + '><span class="track"></span></label>';
|
||||
var chev = '<button class="chev-btn" data-open="1" aria-expanded="false" aria-label="Details for ' + esc(r.name) + '"><span class="chev"></span></button>';
|
||||
var subs = [];
|
||||
if (r.sub) subs.push('<div class="msg' + (r.tone === 'bad' ? '' : ' dim') + '">' + esc(r.sub) + '</div>');
|
||||
if (cells.noReading && r.on) subs.push('<div class="msg dim">' + esc(cells.noReading) + '</div>');
|
||||
var tune = tw.kind === 'none' ? '' : '<div class="gl-tune ' + tw.kind + '"><span class="t">' + esc(tw.text) + '</span>' + (tw.note ? '<span class="n dim">' + esc(tw.note) + '</span>' : '') + (tw.progress >= 0 ? '<i class="bar"><b style="width:' + Math.round(tw.progress * 100) + '%"></b></i>' : '') + '</div>';
|
||||
var tune = em.kind === 'none' ? '' : '<div class="gl-tune ' + em.kind + '">' + flameHtml(em.fill, em.kind) + '<span class="t">' + esc(em.label) + '</span>' + (em.sub ? '<span class="n dim">' + esc(em.sub) + '</span>' : '') + (em.savingText ? '<span class="n save">' + esc(em.savingText) + '</span>' : '') + (em.kind === 'running' ? '<i class="bar"><b style="width:' + Math.round(em.fill * 100) + '%"></b></i>' : '') + '</div>';
|
||||
return '<div class="gpu-line' + (r.on ? ' on' : ' off') + (r.removed ? ' removed' : '') + (r.unusable ? ' unusable' : '') + '" data-key="' + esc(r.key) + '">' +
|
||||
'<div class="gl-main"><div class="badge ' + esc(r.vendor) + '">' + badgeHtml({ vendor: r.vendor }) + '</div>' +
|
||||
'<div class="who"><div class="name" title="' + esc(r.title) + '">' + esc(r.name) + ' <span class="kind ' + esc(r.kind) + '">' + esc(r.kindWord) + '</span></div>' +
|
||||
|
|
@ -1244,17 +1495,46 @@ if (typeof document !== 'undefined') (function () {
|
|||
'<div class="acts">' + tuneBtn + sw + chev + '</div></div>' + tune +
|
||||
'<div class="gl-details" hidden>' + detailsHtml(cd, s) + '</div></div>';
|
||||
}
|
||||
// the flame mark (the brand mark's two polygons) filled to a fraction: outline when not tuned, filling while tuning, solid when tuned
|
||||
var flameN = 0;
|
||||
function flameHtml(fill, kind) {
|
||||
var id = 'fl' + (++flameN), h = Math.round(100 * (1 - Math.max(0, Math.min(1, fill))));
|
||||
return '<svg class="flame ' + kind + '" viewBox="0 0 100 100" width="16" height="16" aria-hidden="true"><defs><clipPath id="' + id + '"><rect x="0" y="' + h + '" width="100" height="' + (100 - h) + '"/></clipPath></defs>' +
|
||||
'<path class="fo" d="M50 4 L78 50 L50 96 L22 50 Z M50 34 L62 50 L50 66 L38 50 Z" fill-rule="evenodd"/>' +
|
||||
'<path class="ff" clip-path="url(#' + id + ')" d="M50 4 L78 50 L50 96 L22 50 Z M50 34 L62 50 L50 66 L38 50 Z" fill-rule="evenodd"/></svg>';
|
||||
}
|
||||
// a tiny sparkline of the tune's ladder: MH/W per step, the chosen step as a dot
|
||||
function sparkHtml(points) {
|
||||
var effs = points.map(function (p) { return p.eff; }), lo = Math.min.apply(null, effs), hi = Math.max.apply(null, effs), n = points.length, W = 44, H = 12;
|
||||
var pts = points.map(function (p, i) { var x = n > 1 ? i / (n - 1) * (W - 2) + 1 : W / 2, y = hi > lo ? H - 1 - (p.eff - lo) / (hi - lo) * (H - 2) : H / 2; return { x: x, y: y, c: p.chosen, m: p.marked }; });
|
||||
var d = pts.map(function (p, i) { return (i ? 'L' : 'M') + p.x.toFixed(1) + ' ' + p.y.toFixed(1); }).join(' ');
|
||||
var dot = pts.filter(function (p) { return p.c; })[0];
|
||||
return '<svg class="spark" viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H + '" aria-hidden="true"><path d="' + d + '"/>' + (dot ? '<circle cx="' + dot.x.toFixed(1) + '" cy="' + dot.y.toFixed(1) + '" r="2"/>' : '') + '</svg>';
|
||||
}
|
||||
// the tune's curve behind the chevron: W across, MH/s up, every step a dot, the chosen one ringed, a marked one hollow
|
||||
function curveHtml(points) {
|
||||
if (points.length < 2) return '';
|
||||
var ws = points.map(function (p) { return p.w; }), ms = points.map(function (p) { return p.mhs; });
|
||||
var w0 = Math.min.apply(null, ws), w1 = Math.max.apply(null, ws), m0 = Math.min.apply(null, ms), m1 = Math.max.apply(null, ms), W = 320, H = 110, L = 34, B = 18;
|
||||
var X = function (w) { return L + (w1 > w0 ? (w - w0) / (w1 - w0) : 0.5) * (W - L - 8); }, Y = function (m) { return 6 + (m1 > m0 ? 1 - (m - m0) / (m1 - m0) : 0.5) * (H - B - 6); };
|
||||
var sorted = points.slice().sort(function (a, b) { return a.w - b.w; });
|
||||
var path = sorted.map(function (p, i) { return (i ? 'L' : 'M') + X(p.w).toFixed(1) + ' ' + Y(p.mhs).toFixed(1); }).join(' ');
|
||||
var dots = sorted.map(function (p) { return '<circle class="' + (p.chosen ? 'chosen' : p.marked ? 'marked' : '') + '" cx="' + X(p.w).toFixed(1) + '" cy="' + Y(p.mhs).toFixed(1) + '" r="' + (p.chosen ? 5 : 3) + '"/>'; }).join('');
|
||||
return '<div class="curve-wrap"><svg class="curve-svg" viewBox="0 0 ' + W + ' ' + H + '" role="img" aria-label="The tune\'s curve: watts across, hash rate up"><path class="ln" d="' + path + '"/>' + dots +
|
||||
'<text x="' + L + '" y="' + (H - 4) + '">' + Math.round(w0) + ' W</text><text x="' + (W - 8) + '" y="' + (H - 4) + '" text-anchor="end">' + Math.round(w1) + ' W</text>' +
|
||||
'<text x="2" y="' + (H - B - 2) + '">' + m0.toFixed(0) + '</text><text x="2" y="12">' + m1.toFixed(0) + '</text></svg><span class="note">Hash rate against power for every step of the last tune. The ringed point is the one Ember kept. A hollow point was rejected: a fault, a hot card or a slowed memory clock.</span></div>';
|
||||
}
|
||||
// the card's details (one chevron): the cap slider, identities, pin, the telemetry, the facts, the last tune's table
|
||||
function detailsHtml(cd, s) {
|
||||
var nv = cd.vendor === 'nvidia' && cd.power_default_w > 0, pct = cd.power_pct || 80, h = '';
|
||||
if (cd.removed_at > 0) return '<p class="help">The card was unplugged; its worker stopped. The row goes in five minutes.</p>';
|
||||
if (cd.problem) return '<p class="help">' + esc(cd.reason || 'No worker can drive this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.</p>';
|
||||
if (cd.removed_at > 0) return '<p class="help">The card was unplugged. Its row goes in five minutes.</p>';
|
||||
if (cd.problem) return '<p class="help">' + esc(cd.reason || 'This app cannot use this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.</p>';
|
||||
if (nv) {
|
||||
var want = Math.round(cd.power_default_w * pct / 100);
|
||||
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 mono">' + pct + '% · ' + want + ' 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 tune leaves it. <button class="btn tiny ghost" data-pin="0">Unpin</button>' : 'Ember Tune may move it.') + '</p></div>';
|
||||
h += cd.power_applied ? '<div class="line ok">cap applied: <b>' + want + ' W</b>' + (cd.sweep_pct === pct && cd.sweep_pct ? ' <span class="pin">chosen by the tune</span>' : '') + '</div>' : '<div class="line hot">cap not applied: Windows needs the administrator prompt <button class="btn tiny" data-power-retry="1">Retry</button></div>';
|
||||
} else if (cd.vendor === 'apple') h += '<p class="help">Apple silicon manages its own power; there is no cap to set.</p>';
|
||||
'<p class="help">Lower is cooler and quieter. Most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so Ember leaves it. <button class="btn tiny ghost" data-pin="0">Back to Default</button>' : 'Default: Ember may move it.') + '</p></div>';
|
||||
h += cd.power_applied ? '<div class="line ok">Cap in force: <b>' + want + ' W</b>' + (cd.sweep_pct === pct && cd.sweep_pct ? ' <span class="pin">Ember’s choice</span>' : '') + '</div>' : '<div class="line hot">Cap not set yet. Windows needs the administrator prompt. <button class="btn tiny" data-power-retry="1">Retry</button></div>';
|
||||
} else if (cd.vendor === 'apple') h += '<p class="help">Apple silicon manages its own power. There is no cap to set.</p>';
|
||||
var tl = [];
|
||||
if (cd.temp_mem > 0) tl.push('memory <b>' + Math.round(cd.temp_mem) + ' °C</b>');
|
||||
if (cd.gclk_mhz > 0) tl.push('core clock <b>' + Math.round(cd.gclk_mhz) + ' MHz</b>' + (cd.clock_cap_mhz > 0 ? ' (cap ' + cd.clock_cap_mhz + ')' : ''));
|
||||
|
|
@ -1262,13 +1542,15 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (cd.fan_pct > 0) tl.push('fan <b>' + Math.round(cd.fan_pct) + '%</b>');
|
||||
if (cd.util_pct > 0) tl.push('busy <b>' + Math.round(cd.util_pct) + '%</b>');
|
||||
if (tl.length) h += '<div class="line">' + tl.join('<span class="sep"></span>') + '</div>';
|
||||
var tm = TuneLine.model(cd, s.now);
|
||||
if (tm.kind === 'tuned' || tm.kind === 'measured') h += '<div class="line"><b>' + esc(tm.text) + '</b>' + (tm.note ? ' <span class="dim">' + esc(tm.note) + '</span>' : '') + '</div>';
|
||||
if (cd.tune_curve && cd.tune_curve.length) {
|
||||
h += '<table class="curve"><thead><tr><th>step</th><th>clock</th><th>power</th><th>MH/s</th><th>W</th><th>MH/W</th><th>note</th></tr></thead><tbody>' + cd.tune_curve.map(function (r, i) { return '<tr' + (r.mark ? ' class="marked"' : '') + '><td>' + (i + 1) + '</td><td>' + (r.clock_mhz ? r.clock_mhz + ' MHz' : 'unlocked') + '</td><td>' + (r.power_pct || 0) + '%</td><td>' + (r.mhs || 0).toFixed(1) + '</td><td>' + Math.round(r.watts || 0) + '</td><td>' + (r.eff || 0).toFixed(3) + '</td><td>' + esc(r.mark || '') + '</td></tr>'; }).join('') + '</tbody></table>';
|
||||
var tm = TuneLine.model(cd, s.now), em = View.ember(cd, s, s.now);
|
||||
if (View.tunable(cd)) {
|
||||
var g = cd.tune_goal || '', goalSeg = '<div class="seg small" data-card-goal="1" role="radiogroup" aria-label="Goal for ' + esc(cd.name) + '">' + [['', 'Default', 'the goal set for all cards'], ['efficiency', 'Efficiency', 'most hashes per watt, a little rate traded'], ['balanced', 'Balanced', 'a little rate for most of the saving'], ['rate', 'Maximum', 'most hashes, watts second']].map(function (o) { return '<button class="seg-b' + (g === o[0] ? ' on' : '') + '" data-goal="' + o[0] + '" role="radio" aria-checked="' + (g === o[0] ? 'true' : 'false') + '" title="' + o[2] + '">' + o[1] + (o[0] === '' ? ' <span class="dim">· ' + esc(View.GOAL_NAMES[goalNow()]) + '</span>' : '') + '</button>'; }).join('') + '</div>';
|
||||
h += '<div class="ctl"><span class="k">Goal</span>' + goalSeg + (em.kind === 'running' ? '<button class="btn small" data-stop="1">Stop</button>' : '<button class="btn small" data-tune="1">' + (em.kind === 'tuned' ? 'Retune' : 'Tune now') + '</button>') + '<p class="help">' + (cd.tune_goal === undefined ? 'A goal for one card needs the next Ember Tune update. Until then every card follows the goal above.' : 'This card’s own goal. Default follows the goal set for all cards. A new goal applies from the next tune.') + '</p></div>';
|
||||
}
|
||||
h += '<div class="ctl"><span class="k">Identities</span><p class="help">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>';
|
||||
var facts = [cd.worker ? 'worker ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
if (tm.kind === 'tuned' || tm.kind === 'measured') h += '<div class="line"><b>' + esc(tm.text) + '</b>' + (tm.note ? ' <span class="dim">' + esc(tm.note) + '</span>' : '') + (em.savingText ? ' <span class="save">' + esc(em.savingText) + '</span>' : '') + '</div>';
|
||||
h += curveHtml(em.points);
|
||||
h += '<div class="ctl"><span class="k">Miners on this card</span><p class="help">How many miners this card runs as. Each one votes and is paid on its own. 8 suits a big card, 2 a small one, 1 a built-in GPU. Changing it restarts the card.</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>';
|
||||
var facts = [cd.worker ? 'runs on ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
if (facts.length) h += '<p class="note mono small">' + esc(facts.join(' · ')) + '</p>';
|
||||
return h;
|
||||
}
|
||||
|
|
@ -1281,10 +1563,10 @@ if (typeof document !== 'undefined') (function () {
|
|||
var mining = presentCards.filter(function (c) { return c.state === 'mining'; }).length;
|
||||
// the live total: rate, watts, £ a day, blocks
|
||||
setText('t-hash', m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1));
|
||||
setText('t-hash-sub', m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'worker starting' : 'waiting for the node') : m.state === 'idle' ? 'no GPU worker' : m.state);
|
||||
var watts = View.wattsTotal(m.cards), price = priceNow(), eff = watts > 0 && m.hash_total > 0 ? View.effText(m.hash_total / watts) : '';
|
||||
setText('t-hash-sub', m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'card starting' : 'waiting for the node') : m.state === 'idle' ? 'no card mining' : m.state);
|
||||
var watts = View.fleetWatts(m, m.cards), price = priceNow(), eff = watts > 0 && m.hash_total > 0 ? View.effText(m.hash_total / watts) : '';
|
||||
setHidden('t-power-cell', !(watts > 0)); setText('t-power', String(Math.round(watts))); setText('t-power-sub', eff || 'drawn now');
|
||||
var ppd = View.poundsPerDay(watts, price);
|
||||
var ppd = View.fleetPounds(m, m.cards, price);
|
||||
setHidden('t-money-cell', !(watts > 0)); setText('t-money', ppd === null ? 'Set a price' : money(ppd)); $('t-money').classList.toggle('ask-price', ppd === null); setText('t-money-sub', ppd === null ? 'in Settings, pence per kWh' : 'electricity at ' + price + 'p/kWh');
|
||||
setText('t-blocks', withCommas(m.accepted_session)); setText('t-blocks-sub', 'this run · ' + withCommas(m.accepted_total) + ' lifetime' + (m.rejected_session ? ' · ' + m.rejected_session + ' rejected' : ''));
|
||||
// the rows: every card, on or off; rebuilt only when their words change and never under a hand on a slider
|
||||
|
|
@ -1293,12 +1575,12 @@ if (typeof document !== 'undefined') (function () {
|
|||
var on = rows.filter(function (r) { return r.on; }).length;
|
||||
setText('d-cards-eyebrow', s.detecting ? 'detecting' : !shown.length ? 'none found' : on + ' of ' + presentCards.length + ' on');
|
||||
var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; });
|
||||
setHidden('btn-tune-all', !(tunables.length > 1) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards));
|
||||
setHidden('btn-tune-all', !(tunables.length > 0) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards));
|
||||
var box = $('d-cards'), busy = box.querySelector('input[type=range]:active') || (box.contains(document.activeElement) && document.activeElement.tagName === 'INPUT');
|
||||
var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length]; })]);
|
||||
var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs]; }), goalNow()]);
|
||||
if (sig !== mineRowsSig && !busy) {
|
||||
mineRowsSig = sig;
|
||||
box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '<div class="empty' + (s.detecting ? '' : ' err') + '">' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can drive was found. The node runs on its own.') + '</div>';
|
||||
box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '<div class="empty' + (s.detecting ? '' : ' err') + '">' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '</div>';
|
||||
box.querySelectorAll('.gpu-line').forEach(applyOpen);
|
||||
}
|
||||
var note = !shown.length && s.detect_message ? s.detect_message : '';
|
||||
|
|
@ -1311,7 +1593,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var key = s.events.length ? s.events[0].t + ':' + s.events.length : '';
|
||||
if (key !== lastEventsKey) {
|
||||
lastEventsKey = key;
|
||||
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { return '<div><span><span class="k ' + esc(e.kind) + '">' + esc(e.kind) + '</span>' + esc(View.eventLine(e.text)) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { var pe = View.plainEvent(e.text); return '<div' + (pe.tech ? ' title="' + esc(pe.tech) + '"' : '') + '><span><i class="k ' + esc(e.kind) + '"></i>' + esc(pe.text) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
}
|
||||
}
|
||||
// the Tuning card on Mine and the Tuning rows on Settings share the goal, the schedule and the Power control switch
|
||||
|
|
@ -1321,7 +1603,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
setText('goal-line', gw.consequence); setText('goal-line-2', gw.consequence);
|
||||
var sched = View.schedule(s.mining.cards, s, s.now);
|
||||
setText('tune-schedule', sched); setText('s-tune-schedule', sched);
|
||||
setText('tune-eyebrow', st.tuning_off ? 'paused by Igneum' : st.sweep === false ? 'Ember Tune off' : 'Ember Tune');
|
||||
setText('fleet-saving', View.fleetSaving(s.mining.cards, s, s.now, priceNow()));
|
||||
var nvOff = cards.some(function (c) { return c.vendor === 'nvidia' && View.present(c); }) && !st.power_control;
|
||||
setHidden('m-power-row', !nvOff);
|
||||
if (document.activeElement !== $('m-power-control')) $('m-power-control').checked = !!st.power_control;
|
||||
|
|
@ -1331,29 +1613,31 @@ if (typeof document !== 'undefined') (function () {
|
|||
var switchesSig = '';
|
||||
function renderNode(s) {
|
||||
var n = s.node, f = s.finality, p = s.program, eta = syncEta(n, s.now);
|
||||
var nl = View.nodeLine(n, s.clock, eta);
|
||||
setText('node-line-text', nl.text); setText('node-sub', nl.sub);
|
||||
var nl = View.nodeLine(n, s.clock, eta), fw = View.finalityWords(f);
|
||||
setText('node-line-text', nl.text); setText('node-sub', fw.paused && nl.tone !== 'bad' ? fw.note : nl.sub);
|
||||
$('node-dot').className = 'dot' + (nl.tone === 'ok' ? ' live' : nl.tone === 'bad' ? ' bad' : '');
|
||||
$('node-card').classList.toggle('bad', nl.tone === 'bad');
|
||||
setText('n-state-v', nl.text.replace(/^Node /, '')); setText('n-state-m', nl.sub + (n.override_restart_wait ? ' ' + n.override_restart_wait + '.' : ''));
|
||||
var ns = View.nodeSource(n); setText('n-source', ns.text); setText('n-source-m', ns.sub); setHidden('n-source', !ns.text);
|
||||
setText('n-blocks', withCommas(n.blocks)); setText('n-blocks-sub', View.heightLine(n));
|
||||
setText('n-peers', String(n.peers)); setText('n-peers-sub', View.peersLine(n));
|
||||
setText('n-version', n.version ? n.version.replace(/^igneumd\s*/, '') : '--');
|
||||
setText('d-node-net', s.chain || s.network || '');
|
||||
setText('n-headers', withCommas(n.headers)); setText('n-daa', withCommas(n.daa)); setText('n-diff', compact(n.difficulty)); setText('n-tips', String(n.tips)); setText('n-blue', withCommas(n.blue));
|
||||
if (n.daa > 0 && p && p.eta_s >= 0) { setText('d-eta', hms(p.eta_s)); setText('d-eta-sub', 'until the next hourly mining program' + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : '') + (p.message ? ' · ' + p.message : '')); }
|
||||
else { setText('d-eta', '--:--'); setText('d-eta-sub', n.state === 'syncing' ? 'known after the node is synced' : 'known once the node runs'); }
|
||||
setText('n-digest', n.consensus_digest || (n.state === 'stopped' || n.state === 'failed' ? 'the node is not running' : 'not printed yet'));
|
||||
if (n.daa > 0 && p && p.eta_s >= 0) { setText('d-eta', hms(p.eta_s)); setText('d-eta-sub', 'until the next hourly program' + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : '') + (p.message ? ' · ' + p.message : '')); }
|
||||
else { setText('d-eta', '--:--'); setText('d-eta-sub', n.state === 'syncing' ? 'known after the node is synced' : 'known once the node is running'); }
|
||||
setText('n-digest', n.consensus_digest || (n.state === 'stopped' || n.state === 'failed' ? 'the node is not running' : 'not read yet'));
|
||||
var sw = View.nextSwitch(n.consensus_switches, n.daa);
|
||||
setText('n-switch', sw ? sw.name + ' at ' + withCommas(sw.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'DAA ' + withCommas(n.consensus_switch_daa) : (n.consensus_switches && n.consensus_switches.length ? 'none ahead' : 'none planned')));
|
||||
setText('n-switch-help', sw ? View.switchLine(sw, n.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'The update manifest announces a switch at DAA ' + withCommas(n.consensus_switch_daa) + ' (' + withCommas(n.consensus_switch_daa - n.daa) + ' blocks away).' : View.switchLine(null, n.daa)));
|
||||
setText('n-switch-help', sw ? View.switchLine(sw, n.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'A rule change is announced at block ' + withCommas(n.consensus_switch_daa) + ' (' + withCommas(n.consensus_switch_daa - n.daa) + ' blocks away).' : View.switchLine(null, n.daa)));
|
||||
var sig = JSON.stringify([n.consensus_switches, sw && sw.key]);
|
||||
if (sig !== switchesSig) {
|
||||
switchesSig = sig;
|
||||
$('n-switches').innerHTML = (n.consensus_switches || []).map(function (x) { return '<span class="' + (sw && x.key === sw.key ? 'next' : x.daa <= n.daa ? 'past' : '') + '"><span>' + esc(x.name) + (x.daa <= n.daa ? ' (applied)' : '') + '</span><span>' + withCommas(x.daa) + '</span></span>'; }).join('');
|
||||
}
|
||||
if (f.last_lock > 0) { setText('f-lock', '#' + withCommas(f.last_lock) + ' · ' + rel(f.age_s)); setText('f-note', 'a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight'); }
|
||||
else { setText('f-lock', 'none yet'); setText('f-note', f.message || 'locks appear once the miner votes on checkpoints'); }
|
||||
setText('f-lock', (f.last_lock > 0 ? '#' + withCommas(f.last_lock) : 'none yet') + ' · ' + fw.age);
|
||||
setText('f-note', fw.paused ? fw.note : f.last_lock > 0 ? 'a point the miners agreed can never be undone. It locks at two thirds of the last 30 days of mining' : (f.message || 'checkpoints lock once this machine and the others vote'));
|
||||
$('f-lock').classList.toggle('warn', !!fw.paused);
|
||||
setText('f-votes', withCommas(f.votes));
|
||||
}
|
||||
|
||||
|
|
@ -1363,19 +1647,21 @@ if (typeof document !== 'undefined') (function () {
|
|||
var ign = pv.paid_wei && Number(pv.paid_wei) ? (Number(pv.paid_wei) / 1e18).toFixed(4) : '0.0000';
|
||||
setText('e-ign', ign + ' IGN'); setText('e-ign-sub', 'from ' + (pv.paid || 0) + ' proof' + (pv.paid === 1 ? '' : 's') + (pv.segments_paid ? ' and ' + pv.segments_paid + ' segment' + (pv.segments_paid === 1 ? '' : 's') : ''));
|
||||
setText('e-blocks', withCommas(m.accepted_total) + ' block' + (m.accepted_total === 1 ? '' : 's')); setText('e-blocks-sub', 'lifetime · ' + withCommas(m.accepted_session) + ' this run, ' + uptime(s.uptime_s));
|
||||
var watts = View.wattsTotal(m.cards), ppd = View.poundsPerDay(watts, price);
|
||||
var watts = View.fleetWatts(m, m.cards), ppd = View.fleetPounds(m, m.cards, price);
|
||||
var bw = View.balanceWords(a, s.now);
|
||||
if (bw.text) { setText('e-earned', bw.pounds !== null ? money(bw.pounds) : bw.text); setText('e-earned-sub', bw.pounds !== null ? 'at the signed price · ' + bw.text + ' ' + bw.sub : (bw.text === 'not read yet' ? 'balance ' + bw.sub : bw.sub + '. Nothing is bought or sold on devnet')); }
|
||||
setText('e-cost', ppd === null ? (watts > 0 ? Math.round(watts) + ' W' : 'no draw') : money(ppd) + ' a day');
|
||||
setText('e-cost-sub', ppd === null ? (watts > 0 ? 'electricity: set a price to see the cost' : 'electricity: no card reports its draw') : 'electricity at ' + price + 'p/kWh for ' + Math.round(watts) + ' W');
|
||||
setHidden('e-price-btn', !(watts > 0 && ppd === null));
|
||||
if (document.activeElement !== $('s-devfee')) $('s-devfee').checked = !!(s.settings && s.settings.dev_fee);
|
||||
setText('s-devfee-text', s.settings.dev_fee ? 'Dev fee: 1 block in 100 pays the miner software’s author' : 'Dev fee off: every block pays your address');
|
||||
setText('r-devfee-line', s.settings.dev_fee ? 'The protocol takes nothing; this is the miner software’s fee, the same way every GPU miner takes one' + (m.fee_total ? ' (' + withCommas(m.fee_total) + ' block' + (m.fee_total === 1 ? '' : 's') + ' so far)' : '') + '.' : 'The protocol takes nothing either.');
|
||||
setText('s-devfee-text', s.settings.dev_fee ? 'Dev fee: 1 block in 100 pays the people who make this app' : 'Dev fee off. Every block pays your address');
|
||||
setText('r-devfee-line', s.settings.dev_fee ? 'The network itself takes nothing. This is the app’s fee, the same way every mining app takes one' + (m.fee_total ? ' (' + withCommas(m.fee_total) + ' block' + (m.fee_total === 1 ? '' : 's') + ' so far)' : '') + '.' : 'The network itself takes nothing either.');
|
||||
setText('r-address', a.display || 'not set');
|
||||
setText('r-source', a.source === 'generated' ? 'made on this machine' : a.source === 'pasted' ? 'your own address' : '');
|
||||
setText('r-address-help', a.display ? 'Every block this machine finds pays this address.' : 'No address yet. Finish the setup to choose one.');
|
||||
var gen = a.source === 'generated';
|
||||
setHidden('key-toggle', !gen); if (!gen) { $('key-details').hidden = true; hideKey(); }
|
||||
setText('r-key-help', 'The key for this address was made on this machine and is stored in the app folder, readable by your user only. Keep a copy somewhere safe.');
|
||||
setText('r-key-help', 'The key for this address was made on this machine and is stored in the app folder. Only your user can read it. Keep a copy somewhere safe.');
|
||||
setText('r-key-file', gen && a.wallet_file ? a.wallet_file : '');
|
||||
}
|
||||
|
||||
|
|
@ -1393,10 +1679,10 @@ if (typeof document !== 'undefined') (function () {
|
|||
setText('pv-note', (!enabled && pv.default_note) ? pv.default_note + '.' : w.note);
|
||||
var v = View.verifierWords(pv);
|
||||
setText('pv-verifier', v.word); $('pv-verifier').className = 'v ' + v.tone;
|
||||
setText('pv-verifier-help', pv.verifier_note || 'the node checks a proof before it counts');
|
||||
setText('pv-verifier-help', pv.verifier_note || 'the verifier: the node checks a proof before it counts');
|
||||
$('pv-setup-row').hidden = !((pv.enabled && !pv.available && pv.setup_hint) || v.needsSetup);
|
||||
var seg = pv.segments_submitted ? 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)' : '') + (pv.segment_last_s ? ', the last in ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' held for the chain rule' : '') : 'none yet';
|
||||
setText('pv-seg', seg); setText('pv-seg-note', pv.segment_note && enabled ? pv.segment_note : 'whole segments this machine proved');
|
||||
var seg = pv.segments_submitted ? 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)' : '') + (pv.segment_last_s ? ', the last took ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' waiting for the chain' : '') : 'none yet';
|
||||
setText('pv-seg', seg); setText('pv-seg-note', pv.segment_note && enabled ? pv.segment_note : 'whole segments (runs of 8 blocks) this machine proved');
|
||||
setText('pv-program', pv.program_id || 'not read yet');
|
||||
setText('pv-aggregator', pv.aggregator_id || 'not read yet');
|
||||
}
|
||||
|
|
@ -1436,24 +1722,16 @@ if (typeof document !== 'undefined') (function () {
|
|||
}
|
||||
|
||||
function renderPage(s) {
|
||||
if (page === 'mine') renderMine(s);
|
||||
if (page === 'overview' || page === 'cards') renderMine(s);
|
||||
else if (page === 'earnings') renderEarnings(s);
|
||||
else if (page === 'prove') renderProve(s);
|
||||
else if (page === 'settings') renderSettings(s);
|
||||
}
|
||||
function renderPill(s) {
|
||||
var pill = $('pill'), text;
|
||||
var pill = $('pill'), pw = View.pill(s);
|
||||
pill.classList.remove('on', 'warn');
|
||||
if (s.quitting) text = 'stopping';
|
||||
else if (!s.setup_done) text = s.detecting ? 'detecting' : 'set up';
|
||||
else if (View.tuningCard(s.mining.cards)) { text = 'tuning'; pill.classList.add('on'); }
|
||||
else if (s.mining.state === 'mining') { text = s.mining.hash_total.toFixed(1) + ' MH/s'; pill.classList.add('on'); }
|
||||
else if (s.mining.state === 'paused') text = 'paused';
|
||||
else if (s.node.state === 'syncing' || s.node.state === 'starting') text = 'node ' + s.node.state;
|
||||
else if (s.node.state === 'failed' || s.node.state === 'restarting') { text = 'node ' + s.node.state; pill.classList.add('warn'); }
|
||||
else if (s.mining.state === 'idle' && s.node.state === 'synced') { text = 'node synced'; pill.classList.add('on'); }
|
||||
else text = s.mining.state;
|
||||
$('pill-text').textContent = text;
|
||||
if (pw.tone === 'on') pill.classList.add('on'); else if (pw.tone === 'bad') pill.classList.add('warn');
|
||||
$('pill-text').textContent = pw.text;
|
||||
var foot = (s.display_name ? '<b title="' + esc(s.display_name) + '">' + esc(s.display_name) + '</b><br>' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '<br>up ' + uptime(s.uptime_s);
|
||||
if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; }
|
||||
var fv = 'v' + esc(s.version) + '<span class="chain"> · ' + esc(s.chain || s.network || '') + '</span>';
|
||||
|
|
@ -1492,7 +1770,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
el.className = 'notice ' + n.kind + (n.tone ? ' ' + n.tone : '');
|
||||
el.dataset.key = n.key;
|
||||
$('notice-text').textContent = n.text; $('notice-text').title = n.title || '';
|
||||
$('notice-detail').textContent = n.detail; $('notice-detail').hidden = !n.detail; $('notice-detail').title = n.detail;
|
||||
$('notice-detail').textContent = n.detail; $('notice-detail').hidden = true; $('notice-detail').title = n.detail;
|
||||
$('notice-more').hidden = !n.detail; $('notice-more').setAttribute('aria-expanded', 'false'); $('notice-more').classList.remove('open');
|
||||
$('notice-actions').innerHTML = n.actions.map(function (a) { return '<button class="btn small ' + (a.primary ? 'primary' : 'ghost') + '" data-act="' + esc(a.act) + '"' + (a.disabled ? ' disabled' : '') + '>' + esc(a.label) + '</button>'; }).join('');
|
||||
$('notice-prog').hidden = !(n.progress >= 0);
|
||||
strip.hidden = false;
|
||||
|
|
@ -1514,6 +1793,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var b = e.target.closest('[data-act]'); if (!b || b.disabled) return;
|
||||
var act = b.dataset.act, key = $('notice').dataset.key;
|
||||
if (act === 'close') { dismissed[key] = true; renderNotices(state || {}); }
|
||||
else if (act === 'more') { var open = $('notice-detail').hidden; $('notice-detail').hidden = !open; b.setAttribute('aria-expanded', open ? 'true' : 'false'); b.classList.toggle('open', open); layoutStrip(); }
|
||||
else if (act === 'install') { api('api/update/install', {}); toast('Installing at once'); }
|
||||
else if (act === 'retry') { api('api/update/install', {}); toast('Trying the update again'); }
|
||||
else if (act === 'open') api('api/update/open', {});
|
||||
|
|
@ -1584,11 +1864,11 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (u.rolled_back) parts.push('Rolled back: ' + u.rolled_back + '.');
|
||||
if (n && n.kind !== 'update-installed' && !(u.rolled_back && u.status === 'error')) parts.push(n.text + (n.detail ? ' ' + Notices.endDot(n.detail) : ''));
|
||||
else if (u.status === 'checking') parts.push('Checking.');
|
||||
else if (u.status === 'current') parts.push('Up to date' + (u.checked_at ? ', checked ' + rel(state.now - u.checked_at) : '') + '.');
|
||||
else if (u.status === 'current') parts.push('Up to date' + (u.checked_at ? '. Checked ' + rel(state.now - u.checked_at) : '') + '.');
|
||||
else if (/no update manifest configured/.test(u.error || '')) parts.push('This build has no update address; updates come as a new download.');
|
||||
else if (u.error) parts.push(u.error);
|
||||
else if (!u.status || u.status === 'unknown') parts.push('Not checked yet. The engine checks by itself every hour.');
|
||||
if (u.activation_height && !u.urgent && state && state.node && u.activation_height > state.node.daa) parts.push('A rule switch comes at height ' + withCommas(u.activation_height) + '.');
|
||||
else if (!u.status || u.status === 'unknown') parts.push('Not checked yet. The app checks by itself every hour.');
|
||||
if (u.activation_height && !u.urgent && state && state.node && u.activation_height > state.node.daa) parts.push('A rule change comes at block ' + withCommas(u.activation_height) + '.');
|
||||
return parts.join(' ');
|
||||
}
|
||||
function sampleUpdate(kind) {
|
||||
|
|
@ -1620,7 +1900,9 @@ if (typeof document !== 'undefined') (function () {
|
|||
function sampleTune(s, kind) {
|
||||
var c = s.mining.cards[0]; if (!c) return;
|
||||
if (kind === 'running') { c.state = 'tuning'; c.sweep_state = 'running'; c.tune_step = 7; c.tune_steps = 9; c.tune_eta_s = 230; c.tune_plan = 'full'; c.sweep_note = 'tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)'; if (!(c.power_w > 0)) c.power_w = 219.6; if (!(c.hash_now > 0)) c.hash_now = 123.4; }
|
||||
if (kind === 'tuned') { c.tune_line = 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)'; c.tune_source = 'full'; c.tune_control = true; c.tune_clock_mhz = 2470; c.sweep_pct = 100; c.sweep_at = s.now - 7200; c.sweep_watts = 290; }
|
||||
var curve = [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 90, watts: 340, mhs: 122.4, eff: 0.36 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 0, power_pct: 70, watts: 264, mhs: 118.1, eff: 0.447 }, { clock_mhz: 0, power_pct: 60, watts: 228, mhs: 104.9, eff: 0.46, mark: 'hot' }, { clock_mhz: 2780, power_pct: 80, watts: 298, mhs: 122.4, eff: 0.411 }, { clock_mhz: 2470, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }, { clock_mhz: 2160, power_pct: 80, watts: 281, mhs: 117.0, eff: 0.416 }, { clock_mhz: 1850, power_pct: 80, watts: 270, mhs: 109.2, eff: 0.404 }];
|
||||
if (kind === 'tuned' || kind === 'mix') { c.tune_line = 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)'; c.tune_source = 'full'; c.tune_control = true; c.tune_clock_mhz = 2470; c.sweep_pct = 80; c.sweep_at = s.now - 7200; c.sweep_watts = 290; c.sweep_mhs = 122.3; c.tune_curve = curve; c.power_default_w = c.power_default_w || 374; }
|
||||
if (kind === 'mix' && s.mining.cards[1]) { var d = s.mining.cards[1]; d.state = 'tuning'; d.sweep_state = 'running'; d.tune_step = 4; d.tune_steps = 9; d.tune_eta_s = 410; d.tune_plan = 'full'; d.tune_control = true; d.hash_now = d.hash_now || 98.1; }
|
||||
if (kind === 'measured') { c.tune_line = 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)'; c.tune_source = 'baseline'; c.tune_control = false; c.sweep_at = s.now - 600; c.sweep_note = 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed'; }
|
||||
}
|
||||
function render(s) {
|
||||
|
|
@ -1659,11 +1941,11 @@ if (typeof document !== 'undefined') (function () {
|
|||
}).catch(function () {
|
||||
failures++;
|
||||
if (failures >= 4) {
|
||||
$('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'stopped' : 'engine away';
|
||||
$('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'Stopped' : 'App not answering';
|
||||
if (phase === 'dashboard') {
|
||||
setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the engine is not answering; the window reconnects by itself'); $('node-card').classList.add('bad');
|
||||
setText('btn-toggle-sub', 'the engine is not answering'); $('btn-toggle').disabled = true;
|
||||
setText('t-hash-sub', 'no answer from the engine');
|
||||
setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the app is not answering. The window reconnects by itself'); $('node-card').classList.add('bad');
|
||||
setText('btn-toggle-sub', 'the app is not answering'); $('btn-toggle').disabled = true;
|
||||
setText('t-hash-sub', 'no answer from the app');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="icon" href="mark.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body class="phase-welcome" data-phase="welcome" data-page="mine">
|
||||
<body class="phase-welcome" data-phase="welcome" data-page="overview">
|
||||
|
||||
<!-- the rail: four sections (miner-ui-3: Mine, Earnings, Prove, Settings), Logs and Quit in the foot. Under 720 px it
|
||||
is a bottom tab bar. The setup screens have no rail. -->
|
||||
|
|
@ -18,7 +18,8 @@
|
|||
<span class="word">IGNEUM</span>
|
||||
</div>
|
||||
<nav class="rail-nav" id="rail-nav" aria-label="Sections">
|
||||
<button class="nav on" data-page="mine" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg><span>Mine</span></button>
|
||||
<button class="nav on" data-page="overview" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg><span>Overview</span></button>
|
||||
<button class="nav" data-page="cards"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="11" rx="2"/><path d="M7 20h10M9 16v4M15 16v4M7 10h3M14 10h3"/></svg><span>Cards</span></button>
|
||||
<button class="nav" data-page="earnings"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 15h2"/></svg><span>Earnings</span></button>
|
||||
<button class="nav" data-page="prove"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 4 5v6c0 5 3.4 9.4 8 11 4.6-1.6 8-6 8-11V5l-8-3z"/><path d="m9 12 2 2 4-4"/></svg><span>Prove</span></button>
|
||||
<button class="nav" data-page="settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg><span>Settings</span><i class="nav-dot" id="nav-updates-dot" hidden></i></button>
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
<span class="word">IGNEUM</span><span class="miner">MINER</span>
|
||||
</div>
|
||||
<div class="page-title" id="page-title" hidden>
|
||||
<h1 id="page-title-text">Mine</h1>
|
||||
<h1 id="page-title-text">Overview</h1>
|
||||
<span class="page-sub" id="page-sub"></span>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
|
|
@ -48,8 +49,10 @@
|
|||
<!-- the status strip: one notice at a time (app.js, Notices) -->
|
||||
<div class="notices" id="notices" hidden>
|
||||
<div class="notice" id="notice" role="status" aria-live="polite">
|
||||
<i class="notice-dot" aria-hidden="true"></i>
|
||||
<span class="notice-text" id="notice-text"></span>
|
||||
<span class="notice-actions" id="notice-actions"></span>
|
||||
<button class="notice-more" id="notice-more" data-act="more" title="The technical line" aria-label="Show the technical line" aria-expanded="false" hidden><span class="chev"></span></button>
|
||||
<button class="notice-close" id="notice-close" data-act="close" title="Close" aria-label="Close">×</button>
|
||||
<span class="notice-detail mono" id="notice-detail" hidden></span>
|
||||
<span class="prog" id="notice-prog" hidden><i></i></span>
|
||||
|
|
@ -115,7 +118,7 @@
|
|||
</div>
|
||||
<p class="note" id="cards-note" hidden></p>
|
||||
<p class="note" id="cards-power" hidden>NVIDIA cards start at 80% of their power limit, which keeps them stable. Windows asks for administrator rights once for that. The card row has a slider.</p>
|
||||
<p class="note" id="cards-help" hidden>An integrated GPU is off by default: it is slow and shares the machine's memory.</p>
|
||||
<p class="note" id="cards-help" hidden>A built-in GPU starts off. It is slow and shares the machine's memory.</p>
|
||||
<div class="cta">
|
||||
<button class="btn primary" id="btn-cards-next" disabled>Continue</button>
|
||||
<button class="btn ghost" id="btn-cards-retry" hidden>Detect again</button>
|
||||
|
|
@ -143,7 +146,7 @@
|
|||
<span class="radio"></span>
|
||||
<span class="body">
|
||||
<span class="t">Use my own address</span>
|
||||
<span class="s">Paste an Ethereum-style address you control: 0x and 40 characters.</span>
|
||||
<span class="s">Paste an address you control. It starts with 0x and has 40 characters after it.</span>
|
||||
<input type="text" class="addr-input mono" id="addr-input" placeholder="0x" spellcheck="false" autocomplete="off">
|
||||
<span class="err" id="addr-err" hidden>That is not an address. 0x followed by 40 hex characters.</span>
|
||||
</span>
|
||||
|
|
@ -160,8 +163,8 @@
|
|||
<!-- 4. the dashboard: four pages behind the rail -->
|
||||
<section class="screen" id="screen-dashboard">
|
||||
|
||||
<!-- Mine -->
|
||||
<section class="page" id="page-mine" data-page="mine">
|
||||
<!-- Overview: the fleet hero, the chain scene, the node line, activity -->
|
||||
<section class="page" id="page-overview" data-page="overview">
|
||||
<div class="clock-card" id="m-clock" hidden>
|
||||
<p class="clock-msg" id="m-clock-msg"></p>
|
||||
<div class="row"><button class="btn small primary" id="m-clock-sync">Sync clock</button><span class="note mono small" id="m-clock-result"></span></div>
|
||||
|
|
@ -182,24 +185,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span><button class="btn small" id="btn-tune-all" hidden>Tune all</button></div></div>
|
||||
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
|
||||
<p class="note" id="d-cards-note" hidden></p>
|
||||
<div class="card dag-card" id="dag-card">
|
||||
<div class="card-head"><h3>The chain, live</h3><div class="head-right"><span class="eyebrow" id="dag-state">connecting</span><button class="btn tiny ghost" id="dag-pause" aria-pressed="false">Pause</button></div></div>
|
||||
<div class="dag-wrap">
|
||||
<canvas id="dag-live" aria-label="The last two minutes of blocks: your row and the other miners, the selected chain as a lit path, checkpoints as bands"></canvas>
|
||||
<div class="dag-tip mono" id="dag-tip" hidden></div>
|
||||
<span class="dag-chip mono" id="dag-chip" hidden>scroll to zoom · Esc to release</span>
|
||||
<canvas id="dag" aria-hidden="true" hidden></canvas>
|
||||
</div>
|
||||
|
||||
<div class="card" id="tune-card">
|
||||
<div class="card-head"><h3>Tuning</h3><span class="eyebrow" id="tune-eyebrow">Ember Tune</span></div>
|
||||
<div class="goal-row">
|
||||
<div class="seg" id="goal-seg" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line"></span>
|
||||
</div>
|
||||
<p class="line-text" id="tune-schedule"></p>
|
||||
<label class="switch" id="m-power-row" hidden><input type="checkbox" id="m-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once.</span></span></label>
|
||||
<div class="legend"><span><i class="lg you"></i>yours</span><span><i class="lg pending"></i>pending</span><span><i class="lg included"></i>included</span><span><i class="lg excluded"></i>excluded</span><span><i class="lg proven"></i>proven</span><span><i class="lg locked"></i>locked checkpoint</span><span class="dim" id="dag-note"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="card node-card" id="node-card">
|
||||
|
|
@ -210,25 +204,26 @@
|
|||
<div class="details" id="node-details" hidden>
|
||||
<div class="kv">
|
||||
<div><span class="k">state</span><span class="v mono" id="n-state-v"></span><span class="m" id="n-state-m"></span></div>
|
||||
<div><span class="k">whose node</span><span class="v" id="n-source"></span><span class="m" id="n-source-m"></span></div>
|
||||
<div><span class="k">height</span><span class="v mono" id="n-blocks">0</span><span class="m" id="n-blocks-sub">blocks this node holds</span></div>
|
||||
<div><span class="k">peers</span><span class="v mono" id="n-peers">0</span><span class="m" id="n-peers-sub">other nodes it talks to</span></div>
|
||||
<div><span class="k">version</span><span class="v mono" id="n-version">--</span><span class="m" id="d-node-net">devnet v4</span></div>
|
||||
<div><span class="k">headers</span><span class="v mono" id="n-headers">0</span><span class="m">headers arrive before blocks</span></div>
|
||||
<div><span class="k">DAA score</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made</span></div>
|
||||
<div><span class="k">network blocks</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made (the DAA score)</span></div>
|
||||
<div><span class="k">difficulty</span><span class="v mono" id="n-diff">0</span><span class="m">how hard the next block is to find</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the block DAG right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the chain right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain (the blue score)</span></div>
|
||||
<div><span class="k">next program</span><span class="v mono" id="d-eta">--:--</span><span class="m" id="d-eta-sub">the hourly mining program</span></div>
|
||||
<div><span class="k">last lock</span><span class="v mono" id="f-lock">none yet</span><span class="m" id="f-note">a point the miners agreed can never be undone</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a checkpoint every 30 s</span></div>
|
||||
<div><span class="k">last checkpoint</span><span class="v mono" id="f-lock">none yet</span><span class="m" id="f-note">a point the miners agreed can never be undone</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a checkpoint every 30 s (a point the miners agree can never be undone)</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">rules digest</div>
|
||||
<div class="k">rules fingerprint</div>
|
||||
<div class="box mono"><span id="n-digest">not printed yet</span><button class="btn tiny" data-copy="n-digest">Copy</button></div>
|
||||
<p class="help">Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">next rule switch</div>
|
||||
<div class="k">next rule change</div>
|
||||
<div class="line-text" id="n-switch">none planned</div>
|
||||
<p class="help" id="n-switch-help"></p>
|
||||
<div class="switch-list mono" id="n-switches"></div>
|
||||
|
|
@ -238,11 +233,35 @@
|
|||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Activity</h3><div class="head-right"><span class="stats mono" id="d-stats"></span><button class="btn small ghost" id="btn-open-log">Open the log</button></div></div>
|
||||
<canvas id="dag" aria-hidden="true"></canvas>
|
||||
<div class="feed" id="d-events"><div class="empty">No events yet.</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cards: every card with Ember as its second layer -->
|
||||
<section class="page" id="page-cards" data-page="cards" hidden>
|
||||
<div class="card tune-strip" id="tune-card">
|
||||
<div class="card-head"><h3>Ember Tune</h3><span class="eyebrow">tunes every card for hashes per watt</span></div>
|
||||
<div class="ts-row">
|
||||
<div class="seg" id="goal-seg" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false" title="a little rate for most of the saving">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false" title="most hashes, watts second">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line"></span>
|
||||
<span class="ts-saving" id="fleet-saving"></span>
|
||||
<button class="btn small" id="btn-tune-all" hidden>Tune all</button>
|
||||
</div>
|
||||
<p class="line-text" id="tune-schedule"></p>
|
||||
<label class="switch" id="m-power-row" hidden><input type="checkbox" id="m-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once.</span></span></label>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span></div></div>
|
||||
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
|
||||
<p class="note" id="d-cards-note" hidden></p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Earnings -->
|
||||
<section class="page" id="page-earnings" data-page="earnings" hidden>
|
||||
<div class="card">
|
||||
|
|
@ -252,7 +271,7 @@
|
|||
<div class="money-row"><span class="v small" id="e-blocks">0 blocks</span><span class="s" id="e-blocks-sub">lifetime</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-cost">£0.00 a day</span><span class="s" id="e-cost-sub">electricity</span><button class="btn tiny ghost" id="e-price-btn">Set the price</button></div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the miner software's author</span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the people who make this app</span></label>
|
||||
<p class="help" id="r-devfee-line"></p>
|
||||
</div>
|
||||
|
||||
|
|
@ -297,19 +316,19 @@
|
|||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3>Prove on this machine</h3>
|
||||
<p class="help">Every block is turned into a short proof. The chain hands pieces to your cards; each piece proven pays IGN.</p>
|
||||
<p class="help">Every block is turned into a short proof, in pieces called shards. Your cards prove shards and earn IGN for each one.</p>
|
||||
</div>
|
||||
<label class="switch lg" title="Prove on this machine"><input type="checkbox" id="s-prove" aria-label="Prove on this machine"><span class="track"></span></label>
|
||||
</div>
|
||||
<ul class="tier-list" id="pv-tiers"></ul>
|
||||
<p class="line-text" id="pv-line"></p>
|
||||
<p class="note" id="pv-note"></p>
|
||||
<div class="row" id="pv-setup-row" hidden><button class="btn small primary" id="pv-setup">Set up</button><span class="note">About 20 minutes, once.</span></div>
|
||||
<div class="row" id="pv-setup-row" hidden><button class="btn small primary" id="pv-setup">Set up</button><span class="note">Takes about 20 minutes, once.</span></div>
|
||||
<button class="disclose" id="pv-toggle" aria-expanded="false" aria-controls="pv-details"><span>Details</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="pv-details" hidden>
|
||||
<div class="kv">
|
||||
<div><span class="k">verifier</span><span class="v" id="pv-verifier">not read yet</span><span class="m" id="pv-verifier-help">the node checks a proof before it counts</span></div>
|
||||
<div><span class="k">segments</span><span class="v" id="pv-seg">none yet</span><span class="m" id="pv-seg-note">whole segments this machine proved</span></div>
|
||||
<div><span class="k">segments</span><span class="v" id="pv-seg">none yet</span><span class="m" id="pv-seg-note">whole segments (runs of 8 blocks) this machine proved</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">shard program id</div>
|
||||
|
|
@ -318,7 +337,7 @@
|
|||
<div class="field">
|
||||
<div class="k">aggregator id</div>
|
||||
<div class="box mono"><span id="pv-aggregator">not read yet</span><button class="btn tiny" data-copy="pv-aggregator">Copy</button></div>
|
||||
<p class="help">Every proof names the program that made it. Other nodes accept a proof only from these two ids.</p>
|
||||
<p class="help">Every proof names the program that made it: the shard program for one shard, the aggregator for a whole segment. Other nodes accept a proof only from these two ids.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -330,19 +349,19 @@
|
|||
<div class="card-head"><h3>Tuning</h3><span class="eyebrow" id="s-tune-eyebrow">Ember Tune</span></div>
|
||||
<div class="goal-row">
|
||||
<div class="seg" id="goal-seg-2" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false">Maximum</button>
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false" title="a little rate for most of the saving">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false" title="most hashes, watts second">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line-2"></span>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span class="sw-text"><b>Ember Tune</b> <span class="dim">Tunes every card for hashes per watt: once after install, then every 7 days, and after a driver or program change.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once. Off, NVIDIA cards are measured only.</span><span class="dim" id="s-power-note"></span></span></label>
|
||||
<label class="switch" id="s-climb-row" hidden><input type="checkbox" id="s-climb"><span class="track"></span><span class="sw-text"><b>Hill climb</b> <span class="dim">Searches around the best point instead of walking the ladders.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span class="sw-text"><b>Ember Tune</b> <span class="dim">Tunes every card for the most hashes per watt. Once after install, then every 7 days, and after a driver or program change.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once. Off, NVIDIA cards are only measured.</span><span class="dim" id="s-power-note"></span></span></label>
|
||||
<label class="switch" id="s-climb-row" hidden><input type="checkbox" id="s-climb"><span class="track"></span><span class="sw-text"><b>Fine tuning</b> <span class="dim">Searches around the best point instead of stepping down a ladder.</span></span></label>
|
||||
<div class="field">
|
||||
<div class="k">electricity price</div>
|
||||
<div class="row">
|
||||
<input type="number" class="addr-input mono short" id="s-price" min="0" max="200" step="0.1" placeholder="28" aria-label="Electricity price in pence per kWh"><span class="note">pence per kWh. Every £ figure uses it.</span>
|
||||
<input type="number" class="addr-input mono short" id="s-price" min="0" max="200" step="0.1" placeholder="28" aria-label="Electricity price in pence per kWh"><span class="note">pence per kWh. Every £ figure uses this.</span>
|
||||
<button class="btn small" id="s-price-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -352,7 +371,7 @@
|
|||
<div class="card">
|
||||
<div class="card-head"><h3>This machine</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span class="sw-text"><b>Start at login</b> <span class="dim">Opens when you sign in and keeps mining in the background.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-jobs-allow"><span class="track"></span><span class="sw-text"><b>Allow remote jobs from Igneum</b> <span class="dim">Signed jobs (a benchmark, a script, logs to collect) run here once and report back.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-jobs-allow"><span class="track"></span><span class="sw-text"><b>Allow remote jobs from Igneum</b> <span class="dim">Signed jobs from the team (a benchmark, a script, logs to collect) run here once and report back.</span></span></label>
|
||||
<div class="row wrap indent"><span class="note" id="s-jobs-note"></span><button class="btn tiny ghost" id="s-jobs-check">Check now</button><button class="btn tiny ghost" id="s-jobs-history-btn" aria-expanded="false">History</button></div>
|
||||
<div class="details indent" id="s-jobs-history" hidden></div>
|
||||
<p class="note mono small indent" id="s-jobs-key" hidden></p>
|
||||
|
|
@ -362,7 +381,7 @@
|
|||
<input type="text" class="addr-input" id="s-name" placeholder="a name for this machine" maxlength="40" spellcheck="false" aria-label="Machine name">
|
||||
<button class="btn small" id="s-name-save">Rename</button>
|
||||
</div>
|
||||
<p class="help">A label for you only.</p>
|
||||
<p class="help">A name for you only.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">appearance</div>
|
||||
|
|
@ -401,8 +420,8 @@
|
|||
|
||||
<details class="card adv" id="s-advanced">
|
||||
<summary><h3>Advanced</h3><span class="eyebrow">devnet</span></summary>
|
||||
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span class="sw-text"><b>Vote on finality checkpoints</b> <span class="dim">Signs a checkpoint every 30 s. Votes lock the chain; leave it on.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span class="sw-text"><b>Trust proof records without verifying them</b> <span class="dim">Devnet only. Without a verifier the node includes records it never checked. Changing this restarts the node.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span class="sw-text"><b>Vote on checkpoints</b> <span class="dim">Signs a checkpoint every 30 s. Votes are what lock the chain. Leave it on.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span class="sw-text"><b>Trust proofs without checking them</b> <span class="dim">Devnet only. The node includes proofs it never checked. Changing this restarts the node.</span></span></label>
|
||||
<div class="ask inline" id="ask-trust" hidden>
|
||||
<span class="ask-text" id="ask-trust-text"></span>
|
||||
<button class="btn small primary" id="ask-trust-yes">Confirm</button>
|
||||
|
|
@ -494,6 +513,7 @@
|
|||
</div>
|
||||
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
<script src="live-dag.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
236
app/igneum-app/ui/live-dag.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/* Igneum live DAG view (docs/plans/site-ui-3.md section 6, 6 October 2026).
|
||||
One module for /live and the home page. Reads /api/live only; every pixel is a block, edge or checkpoint the observer
|
||||
stored. Blocks are placed by header time on x (the axis is labelled in DAA score) and by miner lane on y. Parent edges
|
||||
are drawn; the edge between two selected-chain blocks is heavier and the chain glows as one path. Colour words are the
|
||||
ledger's: pending, included (blue), excluded (red), proven (every shard verified or paid), locked (a checkpoint block).
|
||||
Checkpoints are vertical bands at their blue score with the lock weight; a locked band is solid, a pending one dashed.
|
||||
The view scrolls at chain speed, new blocks arrive from the right with a brief glow. The page's scroll passes through the
|
||||
scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene (engaged, released by Escape or a click
|
||||
outside; opts.chip is the "scroll to zoom, Esc to release" element); window 30 to 300 s (the API's ?window=); a pause button;
|
||||
hover and tap show a block's details.
|
||||
|
||||
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, pauseButton, tooltip, chip })
|
||||
- url: the /api/live endpoint (default '/api/live'); poll: false stops the module's own 2 s fetch, the host then calls
|
||||
dag.push(reply) with a reply of the same shape ({ok, state:{stale, age_s, height, ...}, blocks:[...], finality:{checkpoints:[...]}, proving})
|
||||
whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
|
||||
- block record (the /api/live shape): hash (string, 16 hex), ts (ms, header time), blue_score, daa (numbers), parents ([hash]),
|
||||
chain (bool, selected chain), color ('blue' | 'red' | 'pending'), miner (string lane key, 8 hex of the vote key hash),
|
||||
locked (bool, a locked checkpoint block), final (bool, at or before the newest lock), shards ([{state: 'planned' | 'proving' |
|
||||
'verified' | 'paid'}]), proven (bool, every shard verified or paid). Checkpoints: {index, blue_score, state: 'locked' | 'pending', fraction_total}.
|
||||
- mine: a miner lane key (string) or function(block) -> bool; the user's own blocks take a molten ring and the lane is labelled "you".
|
||||
- The legend is the host's; the module draws no HTML. Colours come from the CSS custom properties on :root (--ember, --ember-hi,
|
||||
--molten, --bone, --ash, --line, --line-2, --row or --obsidian), re-read on data-theme and prefers-color-scheme changes.
|
||||
- Sizing: the canvas fills its CSS box (width and height from the stylesheet); 280 px wide works, 600 or more shows lane labels well.
|
||||
onState(state, reason): 'connecting' | 'live' | 'stale' | 'failed'; onData(d) with every fresh reply. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var COL = {
|
||||
ember: '#F2541B', emberHi: '#FF6A2B', molten: '#FFB35C', bone: '#F4F1EC', ash: '#9A9A9E', line: '#2A2A30', line2: '#3A3A42', obsidian: '#0C0C0E',
|
||||
red: '#B0362B'
|
||||
};
|
||||
function cssVar(name, fallback) { try { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; } }
|
||||
function readTheme() {
|
||||
COL.ember = cssVar('--ember', COL.ember); COL.emberHi = cssVar('--ember-hi', COL.emberHi); COL.molten = cssVar('--molten', COL.molten);
|
||||
COL.bone = cssVar('--bone', COL.bone); COL.ash = cssVar('--ash', COL.ash); COL.line = cssVar('--line', COL.line); COL.line2 = cssVar('--line-2', COL.line2);
|
||||
COL.obsidian = cssVar('--row', cssVar('--obsidian', COL.obsidian));
|
||||
}
|
||||
function rgba(hex, a) { var h = hex.replace('#', ''); if (h.length !== 6) return hex; return 'rgba(' + parseInt(h.slice(0, 2), 16) + ',' + parseInt(h.slice(2, 4), 16) + ',' + parseInt(h.slice(4, 6), 16) + ',' + a + ')'; }
|
||||
function fmt(n) { return n === null || n === undefined ? 'pending' : Number(n).toLocaleString('en-GB'); }
|
||||
function fmtAge(sec) { sec = Math.round(sec); return sec < 90 ? sec + ' s' : sec < 5400 ? Math.round(sec / 60) + ' min' : Math.round(sec / 3600) + ' h'; }
|
||||
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var compact = !!opts.compact, windowS = Math.max(30, Math.min(300, opts.window || (compact ? 90 : 120)));
|
||||
var reduce = root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var isMine = typeof opts.mine === 'function' ? opts.mine : opts.mine ? function (b) { return b.miner === opts.mine; } : null;
|
||||
var ctx = canvas.getContext('2d'), dpr = Math.min(root.devicePixelRatio || 1, 2), W = 0, H = 0;
|
||||
var blocks = [], byHash = {}, lanes = [], laneOf = {}, cps = [], data = null, state = 'connecting', reason = 'connecting';
|
||||
var paused = false, offsetMs = 0, hover = null, pinned = null, lastPoll = 0, failures = 0, running = false, inView = true, lastT = 0;
|
||||
var vnow = function () { return Date.now() - 2500 - offsetMs; }; // the right edge sits 2.5 s behind the clock so a block has arrived before it is drawn
|
||||
var padL = compact ? 10 : 56, padR = 14, padT = compact ? 12 : 22, padB = compact ? 10 : 26;
|
||||
|
||||
function size() { W = canvas.clientWidth; H = canvas.clientHeight; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); layout(); }
|
||||
root.addEventListener('resize', size);
|
||||
|
||||
// ---- data ----
|
||||
function apply(d) {
|
||||
data = d;
|
||||
var seen = {}, now = performance.now();
|
||||
d.blocks.forEach(function (b) {
|
||||
seen[b.hash] = 1;
|
||||
var e = byHash[b.hash];
|
||||
if (e) { e.chain = !!b.chain; e.color = b.color || 'pending'; e.locked = !!b.locked; e.final = !!b.final; e.shards = b.shards || []; e.proven = !!b.proven; e.blue = b.blue_score; e.daa = b.daa; return; }
|
||||
e = { hash: b.hash, ts: b.ts, blue: b.blue_score, daa: b.daa, parents: b.parents || [], chain: !!b.chain, color: b.color || 'pending', miner: b.miner || '?', locked: !!b.locked, final: !!b.final, shards: b.shards || [], proven: !!b.proven, born: now, x: 0, y: 0 };
|
||||
byHash[b.hash] = e; blocks.push(e);
|
||||
});
|
||||
var cut = vnow() - windowS * 1000 - 20000;
|
||||
blocks = blocks.filter(function (b) { if (b.ts < cut && !seen[b.hash]) { delete byHash[b.hash]; return false; } return true; });
|
||||
blocks.sort(function (a, b) { return a.ts - b.ts; });
|
||||
cps = (d.finality && d.finality.checkpoints) || [];
|
||||
layout();
|
||||
}
|
||||
function layout() {
|
||||
// lanes: miners of the visible window ordered by blocks, as many as fit at 22 px, the rest in one "others" lane
|
||||
var vis = vnow() - windowS * 1000, count = {};
|
||||
blocks.forEach(function (b) { if (b.ts >= vis) count[b.miner] = (count[b.miner] || 0) + 1; });
|
||||
var ids = Object.keys(count).sort(function (a, b) { return count[b] - count[a] || (a < b ? -1 : 1); });
|
||||
var usable = H - padT - padB, laneH = compact ? 26 : 22, maxLanes = Math.max(2, Math.floor(usable / laneH));
|
||||
var shown = ids.length > maxLanes ? ids.slice(0, maxLanes - 1) : ids;
|
||||
lanes = shown.slice(); if (ids.length > shown.length) lanes.push('others');
|
||||
laneOf = {}; shown.forEach(function (id, i) { laneOf[id] = i; });
|
||||
}
|
||||
function laneY(b) { var i = b.miner in laneOf ? laneOf[b.miner] : lanes.length - 1; var n = Math.max(1, lanes.length); return padT + (i + 0.5) * ((H - padT - padB) / n); }
|
||||
function xOf(ts) { return padL + (W - padL - padR) * (1 - (vnow() - ts) / (windowS * 1000)); }
|
||||
|
||||
// the host can feed replies itself (poll: false); the same path the fetch takes
|
||||
function push(d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) { setState('failed', 'live feed unavailable'); return; }
|
||||
failures = 0;
|
||||
if (d.state.stale) setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown'));
|
||||
else setState('live', null);
|
||||
apply(d); if (opts.onData) opts.onData(d); kick();
|
||||
}
|
||||
function poll() {
|
||||
if (opts.poll === false || document.hidden) return;
|
||||
var u = (opts.url || '/api/live') + '?window=' + windowS;
|
||||
fetch(u, { cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function (d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) throw new Error('bad reply');
|
||||
failures = 0;
|
||||
if (d.state.stale) { setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); }
|
||||
else setState('live', null);
|
||||
apply(d); if (opts.onData) opts.onData(d);
|
||||
kick();
|
||||
}).catch(function () { failures++; if (failures >= 2) setState('failed', 'live feed unavailable'); });
|
||||
}
|
||||
function setState(s, r) { if (s === state && r === reason) return; state = s; reason = r; if (opts.onState) opts.onState(s, r); }
|
||||
|
||||
// ---- drawing ----
|
||||
function draw(t) {
|
||||
if (!W || !H) return;
|
||||
var dt = lastT ? Math.min(50, t - lastT) : 16; lastT = t;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
var vis = vnow() - windowS * 1000, n = Math.max(1, lanes.length), lh = (H - padT - padB) / n;
|
||||
// lane rails and labels
|
||||
ctx.lineWidth = 1;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var y = padT + (i + 0.5) * lh;
|
||||
ctx.strokeStyle = rgba(COL.line, 0.9); ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke();
|
||||
if (!compact) { var mineLane = isMine && typeof opts.mine === 'string' && lanes[i] === opts.mine; ctx.fillStyle = mineLane ? COL.molten : COL.ash; ctx.font = (mineLane ? '500' : '400') + ' 10px IBM Plex Mono, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(mineLane ? 'you' : lanes[i] === 'others' ? 'others' : String(lanes[i]).slice(0, 8), 6, y); }
|
||||
}
|
||||
// checkpoint bands at their blue score (x from the chain block carrying that score, else the nearest)
|
||||
cps.forEach(function (c) {
|
||||
var ref = null, best = Infinity;
|
||||
for (var k = blocks.length - 1; k >= 0; k--) { var b = blocks[k]; if (b.blue === null || b.blue === undefined) continue; var dd = Math.abs(b.blue - c.blue_score); if (dd < best) { best = dd; ref = b; } }
|
||||
if (!ref || best > 3 || ref.ts < vis) return;
|
||||
var x = xOf(ref.ts), locked = c.state === 'locked';
|
||||
ctx.strokeStyle = rgba(COL.ember, locked ? 0.55 : 0.3); ctx.lineWidth = locked ? 1.5 : 1; ctx.setLineDash(locked ? [] : [4, 6]);
|
||||
ctx.beginPath(); ctx.moveTo(x, padT - 6); ctx.lineTo(x, H - padB + 6); ctx.stroke(); ctx.setLineDash([]);
|
||||
if (!compact) { ctx.fillStyle = rgba(COL.ember, 0.9); ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText((locked ? 'locked ' : 'pending ') + (c.fraction_total !== null && c.fraction_total !== undefined ? Math.round(c.fraction_total * 100) + '%' : ''), x, H - 8); }
|
||||
});
|
||||
// positions
|
||||
blocks.forEach(function (b) { b.x = xOf(b.ts); b.y = laneY(b); b.on = b.ts >= vis && b.x >= padL - 10; });
|
||||
// edges: merge edges thin, chain-to-chain heavier; the chain as one glowing path
|
||||
blocks.forEach(function (b) {
|
||||
if (!b.on) return;
|
||||
b.parents.forEach(function (h) {
|
||||
var p = byHash[h]; if (!p || !p.on) return;
|
||||
var chainEdge = b.chain && p.chain;
|
||||
ctx.strokeStyle = chainEdge ? rgba(COL.ember, 0.75) : rgba(COL.ash, b.color === 'red' ? 0.12 : 0.22);
|
||||
ctx.lineWidth = chainEdge ? 2 : 1;
|
||||
ctx.beginPath(); ctx.moveTo(b.x, b.y); var mx = (b.x + p.x) / 2; ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke();
|
||||
if (chainEdge) { ctx.strokeStyle = rgba(COL.ember, 0.18); ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke(); }
|
||||
});
|
||||
});
|
||||
// blocks
|
||||
var S = compact ? 9 : 8, now = performance.now();
|
||||
blocks.forEach(function (b) {
|
||||
if (!b.on) return;
|
||||
var age = now - b.born, glow = age < 1600 ? 1 - age / 1600 : 0, ex = b.color === 'red';
|
||||
var r = b.chain ? S + 1 : S;
|
||||
if (glow > 0) { var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r * 3); g.addColorStop(0, rgba(COL.ember, 0.4 * glow)); g.addColorStop(1, rgba(COL.ember, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, r * 3, 0, Math.PI * 2); ctx.fill(); }
|
||||
ctx.globalAlpha = ex ? 0.45 : 1;
|
||||
// fill: proven = ember, otherwise the row surface; outline: included (blue or chain) = ember, pending = grey, excluded = dim
|
||||
var proving = !b.proven && b.shards.some(function (s) { return s.state === 'proving' || s.state === 'verified' || s.state === 'paid'; });
|
||||
ctx.beginPath(); rr(b.x - r, b.y - r, 2 * r, 2 * r, 3);
|
||||
ctx.fillStyle = b.proven ? COL.ember : COL.obsidian; ctx.fill();
|
||||
ctx.lineWidth = b.chain ? 2 : 1.5;
|
||||
// chain blocks in ember (the path), included blocks lit in bone, pending in ash, excluded dim
|
||||
ctx.strokeStyle = b.proven ? COL.ember : proving ? COL.molten : b.chain ? rgba(COL.ember, 0.9) : b.color === 'blue' ? rgba(COL.bone, 0.8) : ex ? rgba(COL.ash, 0.5) : rgba(COL.ash, 0.75);
|
||||
ctx.stroke();
|
||||
if (b.locked) { ctx.strokeStyle = rgba(COL.ember, 0.8 + 0.2 * Math.sin(t / 350)); ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.9, 0, Math.PI * 2); ctx.stroke(); }
|
||||
if (isMine && isMine(b)) { ctx.strokeStyle = rgba(COL.molten, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.5, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
// hover or pinned ring
|
||||
var f = pinned || hover; if (f && f.on) { ctx.strokeStyle = COL.bone; ctx.lineWidth = 1; ctx.setLineDash([2, 3]); ctx.beginPath(); ctx.arc(f.x, f.y, S * 2.4, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); }
|
||||
// axis in DAA at the left and right edges, and the lock line label
|
||||
if (!compact) {
|
||||
ctx.fillStyle = COL.ash; ctx.font = '400 10px IBM Plex Mono, monospace'; ctx.textBaseline = 'alphabetic';
|
||||
var first = null, last = null; blocks.forEach(function (b) { if (!b.on) return; if (!first || b.ts < first.ts) first = b; if (!last || b.ts > last.ts) last = b; });
|
||||
if (first && last) { ctx.textAlign = 'left'; ctx.fillText('daa ' + fmt(first.daa), padL, padT - 8); ctx.textAlign = 'right'; ctx.fillText('daa ' + fmt(last.daa) + (paused ? ' paused' : ''), W - padR, padT - 8); }
|
||||
}
|
||||
if (paused) { ctx.fillStyle = rgba(COL.obsidian, 0.0); }
|
||||
}
|
||||
function rr(x, y, w, h, r) { ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
|
||||
|
||||
function frame(t) { if (!running) return; if (!paused) draw(t); if (reduce || document.hidden || !inView) { running = false; return; } requestAnimationFrame(frame); }
|
||||
function start() { if (running) return; running = true; requestAnimationFrame(frame); }
|
||||
function kick() { if (!running) requestAnimationFrame(function (t) { draw(t); if (!reduce && !document.hidden && inView) start(); }); }
|
||||
|
||||
// ---- interaction ----
|
||||
function hit(ev) {
|
||||
var rect = canvas.getBoundingClientRect(), px = ev.clientX - rect.left, py = ev.clientY - rect.top, best = null, bd = 14 * 14;
|
||||
blocks.forEach(function (b) { if (!b.on) return; var dx = b.x - px, dy = b.y - py, d = dx * dx + dy * dy; if (d < bd) { bd = d; best = b; } });
|
||||
return best;
|
||||
}
|
||||
function words(b) {
|
||||
var state = b.locked ? 'locked checkpoint' : b.proven ? 'proven' : b.chain ? 'included, selected chain' : b.color === 'blue' ? 'included' : b.color === 'red' ? 'excluded' : 'pending';
|
||||
var sh = b.shards.length ? b.shards.map(function (s) { return s.state; }).join(', ') : 'no shard plan yet';
|
||||
return { title: b.hash + ' ' + state, lines: [(isMine && isMine(b) ? 'your block, miner ' : 'miner ') + b.miner + (b.miner in laneOf ? '' : ' (others lane)'), 'blue score ' + fmt(b.blue) + ' daa ' + fmt(b.daa), b.parents.length + (b.parents.length === 1 ? ' parent' : ' parents'), 'proof: ' + sh + (b.final ? ' final' : '')] };
|
||||
}
|
||||
var tip = opts.tooltip || null;
|
||||
function showTip(b, ev) {
|
||||
if (!tip) return;
|
||||
if (!b) { tip.hidden = true; return; }
|
||||
var w = words(b); tip.hidden = false; tip.innerHTML = '<b>' + w.title + '</b>' + w.lines.map(function (l) { return '<span>' + l + '</span>'; }).join('');
|
||||
var rect = canvas.getBoundingClientRect(); var x = Math.min(rect.width - tip.offsetWidth - 8, Math.max(8, b.x + 12)), y = Math.max(8, b.y - tip.offsetHeight - 10); if (y < 8) y = b.y + 14;
|
||||
tip.style.transform = 'translate(' + Math.round(x) + 'px,' + Math.round(y) + 'px)';
|
||||
}
|
||||
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); });
|
||||
canvas.addEventListener('mouseleave', function () { hover = null; if (!pinned) showTip(null); kick(); });
|
||||
canvas.addEventListener('click', function (ev) { engage(true); var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); });
|
||||
// the wheel is never captured by default (6 Oct 2026, the owner: scrolling past the scene zoomed it): the page scrolls through the
|
||||
// scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene, which engages it (opts.chip shows
|
||||
// "scroll to zoom, Esc to release"); Escape, a click outside or leaving the scene releases it
|
||||
var engaged = false, chip = opts.chip || null;
|
||||
function engage(on) { if (on === engaged) return; engaged = on; canvas.classList.toggle('engaged', on); if (chip) chip.hidden = !on; if (opts.onEngage) opts.onEngage(on); }
|
||||
canvas.addEventListener('wheel', function (ev) {
|
||||
var zoomKey = ev.ctrlKey || ev.metaKey;
|
||||
if (!zoomKey && !engaged) return; // a plain wheel or a two-finger scroll passes through to the page
|
||||
if (Math.abs(ev.deltaY) < 1) return;
|
||||
ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15);
|
||||
}, { passive: false });
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && engaged) engage(false); });
|
||||
document.addEventListener('pointerdown', function (ev) { if (engaged && !canvas.contains(ev.target) && !(chip && chip.contains(ev.target))) engage(false); });
|
||||
var pinch = null;
|
||||
canvas.addEventListener('touchstart', function (ev) { if (ev.touches.length === 2) pinch = dist(ev.touches); }, { passive: true });
|
||||
canvas.addEventListener('touchmove', function (ev) { if (ev.touches.length === 2 && pinch) { var d = dist(ev.touches); if (Math.abs(d - pinch) > 12) { zoom(d > pinch ? 1 / 1.1 : 1.1); pinch = d; } } }, { passive: true });
|
||||
canvas.addEventListener('touchend', function () { pinch = null; }, { passive: true });
|
||||
function dist(t) { var dx = t[0].clientX - t[1].clientX, dy = t[0].clientY - t[1].clientY; return Math.sqrt(dx * dx + dy * dy); }
|
||||
function zoom(f) { var w = Math.round(Math.max(30, Math.min(300, windowS * f))); if (w === windowS) return; windowS = w; layout(); if (opts.onWindow) opts.onWindow(w); poll(); kick(); }
|
||||
function setPaused(p) { paused = !!p; if (paused) { offsetMs = 0; pausedAt = Date.now(); } else { offsetMs = 0; } if (opts.pauseButton) { opts.pauseButton.setAttribute('aria-pressed', paused ? 'true' : 'false'); opts.pauseButton.textContent = paused ? 'Resume' : 'Pause'; } kick(); }
|
||||
var pausedAt = 0;
|
||||
if (opts.pauseButton) opts.pauseButton.addEventListener('click', function () { setPaused(!paused); });
|
||||
// while paused the clock stops: offsetMs grows so the scene holds still but keeps its blocks
|
||||
setInterval(function () { if (paused) offsetMs = Date.now() - pausedAt; }, 250);
|
||||
|
||||
if ('IntersectionObserver' in root) { new IntersectionObserver(function (es) { es.forEach(function (e) { inView = e.isIntersecting; if (inView) kick(); }); }, { threshold: 0.05 }).observe(canvas); }
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) { poll(); kick(); } });
|
||||
if (root.matchMedia) { try { root.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { readTheme(); kick(); }); } catch (e) { } }
|
||||
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
|
||||
readTheme(); size(); if (opts.poll !== false) { poll(); setInterval(poll, 2000); } kick();
|
||||
return { push: push, setPaused: setPaused, zoom: zoom, engage: engage, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
|
||||
}
|
||||
root.IgneumDag = { mount: mount };
|
||||
})(window);
|
||||
|
|
@ -96,7 +96,7 @@ test('update wording: available, downloading with percent, installing, failed wi
|
|||
assert.equal(updateNotice(upd({ status: 'deferred' }), s()).text, 'Igneum Miner 0.3.6 is waiting for permission. It installs the next time someone is at this PC. Mining continues.');
|
||||
// updated: gone within 60 s of the new version starting
|
||||
const cur = upd({ status: 'current', available: false, ready: false, downloaded: false, updated_from: '0.3.4' });
|
||||
assert.equal(updateNotice(cur, s({ version: '0.3.6', uptime_s: 59 })).text, 'Updated to Igneum Miner 0.3.6 from 0.3.4.');
|
||||
assert.match(updateNotice(cur, s({ version: '0.3.6', uptime_s: 59, now: 1_800_000_000 })).text, /^Updated to 0\.3\.6 at \d\d:\d\d\.$/);
|
||||
assert.equal(updateNotice(cur, s({ version: '0.3.6', uptime_s: 60 })), null);
|
||||
assert.equal(updateNotice(upd({ status: 'current', available: false }), s()), null);
|
||||
// a build with no manifest reports an error the user cannot act on: no notice (the Settings note still says it)
|
||||
|
|
@ -106,22 +106,26 @@ test('update wording: available, downloading with percent, installing, failed wi
|
|||
|
||||
test('job wording: running with minutes and stage, done goes after 5 minutes, failed stays with the first error line', () => {
|
||||
const r = jobNotice(run(), NOW);
|
||||
assert.equal(r.text, 'Job: shard benchmark running, 6 min. Proving shard 3 of 8.');
|
||||
assert.equal(r.detail, 'RESULT shard=2 prove_s=39.8');
|
||||
assert.equal(jobNotice(run({ stage: '' }), NOW).text, 'Job: shard benchmark running, 6 min.');
|
||||
assert.equal(r.text, 'A job from the team is running: shard benchmark, 6 min. Proving shard 3 of 8.');
|
||||
assert.equal(r.detail, 'shard benchmark · RESULT shard=2 prove_s=39.8', 'the technical line goes behind the chevron');
|
||||
assert.equal(jobNotice(run({ stage: '' }), NOW).text, 'A job from the team is running: shard benchmark, 6 min.');
|
||||
const d = jobNotice(fin('done'), NOW);
|
||||
assert.equal(d.text, 'Job: build done after 32 min. Report uploaded.');
|
||||
assert.equal(d.text, 'A job from the team ran: build, 32 min.');
|
||||
assert.equal(d.key, 'job:done:job-6');
|
||||
assert.ok(jobNotice(fin('done'), NOW - 60 + N.JOB_DONE_S));
|
||||
assert.equal(jobNotice(fin('done'), NOW - 60 + N.JOB_DONE_S + 1), null);
|
||||
const f = jobNotice(fin('failed'), NOW);
|
||||
assert.equal(f.text, 'Job: build failed after 32 min, exit 1. Report not uploaded.');
|
||||
assert.equal(f.detail, 'BUILD FAILED: error[E0425]: cannot find value `foo`');
|
||||
assert.equal(f.text, 'A job from the team failed: build, after 32 min. The report did not upload.');
|
||||
assert.match(f.detail, /BUILD FAILED: error\[E0425\]: cannot find value `foo` · exit 1$/);
|
||||
assert.equal(f.tone, 'bad');
|
||||
assert.ok(jobNotice(fin('failed'), NOW + 86400 * 7), 'a failed job stays until closed');
|
||||
// no error line among the results: the summary is the cause
|
||||
assert.equal(jobNotice(fin('failed', { last_results: ['STAGE build'] }), NOW).detail, 'build exited with code 1');
|
||||
assert.equal(jobNotice(fin('timeout'), NOW).text, 'Job: build hit its time cap after 32 min, exit 1. Report not uploaded.');
|
||||
assert.match(jobNotice(fin('failed', { last_results: ['STAGE build'] }), NOW).detail, /^build: build exited with code 1 · exit 1$/);
|
||||
assert.equal(jobNotice(fin('timeout'), NOW).text, 'A job from the team ran out of time: build, after 32 min. The report did not upload.');
|
||||
assert.equal(N.jobWord('', 'update-now-0313-switch-d937c69d'), 'update check');
|
||||
assert.equal(N.jobWord('restart', 'restart-miners-0312'), 'restart');
|
||||
assert.equal(N.jobWord('collect', ''), 'log collection');
|
||||
assert.equal(N.jobWord('', 'ember-tune-pc1-5'), 'tuning check');
|
||||
assert.equal(jobNotice({ active: false, last: {} }, NOW), null);
|
||||
assert.equal(jobNotice(null, NOW), null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ const V = mod.exports.View;
|
|||
|
||||
const card = (over) => ({ key: 'nvidia:0:RTX 5090', name: 'NVIDIA GeForce RTX 5090', vendor: 'nvidia', kind: 'discrete', worker: 'CUDA', vram_mb: 32768, enabled: true, state: 'mining', hash_now: 124.3, hash_avg: 120, accepted: 3, rejected: 0, identities: 8, ids: [], prepared: true, restart_in_s: 0, message: '', reason: '', power_w: 410.2, power_limit_w: 460, power_default_w: 575, power_pct: 80, power_applied: true, temp_gpu: 61, temp_mem: 72, telemetry_at: 1, ...over });
|
||||
|
||||
test('the four sections and their order (miner-ui-3)', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'earnings', 'prove', 'settings']);
|
||||
test('the five sections and their order (miner-ui-4)', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['overview', 'cards', 'earnings', 'prove', 'settings']);
|
||||
assert.equal(V.page('cards').title, 'Cards');
|
||||
assert.equal(V.page('earnings').title, 'Earnings');
|
||||
assert.equal(V.page('nonsense').id, 'mine');
|
||||
assert.equal(V.page('nonsense').id, 'overview');
|
||||
});
|
||||
|
||||
test('a GPU row: name, kind, the numbers where known, the switch', () => {
|
||||
|
|
@ -72,8 +73,8 @@ test('the big button: start when paused, stop when mining, disabled with no card
|
|||
const none = V.toggle(m({ cards: [card({ enabled: false })] }), { synced: true }, {});
|
||||
assert.equal(none.disabled, true);
|
||||
assert.equal(none.act, '');
|
||||
assert.equal(none.sub, 'switch a GPU on below first');
|
||||
assert.equal(V.toggle(m({ cards: [] }), { synced: true }, {}).sub, 'no GPU this app can drive');
|
||||
assert.equal(none.sub, 'switch a card on in Cards first');
|
||||
assert.equal(V.toggle(m({ cards: [] }), { synced: true }, {}).sub, 'no card this app can use');
|
||||
assert.equal(V.toggle(m({ state: 'waiting' }), { synced: false }, {}).sub, 'waiting for the node to sync');
|
||||
assert.equal(V.toggle(m(), { synced: true }, { quitting: true }).disabled, true);
|
||||
});
|
||||
|
|
@ -106,11 +107,11 @@ test('the next consensus switch is the first height above the DAA score, in plai
|
|||
const next = V.nextSwitch(sw, 140000);
|
||||
assert.equal(next.name, 'Fees v1');
|
||||
assert.equal(next.away, 70000);
|
||||
assert.equal(V.switchLine(next, 140000), 'Fees v1 at DAA 210,000: 70,000 blocks away, about 19 h 27 min at one block a second.');
|
||||
assert.equal(V.switchLine(next, 140000), 'Fees v1 at block 210,000: 70,000 blocks away, about 19 h 27 min at one block a second.');
|
||||
assert.equal(V.nextSwitch(sw, 20000).name, 'Difficulty v2');
|
||||
assert.equal(V.nextSwitch(sw, 300000), null);
|
||||
assert.match(V.switchLine(null, 300000), /Every planned switch is behind this node/);
|
||||
assert.match(V.switchLine(null, 0), /^A switch is a planned rule change/);
|
||||
assert.match(V.switchLine(null, 300000), /Every planned rule change is behind this node/);
|
||||
assert.match(V.switchLine(null, 0), /^A rule change is planned ahead/);
|
||||
assert.equal(V.nextSwitch([], 5), null);
|
||||
});
|
||||
|
||||
|
|
@ -139,9 +140,9 @@ test('the dev-fee lines name the share and the switch that turns it off', () =>
|
|||
|
||||
test('the remote-jobs line and the helpers', () => {
|
||||
const title = (j) => j.title || j.kind;
|
||||
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off: nothing runs here until the switch above allows remote jobs.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: false }, 1000, title), 'No jobs address in this build.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, active: true, id: 'job-3', kind: 'build', title: 'build' }, 1000, title), 'Running build (job-3).');
|
||||
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off. Nothing runs here until the switch above allows jobs from the team.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: false }, 1000, title), 'This build has no jobs address.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, active: true, id: 'job-3', kind: 'build', title: 'build' }, 1000, title), 'Running: build.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, checked_at: 940, queued: 2 }, 1000, title), 'Nothing running; checked 1 min ago; 2 queued.');
|
||||
assert.equal(V.shortHex('0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a'), '0x2b1a81…79ef7a');
|
||||
assert.equal(V.shortHex('0xabc'), '0xabc');
|
||||
|
|
@ -159,7 +160,7 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
|||
assert.equal(r.word, 'removed');
|
||||
assert.equal(r.tone, 'off');
|
||||
assert.equal(r.hash, '');
|
||||
assert.match(r.sub, /^unplugged; its worker stopped/);
|
||||
assert.match(r.sub, /^unplugged\. Its row goes/);
|
||||
const bad = card({ key: 'amd:gfx1201#2', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd', enabled: false, state: 'unusable', problem: 'Code 43', message: 'not usable (Code 43)', reason: 'reboot with the card attached; if it persists, reinstall the driver with the card attached', added_at: 900, removed_at: 0, device: '1', platform: 'AMD Accelerated Parallel Processing', bus: '0000:03:00.0' });
|
||||
const b = V.cardRow(bad);
|
||||
assert.equal(b.unusable, true);
|
||||
|
|
@ -194,7 +195,7 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
|||
assert.equal(V.present(bad), false);
|
||||
const t = V.toggle({ state: 'mining', paused: false, cards: [gone, bad] }, { synced: true }, {});
|
||||
assert.equal(t.disabled, true);
|
||||
assert.equal(t.sub, 'no GPU this app can drive');
|
||||
assert.equal(t.sub, 'no card this app can use');
|
||||
const t2 = V.toggle({ state: 'mining', paused: false, cards: [card(), gone] }, { synced: true }, {});
|
||||
assert.equal(t2.sub, 'mining on 1 of 1 card');
|
||||
});
|
||||
|
|
@ -232,6 +233,7 @@ test('money: £ a day from watts at a price, nothing without one, the cells leav
|
|||
assert.equal(apple.noReading, 'No power or temperature reading on Apple silicon');
|
||||
assert.equal(V.cells(card({ kind: 'integrated', vendor: 'other', power_w: 0, temp_gpu: 0, eff_mhw: 0 }), 28).noReading, 'No power or temperature reading on an integrated GPU');
|
||||
assert.equal(V.wattsTotal([card(), card({ key: 'b', power_w: 100 }), card({ key: 'c', power_w: 50, removed_at: 5 })]), 510.2);
|
||||
assert.equal(V.wattsTotal([card(), card({ key: 'd', power_w: 48, state: 'off', enabled: false })]), 410.2, 'an idle card’s standby draw is not counted');
|
||||
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0.422 }))), '0.42 MH/W');
|
||||
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0, power_w: 0 }))), '');
|
||||
});
|
||||
|
|
@ -249,7 +251,7 @@ test('the tune line: never run, running with a step and a bar, tuned with the po
|
|||
assert.equal(tuned.kind, 'tuned'); assert.equal(tuned.note, '122.3 MH/s at 290 W (0.422 MH/W)'); assert.equal(tuned.button, 'tune');
|
||||
assert.match(tuned.text, /^Tuned 2 h ago · 2470 MHz at 100% · next check /);
|
||||
const prior = V.tuneWords(nv({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'confirm', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 120 }), settings(), NOW);
|
||||
assert.match(prior.text, /^Tuned 2 min ago from the fleet prior, confirmed · 2470 MHz at 100%/);
|
||||
assert.match(prior.text, /^Tuned 2 min ago from what other miners found, confirmed · 2470 MHz at 100%/);
|
||||
const measured = V.tuneWords(nv({ tune_line: 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed' }), settings(), NOW);
|
||||
assert.equal(measured.kind, 'measured'); assert.equal(measured.text, 'Measured 26.7 MH/s at 38 W (0.703 MH/W) as it runs, 10 min ago');
|
||||
assert.equal(measured.note, 'Measured only on Apple silicon: the system sets the clocks and the power; no control exposed');
|
||||
|
|
@ -279,7 +281,7 @@ test('the next check is a weekday inside a week, a date beyond it, due now when
|
|||
test('the Tuning card schedule and the goal consequence', () => {
|
||||
const nv = (over) => card({ sweep_at: 0, tune_steps: 0, ...over });
|
||||
assert.equal(V.schedule([nv()], settings(), NOW), 'Not tuned yet: the first tune starts 2 min into steady mining.');
|
||||
assert.equal(V.schedule([nv()], settings({ sweep: false }), NOW), 'Ember Tune is off. Tune and Tune all still work by hand.');
|
||||
assert.equal(V.schedule([nv()], settings({ sweep: false }), NOW), 'Ember Tune is off. Tune and Tune all still work when you press them.');
|
||||
assert.equal(V.schedule([nv({ state: 'tuning', tune_step: 2, tune_steps: 9, tune_eta_s: 500 })], settings(), NOW), 'Tuning now: NVIDIA GeForce RTX 5090, step 2 of 9, mining again in about 8 min.');
|
||||
assert.match(V.schedule([nv({ sweep_at: NOW - 7200 }), nv({ key: 'b', name: 'B', sweep_at: NOW - 60 })], settings(), NOW), /^Last tune 1 min ago \(B\) · next check /);
|
||||
assert.equal(V.schedule([], settings(), NOW), 'No card here can be tuned.');
|
||||
|
|
@ -296,7 +298,7 @@ test('the prove tier sentence per card, and the counts in one line', () => {
|
|||
assert.equal(V.proveTier(card()), 'NVIDIA GeForce RTX 5090: proves while it mines (32 GB; a full shard needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'NVIDIA GeForce RTX 4070', vram_mb: 12288 })), 'NVIDIA GeForce RTX 4070: cannot prove a full shard (12 GB; a full shard needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'Apple M5 Max', vendor: 'apple' })), 'Apple M5 Max: proves on the CPU, slowly.');
|
||||
assert.equal(V.proveTier(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd', vram_mb: 16384 })), 'AMD Radeon RX 9070 XT: cannot prove yet (the prover is CUDA only).');
|
||||
assert.equal(V.proveTier(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd', vram_mb: 16384 })), 'AMD Radeon RX 9070 XT: cannot prove yet (proving needs an NVIDIA card).');
|
||||
assert.equal(V.proveLine({ assigned: 0, submitted: 0, paid: 0, paid_wei: '0' }, false, true), 'Off · not proving · 0 assigned · 0 proven · 0 paid · 0.0000 IGN');
|
||||
assert.equal(V.proveLine({ status: 'idle', assigned: 3, submitted: 2, paid: 1, paid_wei: '1500000000000000000', available: true, enabled: true }, true, true), 'Idle · nothing assigned · 3 assigned · 2 proven · 1 paid · 1.5000 IGN');
|
||||
});
|
||||
|
|
@ -310,8 +312,151 @@ test('the node line, the activity line, the asks', () => {
|
|||
assert.equal(V.eventLine('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}'), 'consensus parameters from the signed manifest (details in the log)');
|
||||
assert.equal(V.eventLine('node synced: 151512 blocks, 6 peer(s)'), 'node synced: 151512 blocks, 6 peer(s)');
|
||||
assert.equal(V.eventLine('a '.repeat(100), 40).length <= 41, true);
|
||||
assert.equal(V.addressAsk('0x1234567890abcdef1234567890abcdef12345678'), 'Pay 0x1234…5678 from the next block? The miner restarts.');
|
||||
assert.match(V.trustAsk(true), /^Include proof records the node never checked\? Devnet only; the node restarts\.$/);
|
||||
assert.match(V.trustAsk(false), /^Verify proof records again\?/);
|
||||
assert.equal(V.addressAsk('0x1234567890abcdef1234567890abcdef12345678'), 'Pay 0x1234…5678 from the next block? Mining restarts.');
|
||||
assert.match(V.trustAsk(true), /^Include proofs the node never checked\? Devnet only\. The node restarts\.$/);
|
||||
assert.match(V.trustAsk(false), /^Check proofs again\?/);
|
||||
assert.equal(V.ago(30), 'just now'); assert.equal(V.ago(7200), '2 h ago'); assert.equal(V.ago(200000), '2 d ago');
|
||||
});
|
||||
|
||||
test('the activity feed and the pill speak in the user\'s voice; the engine line is kept as the tooltip', () => {
|
||||
const pe = V.plainEvent('job update-now-0313-switch-d937c69d (Exec restart: re-read the manifest (exec_restart_number 27276) and restart the node with it) done after 0 min, exit 0, report uploaded: update check and install asked');
|
||||
assert.equal(pe.text, 'A job from the team ran: update check, 0 min');
|
||||
assert.match(pe.tech, /^job update-now/);
|
||||
assert.equal(V.plainEvent('updated to Igneum Miner 0.3.14 from 0.3.13').text, 'Updated to 0.3.14');
|
||||
assert.equal(V.plainEvent('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}').text, 'Rule settings read from the team');
|
||||
assert.equal(V.plainEvent('node synced: 151512 blocks, 6 peer(s)').text, 'Node synced: 151,512 blocks, 6 peers');
|
||||
assert.equal(V.plainEvent('block accepted by the node (Apple M5 Max)').text, 'Block found by Apple M5 Max');
|
||||
assert.equal(V.plainEvent('1 remote job received from Igneum').text, '1 job from the team received');
|
||||
assert.equal(V.plainEvent('remote job: update check now; a newer version installs at once').text, 'The team asked for an update check');
|
||||
assert.equal(V.plainEvent('a node already answers on 127.0.0.1:26610; using it (it is not stopped by this app)').text, 'Using the node already running on this machine');
|
||||
assert.equal(V.plainEvent('igneumd exited with code 101 after 300 s; restarting').text, 'The node stopped (code 101). Restarting');
|
||||
assert.equal(V.plainEvent('miner apple:0 exited with code 1; restarting in 8 s').text, 'A card stopped (code 1). Restarting in 8 s');
|
||||
assert.equal(V.plainEvent('NVIDIA GeForce RTX 5090 kernel race: l128w16 at 122.3 MH/s (+4.1% over base, 6 variants, 40 s)').text, 'NVIDIA GeForce RTX 5090: fastest program chosen, 122.3 MH/s');
|
||||
assert.equal(V.plainEvent('block 59199 shard 0 paid 0.0125 IGN').text, 'Proof paid: 0.0125 IGN');
|
||||
assert.equal(V.plainEvent('rewards go to 0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8').text, 'Rewards go to 0xdd44…86e8');
|
||||
assert.equal(V.plainEvent('mining resumed').text, 'Mining resumed');
|
||||
assert.equal(V.plainEvent('something new the engine says').text, 'Something new the engine says');
|
||||
assert.equal(V.plainEvent('something new the engine says').tech, '');
|
||||
const base = { setup_done: true, quitting: false, mining: { state: 'mining', hash_total: 510.6, cards: [] }, node: { state: 'synced' } };
|
||||
assert.deepEqual(V.pill(base), { text: 'Mining · 511 MH/s', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 17.04, cards: [] } }), { text: 'Mining · 17.0 MH/s', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'paused', hash_total: 0, cards: [] } }), { text: 'Paused', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'syncing' } }), { text: 'Syncing the node', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'restarting' } }), { text: 'Node restarting', tone: 'bad' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 1, cards: [{ state: 'tuning' }] } }), { text: 'Tuning', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, setup_done: false, detecting: true }), { text: 'Detecting cards', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, quitting: true }), { text: 'Stopping', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'idle', hash_total: 0, cards: [] } }), { text: 'Not mining', tone: '' });
|
||||
});
|
||||
|
||||
test('Ember on the row (miner-ui-4): the flame fill, before -> after, the saving, the sparkline points, the plain label', () => {
|
||||
const curve = [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 0, power_pct: 60, watts: 228, mhs: 104.9, eff: 0.46, mark: 'hot' }, { clock_mhz: 2470, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }];
|
||||
const tuned = card({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 2470, sweep_pct: 80, sweep_at: NOW - 7200, sweep_watts: 290, sweep_mhs: 122.3, tune_curve: curve, sweep_state: 'idle', sweep_note: '' });
|
||||
const e = V.ember(tuned, settings(), NOW);
|
||||
assert.equal(e.kind, 'tuned'); assert.equal(e.fill, 1);
|
||||
assert.equal(e.label, 'Tuned 2 h ago');
|
||||
assert.match(e.sub, /^2470 MHz at 80% · next check /);
|
||||
assert.equal(e.before, 374); assert.equal(e.after, 290); assert.equal(e.savingW, 84);
|
||||
assert.equal(e.savingText, 'saves 84 W, 0.16% of rate');
|
||||
assert.equal(e.points.length, 4); assert.equal(e.points.filter((p) => p.chosen).length, 1); assert.equal(e.points.filter((p) => p.marked).length, 1);
|
||||
// Ember's own fields win over the curve (tune_before_watts / tune_before_mhs, step 0 of the last full plan)
|
||||
const withFields = V.ember(card({ ...tuned, tune_before_watts: 380, tune_before_mhs: 122.4, tune_source: 'confirm' }), settings(), NOW);
|
||||
assert.equal(withFields.before, 380); assert.equal(withFields.savingW, 90); assert.match(withFields.label, /from what other miners found$/);
|
||||
// a confirm plan without the fields claims no saving (its curve's first row is the prior, not an untuned point)
|
||||
assert.equal(V.ember(card({ ...tuned, tune_source: 'confirm' }), settings(), NOW).savingText, '');
|
||||
assert.equal(V.ember(card({ ...tuned, tune_before_watts: 0 }), settings(), NOW).savingText, '', 'zero means never measured');
|
||||
const running = V.ember(card({ state: 'tuning', tune_step: 4, tune_steps: 9, tune_eta_s: 410, sweep_state: 'running' }), settings(), NOW);
|
||||
assert.equal(running.kind, 'running'); assert.equal(running.fill.toFixed(2), '0.33'); assert.equal(running.label, 'Tuning: step 4 of 9'); assert.equal(running.sub, 'about 7 min left');
|
||||
const measured = V.ember(card({ tune_line: 'Measured: 122.3 MH/s at 410 W (0.298 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only until Power control is on in Settings (Windows asks for administrator rights once)', sweep_state: 'idle' }), settings({ power_control: false }), NOW);
|
||||
assert.equal(measured.kind, 'measured'); assert.equal(measured.label, 'Measured, not tuned'); assert.equal(measured.sub, 'Power control is off'); assert.equal(measured.fill, 0.5);
|
||||
const idle = V.ember(card({ sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0 }), settings(), NOW);
|
||||
assert.equal(idle.kind, 'idle'); assert.equal(idle.fill, 0); assert.equal(idle.label, 'Not tuned yet'); assert.equal(idle.sub, 'starts after 2 min of steady mining');
|
||||
const off = V.ember(card({ sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0 }), settings({ sweep: false }), NOW);
|
||||
assert.equal(off.label, 'Not tuned: Ember Tune is off'); assert.equal(off.sub, 'Tune starts one by hand');
|
||||
assert.equal(V.ember(card({ vendor: 'other', kind: 'integrated', sweep_supported: false }), settings(), NOW).kind, 'none');
|
||||
assert.equal(V.fleetSaving([tuned, card({ key: 'b', sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), 'Ember saves 84 W, £0.56 a day across 1 tuned card');
|
||||
assert.equal(V.fleetSaving([card({ sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), '');
|
||||
assert.deepEqual(V.beforeOf({ tune_before_watts: 0, tune_curve: curve }), null);
|
||||
});
|
||||
|
||||
test('the 0.3.16 engine fields (ember-tune fd03f35): the balance row, the fleet totals, the chain scene note', () => {
|
||||
assert.deepEqual(V.balanceWords({}, NOW), { text: '', sub: '', pounds: null }, 'an older engine has no balance field: the row stays hidden');
|
||||
const unread = V.balanceWords({ balance_wei: null, balance_age_s: -1, balance_note: '', price_gbp_per_ign: null }, NOW);
|
||||
assert.equal(unread.text, 'not read yet'); assert.equal(unread.pounds, null);
|
||||
const read = V.balanceWords({ balance_wei: '12345600000000000000', balance_age_s: 20, balance_note: '', price_gbp_per_ign: null }, NOW);
|
||||
assert.equal(read.text, '12.3456 IGN'); assert.equal(read.sub, 'in the wallet · read just now'); assert.equal(read.pounds, null);
|
||||
const priced = V.balanceWords({ balance_wei: '12345600000000000000', balance_age_s: 90, balance_note: '', price_gbp_per_ign: 0.5 }, NOW);
|
||||
assert.equal(priced.pounds.toFixed(2), '6.17'); assert.equal(priced.sub, 'in the wallet · read 1 min ago');
|
||||
assert.equal(V.balanceWords({ balance_wei: '5000000000000000000000', balance_age_s: 3, balance_note: 'the node answered 0x for the balance, not a quantity' }, NOW).sub, 'in the wallet · read just now · the node answered 0x for the balance, not a quantity');
|
||||
assert.equal(V.balanceWords({ balance_wei: '5000000000000000000000', balance_age_s: 3 }, NOW).text, '5,000 IGN');
|
||||
assert.equal(V.ign('1500000000000000000'), 1.5); assert.equal(V.ign('x'), null);
|
||||
const cards = [card(), card({ key: 'b', power_w: 100 })];
|
||||
assert.equal(V.fleetWatts({ watts_total: 0 }, cards), 510.2, 'no engine figure: the UI sums the cards');
|
||||
assert.equal(V.fleetWatts({ watts_total: 629 }, cards), 629, 'the engine figure wins');
|
||||
assert.equal(V.fleetPounds({ watts_total: 629, pounds_per_day: 4.23 }, cards, 28), 4.23);
|
||||
assert.equal(V.fleetPounds({ watts_total: 629, pounds_per_day: 0 }, cards, 28).toFixed(2), '4.23', 'no engine £: computed from the watts and the price');
|
||||
assert.equal(V.fleetPounds({}, cards, 0), null);
|
||||
assert.equal(V.liveNote({ ok: true, source: 'node', blocks: [] }), 'From your node. Your blocks are ringed.');
|
||||
assert.equal(V.liveNote({ ok: true, source: 'site', blocks: [] }), 'From Igneum’s observer. Your blocks are ringed.');
|
||||
assert.equal(V.liveNote({ ok: true, state: { source: 'site' } }), 'From Igneum’s observer. Your blocks are ringed.', 'the 0.3.15 engine carries the source under state');
|
||||
assert.equal(V.liveNote({ ok: false, pending: true, source: 'site' }), 'connecting');
|
||||
assert.equal(V.liveNote(null), '');
|
||||
const mineCards = [card({ ids: ['8fafda27', '1b2c3d4e'] })];
|
||||
assert.equal(V.isMine('8fafda27', mineCards), true, 'the site: the first 8 hex of the vote key hash');
|
||||
assert.equal(V.isMine('8fafda..e01f22', mineCards), true, 'the 0.3.16 engine: head6..tail6 of the same hash');
|
||||
assert.equal(V.isMine('8fafda27a9c1', mineCards), true, 'a longer prefix of the same hash');
|
||||
assert.equal(V.isMine('0x1B2C3D4E', mineCards), true);
|
||||
assert.equal(V.isMine('8fafdb..e01f22', mineCards), false, 'one hex off');
|
||||
assert.equal(V.isMine('deadbeef', mineCards), false);
|
||||
assert.equal(V.isMine('8faf', mineCards), false, 'under 6 hex never matches');
|
||||
assert.equal(V.isMine('', mineCards), false);
|
||||
});
|
||||
|
||||
test('N4 (6 October 2026): a frozen tip reads behind, never synced; the tip age is on the node line', () => {
|
||||
const n = (over) => ({ state: 'synced', blocks: 133000, headers: 133000, peers: 1, daa: 140000, last_reading_age_s: 4, tip_age_s: 3, message: '', restart_in_s: 0, ...over });
|
||||
const ok = V.nodeLine(n(), { severity: 'none' }, '');
|
||||
assert.equal(ok.text, 'Node synced · 1 peer · 133,000 blocks · last block 3 s ago'); assert.equal(ok.tone, 'ok');
|
||||
assert.match(ok.sub, /Last block 3 s ago/);
|
||||
const behind = V.nodeLine(n({ state: 'behind', synced: false, tip_age_s: 3180, sync_cause: 'frozen' }), { severity: 'none' }, '');
|
||||
assert.equal(behind.text, 'Node behind · 1 peer · 133,000 blocks · last block 53 min ago'); assert.equal(behind.tone, 'bad');
|
||||
assert.match(behind.sub, /^No new block for 53 min\. The chain may have moved on without this node\./);
|
||||
const none = V.nodeLine(n({ state: 'no peers', synced: false, peers: 0, tip_age_s: 500 }), { severity: 'none' }, '');
|
||||
assert.equal(none.text, 'No peers · 133,000 blocks'.replace('No peers', 'Node no peers')); assert.equal(none.tone, 'bad');
|
||||
assert.match(none.sub, /^No other node on our chain is connected/);
|
||||
// an older node without the age keeps the old words
|
||||
assert.equal(V.nodeLine(n({ tip_age_s: -1 }), { severity: 'none' }, '').text, 'Node synced · 1 peer · 133,000 blocks');
|
||||
assert.equal(V.tipAge(12), '12 s'); assert.equal(V.tipAge(240), '4 min'); assert.equal(V.tipAge(3180), '53 min'); assert.equal(V.tipAge(7300), '2 h');
|
||||
const base = { setup_done: true, quitting: false, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'behind' } };
|
||||
assert.deepEqual(V.pill(base), { text: 'Node behind', tone: 'bad' });
|
||||
assert.deepEqual(V.pill({ ...base, node: { state: 'no peers' } }), { text: 'No peers', tone: 'bad' });
|
||||
assert.equal(V.toggle({ state: 'waiting', paused: false, cards: [card({ state: 'waiting' })] }, { synced: false, state: 'behind' }, {}).sub, 'waiting: the node is behind the chain');
|
||||
});
|
||||
|
||||
test('finality paused (ember-tune 1357d280): the words, no lock called final while paused', () => {
|
||||
const p = V.finalityWords({ paused: true, last_lock: 412, age_s: 1200, line: 'Finality paused since 18:39 UTC: under two thirds of the weight is signing' });
|
||||
assert.equal(p.paused, true); assert.equal(p.age, 'paused'); assert.equal(p.note, 'Finality paused since 18:39 UTC: under two thirds of the weight is signing');
|
||||
assert.equal(/\bfinal\b/i.test(p.note), false);
|
||||
assert.equal(V.finalityWords({ paused: true, last_lock: 0, age_s: 0 }).note, 'Finality paused: under two thirds of the weight is signing');
|
||||
const ok = V.finalityWords({ paused: false, last_lock: 412, age_s: 90 });
|
||||
assert.equal(ok.paused, false); assert.equal(ok.age, '1 min ago'); assert.match(ok.note, /never be undone/);
|
||||
const none = V.finalityWords({ paused: false, last_lock: 0, message: 'waiting for the miner' });
|
||||
assert.equal(none.age, 'n/a'); assert.equal(none.note, 'waiting for the miner');
|
||||
// N5: a node built without the mining engine is a fault
|
||||
assert.equal(V.nodeWords({ state: 'stub', blocks: 0, headers: 0, peers: 0 }, { severity: 'none' }, '').word, 'stub engine');
|
||||
assert.deepEqual(V.pill({ setup_done: true, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'stub' } }), { text: 'Node fault', tone: 'bad' });
|
||||
});
|
||||
|
||||
test('whose node (publish 2): the app’s own, another node on this machine checked or unchecked, or none when the ports are taken by other rules', () => {
|
||||
assert.deepEqual(V.nodeSource({ source: 'app' }), { text: 'this app’s own node', sub: '' });
|
||||
assert.deepEqual(V.nodeSource({ source: 'external', rules_check: 'match' }), { text: 'another node on this machine', sub: 'same network and rules, checked' });
|
||||
assert.deepEqual(V.nodeSource({ source: 'external', rules_check: 'unknown' }), { text: 'another node on this machine', sub: 'its rules could not be checked (it predates 0.3.17)' });
|
||||
assert.deepEqual(V.nodeSource({ source: 'none', port_note: 'Node not started: port 26611 is taken by a node on another network (chain id 7778, ours 7777)' }), { text: 'no node', sub: 'Node not started: port 26611 is taken by a node on another network (chain id 7778, ours 7777)' });
|
||||
assert.deepEqual(V.nodeSource({}), { text: '', sub: '' });
|
||||
// 7 October 2026: the mode is re-decided; after the other node leaves for 60 s the app runs its own and says why
|
||||
assert.deepEqual(V.nodeSource({ source: 'app', mode: 'own', mode_reason: 'the node this app was reading left port 26611 for 60 s, so the app started its own node' }), { text: 'this app’s own node', sub: 'the node this app was reading left port 26611 for 60 s, so the app started its own node' });
|
||||
const gone = V.nodeWords({ state: 'stopped', source: 'external', message: 'the other node on port 26611 went away; this app starts its own node in 45 s unless it comes back', blocks: 0, headers: 0, peers: 0 }, { severity: 'none' }, '');
|
||||
assert.equal(gone.word, 'stopped'); assert.match(gone.line, /starts its own node in 45 s/);
|
||||
const taken = V.nodeWords({ state: 'stopped', source: 'none', message: 'Node not started: port 26611 is taken by a node on other rules (fees_v1_activation_daa 200000, ours 210000)', blocks: 0, headers: 0, peers: 0 }, { severity: 'none' }, '');
|
||||
assert.equal(taken.word, 'not started'); assert.equal(taken.tone, 'bad'); assert.match(taken.line, /^Node not started: port 26611 is taken/);
|
||||
assert.deepEqual(V.pill({ setup_done: true, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'stopped', source: 'none' } }), { text: 'Node not started', tone: 'bad' });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@ final class App: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDele
|
|||
}
|
||||
} else if line.hasPrefix("STATE ") {
|
||||
applyState(String(line.dropFirst(6)))
|
||||
} else if line.hasPrefix("SHOT ") {
|
||||
// Miner UI 4 (b): the engine's POST /api/shot asks for a PNG of what the window shows, at this path
|
||||
shotTo(String(line.dropFirst(5)).trimmingCharacters(in: .whitespaces))
|
||||
} else if line.hasPrefix("FATAL ") {
|
||||
fail(String(line.dropFirst(6)))
|
||||
} else if line == "EXIT" {
|
||||
|
|
@ -322,6 +325,19 @@ final class App: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDele
|
|||
let a = NSAlert(); a.messageText = "Igneum Miner"; a.informativeText = message; a.runModal(); completionHandler()
|
||||
}
|
||||
|
||||
// ---- a screenshot on request (the SHOT line), the app keeps running ----
|
||||
func shotTo(_ path: String) {
|
||||
let conf = WKSnapshotConfiguration()
|
||||
conf.rect = web.bounds
|
||||
web.takeSnapshot(with: conf) { image, error in
|
||||
guard let img = image, let tiff = img.tiffRepresentation, let rep = NSBitmapImageRep(data: tiff), let png = rep.representation(using: .png, properties: [:]) else {
|
||||
FileHandle.standardError.write("shot failed: \(error?.localizedDescription ?? "no image")\n".data(using: .utf8)!)
|
||||
return
|
||||
}
|
||||
do { try png.write(to: URL(fileURLWithPath: path)) } catch { FileHandle.standardError.write("shot: could not write \(path): \(error)\n".data(using: .utf8)!) }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- snapshot ----
|
||||
func snapshot(to path: String) {
|
||||
if let js = ProcessInfo.processInfo.environment["IGNEUM_PROBE"] {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
#endif
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlwapi.h>
|
||||
#include <dbt.h>
|
||||
#include <wrl.h>
|
||||
#include <string>
|
||||
|
|
@ -39,6 +40,13 @@ static HWND g_hwnd = nullptr;
|
|||
static HANDLE g_engine = nullptr, g_engineIn = nullptr, g_engineOut = nullptr;
|
||||
static ComPtr<ICoreWebView2Controller> g_controller;
|
||||
static ComPtr<ICoreWebView2> g_webview;
|
||||
// a PNG of the web view at `path` (the SHOT line); the stream is released by the completion handler
|
||||
static void capturePreview(const std::wstring& path) {
|
||||
if (!g_webview) return;
|
||||
ComPtr<IStream> stream;
|
||||
if (FAILED(SHCreateStreamOnFileEx(path.c_str(), STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE, FILE_ATTRIBUTE_NORMAL, TRUE, nullptr, &stream))) return;
|
||||
g_webview->CapturePreview(COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG, stream.Get(), Callback<ICoreWebView2CapturePreviewCompletedHandler>([stream](HRESULT) -> HRESULT { return S_OK; }).Get());
|
||||
}
|
||||
static std::wstring g_url, g_status = L"starting the engine";
|
||||
static bool g_paused = false, g_quitting = false, g_exited = false, g_webviewOk = false, g_hintShown = false;
|
||||
static NOTIFYICONDATAW g_nid = {};
|
||||
|
|
@ -308,6 +316,9 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||
applyState(line->substr(6));
|
||||
} else if (line->rfind("ELEVATE ", 0) == 0) {
|
||||
runElevated(widen(line->substr(8)));
|
||||
} else if (line->rfind("SHOT ", 0) == 0) {
|
||||
// Miner UI 4 (b): the engine's POST /api/shot asks for a PNG of what the window shows, at this path
|
||||
capturePreview(widen(line->substr(5)));
|
||||
} else if (line->rfind("FATAL ", 0) == 0) {
|
||||
g_status = widen(line->substr(6));
|
||||
repaintStatus();
|
||||
|
|
|
|||
|
|
@ -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.14"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,14,0
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.17"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,17,0
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -2544,6 +2544,35 @@ task now runs ...Programs\Igneum Miner\igneum-app.exe --power-helper"), then the
|
|||
task with no prompt ("-pl 460: set to 460.00 W from 575.00 W", "-pl 160: set to 160.00 W from 100.00 W"). Task
|
||||
Running, Highest, user Admin. Cards: 5090 221 W at 1845 MHz, 4070 75.8 W at 1860 MHz, 9070 XT 202 W, all mining
|
||||
through the installed app. Ember closed 16:53Z.
|
||||
### 6 October 2026, 17:55Z to 17:57Z, the first segment above the restart cut from a snapshot-restored node on the new export (Mac)
|
||||
|
||||
A copy of node 1's data dir started on the shipper's 0.3.14 Mac binary (target-0314, release-0.3.14-node 4c6b129d) with
|
||||
`--igneum-exec-snapshot=node1-copy-snapshot.bin,0xac101f13...` (the hands' recovery file, tip 130,272) and node 1 as its
|
||||
peer: IBD of 8,049 blocks in 29 s, the executor resumed from the file and reached the tip 140,023 with no restart replay.
|
||||
`igneum_exportSegments [139951, 139959]` answered in under 0.1 s with 9 segments, `preState` 79 accounts, `execRestart`
|
||||
27,276. The branch exporter (exec-app-0314 bfebff1) cut all 8 blocks of segment 139,952..139,959 in under 0.1 s each:
|
||||
"replayed 8 segments from the account dump; every state root equals the node's". The first block's fixture carries
|
||||
pre-state root 0x64304441... and post-state root 0x60d60bba..., equal to node 1's eth_getBlockByNumber roots at 139,951
|
||||
and 139,952 (an independent node). Consequence: a prover on a snapshot-restored node (every hand, nine fleet boxes) cuts a
|
||||
provable fixture again with the 0.3.14 app and exporter; what is left for a paid record is the proof itself (the PCs or
|
||||
the fleet, not this Mac).
|
||||
|
||||
### 6 October 2026, 18:11Z to 18:13Z, the PC shape over p2p (Mac, `tools/lock/with-lock.sh run`)
|
||||
|
||||
Fork 1255c0f9. `node tools/exec-sync/net.mjs`: 18/18 in 117.5 s. Case 7: a node whose data dir holds the tip-0 pair the 0.3.13
|
||||
genesis replay left behind, with `--igneum-exec-snapshot=peer`, sets the pair aside (.bin.tip0), asks A over p2p and loads
|
||||
A's state with no hand action; its state root at 60 equals A's. `reorg.mjs` 18/18 in 262.1 s on the same binary; the exec
|
||||
suite 20. Consequence: a PC or a box left with the tip-0 pair recovers on its own once a hand serves (the 5-minute
|
||||
no-progress rule fires the ask even when the error text is new); until 0.3.14.1 the recovery file by hand is the way.
|
||||
|
||||
## 6 October 2026, 18:18Z: the shipped 0.3.14 app asked to tune itself on PC 1 (ember-installed-pc1-1): FAILED, no tune ran
|
||||
|
||||
Gate ok (0.3.14, Power control on, the task on the installed exe). Every card's "after" was its 17:55Z failure from
|
||||
the app's own first tune: 5090 and 4070 "did not take within 30 s (card reports 460 / 160 W, acknowledged true)",
|
||||
9070 XT "the card refused the setting"; the three "Tune now" requests sat queued behind the hour's back-off. Five
|
||||
causes (A to E) and their fixes in the plan's 0.3.14 window section; the window repeats on 0.3.15. The cards kept
|
||||
mining at the driver's held locks: 5090 222.9 W at 1845 MHz, 4070 75.9 W at 1860 MHz, 9070 XT 201 W. Lever-2 TUNING
|
||||
records during the window: 5090 135.6 MH/s at 236 W (0.574 MH/W), 4070 31.1 MH/s at 79 W (0.393).
|
||||
|
||||
## Prover tiers on real cards: the rented fleet, 6 October 2026 (branch gpu-fleet)
|
||||
|
||||
|
|
@ -2597,3 +2626,46 @@ USD 20 an hour on community pods, against a devnet of 1.16 GH/s.
|
|||
Consequence: the devnet's hash is rentable for the price of a dinner, so nothing on it is a security result; the counter-ASIC and
|
||||
finality work is tested there for correctness, not for cost. The cost argument only starts at the TH/s scale, where the rental
|
||||
market's supply (not its price) is the limit, and that number belongs in the litepaper with this caveat.
|
||||
### 6 October 2026, 20:38Z to 21:03Z, proof verification on the consensus path (fork exec-sync-0313 da2d17ec)
|
||||
|
||||
The in-process SP1 verifier (sp1-sdk LightProver, the embedded keys) on a compressed shard proof of
|
||||
block-58927-empty-reward (1,272,897 B), one core, including the LightProver setup each call:
|
||||
|
||||
| Machine | Profile | Verify | Command |
|
||||
|---|---|---|---|
|
||||
| M5 Max (this Mac) | release | 0.204 s, 0.232 s (two runs) | `cargo test --release -p igneum-exec nativeverify -- --nocapture` with the fixture |
|
||||
| M5 Max | debug | 1.226 s | the same without `--release` |
|
||||
| igneum-build-1 (Linux, the build box) | release | 0.400 s | `tools/build-remote.sh --no-fetch -- test --release -p igneum-exec nativeverify -- --nocapture` |
|
||||
| EPYC 7K62, 2019 Zen 2 (rz-4090, earlier today) | release, through the host | 0.53 s | bench/proof-systems rows |
|
||||
|
||||
`node tools/exec-sync/forged.mjs` (fast-time simnet, `proving_consensus_verify_daa` 0 under the embedded ids): 8/8 in
|
||||
23.6 s. The attacker A (`IGNEUM_TEST_SKIP_PROOF_RULE=1`, Trust pool) carried a forged shard record at block 27; B asked
|
||||
A for the proof (22:03:31.270 local), held it 1 ms later, refused the block at 31.522 ("the proof bytes are not a bincode
|
||||
SP1 proof"; the verify itself 0.000 s), never included it and paid nothing; before the forgery B followed A block for
|
||||
block. An unmodified A refused its own carrying block ("block is known to be invalid", 20:45Z run).
|
||||
|
||||
Consequence per tier: the rule costs an honest node nothing on the hot path (a proof verified at relay time is a
|
||||
cache hit); a cold proof costs a 2019 core 0.4 to 0.5 s and a current one 0.2 s, in parallel per record; a forger
|
||||
loses its block and its peer. Until 0.3.17.1's finality-horizon skip, a fresh joiner fetches every carried proof in
|
||||
its IBD window from its syncer (1.27 MB a record), so a syncer must still hold them.
|
||||
|
||||
### 6 October 2026, 22:10Z to 22:28Z, Devnet 2: the zero program-id class and its close (the fleet's reads)
|
||||
|
||||
Devnet 2 (igneum-devnet-2, five nodes on 4c6b129d, 1 block/s, every switch at DAA 0): the nodes ran without a verifier
|
||||
host or IGNEUM_PROOF_PROGRAM_IDS, so their expected segment statements carried zero program ids at hex offset 472 and
|
||||
every segment record from the shipped host was refused while 8 of 8 shards passed (seg 2868: chain proof 1.27 MB in
|
||||
281 s on a 4090). Fix on 4c6b129d: IGNEUM_PROOF_PROGRAM_IDS=0x2b1a81cb...,0x474678f3... on each node process, restart
|
||||
22:10Z to 22:16Z, same genesis. Result: paidSegments 2 on every node at 22:22:28Z and 4 at 22:28:10Z (two provers,
|
||||
8-block segments), 0 PoW rejected, one exec state root on all five at height 3,792 (0xab19a0be...), one version
|
||||
(2.1.0-1279a1d6, digest 4a0b8726). On the 0.3.17 node (fork a344fea3) the override object's proving_shard_program_id
|
||||
and proving_aggregator_id are the statement's ids when set, so the env is not needed. Consequence per tier: a node
|
||||
without a host (a pool hand, a seed, a home node that does not prove) must carry the ids in its object or its env, or
|
||||
it refuses every segment record and pays no aggregator; the 0.3.17 object carries them.
|
||||
## 6 October 2026, 22:16Z: the shipped 0.3.16 app asked to tune itself on PC 1 (ember-installed-pc1-2): A to E held, no climb (cause F)
|
||||
|
||||
Gate ok (0.3.16, Power control on, task on the installed exe, Running at the end), prompts 0, the helper up once per
|
||||
NVIDIA card. RX 9070 XT measured as it runs: 19.2 MH/s at 202 W (0.095 MH/W), no set sent (measure only: the probe
|
||||
gave no tune line; open). RTX 5090 and 4070 climbs stopped at request 1: "the helper did not run sequence 1 within
|
||||
15 s"; cmd.txt held "00 dev 1 / 01 pl 160 / 02 rgc / 03 rmc", two-digit wire numbers below the cap path's unix-based
|
||||
ones, so the helper skipped them as stale (cause F, fixed cbd4d51 for 0.3.17). Draws unchanged: 5090 208 W at
|
||||
1845 MHz, 4070 75.5 W at 1860 MHz, 9070 XT 203 W.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 362.8 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x173, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 365.3 ms
|
||||
shadow: 256 instructions x 173 passes per iteration, 354304 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.445 ms, 4096 items derived, lane0 74dbb9c7808b6c12 lane31 14a606e8cc2988ee
|
||||
warp base 4096: single cold run 7.241 ms, 4096 items derived, lane0 361270e067803cb3 lane31 979c376d679d74b2
|
||||
warp base 1000000: single cold run 7.106 ms, 4096 items derived, lane0 173be6655be48c63 lane31 697bcf4f945c5bf9
|
||||
CPU verify: 7.004 ms per 32-lane warp, avg of 50 (checksum e9371a9db9fc1817)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 363.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x267, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 372.7 ms
|
||||
shadow: 256 instructions x 267 passes per iteration, 546816 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.905 ms, 4096 items derived, lane0 14e8e692f6f1f094 lane31 a25fb8045d382322
|
||||
warp base 4096: single cold run 8.731 ms, 4096 items derived, lane0 eed0185d04e6d213 lane31 6b9af2c86bf9db03
|
||||
warp base 1000000: single cold run 8.723 ms, 4096 items derived, lane0 a38b5590afcba174 lane31 39664cb2e742eb67
|
||||
CPU verify: 8.714 ms per 32-lane warp, avg of 50 (checksum a30f9fb7574d0db3)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 368.9 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 365.5 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.089 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370
|
||||
warp base 4096: single cold run 4.944 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055
|
||||
warp base 1000000: single cold run 4.935 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62
|
||||
CPU verify: 4.934 ms per 32-lane warp, avg of 50 (checksum 17e36e7905b81375)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.2 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x35, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 368.1 ms
|
||||
shadow: 256 instructions x 35 passes per iteration, 71680 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.201 ms, 4096 items derived, lane0 177dd71be37996bc lane31 792114b1b40ad79f
|
||||
warp base 4096: single cold run 5.057 ms, 4096 items derived, lane0 fe6701666e2e483a lane31 fc2105cb89fcb8b7
|
||||
warp base 1000000: single cold run 5.059 ms, 4096 items derived, lane0 6401cfb6e9ddfed6 lane31 89445da95a2e7184
|
||||
CPU verify: 5.067 ms per 32-lane warp, avg of 50 (checksum 4632b2b31b01b311)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 363.5 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x53, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 366.6 ms
|
||||
shadow: 256 instructions x 53 passes per iteration, 108544 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.512 ms, 4096 items derived, lane0 9771dab715d71261 lane31 525676cf580b6f11
|
||||
warp base 4096: single cold run 5.350 ms, 4096 items derived, lane0 1afa4ca3c965c3f5 lane31 4e238e93b5f0101d
|
||||
warp base 1000000: single cold run 5.342 ms, 4096 items derived, lane0 21a07085d7c0bd82 lane31 39fd0197590efb2a
|
||||
CPU verify: 5.342 ms per 32-lane warp, avg of 50 (checksum 2efdcc71de21f16f)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x88, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 365.0 ms
|
||||
shadow: 256 instructions x 88 passes per iteration, 180224 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 6.044 ms, 4096 items derived, lane0 84beec0429c7ef83 lane31 334b204a4a68c30d
|
||||
warp base 4096: single cold run 5.887 ms, 4096 items derived, lane0 32be0f5b7d992255 lane31 2c8dbd443403bc84
|
||||
warp base 1000000: single cold run 5.887 ms, 4096 items derived, lane0 78541813d6547a59 lane31 fd7fb2e05bbc316f
|
||||
CPU verify: 5.900 ms per 32-lane warp, avg of 50 (checksum 77189c69c05913b5)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 542.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x173, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 486.8 ms
|
||||
shadow: 256 instructions x 173 passes per iteration, 354304 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.386 ms, 4096 items derived, lane0 74dbb9c7808b6c12 lane31 14a606e8cc2988ee
|
||||
warp base 4096: single cold run 7.244 ms, 4096 items derived, lane0 361270e067803cb3 lane31 979c376d679d74b2
|
||||
warp base 1000000: single cold run 7.254 ms, 4096 items derived, lane0 173be6655be48c63 lane31 697bcf4f945c5bf9
|
||||
CPU verify: 7.231 ms per 32-lane warp, avg of 50 (checksum e9371a9db9fc1817)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 541.5 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x267, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 525.0 ms
|
||||
shadow: 256 instructions x 267 passes per iteration, 546816 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.834 ms, 4096 items derived, lane0 14e8e692f6f1f094 lane31 a25fb8045d382322
|
||||
warp base 4096: single cold run 8.701 ms, 4096 items derived, lane0 eed0185d04e6d213 lane31 6b9af2c86bf9db03
|
||||
warp base 1000000: single cold run 8.717 ms, 4096 items derived, lane0 a38b5590afcba174 lane31 39664cb2e742eb67
|
||||
CPU verify: 8.712 ms per 32-lane warp, avg of 50 (checksum a30f9fb7574d0db3)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 540.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 426.2 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.078 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370
|
||||
warp base 4096: single cold run 4.936 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055
|
||||
warp base 1000000: single cold run 4.928 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62
|
||||
CPU verify: 4.931 ms per 32-lane warp, avg of 50 (checksum 17e36e7905b81375)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 542.5 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x35, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 429.9 ms
|
||||
shadow: 256 instructions x 35 passes per iteration, 71680 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.201 ms, 4096 items derived, lane0 177dd71be37996bc lane31 792114b1b40ad79f
|
||||
warp base 4096: single cold run 5.070 ms, 4096 items derived, lane0 fe6701666e2e483a lane31 fc2105cb89fcb8b7
|
||||
warp base 1000000: single cold run 5.062 ms, 4096 items derived, lane0 6401cfb6e9ddfed6 lane31 89445da95a2e7184
|
||||
CPU verify: 5.055 ms per 32-lane warp, avg of 50 (checksum 4632b2b31b01b311)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 542.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x53, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 436.9 ms
|
||||
shadow: 256 instructions x 53 passes per iteration, 108544 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.497 ms, 4096 items derived, lane0 9771dab715d71261 lane31 525676cf580b6f11
|
||||
warp base 4096: single cold run 5.360 ms, 4096 items derived, lane0 1afa4ca3c965c3f5 lane31 4e238e93b5f0101d
|
||||
warp base 1000000: single cold run 5.359 ms, 4096 items derived, lane0 21a07085d7c0bd82 lane31 39fd0197590efb2a
|
||||
CPU verify: 5.350 ms per 32-lane warp, avg of 50 (checksum 2efdcc71de21f16f)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 539.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x88, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 450.4 ms
|
||||
shadow: 256 instructions x 88 passes per iteration, 180224 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 6.066 ms, 4096 items derived, lane0 84beec0429c7ef83 lane31 334b204a4a68c30d
|
||||
warp base 4096: single cold run 5.881 ms, 4096 items derived, lane0 32be0f5b7d992255 lane31 2c8dbd443403bc84
|
||||
warp base 1000000: single cold run 5.884 ms, 4096 items derived, lane0 78541813d6547a59 lane31 fd7fb2e05bbc316f
|
||||
CPU verify: 5.829 ms per 32-lane warp, avg of 50 (checksum 77189c69c05913b5)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
host igneum-build-1 load 26.61 13.24 8.88 freq40 1519940 kHz siblings 40,88 pow 3f7623f208335f5b
|
||||
load after 19.77 12.55 8.74
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x173, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 547.4 ms
|
||||
shadow: 256 instructions x 173 passes per iteration, 354304 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 9.918 ms, 4096 items derived, lane0 74dbb9c7808b6c12 lane31 14a606e8cc2988ee
|
||||
warp base 4096: single cold run 9.703 ms, 4096 items derived, lane0 361270e067803cb3 lane31 979c376d679d74b2
|
||||
warp base 1000000: single cold run 9.691 ms, 4096 items derived, lane0 173be6655be48c63 lane31 697bcf4f945c5bf9
|
||||
CPU verify: 10.870 ms per 32-lane warp, avg of 50 (checksum e9371a9db9fc1817)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.6 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x267, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 545.8 ms
|
||||
shadow: 256 instructions x 267 passes per iteration, 546816 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 11.728 ms, 4096 items derived, lane0 14e8e692f6f1f094 lane31 a25fb8045d382322
|
||||
warp base 4096: single cold run 11.491 ms, 4096 items derived, lane0 eed0185d04e6d213 lane31 6b9af2c86bf9db03
|
||||
warp base 1000000: single cold run 11.493 ms, 4096 items derived, lane0 a38b5590afcba174 lane31 39664cb2e742eb67
|
||||
CPU verify: 13.125 ms per 32-lane warp, avg of 50 (checksum a30f9fb7574d0db3)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 365.9 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 545.3 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.146 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370
|
||||
warp base 4096: single cold run 6.915 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055
|
||||
warp base 1000000: single cold run 6.897 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62
|
||||
CPU verify: 7.421 ms per 32-lane warp, avg of 50 (checksum 17e36e7905b81375)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x35, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 547.5 ms
|
||||
shadow: 256 instructions x 35 passes per iteration, 71680 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.291 ms, 4096 items derived, lane0 177dd71be37996bc lane31 792114b1b40ad79f
|
||||
warp base 4096: single cold run 7.057 ms, 4096 items derived, lane0 fe6701666e2e483a lane31 fc2105cb89fcb8b7
|
||||
warp base 1000000: single cold run 7.070 ms, 4096 items derived, lane0 6401cfb6e9ddfed6 lane31 89445da95a2e7184
|
||||
CPU verify: 7.587 ms per 32-lane warp, avg of 50 (checksum 4632b2b31b01b311)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x53, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 547.1 ms
|
||||
shadow: 256 instructions x 53 passes per iteration, 108544 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.627 ms, 4096 items derived, lane0 9771dab715d71261 lane31 525676cf580b6f11
|
||||
warp base 4096: single cold run 7.404 ms, 4096 items derived, lane0 1afa4ca3c965c3f5 lane31 4e238e93b5f0101d
|
||||
warp base 1000000: single cold run 7.380 ms, 4096 items derived, lane0 21a07085d7c0bd82 lane31 39fd0197590efb2a
|
||||
CPU verify: 8.023 ms per 32-lane warp, avg of 50 (checksum 2efdcc71de21f16f)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x88, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 544.8 ms
|
||||
shadow: 256 instructions x 88 passes per iteration, 180224 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.290 ms, 4096 items derived, lane0 84beec0429c7ef83 lane31 334b204a4a68c30d
|
||||
warp base 4096: single cold run 8.085 ms, 4096 items derived, lane0 32be0f5b7d992255 lane31 2c8dbd443403bc84
|
||||
warp base 1000000: single cold run 8.051 ms, 4096 items derived, lane0 78541813d6547a59 lane31 fd7fb2e05bbc316f
|
||||
CPU verify: 8.846 ms per 32-lane warp, avg of 50 (checksum 77189c69c05913b5)
|
||||
10
docs/design/latency-ladder-bench/20261006T220642Z/table.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
host igneum-build-1 load 26.61 13.24 8.88 freq40 1519940 kHz siblings 40,88 pow 3f7623f208335f5b
|
||||
load after 19.77 12.55 8.74
|
||||
| Rung | reps | Shadow instrs per hash | Counted ops (approx) | Cold, core alone (ms) | Avg of 50, alone (ms) | Cold, sibling loaded (ms) | Avg of 50, sibling loaded (ms) | Under 10 ms loaded |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 0 | 27 | 55296 | 102,122 | 5.09 | 4.93 | 5.08 | 4.93 | yes |
|
||||
| 1 | 35 | 71680 | 132,105 | 5.20 | 5.07 | 5.20 | 5.05 | yes |
|
||||
| 2 | 53 | 108544 | 199,566 | 5.51 | 5.34 | 5.50 | 5.35 | yes |
|
||||
| 3 | 88 | 180224 | 330,740 | 6.04 | 5.90 | 6.07 | 5.83 | yes |
|
||||
| 4 | 173 | 354304 | 649,307 | 7.45 | 7.00 | 7.39 | 7.23 | yes |
|
||||
| 5 | 267 | 546816 | 1,001,604 | 8.90 | 8.71 | 8.83 | 8.71 | yes |
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 363.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x173, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 365.6 ms
|
||||
shadow: 256 instructions x 173 passes per iteration, 354304 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.380 ms, 4096 items derived, lane0 74dbb9c7808b6c12 lane31 14a606e8cc2988ee
|
||||
warp base 4096: single cold run 7.232 ms, 4096 items derived, lane0 361270e067803cb3 lane31 979c376d679d74b2
|
||||
warp base 1000000: single cold run 7.230 ms, 4096 items derived, lane0 173be6655be48c63 lane31 697bcf4f945c5bf9
|
||||
CPU verify: 7.233 ms per 32-lane warp, avg of 50 (checksum e9371a9db9fc1817)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 364.0 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x267, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 367.0 ms
|
||||
shadow: 256 instructions x 267 passes per iteration, 546816 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.881 ms, 4096 items derived, lane0 14e8e692f6f1f094 lane31 a25fb8045d382322
|
||||
warp base 4096: single cold run 8.723 ms, 4096 items derived, lane0 eed0185d04e6d213 lane31 6b9af2c86bf9db03
|
||||
warp base 1000000: single cold run 8.707 ms, 4096 items derived, lane0 a38b5590afcba174 lane31 39664cb2e742eb67
|
||||
CPU verify: 8.499 ms per 32-lane warp, avg of 50 (checksum a30f9fb7574d0db3)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 460.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 368.6 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.141 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370
|
||||
warp base 4096: single cold run 4.954 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055
|
||||
warp base 1000000: single cold run 4.969 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62
|
||||
CPU verify: 4.946 ms per 32-lane warp, avg of 50 (checksum 17e36e7905b81375)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 456.9 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x35, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 437.3 ms
|
||||
shadow: 256 instructions x 35 passes per iteration, 71680 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.571 ms, 4096 items derived, lane0 177dd71be37996bc lane31 792114b1b40ad79f
|
||||
warp base 4096: single cold run 5.327 ms, 4096 items derived, lane0 fe6701666e2e483a lane31 fc2105cb89fcb8b7
|
||||
warp base 1000000: single cold run 5.203 ms, 4096 items derived, lane0 6401cfb6e9ddfed6 lane31 89445da95a2e7184
|
||||
CPU verify: 5.521 ms per 32-lane warp, avg of 50 (checksum 4632b2b31b01b311)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 368.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x53, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 364.8 ms
|
||||
shadow: 256 instructions x 53 passes per iteration, 108544 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.569 ms, 4096 items derived, lane0 9771dab715d71261 lane31 525676cf580b6f11
|
||||
warp base 4096: single cold run 5.352 ms, 4096 items derived, lane0 1afa4ca3c965c3f5 lane31 4e238e93b5f0101d
|
||||
warp base 1000000: single cold run 5.366 ms, 4096 items derived, lane0 21a07085d7c0bd82 lane31 39fd0197590efb2a
|
||||
CPU verify: 5.351 ms per 32-lane warp, avg of 50 (checksum 2efdcc71de21f16f)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 360.9 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x88, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 361.8 ms
|
||||
shadow: 256 instructions x 88 passes per iteration, 180224 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 6.073 ms, 4096 items derived, lane0 84beec0429c7ef83 lane31 334b204a4a68c30d
|
||||
warp base 4096: single cold run 5.900 ms, 4096 items derived, lane0 32be0f5b7d992255 lane31 2c8dbd443403bc84
|
||||
warp base 1000000: single cold run 5.883 ms, 4096 items derived, lane0 78541813d6547a59 lane31 fd7fb2e05bbc316f
|
||||
CPU verify: 5.892 ms per 32-lane warp, avg of 50 (checksum 77189c69c05913b5)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 548.6 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x173, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 551.8 ms
|
||||
shadow: 256 instructions x 173 passes per iteration, 354304 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 12.383 ms, 4096 items derived, lane0 74dbb9c7808b6c12 lane31 14a606e8cc2988ee
|
||||
warp base 4096: single cold run 11.970 ms, 4096 items derived, lane0 361270e067803cb3 lane31 979c376d679d74b2
|
||||
warp base 1000000: single cold run 12.177 ms, 4096 items derived, lane0 173be6655be48c63 lane31 697bcf4f945c5bf9
|
||||
CPU verify: 12.091 ms per 32-lane warp, avg of 50 (checksum e9371a9db9fc1817)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 548.8 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x267, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 556.3 ms
|
||||
shadow: 256 instructions x 267 passes per iteration, 546816 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 14.959 ms, 4096 items derived, lane0 14e8e692f6f1f094 lane31 a25fb8045d382322
|
||||
warp base 4096: single cold run 14.619 ms, 4096 items derived, lane0 eed0185d04e6d213 lane31 6b9af2c86bf9db03
|
||||
warp base 1000000: single cold run 14.437 ms, 4096 items derived, lane0 a38b5590afcba174 lane31 39664cb2e742eb67
|
||||
CPU verify: 14.580 ms per 32-lane warp, avg of 50 (checksum a30f9fb7574d0db3)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 551.2 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 561.9 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.769 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370
|
||||
warp base 4096: single cold run 8.503 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055
|
||||
warp base 1000000: single cold run 8.469 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62
|
||||
CPU verify: 8.674 ms per 32-lane warp, avg of 50 (checksum 17e36e7905b81375)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 549.4 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x35, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 554.8 ms
|
||||
shadow: 256 instructions x 35 passes per iteration, 71680 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.867 ms, 4096 items derived, lane0 177dd71be37996bc lane31 792114b1b40ad79f
|
||||
warp base 4096: single cold run 8.505 ms, 4096 items derived, lane0 fe6701666e2e483a lane31 fc2105cb89fcb8b7
|
||||
warp base 1000000: single cold run 8.517 ms, 4096 items derived, lane0 6401cfb6e9ddfed6 lane31 89445da95a2e7184
|
||||
CPU verify: 8.510 ms per 32-lane warp, avg of 50 (checksum 4632b2b31b01b311)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 541.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x53, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 549.4 ms
|
||||
shadow: 256 instructions x 53 passes per iteration, 108544 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 9.233 ms, 4096 items derived, lane0 9771dab715d71261 lane31 525676cf580b6f11
|
||||
warp base 4096: single cold run 8.869 ms, 4096 items derived, lane0 1afa4ca3c965c3f5 lane31 4e238e93b5f0101d
|
||||
warp base 1000000: single cold run 8.943 ms, 4096 items derived, lane0 21a07085d7c0bd82 lane31 39fd0197590efb2a
|
||||
CPU verify: 8.908 ms per 32-lane warp, avg of 50 (checksum 2efdcc71de21f16f)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 542.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x88, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 556.0 ms
|
||||
shadow: 256 instructions x 88 passes per iteration, 180224 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 10.081 ms, 4096 items derived, lane0 84beec0429c7ef83 lane31 334b204a4a68c30d
|
||||
warp base 4096: single cold run 9.845 ms, 4096 items derived, lane0 32be0f5b7d992255 lane31 2c8dbd443403bc84
|
||||
warp base 1000000: single cold run 9.944 ms, 4096 items derived, lane0 78541813d6547a59 lane31 fd7fb2e05bbc316f
|
||||
CPU verify: 9.842 ms per 32-lane warp, avg of 50 (checksum 77189c69c05913b5)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
host igneum-build-1 load 25.39 17.85 11.63 freq40 1500000 kHz siblings 40,88 pow 3f7623f208335f5b
|
||||
load after 23.60 18.46 12.05
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 363.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x173, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 364.9 ms
|
||||
shadow: 256 instructions x 173 passes per iteration, 354304 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 7.394 ms, 4096 items derived, lane0 74dbb9c7808b6c12 lane31 14a606e8cc2988ee
|
||||
warp base 4096: single cold run 7.241 ms, 4096 items derived, lane0 361270e067803cb3 lane31 979c376d679d74b2
|
||||
warp base 1000000: single cold run 7.269 ms, 4096 items derived, lane0 173be6655be48c63 lane31 697bcf4f945c5bf9
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 365.1 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x267, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 372.0 ms
|
||||
shadow: 256 instructions x 267 passes per iteration, 546816 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 8.930 ms, 4096 items derived, lane0 14e8e692f6f1f094 lane31 a25fb8045d382322
|
||||
warp base 4096: single cold run 8.716 ms, 4096 items derived, lane0 eed0185d04e6d213 lane31 6b9af2c86bf9db03
|
||||
warp base 1000000: single cold run 8.762 ms, 4096 items derived, lane0 a38b5590afcba174 lane31 39664cb2e742eb67
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 369.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x27, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 370.0 ms
|
||||
shadow: 256 instructions x 27 passes per iteration, 55296 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.129 ms, 4096 items derived, lane0 2576769ee4a14c8d lane31 c58ddcb717dd3370
|
||||
warp base 4096: single cold run 4.944 ms, 4096 items derived, lane0 1ce77a600ec573b4 lane31 03600a05ffba0055
|
||||
warp base 1000000: single cold run 4.947 ms, 4096 items derived, lane0 6b390e64bbdd91ce lane31 91c944d603539c62
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 366.7 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x35, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 368.5 ms
|
||||
shadow: 256 instructions x 35 passes per iteration, 71680 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.247 ms, 4096 items derived, lane0 177dd71be37996bc lane31 792114b1b40ad79f
|
||||
warp base 4096: single cold run 5.071 ms, 4096 items derived, lane0 fe6701666e2e483a lane31 fc2105cb89fcb8b7
|
||||
warp base 1000000: single cold run 5.063 ms, 4096 items derived, lane0 6401cfb6e9ddfed6 lane31 89445da95a2e7184
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 362.3 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x53, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 364.4 ms
|
||||
shadow: 256 instructions x 53 passes per iteration, 108544 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 5.481 ms, 4096 items derived, lane0 9771dab715d71261 lane31 525676cf580b6f11
|
||||
warp base 4096: single cold run 5.346 ms, 4096 items derived, lane0 1afa4ca3c965c3f5 lane31 4e238e93b5f0101d
|
||||
warp base 1000000: single cold run 5.349 ms, 4096 items derived, lane0 21a07085d7c0bd82 lane31 39fd0197590efb2a
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
igneum-pow bench: seed "igneum-genesis", day "2026-10-03", dataset 2^28 words (memory-hard)
|
||||
cache: fill 367.6 ms on one core (2^26 words, 256 MiB, 65536 chains of 64 ChaCha12 blocks), FNV-1a 64 48c4f5bf24166b2e
|
||||
program: class mx8+sh256x88, 128 loads/hash, 512 bytes/hash, widths (1,4,16 words) [16, 0, 0], 4096 items/warp, mixer x8 (72 mixers/item), cache 2^26 words, op mix load=16 add=8 shfl=8 xor=6 mad=5 mul=5 mulhi=5 sub=4 rotl=3 rotr=3 or=1; epoch built in 364.7 ms
|
||||
shadow: 256 instructions x 88 passes per iteration, 180224 shadow instructions per hash, op mix add=47 rotl=30 xor=30 shfl=29 mad=27 mul=22 sub=21 rotr=20 mulhi=18 or=12
|
||||
warp base 0: single cold run 6.058 ms, 4096 items derived, lane0 84beec0429c7ef83 lane31 334b204a4a68c30d
|
||||
warp base 4096: single cold run 5.909 ms, 4096 items derived, lane0 32be0f5b7d992255 lane31 2c8dbd443403bc84
|
||||
warp base 1000000: single cold run 5.896 ms, 4096 items derived, lane0 78541813d6547a59 lane31 fd7fb2e05bbc316f
|
||||
10
docs/design/latency-ladder-bench/20261006T221034Z/table.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
host igneum-build-1 load 25.39 17.85 11.63 freq40 1500000 kHz siblings 40,88 pow 3f7623f208335f5b
|
||||
load after 23.60 18.46 12.05
|
||||
| Rung | reps | Shadow instrs per hash | Counted ops (approx) | Cold, core alone (ms) | Avg of 50, alone (ms) | Cold, sibling loaded (ms) | Avg of 50, sibling loaded (ms) | Under 10 ms loaded |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 0 | 27 | 55296 | 102,122 | 5.14 | 4.95 | 8.77 | 8.67 | yes |
|
||||
| 1 | 35 | 71680 | 132,105 | 5.57 | 5.52 | 8.87 | 8.51 | yes |
|
||||
| 2 | 53 | 108544 | 199,566 | 5.57 | 5.35 | 9.23 | 8.91 | yes |
|
||||
| 3 | 88 | 180224 | 330,740 | 6.07 | 5.89 | 10.08 | 9.84 | NO |
|
||||
| 4 | 173 | 354304 | 649,307 | 7.38 | 7.23 | 12.38 | 12.09 | NO |
|
||||
| 5 | 267 | 546816 | 1,001,604 | 8.88 | 8.50 | 14.96 | 14.58 | NO |
|
||||
1028
docs/design/latency-ladder-harness/failed-case.json
Normal file
86
docs/design/latency-ladder-harness/failed-case.log
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
22:15:30.265 signals up/up/none, expect step; class v4 from genesis, the ladder active from DAA 0, window 60 DAA x 7 (the first epoch that can step is 8, DAA 480); 60 DAA per epoch, lead 10; run 900 s or 10 epochs
|
||||
22:15:31.502 n0 up pid 1524201 json 29722 p2p 29721, signals up
|
||||
22:15:32.712 n1 up pid 1524767 json 29732 p2p 29731, signals up
|
||||
22:15:33.921 n2 up pid 1525234 json 29742 p2p 29741, signals none
|
||||
22:15:33.922 n0: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:15:33.922 n1: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:15:33.923 n2: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals none
|
||||
22:15:33.923 n0 digest: b1f606bede788b70
|
||||
22:15:34.935 epoch -1 -> 0 at daa 0, 4.7 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 0 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:15:34.936 t=4.7 s daa 0 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 0/234e082d 0/234e082d 0/234e082d
|
||||
22:15:49.976 t=19.7 s daa 5 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 5/40fa3a38 5/40fa3a38 5/40fa3a38
|
||||
22:16:04.996 t=34.7 s daa 10 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 10/6c4045c3 10/6c4045c3 10/6c4045c3
|
||||
22:16:20.016 t=49.8 s daa 18 epoch 0 rung 0 (27) up 8235 bps weakest 0 blocks/sink per node 18/c1c333f0 18/c1c333f0 18/c1c333f0
|
||||
22:16:35.038 t=64.8 s daa 35 epoch 0 rung 0 (27) up 8529 bps weakest 0 blocks/sink per node 35/7b225e5b 35/7b225e5b 35/7b225e5b
|
||||
22:16:50.055 t=79.8 s daa 50 epoch 0 rung 0 (27) up 7755 bps weakest 0 blocks/sink per node 50/47cc7477 50/47cc7477 50/47cc7477
|
||||
22:17:01.070 epoch 0 -> 1 at daa 60, 90.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7627 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:17:05.076 t=94.8 s daa 66 epoch 1 rung 0 (27) up 7333 bps weakest 0 blocks/sink per node 66/3e771191 66/3e771191 66/3e771191
|
||||
22:17:20.093 t=109.8 s daa 83 epoch 1 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 83/6c5cdee4 83/6c5cdee4 83/6c5cdee4
|
||||
22:17:35.109 t=124.8 s daa 95 epoch 1 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 95/7764cba7 95/7764cba7 95/7764cba7
|
||||
22:17:50.123 t=139.9 s daa 107 epoch 1 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 107/2cb36d19 107/2cb36d19 107/2cb36d19
|
||||
22:18:05.138 t=154.9 s daa 119 epoch 1 rung 0 (27) up 6166 bps weakest 0 blocks/sink per node 119/352afb28 119/352afb28 119/352afb28
|
||||
22:18:06.139 epoch 1 -> 2 at daa 121, 155.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6166 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:18:20.155 t=169.9 s daa 136 epoch 2 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 136/e0952e01 136/e0952e01 136/e0952e01
|
||||
22:18:35.174 t=184.9 s daa 152 epoch 2 rung 0 (27) up 6166 bps weakest 0 blocks/sink per node 152/ed8e34c7 152/ed8e34c7 152/ed8e34c7
|
||||
22:18:50.193 t=199.9 s daa 162 epoch 2 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 162/6065aef4 162/6065aef4 162/6065aef4
|
||||
22:19:05.211 t=214.9 s daa 176 epoch 2 rung 0 (27) up 6333 bps weakest 0 blocks/sink per node 176/317cae30 176/317cae30 176/317cae30
|
||||
22:19:09.216 epoch 2 -> 3 at daa 181, 218.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6500 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:19:20.231 t=230.0 s daa 189 epoch 3 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 189/21d90cfa 189/21d90cfa 189/21d90cfa
|
||||
22:19:35.249 t=245.0 s daa 202 epoch 3 rung 0 (27) up 6333 bps weakest 0 blocks/sink per node 202/f1e7ec5c 202/f1e7ec5c 202/f1e7ec5c
|
||||
22:19:50.265 t=260.0 s daa 218 epoch 3 rung 0 (27) up 6333 bps weakest 0 blocks/sink per node 218/e274c4f7 218/e274c4f7 218/e274c4f7
|
||||
22:20:05.285 t=275.0 s daa 235 epoch 3 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 235/8e665e8d 235/8e665e8d 235/8e665e8d
|
||||
22:20:08.289 epoch 3 -> 4 at daa 240, 278.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:20:20.303 t=290.0 s daa 254 epoch 4 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 254/ce04cd38 254/ce04cd38 254/ce04cd38
|
||||
22:20:35.315 t=305.0 s daa 267 epoch 4 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 267/bf108545 267/bf108545 267/bf108545
|
||||
22:20:50.328 t=320.1 s daa 283 epoch 4 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 283/5a897c6f 283/5a897c6f 283/5a897c6f
|
||||
22:21:05.344 t=335.1 s daa 294 epoch 4 rung 0 (27) up 8000 bps weakest 0 blocks/sink per node 294/2b000b87 294/2b000b87 294/2b000b87
|
||||
22:21:09.348 epoch 4 -> 5 at daa 300, 339.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7833 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:21:20.359 t=350.1 s daa 319 epoch 5 rung 0 (27) up 7333 bps weakest 0 blocks/sink per node 319/ecef2a5f 319/ecef2a5f 319/ecef2a5f
|
||||
22:21:35.378 t=365.1 s daa 335 epoch 5 rung 0 (27) up 7166 bps weakest 0 blocks/sink per node 335/a8fbe817 335/a8fbe817 335/a8fbe817
|
||||
22:21:50.399 t=380.1 s daa 343 epoch 5 rung 0 (27) up 7333 bps weakest 0 blocks/sink per node 343/b1967aab 343/b1967aab 343/b1967aab
|
||||
22:22:05.416 t=395.1 s daa 354 epoch 5 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 354/2a4ccf8a 354/2a4ccf8a 354/2a4ccf8a
|
||||
22:22:13.424 epoch 5 -> 6 at daa 360, 403.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6333 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:22:20.432 t=410.2 s daa 366 epoch 6 rung 0 (27) up 6166 bps weakest 6166 blocks/sink per node 366/a16b8185 366/a16b8185 366/a16b8185
|
||||
22:22:35.449 t=425.2 s daa 377 epoch 6 rung 0 (27) up 6000 bps weakest 6000 blocks/sink per node 377/50316214 377/50316214 377/50316214
|
||||
22:22:50.468 t=440.2 s daa 395 epoch 6 rung 0 (27) up 6666 bps weakest 6166 blocks/sink per node 395/5be9e979 395/5be9e979 395/5be9e979
|
||||
22:23:05.487 t=455.2 s daa 404 epoch 6 rung 0 (27) up 6000 bps weakest 6000 blocks/sink per node 404/3a965599 404/3a965599 404/3a965599
|
||||
22:23:15.500 epoch 6 -> 7 at daa 421, 465.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6333 bps (weakest of 7: 6166), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:23:20.507 t=470.2 s daa 424 epoch 7 rung 0 (27) up 6333 bps weakest 6065 blocks/sink per node 424/097eaa14 424/097eaa14 424/097eaa14
|
||||
22:23:35.529 t=485.3 s daa 439 epoch 7 rung 0 (27) up 6166 bps weakest 6000 blocks/sink per node 439/7d27db8c 439/7d27db8c 439/7d27db8c
|
||||
22:23:50.550 t=500.3 s daa 456 epoch 7 rung 0 (27) up 5833 bps weakest 5833 blocks/sink per node 456/f4675851 456/f4675851 456/f4675851
|
||||
22:24:05.570 t=515.3 s daa 467 epoch 7 rung 0 (27) up 6333 bps weakest 6333 blocks/sink per node 467/a7575707 467/a7575707 467/a7575707
|
||||
22:24:20.593 epoch 7 -> 8 at daa 480, 530.3 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6500 bps (weakest of 7: 6333), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:24:20.593 t=530.3 s daa 480 epoch 8 rung 0 (27) up 6500 bps weakest 6333 blocks/sink per node 480/f8f68e4b 480/f8f68e4b 480/f8f68e4b
|
||||
22:24:35.616 t=545.4 s daa 499 epoch 8 rung 0 (27) up 7000 bps weakest 6000 blocks/sink per node 499/dba81f65 499/dba81f65 499/dba81f65
|
||||
22:24:50.638 t=560.4 s daa 508 epoch 8 rung 0 (27) up 7000 bps weakest 5833 blocks/sink per node 508/e6208807 508/e6208807 508/e6208807
|
||||
22:25:05.661 t=575.4 s daa 521 epoch 8 rung 0 (27) up 6833 bps weakest 6166 blocks/sink per node 521/faf3f2b3 521/faf3f2b3 521/faf3f2b3
|
||||
22:25:20.684 t=590.4 s daa 537 epoch 8 rung 0 (27) up 7166 bps weakest 6166 blocks/sink per node 537/0cec2d26 537/0cec2d26 537/0cec2d26
|
||||
22:25:24.690 epoch 8 -> 9 at daa 543, 594.4 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6666 bps (weakest of 7: 6333), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:25:35.706 t=605.4 s daa 546 epoch 9 rung 0 (27) up 6666 bps weakest 6166 blocks/sink per node 546/75a259c6 546/75a259c6 546/75a259c6
|
||||
22:25:50.729 t=620.5 s daa 573 epoch 9 rung 0 (27) up 6166 bps weakest 6000 blocks/sink per node 573/31d72980 573/31d72980 573/31d72980
|
||||
22:26:05.750 t=635.5 s daa 581 epoch 9 rung 0 (27) up 6333 bps weakest 6166 blocks/sink per node 581/3cfe5b93 581/3cfe5b93 581/3cfe5b93
|
||||
22:26:20.771 t=650.5 s daa 595 epoch 9 rung 0 (27) up 6333 bps weakest 6333 blocks/sink per node 595/84601051 595/84601051 595/84601051
|
||||
22:26:25.777 epoch 9 -> 10 at daa 600, 655.5 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6166 bps (weakest of 7: 6166), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:26:29.069 SUMMARY FAIL (expect step, signals up/up/none): no step; epochs seen e0:r0:0bps e1:r0:0bps e2:r0:0bps e3:r0:0bps e4:r0:0bps e5:r0:0bps e6:r0:0bps e7:r0:6166bps e8:r0:6333bps e9:r0:6333bps e10:r0:6166bps; chain bits {"none":198,"up":406} (6722 bps up); blocks 604 / 0; rejected miners 0/0/0 nodes 0/0/0; sinks 783087038f7096b1 783087038f7096b1 783087038f7096b1 at 603/603/603
|
||||
22:26:29.069 PROGRAM ID epoch 0 seed 234e082d653dc69d reps 27: miners 26fc8f3decca98ed (3 of 3) cli at reps 26fc8f3decca98ed cli rung 0 26fc8f3decca98ed
|
||||
22:26:29.069 PROGRAM ID epoch 1 seed 47cc74777f2067e9 reps 27: miners 4eca8ebd9601cb06 (3 of 3) cli at reps 4eca8ebd9601cb06 cli rung 0 4eca8ebd9601cb06
|
||||
22:26:29.069 PROGRAM ID epoch 2 seed 885cd519d10a63af reps 27: miners d8692bc62ac8b7c1 (3 of 3) cli at reps d8692bc62ac8b7c1 cli rung 0 d8692bc62ac8b7c1
|
||||
22:26:29.069 PROGRAM ID epoch 3 seed 422f6e379baea2d0 reps 27: miners 379b63489b302ff1 (3 of 3) cli at reps 379b63489b302ff1 cli rung 0 379b63489b302ff1
|
||||
22:26:29.069 PROGRAM ID epoch 4 seed 9a339c3a0da7a629 reps 27: miners 5297fd1c191bef91 (3 of 3) cli at reps 5297fd1c191bef91 cli rung 0 5297fd1c191bef91
|
||||
22:26:29.069 PROGRAM ID epoch 5 seed 630d437238ecfbd0 reps 27: miners 188721c15b83df97 (3 of 3) cli at reps 188721c15b83df97 cli rung 0 188721c15b83df97
|
||||
22:26:29.069 PROGRAM ID epoch 6 seed 46d593aedc1dfd37 reps 27: miners 962b4d74afdf7f3a (3 of 3) cli at reps 962b4d74afdf7f3a cli rung 0 962b4d74afdf7f3a
|
||||
22:26:29.069 PROGRAM ID epoch 7 seed 842561a101da3cc8 reps 27: miners 3fc4c1e7704bb8a5 (3 of 3) cli at reps 3fc4c1e7704bb8a5 cli rung 0 3fc4c1e7704bb8a5
|
||||
22:26:29.069 PROGRAM ID epoch 8 seed c0cb56ba970dd961 reps 27: miners 0fa5001f8bc6ffc0 (3 of 3) cli at reps 0fa5001f8bc6ffc0 cli rung 0 0fa5001f8bc6ffc0
|
||||
22:26:29.069 PROGRAM ID epoch 9 seed 3a647e8feb58ab1b reps 27: miners b0a067d5bf44497e (3 of 3) cli at reps b0a067d5bf44497e cli rung 0 b0a067d5bf44497e
|
||||
22:26:29.069 PROGRAM ID epoch 10 seed 5d7277e6f83f9e4d reps 27: miners 84e6cfe07a3316f4 (3 of 3) cli at reps 84e6cfe07a3316f4 cli rung 0 84e6cfe07a3316f4
|
||||
22:26:29.069 FAILED CHECK chain_carries_the_bits
|
||||
22:26:29.069 FAILED CHECK template_stepped_to_rung_1
|
||||
22:26:29.069 FAILED CHECK stepped_at_the_first_full_window_epoch
|
||||
22:26:29.069 FAILED CHECK step_line_on_every_node_same_epoch
|
||||
22:26:29.069 FAILED CHECK weakest_up_at_or_above_threshold
|
||||
22:26:29.069 FAILED CHECK no_second_step_inside_seven_windows
|
||||
22:26:29.069 FAILED CHECK blocks_on_both_sides
|
||||
22:26:29.069 FAILED CHECK rung1_ids_equal_the_cli_rung1_id
|
||||
22:26:29.069 FAILED CHECK rung1_ids_differ_from_the_same_seed_rung0_id
|
||||
22:26:29.069 summary: /tmp/igneum-fast-time-ladder-failed-case/summary.json
|
||||
CASE failed-case rc=1
|
||||
969
docs/design/latency-ladder-harness/no-step.json
Normal file
|
|
@ -0,0 +1,969 @@
|
|||
{
|
||||
"pass": true,
|
||||
"expect": "no-step",
|
||||
"signals": [
|
||||
"up",
|
||||
"up",
|
||||
"none"
|
||||
],
|
||||
"checks": {
|
||||
"zero_rejected_by_miners": true,
|
||||
"zero_rejected_by_nodes": true,
|
||||
"sinks_agree": true,
|
||||
"block_counts_agree": true,
|
||||
"miners_agree_on_every_program": true,
|
||||
"ladder_line_on_every_node": true,
|
||||
"every_node_signals_its_bits": true,
|
||||
"every_epoch_class_v4": true,
|
||||
"rung0_ids_equal_the_cli_rung0_id": true,
|
||||
"chain_carries_the_bits": true,
|
||||
"template_never_above_rung_0": true,
|
||||
"no_step_line_on_any_node": true,
|
||||
"ran_the_epochs": true,
|
||||
"passed_the_first_full_window_epoch": true,
|
||||
"up_share_under_threshold_on_chain": true
|
||||
},
|
||||
"window": 60,
|
||||
"windows": 7,
|
||||
"threshold_bps": 9000,
|
||||
"epoch_blocks": 60,
|
||||
"lead": 10,
|
||||
"first_step_epoch": 8,
|
||||
"node": "/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release/igneumd",
|
||||
"miner": "/srv/builds/igneum-wt-ladder/vendor/igneum-node-ladder/target/release/igneum-miner",
|
||||
"pow": "/srv/builds/igneum-wt-ladder/igneum-pow/target/release/igneum-pow",
|
||||
"template_step": null,
|
||||
"run_ended_at_s": 592.5,
|
||||
"final_daa": 600,
|
||||
"max_epoch_seen": 10,
|
||||
"epochs": {
|
||||
"0": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 0,
|
||||
"at": 4.7,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 0,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"1": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 62,
|
||||
"at": 96.8,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 7000,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"2": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 120,
|
||||
"at": 156.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 7166,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"3": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 180,
|
||||
"at": 213.9,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 7166,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"4": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 240,
|
||||
"at": 273,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"5": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 300,
|
||||
"at": 322.1,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"6": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 360,
|
||||
"at": 378.2,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 6333,
|
||||
"up_weakest_bps": 0,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"7": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 420,
|
||||
"at": 436.2,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 5833,
|
||||
"up_weakest_bps": 5833,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"8": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 480,
|
||||
"at": 490.3,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 6166,
|
||||
"up_weakest_bps": 5833,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"9": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 540,
|
||||
"at": 541.4,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 5833,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
},
|
||||
"10": {
|
||||
"class": 4,
|
||||
"reps": 27,
|
||||
"step": 0,
|
||||
"firstSeenDaa": 600,
|
||||
"at": 592.5,
|
||||
"eraSeed": "234e082d653dc69db36d875b256d0f5f4a123b353d6aa069c3c7aa9aa6c46062",
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 5833,
|
||||
"down_bps": 0,
|
||||
"step_epoch": 0
|
||||
}
|
||||
},
|
||||
"blocks": {
|
||||
"total": 602,
|
||||
"before_boundary": 602,
|
||||
"after_boundary": 0,
|
||||
"ladder_bits": {
|
||||
"none": 200,
|
||||
"up": 402
|
||||
},
|
||||
"object_bytes": {
|
||||
"0": 602
|
||||
},
|
||||
"up_share_bps_on_chain": 6678
|
||||
},
|
||||
"programs": [
|
||||
{
|
||||
"epoch": 0,
|
||||
"class": "v4",
|
||||
"program_id": "26fc8f3decca98ed",
|
||||
"seed": "234e082d653dc69d",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 1,
|
||||
"class": "v4",
|
||||
"program_id": "e0910c8d87135035",
|
||||
"seed": "894676c6f3ebb385",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 2,
|
||||
"class": "v4",
|
||||
"program_id": "2eb05375a9d9077d",
|
||||
"seed": "405d6592622d1af9",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 3,
|
||||
"class": "v4",
|
||||
"program_id": "0acdab9caca48471",
|
||||
"seed": "9598288f0a520b69",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"class": "v4",
|
||||
"program_id": "1df476827c33e263",
|
||||
"seed": "9b6e5a23005f0998",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"class": "v4",
|
||||
"program_id": "20003329ca5050d9",
|
||||
"seed": "a0a54f6a687f94f4",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 6,
|
||||
"class": "v4",
|
||||
"program_id": "2e5cee148e6b82b1",
|
||||
"seed": "1b20ae99061e034f",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 7,
|
||||
"class": "v4",
|
||||
"program_id": "d9babb74721f6cd0",
|
||||
"seed": "5545b276021f1ad9",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 8,
|
||||
"class": "v4",
|
||||
"program_id": "94b5809680b13285",
|
||||
"seed": "f959d016b2d47167",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 9,
|
||||
"class": "v4",
|
||||
"program_id": "e1cae64a2a12de45",
|
||||
"seed": "24b28bef5cba00a9",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
},
|
||||
{
|
||||
"epoch": 10,
|
||||
"class": "v4",
|
||||
"program_id": "c7ce4aa0e96ba6b9",
|
||||
"seed": "ccfc16e9107ae720",
|
||||
"miners": 3,
|
||||
"disagree": false
|
||||
}
|
||||
],
|
||||
"program_id_rows": [
|
||||
{
|
||||
"epoch": 0,
|
||||
"seed": "234e082d653dc69d",
|
||||
"reps": 27,
|
||||
"miners_id": "26fc8f3decca98ed",
|
||||
"miners": 3,
|
||||
"cli_rung0": "26fc8f3decca98ed",
|
||||
"cli_at_reps": "26fc8f3decca98ed"
|
||||
},
|
||||
{
|
||||
"epoch": 1,
|
||||
"seed": "894676c6f3ebb385",
|
||||
"reps": 27,
|
||||
"miners_id": "e0910c8d87135035",
|
||||
"miners": 3,
|
||||
"cli_rung0": "e0910c8d87135035",
|
||||
"cli_at_reps": "e0910c8d87135035"
|
||||
},
|
||||
{
|
||||
"epoch": 2,
|
||||
"seed": "405d6592622d1af9",
|
||||
"reps": 27,
|
||||
"miners_id": "2eb05375a9d9077d",
|
||||
"miners": 3,
|
||||
"cli_rung0": "2eb05375a9d9077d",
|
||||
"cli_at_reps": "2eb05375a9d9077d"
|
||||
},
|
||||
{
|
||||
"epoch": 3,
|
||||
"seed": "9598288f0a520b69",
|
||||
"reps": 27,
|
||||
"miners_id": "0acdab9caca48471",
|
||||
"miners": 3,
|
||||
"cli_rung0": "0acdab9caca48471",
|
||||
"cli_at_reps": "0acdab9caca48471"
|
||||
},
|
||||
{
|
||||
"epoch": 4,
|
||||
"seed": "9b6e5a23005f0998",
|
||||
"reps": 27,
|
||||
"miners_id": "1df476827c33e263",
|
||||
"miners": 3,
|
||||
"cli_rung0": "1df476827c33e263",
|
||||
"cli_at_reps": "1df476827c33e263"
|
||||
},
|
||||
{
|
||||
"epoch": 5,
|
||||
"seed": "a0a54f6a687f94f4",
|
||||
"reps": 27,
|
||||
"miners_id": "20003329ca5050d9",
|
||||
"miners": 3,
|
||||
"cli_rung0": "20003329ca5050d9",
|
||||
"cli_at_reps": "20003329ca5050d9"
|
||||
},
|
||||
{
|
||||
"epoch": 6,
|
||||
"seed": "1b20ae99061e034f",
|
||||
"reps": 27,
|
||||
"miners_id": "2e5cee148e6b82b1",
|
||||
"miners": 3,
|
||||
"cli_rung0": "2e5cee148e6b82b1",
|
||||
"cli_at_reps": "2e5cee148e6b82b1"
|
||||
},
|
||||
{
|
||||
"epoch": 7,
|
||||
"seed": "5545b276021f1ad9",
|
||||
"reps": 27,
|
||||
"miners_id": "d9babb74721f6cd0",
|
||||
"miners": 3,
|
||||
"cli_rung0": "d9babb74721f6cd0",
|
||||
"cli_at_reps": "d9babb74721f6cd0"
|
||||
},
|
||||
{
|
||||
"epoch": 8,
|
||||
"seed": "f959d016b2d47167",
|
||||
"reps": 27,
|
||||
"miners_id": "94b5809680b13285",
|
||||
"miners": 3,
|
||||
"cli_rung0": "94b5809680b13285",
|
||||
"cli_at_reps": "94b5809680b13285"
|
||||
},
|
||||
{
|
||||
"epoch": 9,
|
||||
"seed": "24b28bef5cba00a9",
|
||||
"reps": 27,
|
||||
"miners_id": "e1cae64a2a12de45",
|
||||
"miners": 3,
|
||||
"cli_rung0": "e1cae64a2a12de45",
|
||||
"cli_at_reps": "e1cae64a2a12de45"
|
||||
},
|
||||
{
|
||||
"epoch": 10,
|
||||
"seed": "ccfc16e9107ae720",
|
||||
"reps": 27,
|
||||
"miners_id": "c7ce4aa0e96ba6b9",
|
||||
"miners": 3,
|
||||
"cli_rung0": "c7ce4aa0e96ba6b9",
|
||||
"cli_at_reps": "c7ce4aa0e96ba6b9"
|
||||
}
|
||||
],
|
||||
"accepted_per_miner": [
|
||||
202,
|
||||
200,
|
||||
199
|
||||
],
|
||||
"rejected_by_miners": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rejected_by_nodes": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sinks": [
|
||||
"bfc435d62d7c659c",
|
||||
"bfc435d62d7c659c",
|
||||
"bfc435d62d7c659c"
|
||||
],
|
||||
"block_counts": [
|
||||
601,
|
||||
601,
|
||||
601
|
||||
],
|
||||
"step_lines": [
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
],
|
||||
"samples": [
|
||||
{
|
||||
"t": 4.7,
|
||||
"daa": 0,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 0,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"0/234e082d",
|
||||
"0/234e082d",
|
||||
"0/234e082d"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 19.7,
|
||||
"daa": 1,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 0,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"1/350fed25",
|
||||
"1/350fed25",
|
||||
"1/350fed25"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 34.7,
|
||||
"daa": 7,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"7/33ca6cd0",
|
||||
"7/33ca6cd0",
|
||||
"7/33ca6cd0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 49.8,
|
||||
"daa": 12,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6363,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"12/70a47fc6",
|
||||
"12/70a47fc6",
|
||||
"12/70a47fc6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 64.8,
|
||||
"daa": 28,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6296,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"28/1c7719a6",
|
||||
"28/1c7719a6",
|
||||
"28/1c7719a6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 79.8,
|
||||
"daa": 36,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6571,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"36/72330841",
|
||||
"36/72330841",
|
||||
"36/72330841"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 94.8,
|
||||
"daa": 59,
|
||||
"epoch": 0,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7068,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"59/cf02d592",
|
||||
"59/cf02d592",
|
||||
"59/cf02d592"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 109.8,
|
||||
"daa": 74,
|
||||
"epoch": 1,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"74/c65d7a59",
|
||||
"74/c65d7a59",
|
||||
"74/c65d7a59"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 124.8,
|
||||
"daa": 85,
|
||||
"epoch": 1,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7333,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"85/e53510f4",
|
||||
"85/e53510f4",
|
||||
"85/e53510f4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 139.9,
|
||||
"daa": 105,
|
||||
"epoch": 1,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7166,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"105/37769ced",
|
||||
"105/37769ced",
|
||||
"105/37769ced"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 154.9,
|
||||
"daa": 119,
|
||||
"epoch": 1,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7166,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"119/aa8e5664",
|
||||
"119/aa8e5664",
|
||||
"119/aa8e5664"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 169.9,
|
||||
"daa": 131,
|
||||
"epoch": 2,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7500,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"131/bf6ed8b2",
|
||||
"131/bf6ed8b2",
|
||||
"131/bf6ed8b2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 184.9,
|
||||
"daa": 150,
|
||||
"epoch": 2,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 8166,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"150/6a31bf2e",
|
||||
"150/6a31bf2e",
|
||||
"150/6a31bf2e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 199.9,
|
||||
"daa": 165,
|
||||
"epoch": 2,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7500,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"165/83f8d3f6",
|
||||
"165/83f8d3f6",
|
||||
"165/83f8d3f6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 215,
|
||||
"daa": 181,
|
||||
"epoch": 3,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7000,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"181/5be6a01e",
|
||||
"181/5be6a01e",
|
||||
"181/5be6a01e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 230,
|
||||
"daa": 202,
|
||||
"epoch": 3,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"202/5663af4a",
|
||||
"202/5663af4a",
|
||||
"202/5663af4a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 245,
|
||||
"daa": 216,
|
||||
"epoch": 3,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 5833,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"216/3d231639",
|
||||
"216/3d231639",
|
||||
"216/3d231639"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 260,
|
||||
"daa": 228,
|
||||
"epoch": 3,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6500,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"228/88370ed6",
|
||||
"228/88370ed6",
|
||||
"228/88370ed6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 275,
|
||||
"daa": 241,
|
||||
"epoch": 4,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"241/a2dbefc3",
|
||||
"241/a2dbefc3",
|
||||
"241/a2dbefc3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 290,
|
||||
"daa": 248,
|
||||
"epoch": 4,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6500,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"248/0815d37e",
|
||||
"248/0815d37e",
|
||||
"248/0815d37e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 305.1,
|
||||
"daa": 267,
|
||||
"epoch": 4,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"267/09bb3297",
|
||||
"267/09bb3297",
|
||||
"267/09bb3297"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 320.1,
|
||||
"daa": 297,
|
||||
"epoch": 4,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"297/af91ef29",
|
||||
"297/af91ef29",
|
||||
"297/af91ef29"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 335.1,
|
||||
"daa": 312,
|
||||
"epoch": 5,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"312/a015f07f",
|
||||
"312/a015f07f",
|
||||
"312/a015f07f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 350.1,
|
||||
"daa": 330,
|
||||
"epoch": 5,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"330/c9f57620",
|
||||
"330/c9f57620",
|
||||
"330/c9f57620"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 365.1,
|
||||
"daa": 345,
|
||||
"epoch": 5,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6949,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"345/fe4111b9",
|
||||
"345/fe4111b9",
|
||||
"345/fe4111b9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 380.2,
|
||||
"daa": 360,
|
||||
"epoch": 6,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6333,
|
||||
"up_weakest_bps": 0,
|
||||
"nodes": [
|
||||
"360/2acb1c9a",
|
||||
"360/2acb1c9a",
|
||||
"360/2acb1c9a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 395.2,
|
||||
"daa": 368,
|
||||
"epoch": 6,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6779,
|
||||
"up_weakest_bps": 5714,
|
||||
"nodes": [
|
||||
"368/853bece4",
|
||||
"368/853bece4",
|
||||
"368/853bece4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 410.2,
|
||||
"daa": 390,
|
||||
"epoch": 6,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 5666,
|
||||
"up_weakest_bps": 5666,
|
||||
"nodes": [
|
||||
"390/9101251e",
|
||||
"389/6d70c5a8",
|
||||
"390/9101251e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 425.2,
|
||||
"daa": 404,
|
||||
"epoch": 6,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 5500,
|
||||
"up_weakest_bps": 5500,
|
||||
"nodes": [
|
||||
"404/89a08ca7",
|
||||
"404/89a08ca7",
|
||||
"404/89a08ca7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 440.2,
|
||||
"daa": 421,
|
||||
"epoch": 7,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6000,
|
||||
"up_weakest_bps": 6000,
|
||||
"nodes": [
|
||||
"421/a39e260b",
|
||||
"421/a39e260b",
|
||||
"421/a39e260b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 455.3,
|
||||
"daa": 437,
|
||||
"epoch": 7,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 5666,
|
||||
"up_weakest_bps": 5666,
|
||||
"nodes": [
|
||||
"437/9782bb5e",
|
||||
"437/9782bb5e",
|
||||
"437/9782bb5e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 470.3,
|
||||
"daa": 456,
|
||||
"epoch": 7,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 5932,
|
||||
"up_weakest_bps": 5833,
|
||||
"nodes": [
|
||||
"456/2a6dea3c",
|
||||
"456/2a6dea3c",
|
||||
"456/2a6dea3c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 485.3,
|
||||
"daa": 473,
|
||||
"epoch": 7,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6500,
|
||||
"up_weakest_bps": 5500,
|
||||
"nodes": [
|
||||
"473/ab842458",
|
||||
"473/ab842458",
|
||||
"473/ab842458"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 500.3,
|
||||
"daa": 488,
|
||||
"epoch": 8,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6000,
|
||||
"up_weakest_bps": 5666,
|
||||
"nodes": [
|
||||
"488/a16f988f",
|
||||
"488/a16f988f",
|
||||
"488/a16f988f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 515.4,
|
||||
"daa": 505,
|
||||
"epoch": 8,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6333,
|
||||
"up_weakest_bps": 5737,
|
||||
"nodes": [
|
||||
"505/915b20b6",
|
||||
"505/915b20b6",
|
||||
"505/915b20b6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 530.4,
|
||||
"daa": 527,
|
||||
"epoch": 8,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 5333,
|
||||
"nodes": [
|
||||
"527/7161dfa6",
|
||||
"527/7161dfa6",
|
||||
"527/7161dfa6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 545.4,
|
||||
"daa": 541,
|
||||
"epoch": 9,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6666,
|
||||
"up_weakest_bps": 6000,
|
||||
"nodes": [
|
||||
"541/5fcc65e3",
|
||||
"541/5fcc65e3",
|
||||
"541/5fcc65e3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 560.4,
|
||||
"daa": 562,
|
||||
"epoch": 9,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 6833,
|
||||
"up_weakest_bps": 5666,
|
||||
"nodes": [
|
||||
"562/673f1e20",
|
||||
"562/673f1e20",
|
||||
"562/673f1e20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 575.4,
|
||||
"daa": 577,
|
||||
"epoch": 9,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7166,
|
||||
"up_weakest_bps": 5833,
|
||||
"nodes": [
|
||||
"577/67af1166",
|
||||
"577/67af1166",
|
||||
"577/67af1166"
|
||||
]
|
||||
},
|
||||
{
|
||||
"t": 590.5,
|
||||
"daa": 596,
|
||||
"epoch": 9,
|
||||
"step": 0,
|
||||
"reps": 27,
|
||||
"up_bps": 7000,
|
||||
"up_weakest_bps": 5666,
|
||||
"nodes": [
|
||||
"596/2dd560ca",
|
||||
"596/2dd560ca",
|
||||
"596/2dd560ca"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
73
docs/design/latency-ladder-harness/no-step.log
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
22:37:08.927 signals up/up/none, expect no-step; class v4 from genesis, the ladder active from DAA 0, window 60 DAA x 7 (the first epoch that can step is 8, DAA 480); 60 DAA per epoch, lead 10; run 900 s or 10 epochs
|
||||
22:37:10.165 n0 up pid 1658675 json 29722 p2p 29721, signals up
|
||||
22:37:11.374 n1 up pid 1659188 json 29732 p2p 29731, signals up
|
||||
22:37:12.584 n2 up pid 1659706 json 29742 p2p 29741, signals none
|
||||
22:37:12.584 n0: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:37:12.585 n1: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:37:12.585 n2: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals none
|
||||
22:37:12.585 n0 digest: b1f606bede788b70
|
||||
22:37:13.597 epoch -1 -> 0 at daa 0, 4.7 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 0 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:37:13.598 t=4.7 s daa 0 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 0/234e082d 0/234e082d 0/234e082d
|
||||
22:37:28.640 t=19.7 s daa 1 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 1/350fed25 1/350fed25 1/350fed25
|
||||
22:37:43.659 t=34.7 s daa 7 epoch 0 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 7/33ca6cd0 7/33ca6cd0 7/33ca6cd0
|
||||
22:37:58.679 t=49.8 s daa 12 epoch 0 rung 0 (27) up 6363 bps weakest 0 blocks/sink per node 12/70a47fc6 12/70a47fc6 12/70a47fc6
|
||||
22:38:13.696 t=64.8 s daa 28 epoch 0 rung 0 (27) up 6296 bps weakest 0 blocks/sink per node 28/1c7719a6 28/1c7719a6 28/1c7719a6
|
||||
22:38:28.714 t=79.8 s daa 36 epoch 0 rung 0 (27) up 6571 bps weakest 0 blocks/sink per node 36/72330841 36/72330841 36/72330841
|
||||
22:38:43.731 t=94.8 s daa 59 epoch 0 rung 0 (27) up 7068 bps weakest 0 blocks/sink per node 59/cf02d592 59/cf02d592 59/cf02d592
|
||||
22:38:45.733 epoch 0 -> 1 at daa 62, 96.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:38:58.747 t=109.8 s daa 74 epoch 1 rung 0 (27) up 6833 bps weakest 0 blocks/sink per node 74/c65d7a59 74/c65d7a59 74/c65d7a59
|
||||
22:39:13.765 t=124.8 s daa 85 epoch 1 rung 0 (27) up 7333 bps weakest 0 blocks/sink per node 85/e53510f4 85/e53510f4 85/e53510f4
|
||||
22:39:28.785 t=139.9 s daa 105 epoch 1 rung 0 (27) up 7166 bps weakest 0 blocks/sink per node 105/37769ced 105/37769ced 105/37769ced
|
||||
22:39:43.805 t=154.9 s daa 119 epoch 1 rung 0 (27) up 7166 bps weakest 0 blocks/sink per node 119/aa8e5664 119/aa8e5664 119/aa8e5664
|
||||
22:39:45.808 epoch 1 -> 2 at daa 120, 156.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7166 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:39:58.824 t=169.9 s daa 131 epoch 2 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 131/bf6ed8b2 131/bf6ed8b2 131/bf6ed8b2
|
||||
22:40:13.843 t=184.9 s daa 150 epoch 2 rung 0 (27) up 8166 bps weakest 0 blocks/sink per node 150/6a31bf2e 150/6a31bf2e 150/6a31bf2e
|
||||
22:40:28.862 t=199.9 s daa 165 epoch 2 rung 0 (27) up 7500 bps weakest 0 blocks/sink per node 165/83f8d3f6 165/83f8d3f6 165/83f8d3f6
|
||||
22:40:42.878 epoch 2 -> 3 at daa 180, 213.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 7166 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:40:43.880 t=215.0 s daa 181 epoch 3 rung 0 (27) up 7000 bps weakest 0 blocks/sink per node 181/5be6a01e 181/5be6a01e 181/5be6a01e
|
||||
22:40:58.901 t=230.0 s daa 202 epoch 3 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 202/5663af4a 202/5663af4a 202/5663af4a
|
||||
22:41:13.919 t=245.0 s daa 216 epoch 3 rung 0 (27) up 5833 bps weakest 0 blocks/sink per node 216/3d231639 216/3d231639 216/3d231639
|
||||
22:41:28.939 t=260.0 s daa 228 epoch 3 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 228/88370ed6 228/88370ed6 228/88370ed6
|
||||
22:41:41.955 epoch 3 -> 4 at daa 240, 273.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6833 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:41:43.958 t=275.0 s daa 241 epoch 4 rung 0 (27) up 6833 bps weakest 0 blocks/sink per node 241/a2dbefc3 241/a2dbefc3 241/a2dbefc3
|
||||
22:41:58.976 t=290.0 s daa 248 epoch 4 rung 0 (27) up 6500 bps weakest 0 blocks/sink per node 248/0815d37e 248/0815d37e 248/0815d37e
|
||||
22:42:13.998 t=305.1 s daa 267 epoch 4 rung 0 (27) up 6833 bps weakest 0 blocks/sink per node 267/09bb3297 267/09bb3297 267/09bb3297
|
||||
22:42:29.017 t=320.1 s daa 297 epoch 4 rung 0 (27) up 6833 bps weakest 0 blocks/sink per node 297/af91ef29 297/af91ef29 297/af91ef29
|
||||
22:42:31.020 epoch 4 -> 5 at daa 300, 322.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6666 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:42:44.036 t=335.1 s daa 312 epoch 5 rung 0 (27) up 6666 bps weakest 0 blocks/sink per node 312/a015f07f 312/a015f07f 312/a015f07f
|
||||
22:42:59.055 t=350.1 s daa 330 epoch 5 rung 0 (27) up 6833 bps weakest 0 blocks/sink per node 330/c9f57620 330/c9f57620 330/c9f57620
|
||||
22:43:14.072 t=365.1 s daa 345 epoch 5 rung 0 (27) up 6949 bps weakest 0 blocks/sink per node 345/fe4111b9 345/fe4111b9 345/fe4111b9
|
||||
22:43:27.092 epoch 5 -> 6 at daa 360, 378.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6333 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:43:29.095 t=380.2 s daa 360 epoch 6 rung 0 (27) up 6333 bps weakest 0 blocks/sink per node 360/2acb1c9a 360/2acb1c9a 360/2acb1c9a
|
||||
22:43:44.112 t=395.2 s daa 368 epoch 6 rung 0 (27) up 6779 bps weakest 5714 blocks/sink per node 368/853bece4 368/853bece4 368/853bece4
|
||||
22:43:59.132 t=410.2 s daa 390 epoch 6 rung 0 (27) up 5666 bps weakest 5666 blocks/sink per node 390/9101251e 389/6d70c5a8 390/9101251e
|
||||
22:44:14.153 t=425.2 s daa 404 epoch 6 rung 0 (27) up 5500 bps weakest 5500 blocks/sink per node 404/89a08ca7 404/89a08ca7 404/89a08ca7
|
||||
22:44:25.167 epoch 6 -> 7 at daa 420, 436.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 5833 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:44:29.172 t=440.2 s daa 421 epoch 7 rung 0 (27) up 6000 bps weakest 6000 blocks/sink per node 421/a39e260b 421/a39e260b 421/a39e260b
|
||||
22:44:44.192 t=455.3 s daa 437 epoch 7 rung 0 (27) up 5666 bps weakest 5666 blocks/sink per node 437/9782bb5e 437/9782bb5e 437/9782bb5e
|
||||
22:44:59.214 t=470.3 s daa 456 epoch 7 rung 0 (27) up 5932 bps weakest 5833 blocks/sink per node 456/2a6dea3c 456/2a6dea3c 456/2a6dea3c
|
||||
22:45:14.235 t=485.3 s daa 473 epoch 7 rung 0 (27) up 6500 bps weakest 5500 blocks/sink per node 473/ab842458 473/ab842458 473/ab842458
|
||||
22:45:19.242 epoch 7 -> 8 at daa 480, 490.3 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6166 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:45:29.257 t=500.3 s daa 488 epoch 8 rung 0 (27) up 6000 bps weakest 5666 blocks/sink per node 488/a16f988f 488/a16f988f 488/a16f988f
|
||||
22:45:44.278 t=515.4 s daa 505 epoch 8 rung 0 (27) up 6333 bps weakest 5737 blocks/sink per node 505/915b20b6 505/915b20b6 505/915b20b6
|
||||
22:45:59.304 t=530.4 s daa 527 epoch 8 rung 0 (27) up 6666 bps weakest 5333 blocks/sink per node 527/7161dfa6 527/7161dfa6 527/7161dfa6
|
||||
22:46:10.317 epoch 8 -> 9 at daa 540, 541.4 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6666 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:46:14.324 t=545.4 s daa 541 epoch 9 rung 0 (27) up 6666 bps weakest 6000 blocks/sink per node 541/5fcc65e3 541/5fcc65e3 541/5fcc65e3
|
||||
22:46:29.343 t=560.4 s daa 562 epoch 9 rung 0 (27) up 6833 bps weakest 5666 blocks/sink per node 562/673f1e20 562/673f1e20 562/673f1e20
|
||||
22:46:44.364 t=575.4 s daa 577 epoch 9 rung 0 (27) up 7166 bps weakest 5833 blocks/sink per node 577/67af1166 577/67af1166 577/67af1166
|
||||
22:46:59.385 t=590.5 s daa 596 epoch 9 rung 0 (27) up 7000 bps weakest 5666 blocks/sink per node 596/2dd560ca 596/2dd560ca 596/2dd560ca
|
||||
22:47:01.388 epoch 9 -> 10 at daa 600, 592.5 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 6833 bps (weakest of 7: 5833), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:47:04.711 SUMMARY PASS (expect no-step, signals up/up/none): no step; epochs seen e0:r0:0bps e1:r0:0bps e2:r0:0bps e3:r0:0bps e4:r0:0bps e5:r0:0bps e6:r0:0bps e7:r0:5833bps e8:r0:5833bps e9:r0:5833bps e10:r0:5833bps; chain bits {"none":200,"up":402} (6678 bps up); blocks 602 / 0; rejected miners 0/0/0 nodes 0/0/0; sinks bfc435d62d7c659c bfc435d62d7c659c bfc435d62d7c659c at 601/601/601
|
||||
22:47:04.711 PROGRAM ID epoch 0 seed 234e082d653dc69d reps 27: miners 26fc8f3decca98ed (3 of 3) cli at reps 26fc8f3decca98ed cli rung 0 26fc8f3decca98ed
|
||||
22:47:04.711 PROGRAM ID epoch 1 seed 894676c6f3ebb385 reps 27: miners e0910c8d87135035 (3 of 3) cli at reps e0910c8d87135035 cli rung 0 e0910c8d87135035
|
||||
22:47:04.711 PROGRAM ID epoch 2 seed 405d6592622d1af9 reps 27: miners 2eb05375a9d9077d (3 of 3) cli at reps 2eb05375a9d9077d cli rung 0 2eb05375a9d9077d
|
||||
22:47:04.711 PROGRAM ID epoch 3 seed 9598288f0a520b69 reps 27: miners 0acdab9caca48471 (3 of 3) cli at reps 0acdab9caca48471 cli rung 0 0acdab9caca48471
|
||||
22:47:04.711 PROGRAM ID epoch 4 seed 9b6e5a23005f0998 reps 27: miners 1df476827c33e263 (3 of 3) cli at reps 1df476827c33e263 cli rung 0 1df476827c33e263
|
||||
22:47:04.711 PROGRAM ID epoch 5 seed a0a54f6a687f94f4 reps 27: miners 20003329ca5050d9 (3 of 3) cli at reps 20003329ca5050d9 cli rung 0 20003329ca5050d9
|
||||
22:47:04.711 PROGRAM ID epoch 6 seed 1b20ae99061e034f reps 27: miners 2e5cee148e6b82b1 (3 of 3) cli at reps 2e5cee148e6b82b1 cli rung 0 2e5cee148e6b82b1
|
||||
22:47:04.711 PROGRAM ID epoch 7 seed 5545b276021f1ad9 reps 27: miners d9babb74721f6cd0 (3 of 3) cli at reps d9babb74721f6cd0 cli rung 0 d9babb74721f6cd0
|
||||
22:47:04.711 PROGRAM ID epoch 8 seed f959d016b2d47167 reps 27: miners 94b5809680b13285 (3 of 3) cli at reps 94b5809680b13285 cli rung 0 94b5809680b13285
|
||||
22:47:04.711 PROGRAM ID epoch 9 seed 24b28bef5cba00a9 reps 27: miners e1cae64a2a12de45 (3 of 3) cli at reps e1cae64a2a12de45 cli rung 0 e1cae64a2a12de45
|
||||
22:47:04.711 PROGRAM ID epoch 10 seed ccfc16e9107ae720 reps 27: miners c7ce4aa0e96ba6b9 (3 of 3) cli at reps c7ce4aa0e96ba6b9 cli rung 0 c7ce4aa0e96ba6b9
|
||||
22:47:04.711 summary: /tmp/igneum-fast-time-ladder-no-step/summary.json
|
||||
CASE no-step rc=0
|
||||
1012
docs/design/latency-ladder-harness/step.json
Normal file
78
docs/design/latency-ladder-harness/step.log
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
22:26:30.617 signals up/up/up, expect step; class v4 from genesis, the ladder active from DAA 0, window 60 DAA x 7 (the first epoch that can step is 8, DAA 480); 60 DAA per epoch, lead 10; run 900 s or 10 epochs
|
||||
22:26:31.854 n0 up pid 1601845 json 29722 p2p 29721, signals up
|
||||
22:26:33.062 n1 up pid 1602372 json 29732 p2p 29731, signals up
|
||||
22:26:34.272 n2 up pid 1602849 json 29742 p2p 29741, signals up
|
||||
22:26:34.273 n0: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:26:34.273 n1: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:26:34.273 n2: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:26:34.274 n0 digest: b1f606bede788b70
|
||||
22:26:35.285 epoch -1 -> 0 at daa 0, 4.7 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 0 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:26:35.286 t=4.7 s daa 0 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 0/234e082d 0/234e082d 0/234e082d
|
||||
22:26:50.326 t=19.7 s daa 3 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 3/249577e0 3/249577e0 3/249577e0
|
||||
22:27:05.344 t=34.7 s daa 9 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 9/f5f30338 9/f5f30338 9/f5f30338
|
||||
22:27:20.362 t=49.7 s daa 13 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 13/9a570e80 13/9a570e80 13/9a570e80
|
||||
22:27:35.380 t=64.8 s daa 21 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 21/e9eb6357 21/e9eb6357 21/e9eb6357
|
||||
22:27:50.394 t=79.8 s daa 43 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 43/231339b1 43/231339b1 43/231339b1
|
||||
22:28:05.413 t=94.8 s daa 59 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 59/b7fc02c8 59/b7fc02c8 59/b7fc02c8
|
||||
22:28:06.415 epoch 0 -> 1 at daa 60, 95.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:28:20.430 t=109.8 s daa 72 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 72/b2806522 72/b2806522 72/b2806522
|
||||
22:28:35.450 t=124.8 s daa 94 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 94/79f760e8 94/79f760e8 94/79f760e8
|
||||
22:28:50.469 t=139.9 s daa 113 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 113/5561b157 113/5561b157 113/5561b157
|
||||
22:28:59.481 epoch 1 -> 2 at daa 120, 148.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:29:05.490 t=154.9 s daa 126 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 126/2349621f 126/2349621f 126/2349621f
|
||||
22:29:20.507 t=169.9 s daa 140 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 140/bc35ba71 140/bc35ba71 140/bc35ba71
|
||||
22:29:35.525 t=184.9 s daa 156 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 156/63d8089e 156/63d8089e 156/63d8089e
|
||||
22:29:50.544 t=199.9 s daa 171 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 171/18cedc1e 171/18cedc1e 171/18cedc1e
|
||||
22:30:01.556 epoch 2 -> 3 at daa 180, 210.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:30:05.560 t=214.9 s daa 183 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 183/72878aa2 183/72878aa2 183/72878aa2
|
||||
22:30:20.580 t=230.0 s daa 194 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 194/d912a5a6 194/d912a5a6 194/d912a5a6
|
||||
22:30:35.597 t=245.0 s daa 213 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 213/c32e5855 213/c32e5855 213/c32e5855
|
||||
22:30:50.614 t=260.0 s daa 230 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 230/6e0fe9f3 230/6e0fe9f3 230/6e0fe9f3
|
||||
22:30:55.619 epoch 3 -> 4 at daa 240, 265.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:31:05.626 t=275.0 s daa 254 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 254/5374343f 254/5374343f 254/5374343f
|
||||
22:31:20.640 t=290.0 s daa 270 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 270/2102c92b 270/2102c92b 270/2102c92b
|
||||
22:31:35.653 t=305.0 s daa 283 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 283/96ca2f27 283/96ca2f27 283/96ca2f27
|
||||
22:31:50.672 t=320.1 s daa 295 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 295/d8305233 295/d8305233 295/d8305233
|
||||
22:31:54.677 epoch 4 -> 5 at daa 300, 324.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:32:05.692 t=335.1 s daa 308 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 308/a6a3dd91 308/a6a3dd91 308/a6a3dd91
|
||||
22:32:20.714 t=350.1 s daa 322 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 322/6819393b 322/6819393b 322/6819393b
|
||||
22:32:35.737 t=365.1 s daa 346 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 346/354f160d 346/354f160d 346/354f160d
|
||||
22:32:48.751 epoch 5 -> 6 at daa 360, 378.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:32:50.754 t=380.1 s daa 361 epoch 6 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 361/af58d2f8 361/af58d2f8 361/af58d2f8
|
||||
22:33:05.772 t=395.2 s daa 382 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 382/71d14149 382/71d14149 382/71d14149
|
||||
22:33:20.794 t=410.2 s daa 399 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 399/9fb39463 399/9fb39463 399/9fb39463
|
||||
22:33:35.817 t=425.2 s daa 407 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 407/c60fa280 407/c60fa280 407/c60fa280
|
||||
22:33:48.836 epoch 6 -> 7 at daa 420, 438.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:33:50.840 t=440.2 s daa 421 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 421/237fb92b 421/237fb92b 421/237fb92b
|
||||
22:34:05.864 t=455.2 s daa 435 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 435/4b441312 435/4b441312 435/4b441312
|
||||
22:34:20.885 t=470.3 s daa 453 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 453/cd6b10e3 453/cd6b10e3 453/cd6b10e3
|
||||
22:34:35.908 t=485.3 s daa 468 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 468/07607c5a 468/07607c5a 468/07607c5a
|
||||
22:34:50.929 epoch 7 -> 8 at daa 480, 500.3 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
|
||||
22:34:50.930 LADDER STEP: the template is rung 1 (35 shadow passes) from epoch 8 (daa 480) at 500.3 s wall
|
||||
22:34:50.930 t=500.3 s daa 480 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 480/446f5cad 480/446f5cad 480/446f5cad
|
||||
22:35:05.952 t=515.3 s daa 494 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 494/cb995010 494/cb995010 494/cb995010
|
||||
22:35:20.972 t=530.4 s daa 507 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 507/6fdd3fa6 507/6fdd3fa6 507/6fdd3fa6
|
||||
22:35:35.991 t=545.4 s daa 523 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 523/9c372f3f 523/9c372f3f 523/9c372f3f
|
||||
22:35:51.015 t=560.4 s daa 539 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 539/89195e3c 539/89195e3c 539/89195e3c
|
||||
22:35:52.017 epoch 8 -> 9 at daa 540, 561.4 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
|
||||
22:36:06.037 t=575.4 s daa 555 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 555/e9ac1c74 555/e9ac1c74 555/e9ac1c74
|
||||
22:36:21.059 t=590.4 s daa 568 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 568/f092f0fb 568/f092f0fb 568/f092f0fb
|
||||
22:36:36.084 t=605.5 s daa 583 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 583/e3ffdab3 583/e3ffdab3 583/e3ffdab3
|
||||
22:36:51.107 t=620.5 s daa 592 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 592/27682e21 592/27682e21 592/27682e21
|
||||
22:36:57.114 epoch 9 -> 10 at daa 601, 626.5 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
|
||||
22:37:07.379 SUMMARY FAIL (expect step, signals up/up/up): rung 1 (35 passes) from epoch 8 at DAA 480; epochs seen e0:r0:0bps e1:r0:0bps e2:r0:0bps e3:r0:0bps e4:r0:0bps e5:r0:0bps e6:r0:0bps e7:r0:10000bps e8:r1:10000bps e9:r1:10000bps e10:r1:10000bps; chain bits {"none":1,"up":612} (9984 bps up); blocks 481 / 132; rejected miners 0/0/0 nodes 0/0/0; sinks c4fb461282cf9321 c4fb461282cf9321 c4fb461282cf9321 at 612/612/612
|
||||
22:37:07.379 PROGRAM ID epoch 0 seed 234e082d653dc69d reps 27: miners 26fc8f3decca98ed (3 of 3) cli at reps 26fc8f3decca98ed cli rung 0 26fc8f3decca98ed
|
||||
22:37:07.379 PROGRAM ID epoch 1 seed f3f15962195b3ed5 reps 27: miners 8a3027b727010ebb (3 of 3) cli at reps 8a3027b727010ebb cli rung 0 8a3027b727010ebb
|
||||
22:37:07.379 PROGRAM ID epoch 2 seed 08861cd8892dd5bf reps 27: miners 5ce393c9689621a3 (3 of 3) cli at reps 5ce393c9689621a3 cli rung 0 5ce393c9689621a3
|
||||
22:37:07.379 PROGRAM ID epoch 3 seed 1ea70b68ae751655 reps 27: miners 1e113fa683988a88 (3 of 3) cli at reps 1e113fa683988a88 cli rung 0 1e113fa683988a88
|
||||
22:37:07.379 PROGRAM ID epoch 4 seed 6e0fe9f3461ed4b7 reps 27: miners f78fdc10877a6a53 (3 of 3) cli at reps f78fdc10877a6a53 cli rung 0 f78fdc10877a6a53
|
||||
22:37:07.379 PROGRAM ID epoch 5 seed 756db10abdc2caa1 reps 27: miners c54ce8f09a0197fd (3 of 3) cli at reps c54ce8f09a0197fd cli rung 0 c54ce8f09a0197fd
|
||||
22:37:07.379 PROGRAM ID epoch 6 seed 71066f551557cca4 reps 27: miners 4ee83f583fd0d3a6 (3 of 3) cli at reps 4ee83f583fd0d3a6 cli rung 0 4ee83f583fd0d3a6
|
||||
22:37:07.379 PROGRAM ID epoch 7 seed f2f76ce991184adb reps 27: miners 32c35a3b0dfb22e8 (3 of 3) cli at reps 32c35a3b0dfb22e8 cli rung 0 32c35a3b0dfb22e8
|
||||
22:37:07.379 PROGRAM ID epoch 8 seed beb8b40fec4bf2c8 reps 35: miners d4cfc28a1c525077 (3 of 3) cli at reps ea49bc6e870eb34c cli rung 0 ea49bc6e870eb34c
|
||||
22:37:07.379 PROGRAM ID epoch 9 seed 04d782fc0d5def41 reps 35: miners 35c3a81b81cc2b02 (3 of 3) cli at reps 1fe5e84ead8e71e3 cli rung 0 1fe5e84ead8e71e3
|
||||
22:37:07.379 PROGRAM ID epoch 10 seed 3a51ee194be8d47b reps 35: miners 804c18c3a462f05c (3 of 3) cli at reps 4e6c5bbf6463131d cli rung 0 4e6c5bbf6463131d
|
||||
22:37:07.379 FAILED CHECK chain_carries_the_bits
|
||||
22:37:07.379 FAILED CHECK rung1_ids_equal_the_cli_rung1_id
|
||||
22:37:07.379 summary: /tmp/igneum-fast-time-ladder-step/summary.json
|
||||
CASE step rc=1
|
||||
1012
docs/design/latency-ladder-harness/step2.json
Normal file
76
docs/design/latency-ladder-harness/step2.log
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
22:47:26.794 signals up/up/up, expect step; class v4 from genesis, the ladder active from DAA 0, window 60 DAA x 7 (the first epoch that can step is 8, DAA 480); 60 DAA per epoch, lead 10; run 900 s or 10 epochs
|
||||
22:47:28.032 n0 up pid 1734618 json 29722 p2p 29721, signals up
|
||||
22:47:29.240 n1 up pid 1735138 json 29732 p2p 29731, signals up
|
||||
22:47:30.451 n2 up pid 1735605 json 29742 p2p 29741, signals up
|
||||
22:47:30.452 n0: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:47:30.452 n1: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:47:30.453 n2: Latency ladder from the override file: rungs 27, 35, 53, [88], [173], [267] shadow passes (brackets: inadmissible, never entered), active from epoch 0 (DAA score 0 rounded up to the epoch boundary at 0), one rung per decision at 90 percent of blue blocks in each of 7 consecutive windows of 60 DAA ending at an epoch's seed block | this node signals up
|
||||
22:47:30.453 n0 digest: b1f606bede788b70
|
||||
22:47:31.464 epoch -1 -> 0 at daa 0, 4.7 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 0 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:47:31.468 t=4.7 s daa 0 epoch 0 rung 0 (27) up 0 bps weakest 0 blocks/sink per node 0/234e082d 0/234e082d 0/234e082d
|
||||
22:47:46.507 t=19.7 s daa 2 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 2/93be64d1 2/93be64d1 2/93be64d1
|
||||
22:48:01.523 t=34.7 s daa 12 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 12/702814dd 12/702814dd 12/702814dd
|
||||
22:48:16.542 t=49.7 s daa 25 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 25/d0d9d954 25/d0d9d954 25/d0d9d954
|
||||
22:48:31.562 t=64.8 s daa 44 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 44/59a39e34 44/59a39e34 43/fbe3301e
|
||||
22:48:46.581 t=79.8 s daa 58 epoch 0 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 58/5d32aa5d 58/5d32aa5d 58/5d32aa5d
|
||||
22:48:49.585 epoch 0 -> 1 at daa 60, 82.8 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:49:01.599 t=94.8 s daa 72 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 72/d6ae0009 72/d6ae0009 72/d6ae0009
|
||||
22:49:16.617 t=109.8 s daa 89 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 89/37c27f2c 89/37c27f2c 89/37c27f2c
|
||||
22:49:31.633 t=124.8 s daa 102 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 102/cca31beb 102/cca31beb 102/cca31beb
|
||||
22:49:46.650 t=139.9 s daa 115 epoch 1 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 115/22238404 115/22238404 115/22238404
|
||||
22:49:49.654 epoch 1 -> 2 at daa 122, 142.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:50:01.669 t=154.9 s daa 128 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 128/3b025ca2 128/3b025ca2 128/3b025ca2
|
||||
22:50:16.687 t=169.9 s daa 148 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 148/eb6fd081 148/eb6fd081 148/eb6fd081
|
||||
22:50:31.708 t=184.9 s daa 162 epoch 2 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 162/f7591e43 162/f7591e43 162/f7591e43
|
||||
22:50:45.727 epoch 2 -> 3 at daa 180, 198.9 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:50:46.729 t=199.9 s daa 180 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 180/8508ae27 180/8508ae27 180/8508ae27
|
||||
22:51:01.749 t=215.0 s daa 192 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 192/d3cae989 192/d3cae989 192/d3cae989
|
||||
22:51:16.769 t=230.0 s daa 205 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 205/b7e9b38a 205/b7e9b38a 205/b7e9b38a
|
||||
22:51:31.789 t=245.0 s daa 224 epoch 3 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 224/4b576b96 224/4b576b96 224/4b576b96
|
||||
22:51:44.803 epoch 3 -> 4 at daa 240, 258.0 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:51:46.806 t=260.0 s daa 242 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 242/21eac0e7 242/21eac0e7 242/21eac0e7
|
||||
22:52:01.826 t=275.0 s daa 263 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 263/3a7f5eb1 263/3a7f5eb1 263/3a7f5eb1
|
||||
22:52:16.845 t=290.1 s daa 276 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 276/9fa09972 276/9fa09972 276/9fa09972
|
||||
22:52:31.861 t=305.1 s daa 292 epoch 4 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 292/c5cfe43d 292/c5cfe43d 292/c5cfe43d
|
||||
22:52:41.874 epoch 4 -> 5 at daa 301, 315.1 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:52:46.877 t=320.1 s daa 306 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 306/8a223b88 306/8a223b88 306/8a223b88
|
||||
22:53:01.894 t=335.1 s daa 319 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 319/3818e422 319/3818e422 319/3818e422
|
||||
22:53:16.914 t=350.1 s daa 333 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 333/5d9f5abe 333/5d9f5abe 333/5d9f5abe
|
||||
22:53:31.934 t=365.1 s daa 345 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 345/c186686a 345/c186686a 345/c186686a
|
||||
22:53:46.950 t=380.2 s daa 358 epoch 5 rung 0 (27) up 10000 bps weakest 0 blocks/sink per node 358/71308cca 358/71308cca 358/71308cca
|
||||
22:53:49.955 epoch 5 -> 6 at daa 360, 383.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 0), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:54:01.971 t=395.2 s daa 373 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 373/dc7f7051 373/dc7f7051 373/dc7f7051
|
||||
22:54:16.990 t=410.2 s daa 392 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 392/932e6a12 392/932e6a12 392/932e6a12
|
||||
22:54:32.010 t=425.2 s daa 401 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 401/c76de149 401/c76de149 401/c76de149
|
||||
22:54:47.031 t=440.2 s daa 418 epoch 6 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 418/3bfd34a2 418/3bfd34a2 418/3bfd34a2
|
||||
22:54:49.033 epoch 6 -> 7 at daa 420, 442.2 s: template class 4 rung 0 (27 passes), next rung 0 (27), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 0
|
||||
22:55:02.050 t=455.3 s daa 437 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 437/1eb8bef0 437/1eb8bef0 437/1eb8bef0
|
||||
22:55:17.071 t=470.3 s daa 450 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 450/8b056200 450/8b056200 450/8b056200
|
||||
22:55:32.093 t=485.3 s daa 459 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 459/75ec3917 459/75ec3917 459/75ec3917
|
||||
22:55:47.114 t=500.3 s daa 473 epoch 7 rung 0 (27) up 10000 bps weakest 10000 blocks/sink per node 473/022ba23c 473/022ba23c 473/022ba23c
|
||||
22:55:52.120 epoch 7 -> 8 at daa 481, 505.3 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
|
||||
22:55:52.120 LADDER STEP: the template is rung 1 (35 shadow passes) from epoch 8 (daa 481) at 505.3 s wall
|
||||
22:56:02.135 t=515.3 s daa 486 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 486/3e3ac762 486/3e3ac762 486/3e3ac762
|
||||
22:56:17.156 t=530.4 s daa 501 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 501/4de14a5f 501/4de14a5f 501/4de14a5f
|
||||
22:56:32.177 t=545.4 s daa 515 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 515/c64de9c2 515/c64de9c2 515/c64de9c2
|
||||
22:56:47.198 t=560.4 s daa 538 epoch 8 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 538/d24d285a 538/d24d285a 538/d24d285a
|
||||
22:56:50.203 epoch 8 -> 9 at daa 540, 563.4 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
|
||||
22:57:02.221 t=575.4 s daa 555 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 555/d8617bdb 555/d8617bdb 555/d8617bdb
|
||||
22:57:17.243 t=590.4 s daa 570 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 570/c16b6327 570/c16b6327 570/c16b6327
|
||||
22:57:32.263 t=605.5 s daa 586 epoch 9 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 586/f1afc7fb 586/f1afc7fb 586/f1afc7fb
|
||||
22:57:42.280 epoch 9 -> 10 at daa 600, 615.5 s: template class 4 rung 1 (35 passes), next rung 1 (35), up 10000 bps (weakest of 7: 10000), down 0 bps, this node signals 1, step took effect at epoch 8
|
||||
22:57:47.288 t=620.5 s daa 605 epoch 10 rung 1 (35) up 10000 bps weakest 10000 blocks/sink per node 605/407dd224 605/407dd224 605/407dd224
|
||||
22:57:58.524 SUMMARY PASS (expect step, signals up/up/up): rung 1 (35 passes) from epoch 8 at DAA 481; epochs seen e0:r0:0bps e1:r0:0bps e2:r0:0bps e3:r0:0bps e4:r0:0bps e5:r0:0bps e6:r0:0bps e7:r0:10000bps e8:r1:10000bps e9:r1:10000bps e10:r1:10000bps; chain bits {"none":1,"up":617} (9984 bps up); blocks 481 / 137; rejected miners 0/0/0 nodes 0/0/0; sinks f1cbdb99f48e0a19 f1cbdb99f48e0a19 f1cbdb99f48e0a19 at 617/617/617
|
||||
22:57:58.524 PROGRAM ID epoch 0 seed 234e082d653dc69d reps 27: miners 26fc8f3decca98ed (3 of 3) cli at reps 26fc8f3decca98ed cli rung 0 26fc8f3decca98ed
|
||||
22:57:58.524 PROGRAM ID epoch 1 seed 5fce000863234999 reps 27: miners 22db8cc739098c5f (3 of 3) cli at reps 22db8cc739098c5f cli rung 0 22db8cc739098c5f
|
||||
22:57:58.524 PROGRAM ID epoch 2 seed 8217681d05165ddb reps 27: miners 0ec4930eb73fccbd (3 of 3) cli at reps 0ec4930eb73fccbd cli rung 0 0ec4930eb73fccbd
|
||||
22:57:58.524 PROGRAM ID epoch 3 seed 4870884b48c88957 reps 27: miners 436c842444e67520 (3 of 3) cli at reps 436c842444e67520 cli rung 0 436c842444e67520
|
||||
22:57:58.524 PROGRAM ID epoch 4 seed 2505cecc606d69e8 reps 27: miners 4dd9f1e921cb066e (3 of 3) cli at reps 4dd9f1e921cb066e cli rung 0 4dd9f1e921cb066e
|
||||
22:57:58.524 PROGRAM ID epoch 5 seed e31a3a8e73690d75 reps 27: miners fead43c4c01d42da (3 of 3) cli at reps fead43c4c01d42da cli rung 0 fead43c4c01d42da
|
||||
22:57:58.524 PROGRAM ID epoch 6 seed 2674be5e8dbaba18 reps 27: miners 2c6402c1dcc39dae (3 of 3) cli at reps 2c6402c1dcc39dae cli rung 0 2c6402c1dcc39dae
|
||||
22:57:58.524 PROGRAM ID epoch 7 seed de28d9d30dad45b8 reps 27: miners 120eb6f07fdd1e2b (3 of 3) cli at reps 120eb6f07fdd1e2b cli rung 0 120eb6f07fdd1e2b
|
||||
22:57:58.524 PROGRAM ID epoch 8 seed 5fbfb48f6e4c66ba reps 35: miners 65ffdc96f1921062 (3 of 3) cli at reps 65ffdc96f1921062 cli rung 0 3d40916b72ae9b97
|
||||
22:57:58.524 PROGRAM ID epoch 9 seed 367f8d4b525fd620 reps 35: miners 62c19c11cb2909cf (3 of 3) cli at reps 62c19c11cb2909cf cli rung 0 9c6ad9fe883c3330
|
||||
22:57:58.524 PROGRAM ID epoch 10 seed 518f98485a18e924 reps 35: miners 2f5237323ef1a4bb (3 of 3) cli at reps 2f5237323ef1a4bb cli rung 0 2a003e3289fe04dc
|
||||
22:57:58.524 summary: /tmp/igneum-fast-time-ladder-step2/summary.json
|
||||
CASE step2 rc=0
|
||||
127
docs/design/latency-ladder.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# The latency ladder: N as a verifier-bounded genesis ladder stepped by miner signal
|
||||
|
||||
6 October 2026, night UK. Horizon finding 4 (`docs/analysis/horizon/algorithm.md` 5.3a and proposal 5; `frontier.md` 2.3). Branch `ladder` (repo) and `ladder-node` (fork, from `ca3-v4-0316` 8dbb7a23). Status: implemented behind `latency_ladder_activation_daa` (never until set; 0 on the testnet when the project lead says), ships in the 0.3.17 feature tree (0.3.16 went out on 6 October 2026 as a corrective cut of 0.3.15, node f1ea7a38, and carries none of this); not in `docs/spec` until adopted.
|
||||
|
||||
## 1. The finding in one line
|
||||
|
||||
The chip that matters stores the dataset; the reserve and the era draw buy nothing against it; the one lever is the latency-shadow size N, and an unconditional doubling per era retires the M5 Max at era 1 (-10.5 percent at 200,000 ops). So N moves inside a ladder fixed at genesis, one rung at a time, only when 90 percent of mining weight asks for it, never past the rung the reference verifier can still check under 10 ms.
|
||||
|
||||
## 2. Where N lives
|
||||
|
||||
| Item | Place | Value |
|
||||
|---|---|---|
|
||||
| The unit | `igneum_pow::ShadowClass { instrs: 256, reps }`: class v4 is `mx8` plus a 256-instruction shadow block run `reps` times per iteration; N in counted ops = 930 + 2,048 x reps x 1.83 (the convention of `latency-shadow-2026-10-06.md` s1) | reps is what the generator consumes; the ops label is derived (`igneum::latency_ladder_counted_ops`) |
|
||||
| The ladder | `Params::latency_ladder` (`consensus/core/src/config/params.rs`), a genesis list of six rungs `{ reps, admissible }`, in the override file as `latency_ladder: [{"reps": 27, "admissible": true}, ...]`, in the digest once the activation is set | reps 27, 35, 53, 88, 173, 267 = about 102,100, 132,100, 199,600, 330,700, 649,400, 1,001,600 counted ops: the measured rungs of 5.3a (35 passes is the integer rung nearest the 130,000 interpolation), then the two doublings; admissible tonight: rungs 0, 1, 2 (section 5) |
|
||||
| The switch | `Params::latency_ladder_activation_daa` (`u64::MAX` never; in the digest once set, the 0.3.15 rule, so a binary that carries the field peers with one that does not until a file sets it) | never on every network; 0 on the testnet when the project lead says |
|
||||
| The window | `Params::latency_ladder_window_daa` (one window of the seven; in the digest once the activation is set) | 86,400 DAA (one day); 120 on the fast-time profile, the class window's rounding |
|
||||
| The step | consensus state derived from signals, never stored: `processes::latency_ladder::step_of_epoch` walks the epochs' seed blocks down the selected chain exactly as `class_signal::program_class_of_epoch` does, memoised per seed block | rung 0 before the activation and at it |
|
||||
| The era draw | draws 8 (`epoch_len`) and 9 (the ladder) of the era stream are consumed and not used (`igneum_pow::era_draw`), so a later use of either slot changes no other parameter; the first seven draws and every pinned pack are unchanged | the value is set by signal, not by the draw, as `epoch_len` is (spec 01 1.13.1) |
|
||||
| The program | `EpochSeeds.shadow_reps` (0 = the class's own) → `Epoch::chain_program_shadow` → `generate_from_seed_bytes_program_class_shadow`: class v4 at rung r is `LoadClass::era(v4_class_at(reps_r), era, &V3_ALLOWED)`, generator 4; rung 0 is `V4_CLASS` byte for byte | the base program, the 16 loads, the day cache and the era draw are class v3's; only the block count moves |
|
||||
|
||||
## 3. The step rule (`igneum::latency_ladder_step_signalled`, pure; the tally shared with the class signal)
|
||||
|
||||
1. The carrier: two bits of the header `version` high byte. Bit 15 = up, bit 14 = down, both = no signal; the object version (the class signal) keeps bits 8 to 13 (`class_signal_of` masks with 0x3f; every value written so far is 4, so nothing changes). Stamped by the node from `Params::latency_ladder_signal` (`IGNEUM_LADDER_SIGNAL=up|down|none` on devnet and simnet; the app's toggle later) only while the ladder is active; the header version rule accepts the high byte while the class signal or the ladder is active.
|
||||
2. The weight and the windows: blue blocks, the finality rule's convention, `CLASS_SIGNAL_WINDOWS = 7` consecutive windows of `latency_ladder_window_daa` ending at the epoch's seed block `S_e`, one walk bucketed per window (`class_signal::tally_window` with the ladder predicate). The number the decision rests on is the weakest of the seven.
|
||||
3. The decision for epoch e, from the step of epoch e - 1: up one rung when every one of the seven windows is full, each has at least `LADDER_SIGNAL_THRESHOLD_BPS = 9,000` of its blue blocks signalling up, the rung above exists and is admissible, and the oldest window begins at or after the DAA score where the current step took effect; down one rung symmetrically (never below rung 0). Otherwise the step stands. 90 percent, not 95: the ladder is a parameter the genesis rules leave to miners within fixed bounds (the `epoch_len` precedent, spec 01 1.13.1), not a class change; 60 percent (a parameter the rules leave open) is too low for a move that retires cards.
|
||||
4. Where it lands: at the epoch boundary, decided at the seed block, so the miner learns the next epoch's rung one lead (600 DAA) before it, with `next_latency_ladder_reps` in every template; a class never changes inside an epoch, nor does a rung.
|
||||
5. The cool-down (the two-step guard): because the seven windows must lie wholly after the last step, the earliest next decision is 7 x W (seven days) after a step, and every counted signal was cast under the rung it moves from. A miner majority cannot jump two rungs in one decision and cannot take two decisions inside seven days.
|
||||
|
||||
## 4. The verifier bound
|
||||
|
||||
A rung is admissible only if the cold verify of one 32-lane warp on the reference core stays under 10 ms with the SMT sibling loaded. Measured as lane 2 measured class v4: `igneum-pow bench --seed igneum-genesis --day 2026-10-03 --class mx8+sh256x<reps> --warps 50` on igneum-build-1 (EPYC 9454P, core 40 at 3.8 GHz under schedutil, `nice -n 19 taskset -c 40`), the figure is `warp base 0: single cold run` with the same bench on core 88 (the sibling, `thread_siblings_list` 40,88) at the same time; the average of 50 warps and the quiet-core cold run are recorded beside it. The measurement runs under the box's `measure` hold (`remote-run.sh BR_MEASURE=1`: every build slot waits, the capacity layer yields), `tools/ladder/verify-bench-remote.sh`. The table is section 5; a rung over 10 ms is `admissible: false` in the genesis list itself, and the step rule never enters it. Changing a flag after genesis is a code change under the 90 percent upgrade path (spec 05 5.7); the owed O-1.14 laptop run can only tighten the list before genesis.
|
||||
|
||||
## 5. The measured table (igneum-build-1, 6 to 7 October 2026)
|
||||
|
||||
Run 2 (22:10Z, `docs/design/latency-ladder-bench/20261006T221034Z`, `tools/ladder/verify-bench-remote.sh` under the measure hold; core 40 read 1,500,000 kHz at the start under schedutil; box load average 25.4 before and 23.6 after from other agents' processes the hold does not exclude; igneum-pow sha256 3f7623f2...; every rung's lane-0 hash equal across the three runs of a rung):
|
||||
|
||||
| Rung | reps | Shadow instrs per hash | Counted ops (approx) | Cold, core alone (ms) | Avg of 50, alone (ms) | Cold, sibling loaded (ms) | Avg of 50, sibling loaded (ms) | Admissible |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 0 | 27 | 55,296 | 102,100 | 5.14 | 4.95 | 8.77 | 8.67 | yes (class v4 today) |
|
||||
| 1 | 35 | 71,680 | 132,100 | 5.57 | 5.52 | 8.87 | 8.51 | yes |
|
||||
| 2 | 53 | 108,544 | 199,600 | 5.57 | 5.35 | 9.23 | 8.91 | yes |
|
||||
| 3 | 88 | 180,224 | 330,700 | 6.07 | 5.89 | 10.08 | 9.84 | NO (by 0.08 ms under a load of 25; a quiet re-run before genesis may admit it) |
|
||||
| 4 | 173 | 354,304 | 649,400 | 7.38 | 7.23 | 12.38 | 12.09 | NO |
|
||||
| 5 | 267 | 546,816 | 1,001,600 | 8.88 | 8.50 | 14.96 | 14.58 | NO |
|
||||
|
||||
Reading: the loaded slope is about 12.6 us per 1,000 extra shadow instructions (8.77 to 14.96 over 491,520), lane 2's 12.1; rung 0 reads 8.77 against lane 2's 8.23 (its load average was 2.3), so the box's other work costs about 0.5 ms and rung 3 would read about 9.6 ms on a quiet box. The rule is the measurement, not the estimate: the genesis list carries rung 3 as inadmissible tonight, and a re-measurement on a quiet box (or the O-1.14 laptop) before the testnet genesis file is written can admit it. Per tier what the flags mean: the ladder as it ships can take N from 102,100 to 199,600 ops (two steps, each at least seven windows apart); at the top rung the Apple tier has given up about 10 percent of rate, the 5090 2.7 percent at its cap, the 4070 pays 21 W more, and a node on a 2019-class core still verifies a warp inside 10 ms with its sibling busy.
|
||||
|
||||
Lane 2's slopes predicted 8.23 + 12.1 us per 1,000 extra shadow instructions on the loaded core: 8.4, 8.9, 9.7, 11.9, 14.2 ms for rungs 1 to 5, so rungs 4 and 5 were expected out before the run. The genesis list in `params.rs` carries the flags this table gives. First run (22:06Z, `docs/design/latency-ladder-bench/20261006T220642Z`): the "loaded" column was NOT loaded (the known-failed case of the script: a 50-warp sibling finished during the measured run's own cache fill, which the fill time showed, 540 against 366 ms, while the warps then ran alone: 5.08, 5.20, 5.50, 6.07, 7.39, 8.83 ms); the script now keeps the sibling busy for the whole run and the table above is the second run's.
|
||||
|
||||
Also recorded in the first run: the quiet-core cold figures 5.09, 5.20, 5.51, 6.04, 7.45, 8.90 ms for rungs 0 to 5 (lane 2's class v4 cold figure was 5.06), so on the quiet reference core every rung including 1.0 M ops passes 10 ms; the loaded column decides.
|
||||
|
||||
## 5a. The chip anchor: what a determined vendor did to RandomX in seven years (coordinator's data point, 6 October 2026, night)
|
||||
|
||||
Bitmain's Antminer X9 is a shipping RandomX ASIC: about 1 MH/s at 2,472 W (2.47 J per KH), custom RISC-V cores, about USD 5,600, shipping July 2026 (github.com/monero-project/monero/issues/10270; every figure approximate, read from the issue by the coordinator). A Zen 4 desktop CPU at roughly 20 KH/s and 150 W is 7.5 J per KH (approximate), so the chip's per-joule edge over the honest device RandomX was built for is about 3x, seven years after launch. Two consequences for this project's own text and model:
|
||||
|
||||
| Item | Before | After |
|
||||
|---|---|---|
|
||||
| The precedent sentence | `latency-shadow-2026-10-06.md` s5 and `site/litepaper.html`: "no RandomX chip has beaten a CPU per joule", "no chip publicly shipped" | false since July 2026; corrected in the litepaper tonight (section 10 below) and owed in `latency-shadow-2026-10-06.md` and `chip-model-v3.md` |
|
||||
| The k column | k = 0.3 was "the attacker's claim" (a wide-SIMD fixed-datapath array at N5); k = 1 was "ours" and the headline (2.1x over the 5090 at class v4) | a shipped product reached about 1/3 of its honest device's energy on a latency-bound random program, so k about 0.33 is a measured class, not a claim; the honest headline is the range, 2.1x (k = 1) to 3.9x (k = 0.33) over the 5090 at rung 0, with the X9 bracket named. Approximate in both directions: the 5090's ALU is already a wide-SIMD part at 11 pJ per op, so a chip's gain over it on random ALU work should be under its gain over a CPU core, and the memory side (the f = 1 chip) is priced separately and is not moved by k |
|
||||
| The 5.7x | the f = 1 GDDR7 chip's per-joule edge at class v3 (memory only) | unchanged: no core work in it, k does not enter |
|
||||
|
||||
What N buys against a vendor at the X9's scale (energy per hash = memory + N x 11 pJ x k, `latency-shadow-2026-10-06.md` s5; the 5090 and M5 Max rows measured, algorithm.md 5.3a; k = 0.33; approximate):
|
||||
|
||||
| Rung | N (approx) | f = 1 GDDR7 chip, uJ | Edge over the 5090 at its 431 W cap | Edge over the M5 Max |
|
||||
|---|---|---|---|---|
|
||||
| 0 | 102,100 | 0.84 | 3.9x | 1.7x |
|
||||
| 1 | 132,100 | 0.95 | 3.4x | 1.5x |
|
||||
| 2 | 199,600 | 1.19 | 2.8x | 1.3x |
|
||||
| 3 | 330,700 | 1.67 | 3.0x (the 5090 itself is compute-bound at this rung, 86 MH/s) | 1.1x |
|
||||
|
||||
So against a vendor at the X9's scale the ladder narrows the chip's edge over the 5090 from about 3.9x to about 2.8x by rung 2 (the top admissible rung tonight) and does not close it; against the Apple tier it takes the edge to about 1.3x at rung 2 and would take it to about 1.1x at rung 3, where the Mac has given up 21 percent of its rate and which the verifier measurement keeps out until a quiet re-run. The lever that moves every row by the same factor is the honest card's own watts (algorithm.md proposal 7), and the public text says so. The ladder is the chain's only automatic answer; it is not the whole answer.
|
||||
|
||||
## 6. What moves the digest and the wire
|
||||
|
||||
| Change | Digest | Wire |
|
||||
|---|---|---|
|
||||
| The binary carrying the fields, nothing set | no (the 0.3.15 rule: the activation and the window enter only once set; the ladder list enters only once the activation is set) | header version unchanged; `PowEpochInfo` gains fields 25 to 36 with defaults, so an old miner reads reps 0 (the class's own) |
|
||||
| The file setting `latency_ladder_activation_daa` (and the window, and the list) | yes, once, at publish | the high byte may carry bits 14 and 15; a node without the ladder refuses the version when class signalling is inactive, which is the point of the rollout order (binary first, file second) |
|
||||
| A step | no | the program of the epoch changes: `program_id_class` with the `shadow/` bytes (rung 0 keeps `program_id(4, seed, attempt)`), the pack's `IGNEUM_SHADOW_REPS` and `seeds.txt`'s `shadow_reps` line (absent at rung 0, so every pack and job line written today is unchanged; the job line gains no token, because the three hosts strip only `class=` and `era=` from its tail) |
|
||||
|
||||
## 7. Hostile review
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Can a chip-holder vote the ladder down? | Down needs 90 percent of blue blocks in each of seven windows. A chip at 10 percent of hash cannot; at 90 percent the chain has a bigger problem than N, and the honest 10 percent still blocks the move (a 10 percent minority stops any move either way). |
|
||||
| Can a chip-holder stall it? | Yes, with over 10 percent of weight for the whole week: the status quo is kept. The status quo is an admissible rung the cards already run, so a stall costs the chain nothing it does not have today; the ladder is a lever miners pull, not a schedule a chip can be late for. The one defence it does not give is a forced move against a chip that holds 10 percent: that is the honest-card watts and the price per joule (algorithm.md 5.1). |
|
||||
| Can a miner majority jump two steps? | No: one rung per decision, and the next decision's seven windows must begin after the step took effect, so two moves are at least 7 x W apart (`latency_ladder_step_signalled`, tested). |
|
||||
| Can a week be bought? | 90 percent of blue blocks for seven consecutive days is 90 percent of hash for a week, in public, with the pending count on every console (`latency_ladder_up_weakest_bps`); the class signal's argument (one day can be bought, seven cannot unnoticed). |
|
||||
| What happens to a miner mid-step? | The rung is decided at the seed block, 600 DAA before the boundary; the template carries `next_latency_ladder_reps` and the miner prepares the next pack with it (the 2.0 era-boundary shape: a boundary that decides otherwise costs one refused pair and one prepare). A miner on a binary without the ladder keeps mining rung 0 after the step and loses every block until it updates; it can be at most 10 percent of hash by construction, and it had seven days of a rising count to see it. |
|
||||
| An inadmissible rung by a 90 percent majority? | Impossible: the rule reads the flag; the flag is a genesis constant in the digest. |
|
||||
| A proof-synced node | holds no headers below its pruning point, so an epoch whose windows reach below it cannot be tallied; it takes rung 0 (the floor) and logs that a ladder witness is owed, the class signal's shape (counter-asic-3-node.md 6.3). The witness in the proof format is the next node item, shared with the class witness. |
|
||||
| The program id | rung 0 keeps today's id; a rung above goes through `program_id_class`, which carries the shadow size, so two rungs of one seed never share an id and a pack of another rung is refused by the id check as a pack of another class is. |
|
||||
|
||||
## 8. Consequences per tier (measured rungs from algorithm.md 5.3a; the step is the miners' to take)
|
||||
|
||||
| Step | M5 Max | RTX 5090 at 431 W | RTX 4070 at 160 W | RX 9070 XT | Pool user | Verifier (half-core) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 0 → 1 (102,100 → 132,100) | -3.3 points of rate, 0 W more | 0 | 0 | 0 | 0 | +0.2 ms |
|
||||
| 1 → 2 (→ 199,600) | -6 more points | -2.7 percent | +21 W | 0 | 0 | +0.5 ms |
|
||||
| 2 → 3 (→ 330,700) | -21 percent | -35 percent (compute-bound at the cap) | -12 percent | +3.6 percent | 0 | +0.9 ms |
|
||||
| 3 → 4, 4 → 5 | unmeasured on cards; the verifier decides admissibility (section 5) | | | | | |
|
||||
|
||||
A chip's shadow core grows with N at k x 11 pJ per op; at rung 1 the f = 1 GDDR7 chip's edge over the 5090 at k = 1 falls 2.1x to 1.7x, at rung 2 to 1.3x. Which is why the step is the miners' who pay for it: rung 3 is a step the signal would refuse until cards change.
|
||||
|
||||
## 9. What is implemented, tested, owed
|
||||
|
||||
Public text: section 10.
|
||||
|
||||
Test and build lines (igneum-build-1 through `tools/build-remote.sh`, `IGNEUM_AGENT=ladder`, 6 October 2026, 22:00 to 22:20Z): igneum-pow `test --release`: 61 passed (the first run failed on the rung-1 label 130,000 against the arithmetic's 132,105, corrected to the arithmetic); the fork at 1591ee1d: kaspa-consensus-core 114 passed, 2 ignored (`latency_ladder_rule`, `override_params_carry_the_latency_ladder_and_the_digest_moves_only_when_set`, the 60x file test after the file gained the two 0.3.16-lane fields and `exec_restart_state_root` it lacked); kaspa-pow with the feature 16 passed (`latency_ladder_rungs_are_programs_of_their_own_over_one_day_cache`: one cache build for three rungs, rung 1 another pow, 27 passes equal to rung 0); kaspa-consensus lib 131 passed (the template carries bit 15 only while the ladder is active); igneum-miner, kaspa-rpc-core, kaspa-grpc-core green; `igneumd` and `igneum-miner` built, the commit string carried. Harness results: section 11.
|
||||
|
||||
|
||||
Implemented: the ladder, activation, window and signal in `Params`, `OverrideParams`, the digest and the 60x file; the pure rule and the bits in `igneum.rs`; `processes::latency_ladder` (the walk, the memo, the step); `EpochSeeds.shadow_reps` through the engine, the header path, the miner and the pack; `PowEpochInfo` and the gRPC fields; the daemon lines; `igneum-pow`: `v4_class_at`, the shadow program path, the id rule, the consumed draws, `--shadow-reps` on the CLI. Tests, the known-failed case first: a changed N today is a hard fork (the same seeds at reps 35 hash another program with the same class and, before the ladder, the same id); the step moves only by signal; 8,999 bps does not move it; a two-step jump is impossible; down never below 0; an inadmissible rung is never entered; rung 0 is `V4_CLASS` byte for byte. Fast-time harness `infra/fast-time/latency-ladder.mjs` (three cases and the failed case, the class harness's shape). Owed: the ladder witness in the pruning proof; the app's signal toggle and its consequences line; a rung check in the three hosts (today a host compiles whatever `IGNEUM_SHADOW_REPS` the pack carries and the Rust pack check binds the pack to its seeds, so a stale pack of another rung fails the miner's own check, not the host's); the O-1.14 laptop run.
|
||||
|
||||
## 10. Public text (the litepaper's hash-class paragraph, `site/litepaper.html`, Mining section) and the ledger row
|
||||
|
||||
The paragraph as placed in the litepaper: "**The work that waits can grow.** Class v4 adds a block of latency-shadow arithmetic to every hash, about 100,000 integer operations that run while the memory reads are in flight, so a chip that stores the whole dataset still has to pay for a core. That size sits on a ladder fixed at genesis, six rungs from about 100,000 to about 1,000,000 operations, and it moves one rung at a time only when 90 percent of blue blocks in each of seven consecutive days ask for it; it can never move two rungs inside a week and never past a rung the reference verifier cannot check under 10 ms with its sibling thread busy (measured on the build server: every rung's cold verify stays under that gate on the quiet core, 5.1 to 8.9 ms). What it buys, on the measured cards: against a dataset-storing chip whose core costs what an RTX 5090's does per operation, the chip's per-joule edge falls from 2.1x at the first rung to 1.3x at the third; against a core as good as the shipping RandomX chip's (Bitmain's Antminer X9, about 3x per joule over a desktop CPU after seven years, approximate), from 3.9x to 2.8x. What it costs, per rung, is measured too: the Apple tier gives up 3 points of rate at the first step and 6 more at the second, the RTX 5090 nothing until the second; so the miners who pay for a step are the ones who take it." The sentence "Monero has run on RandomX since 2019 with no chip publicly shipped" is replaced by the X9 fact.
|
||||
|
||||
The ledger row is M34 in `docs/fud-ledger.md`.
|
||||
|
||||
## 11. The fast-time gate (igneum-build-1, 6 October 2026, 22:15 to 22:58Z; summaries and logs copied to `docs/design/latency-ladder-harness/`; `infra/fast-time/latency-ladder.mjs`, the ladder fork's Linux binaries built on the box, three nodes and three CPU miners on `override-60x.json` with class v4 from genesis, class signalling off, the ladder active from DAA 0, windows of 60 DAA)
|
||||
|
||||
| Case | Run | SUMMARY | Summary file |
|
||||
|---|---|---|---|
|
||||
| The known-failed case first (`--signal up,up,none --expect step`) | 22:15 to 22:26Z | FAIL as it must: no step over epochs 0 to 10, the weakest-of-seven up share 6,166 to 6,333 bps at the sink from epoch 7 (two CPU miners of three), on the chain 406 blocks up and 198 none (6,722 bps up), no step line on any node, 0 rejected, one sink at 603/603/603; `FAILED CHECK` on eight step checks plus `chain_carries_the_bits`, which was the harness's own fault (genesis carries header version 0 and the low-byte check read it; fixed in the file and recorded there), exit 1 | `/srv/builds/_log/ladder-harness/failed-case.json` |
|
||||
| All three signal up (`--signal up,up,up --expect step`) | 22:26 to 22:37Z | the chain did what the rule says: `Latency ladder step by miner signal: epoch 8 moves to rung 1 (35 shadow passes, from rung 0): up in each of 7 consecutive windows of 60 DAA ending at seed block beb8b40f..., threshold 9000 bps, weakest up 10000 bps, ... 420 of 420 blue blocks up` on 3 of 3 nodes with the same epoch; the template at rung 1 from epoch 8 (DAA 480, the first epoch whose seed block has seven full windows below it) at 500 s wall; epochs 9 and 10 stayed at rung 1 (no second step inside seven windows); 481 / 132 blocks across the boundary; 0 rejected; one sink at 612/612/612; 612 blocks up and genesis none (9,984 bps up), every object byte 0; the three miners' rung-1 ids equal each other and differ from the same seed's rung-0 id on epochs 8, 9 and 10 (e8 `d4cfc28a1c525077` against `ea49bc6e870eb34c`). The harness reported FAIL on two checks of its own: the genesis low-byte fault above and `rung1_ids_equal_the_cli_rung1_id`, because `igneum-pow show` built its program without `--shadow-reps` (fixed at 6b30e85; the miners' ids were the chain's). Re-run as step2 below with both fixes | `step.json` |
|
||||
| Two of three signal up (`--signal up,up,none --expect no-step --epochs 10`) | 22:37 to 22:47Z | PASS: no step over epochs 0 to 10; the weakest-of-seven up share at the sink 5,833 bps from epoch 7 (the third miner's blocks carry no bits); on the chain 402 blocks up and 200 none (6,678 bps up); no step line on any node; 0 rejected; one sink `bfc435d62d7c659c` at 601/601/601; every rung-0 id equal to the CLI's | `no-step.json` |
|
||||
| All three signal up, the fixed harness and CLI (`--signal up,up,up --expect step`, step2) | 22:47 to 22:58Z | PASS on all 18 checks: the step line on 3 of 3 nodes at epoch 8 (weakest up 10,000 bps, 420 of 420 blue blocks up); the template at rung 1 (35 passes) from epoch 8 at 505 s wall; epochs 9 and 10 at rung 1, one step line per node (no second step inside seven windows); 481 / 137 blocks across the boundary; 0 rejected; one sink `f1cbdb99f48e0a19` at 617/617/617; 617 blocks up and genesis none (9,984 bps), every object byte 0, every mined block's low byte 2; the three miners' rung-1 ids on epochs 8, 9 and 10 equal the CLI's `--shadow-reps 35` id and differ from the same seed's rung-0 id | `step2.json` |
|
||||
|
|
@ -182,6 +182,15 @@ Answer: Correct. The measured 2.4 G/s had been read on 6 October as a mapping ar
|
|||
|
||||
Evidence: `docs/analysis/horizon/algorithm.md` section 5.1 (the ceiling table: measured 2.4, tFAW-bound 2.3, tRRD-bound 2.8, bank-bound 11.4, O'Connor 10.7 G reads/s, and the F2 measurement plan), 6 October 2026; JEDEC HBM2 timings as carried by ICCAD 2021 Table I; Shuhai, FCCM 2020, Fig 7.
|
||||
|
||||
### M34. The shadow size N is a constant of the binary, so the one lever against the dataset-storing chip needs a fork to move
|
||||
"Your own Horizon lane says the reserve and the era draw buy nothing against a chip that stores the dataset, and that the only lever is the latency-shadow size N. N is 27 passes of a 256-instruction block, hard-coded in `V4_CLASS`. So when HBM4 doubles a chip's rate per stack in 2028, your answer is a hard fork, and a fork that retires the M5 Max at the first doubling. And now there is a shipping RandomX ASIC."
|
||||
|
||||
Status: Conceded, implemented (6 October 2026, night; `docs/design/latency-ladder.md`, branch `ladder`, fork branch `ladder-node`, the 0.3.17 feature tree, behind `latency_ladder_activation_daa`, never until set, 0 on the testnet when the project lead says): N is a genesis ladder of six rungs (27, 35, 53, 88, 173, 267 passes; about 102,100 to 1,001,600 counted ops) with a measured admissibility flag per rung (cold verify under 10 ms on the reference core with its SMT sibling loaded, igneum-build-1, 6 October 2026: rungs 0 to 2 pass at 8.77, 8.87 and 9.23 ms, rung 3 misses by 0.08 ms under a box load of 25 and is out until a quiet re-run, rungs 4 and 5 are out at 12.38 and 14.96), and the step is consensus state derived from two bits of the header version: up one rung when 90 percent of blue blocks in each of seven consecutive windows ask for it and the rung above is admissible, down one rung symmetrically, never two rungs inside seven windows (the oldest window must begin after the last step took effect), never unconditionally. Tests, the known-failed case first: a changed N today hashes another program under the same program id (a hard fork no pack line told apart); after, rung 0 is class v4 byte for byte, a rung above carries its pass count in the id, 8,999 bps in one window of seven does not move the step, a two-step jump is impossible, down never passes rung 0, an inadmissible rung is never entered. Stated in `site/litepaper.html`, Mining section ("The work that waits can grow").
|
||||
|
||||
Answer: Correct on both counts, and the second was the sharper one. The X9 (Bitmain, about 1 MH/s at 2,472 W, approximate, github.com/monero-project/monero/issues/10270) is a shipped 3x per-joule edge over a desktop CPU on the best-known latency-bound random-program design, seven years after launch; it makes the k = 0.3 column of the chip model a product class rather than an attacker's claim, and the public headline is now the range 2.1x (k = 1) to 3.9x (k = 0.33) over the RTX 5090 at class v4, with the ladder taking the X9 bracket to about 2.8x by rung 2 and the Apple tier's to about 1.1x by rung 3. The ladder does not close the gap; it is the chain's only automatic answer, it moves at the pace of the cards that pay for it, and the honest card's watts remain the lever that moves every row (algorithm lane proposal 7). What the ladder gives up by design: a chip holding over 10 percent of weight can stall it, and the status quo it stalls is a rung the cards already run.
|
||||
|
||||
Evidence: `docs/design/latency-ladder.md` (the rule, the hostile review, the measured verifier table, the X9 arithmetic); `igneum-pow/src/generator.rs` test `latency_ladder_known_failed_a_changed_n_was_a_hard_fork_and_rungs_are_class_v4`; the fork's `consensus/core/src/igneum.rs` test `latency_ladder_rule`, `consensus/pow/src/igneum.rs` test `latency_ladder_rungs_are_programs_of_their_own_over_one_day_cache`; `infra/fast-time/latency-ladder.mjs` (the step, no-step and known-failed cases).
|
||||
|
||||
## 2. Finality and attacks
|
||||
|
||||
### F1. Finality is attackable for the first month
|
||||
|
|
@ -1772,6 +1781,8 @@ Answer: Correct for v0, and by design for now. The consensus check on a carried
|
|||
|
||||
Round 2 (5 October 2026, night): the public text now carries the v0 fact, labelled Open: `site/litepaper.html`, Proving, "How a block gets proven": "Open: in proving v0 the SP1 proof itself is checked off the consensus path, by every block producer before it carries a record, and the native-execution check is what consensus enforces; whether the SP1 verifier moves inside consensus is a decision before the public testnet." Status unchanged: decision owner the project lead, decisions item 11.
|
||||
|
||||
Round 4 (6 October 2026, 21:0x UTC, the 0.3.17 exec work): Fixed on a branch, pending merge. Fork `exec-sync-0313` da2d17ec puts the proof check on the consensus path: from `proving_consensus_verify_daa` (never by default; with `proving_shard_program_id` and `proving_aggregator_id` in the digest once set) body validation in context verifies every carried record's proof in-process (sp1-sdk's LightProver, the keys embedded from `proving/igneum-prove/elf`) against the pinned program id and the record's statement; a failing proof invalidates the block (`RuleError::IgneumInvalidProofRecord`, the relaying peer disconnected); a proof not held is fetched from the relaying peer (`IgneumRequestProofRecords`, payload 78) and the block retried once, never marked. Measured: 0.204 to 0.232 s a proof on one M5 Max core, 0.400 s on igneum-build-1, release, in-process; the harness `tools/exec-sync/forged.mjs` 8/8 in 23.6 s (an attacker with the rule off carries a forged record; the honest peer asks for the proof, refuses the block in 0.000 s of verify, pays nothing). A node whose embedded keys are not the pinned ids refuses to start. Open for 0.3.17.1: the finality-horizon skip (a block a certified checkpoint covers needs no proof check), so a fresh joiner's syncer serves proofs only for the unfinalised window.
|
||||
|
||||
### P22. The rewards and payouts are inputs to the shard proof, not outputs
|
||||
"The shard guest takes the segment's rewards and the prover payouts as data and commits the post-root after them. A host can feed any list and the proof still verifies."
|
||||
|
||||
|
|
@ -2316,6 +2327,13 @@ Same rule as the count above. 167 entries (P23 added by round 2).
|
|||
| Open, found tonight, fix in round 3 | 1 | P23 |
|
||||
| Another agent's tonight | 1 | C4 |
|
||||
|
||||
## Status updates, 6 October 2026 (21:0x UTC, the 0.3.17 exec work)
|
||||
|
||||
### X34. The prover's segment exports filled the disk under the node
|
||||
The fleet's provers (the kit's box-prover.py) exported the chain with `igneum_exportSegments [0, last]` for every segment, 50 to 500 MB a file, and nothing pruned them: between about 13:00Z and 20:44Z on 6 October 2026 the exports reached 42 GB on the hub (586 segment dirs on a 60 GB disk, 100 percent) and 3 to 46 GB on every standing box (p1-4090 and p2-4070-1 at 100 percent with 45 and 46 GB; p1-4070 97 percent, p2-3090-4 94 percent, p1-3080 87 percent, p1-a5000 81 percent, p2-4090-3 79 percent, p2-3090-3 75 percent, p1-5090 74 percent, p2-3090-2 55 percent, p2-3090-1 38 percent, p2-4090-1b 30 percent, the 8x rig 28 GB), 4 to 6 GB an hour a box. The hub's node died three times: 19:58:08Z and 20:01:55Z on the sync KeyNotFound (a peer's gap sync below retention, another class), 20:42:31Z on "No space left on device"; the supervisor restarted it at 19:59:11Z, 20:02:36Z and 20:44:38Z.
|
||||
|
||||
Status: Fixed (6 October 2026, 21:0x UTC). At the source: the 0.3.14 export is `[first-1, last]` with the account dump, a few hundred KB a segment, not 50 to 500 MB. In the kit: gpu-fleet f9ad70e (every standing box): a 20 GB and 168 h cap on the export dir enforced before every export, the export deleted the moment its record is accepted or paid, no export under 10 percent free disk (logged); the supervisor's 20-minute prune stays as the belt. In the app: exec-app-0314 4159818 and proving-v2 717148d (`app/igneum-app/src/provingdir.rs`): the same caps on `<app dir>/proving` (IGNEUM_PROVING_DIR_MAX_GB 20, IGNEUM_PROVING_DIR_MAX_DAYS 7, which leave 80 GB of a 100 GB box to the node and the system), a segment's directory deleted on submission, the free-disk floor before each export with the skip on the Prove page; the cap is a pure function with a unit test. Consequence per tier: a home miner's app never grows its proving dir past 20 GB or a week, and stops exporting (not mining) when the disk is under 10 percent free; a box or a rig the same through the kit.
|
||||
|
||||
## Count by status, 6 October 2026 (17:30 UTC, round 3 merged and the owner's decisions applied)
|
||||
|
||||
Same rule as the counts above. 167 entries.
|
||||
|
|
|
|||
|
|
@ -296,8 +296,76 @@ record. Known in 0.3.13 on PC 1 until 0.3.14: the rows show the app's own earlie
|
|||
to receive the run's result), and the 80% caps are the app's setting; the draw is the tuned one regardless because
|
||||
the driver holds the clock locks and the caps do not bind.
|
||||
|
||||
### The 0.3.14 window (6 October 2026, 18:18 to 18:27Z): no tune ran; five causes
|
||||
|
||||
The shipped 0.3.14 app, Power control on, the task registered, asked over its own API to tune each card
|
||||
(`relay/playbooks/ember-installed-tune-pc1.ps1`). From its log and helper.log:
|
||||
|
||||
| | When (UTC) | What | Cause | Fix (0.3.15) |
|
||||
|---|---|---|---|---|
|
||||
| C | 17:55:25 | the app's own first 5090 tune wrote its first request in the second the helper started; the helper skipped it as "present at my start"; 30 s later "did not take (card reports 460 W, acknowledged true)" | the engine wrote before the helper was up | the helper writes a heartbeat (`helper.alive`) every poll; the engine starts the task and waits for a fresh heartbeat BEFORE writing (`powertask::ensure_running`) |
|
||||
| A | 17:55:55 | the 4070's helper start read the stale `quit` from the 5090's abort and exited in the same second | quit/remove matched before the sequence check | the helper acts only on lines added after its start (`commands_after`) |
|
||||
| D | 17:57:27 | the 9070 XT, measure only ("igneum-gpu-telemetry gave no tune line": the 3.0 coordinator's job was on the ADLX helper), was still sent a set; refused; "tuning stopped" | a measure-only card was set | `ember::may_set`: measure only sets nothing |
|
||||
| E | 18:18 to 18:27 | three "Tune now" requests sat "queued" behind the hour's back-off from the 17:55 failures; none started | `sweep_retry` blocked a forced request silently | a forced request clears the back-off; a held row says "tuning: retry in N min (why)" (`ember::retry_note`) |
|
||||
| B | (latent) | the engine's helper flag stayed true after the helper's idle exit; a later step would have been acknowledged after a blind 4 s sleep | a flag instead of a fact | the heartbeat decides; the acknowledgement is the helper's own log line for the sequence (up to 15 s), a missing line fails the step with that reason |
|
||||
|
||||
What a user on 0.3.14 sees: Power control on, no prompt, and no card tunes (the rows keep the baseline or
|
||||
"tuning stopped"); the lever-2 TUNING records still flow. 0.3.15 closes it; the window repeats there.
|
||||
|
||||
### The 0.3.16 window (6 October 2026, 22:16 to 22:25Z): A to E held; a sixth cause (F) stopped the climbs
|
||||
|
||||
`ember-installed-pc1-2`, unelevated, the installed 0.3.16 app (the branch at 5429e82) tuning itself through the task.
|
||||
Gate ok; prompts 0; the helper started once per NVIDIA card and stayed up (task state Running at the end).
|
||||
|
||||
| Card | Plan | Result |
|
||||
|---|---|---|
|
||||
| RX 9070 XT | baseline, measure (the probe gave no tune line again) | "Measured: 19.2 MH/s at 202 W (0.095 MH/W)", gclk 3298, mclk 2505, 62 C, ok; no set sent (D held). Open: why the installed app's probe gets no tune line when run 6's kit engine did |
|
||||
| RTX 5090 | climb, 5 probes, floor 1390 MHz, mode helper | request 1 "the helper did not run sequence 1 within 15 s (no line in helper.log)"; 211.5 W before, 208 W after, unchanged |
|
||||
| RTX 4070 | climb | the same; 74.8 W before, 75.5 W after |
|
||||
|
||||
Cause F (cmd.txt read back: `00 dev 1 / 01 pl 160 / 02 rgc / 03 rmc`): the tune path wrote its request index as the
|
||||
wire sequence while the cap path had written unix-based numbers earlier in the same helper session; the helper runs
|
||||
only numbers above every one it has seen, so every tune command read as stale. The honest acknowledgement (B) said
|
||||
so within 15 s instead of a blind yes. Fix (cbd4d51, 0.3.17): one monotonic wire space for every writer
|
||||
(`powertask::wire_seq`, the unix time modulo 999,990), the acknowledgement keyed to the wire number; test. The
|
||||
elevated runs never crossed F (they set limits directly), which is why run 6 tuned and the self-tune did not.
|
||||
|
||||
## 8a. Next-cut notes (for the 0.3.12 shipper)
|
||||
|
||||
Correction (6 October 2026, 19:0xZ): release-0.3.15 took ember-tune at 5429e82 (merge 886c075), because Miner UI 4's
|
||||
Cards tab ships in 0.3.15 and reads the per-card goal and the before fields; so the row below is IN 0.3.15 except
|
||||
`card.tune_floor` (995530b), which rides 0.3.17.
|
||||
|
||||
Numbering (the shipper, 6 October 2026, 21:xx UK): 0.3.16 is a corrective cut of 0.3.15 under a new number (the Mac and PC 1 had taken earlier 0.3.15 builds), so everything below written as "0.3.16" ships as 0.3.17; the 0.3.15/0.3.16 content is the branch at 5429e82.
|
||||
|
||||
For 0.3.17 (main, 6 October 2026 evening), the engine fields Miner UI 4 reads, on ember-tune past 5429e82:
|
||||
|
||||
| Field | What | Where |
|
||||
|---|---|---|
|
||||
| `state.mining.watts_total` | the sum of the mining cards' draw (a reading under 60 s old) | `ember::fleet_watts`, engine `derive` + test |
|
||||
| `state.mining.pounds_per_day` | that draw as £ a day at `settings.power_price_pence` (0 when no price) | `ember::pounds_per_day` |
|
||||
| `state.address.balance_wei` | the payout address's balance in wei as a decimal string, `eth_getBalance` through the node's own RPC every 30 s while the node runs (60 s after a failure); null until read; `balance_age_s` (-1 until then), `balance_note` (the last error in words) | engine `tick_balance`, `Cmd::BalanceRead`, `ember::wei_from_hex` + test |
|
||||
| `state.address.price_gbp_per_ign` | null. Its one source will be a SIGNED field of the OTA manifest (`price`: gbp_per_ign, as_of, source), checked like the tuning object; the app never computes or fetches a price itself | state.rs (documented), no code until a market exists |
|
||||
| (F) the helper's wire sequence | one monotonic space for every writer of cmd.txt (`powertask::wire_seq`); the tune path's two-digit request index read as stale beside the cap path's unix-based numbers, so the 0.3.16 self-tune's climbs stopped at their first request | powertask.rs, engine.rs + test |
|
||||
| Horizon polish Q4 (updater) | `fork_is_close`: an activation height at or below the DAA has passed, nothing is pending (the old rule read it as close: every update since the 0.3.14 manifest said "0 blocks away, installing now", stripped Later and skipped every guard; PC 1's 17:52:54Z install under a job came through it); `publish-manifest.sh` refuses an activation height at or below the live DAA (/api/live state.daa) unless `--allow-passed-activation` | manifest.rs + test, packaging/ota/publish-manifest.sh |
|
||||
| Horizon polish Q83/Q84/Q2 (the pause shown) | `state.finality.paused`, `paused_since` (the last lock's time, else the engine's start), `reason`, `held_by`, `line` = "Finality paused since 18:39 UTC: under two thirds of the weight is signing" (the node's cause when it carries one: the engine parses a node log line carrying `finality_reason=<words or "quoted words"> held_by=<id>`, the node lane's to emit); the node line, the Overview's state and the Finality card show the one sentence while paused, the Finality card's age reads "paused", and no surface calls a lock final; `finality.message` carries the sentence too so older UIs show it | ember.rs `finality_paused_line`, `parse_finality_line`, `hhmm_utc` + tests; engine.rs derive and the node-line parse; ui/app.js `View.finalityWords` + test |
|
||||
| the finality rule (updater) | Horizon frontier lane: the updater installs nothing while the network's finality is paused (a synced node with no checkpoint lock for `manifest::FINALITY_PAUSE_S` = 15 min; the last LOCK line's age, else the engine's uptime); the update card reads "waiting for finality: ..."; slot, catch-up and patience rules unchanged otherwise; only the signed manifest's own `urgent` flag installs through a pause (a fork-close or unsupported urgency does not); the known-failed case is the test | manifest.rs `Moment.finality_paused`, `manifest_urgent`, `Manifest.urgent`, `safe_to_apply` + test; ota.rs `Ctx`; engine.rs |
|
||||
| `GET /api/live` | the observer's reply shape from a local source: `"source": "node"` when igneumd carries `igneum_getRecentBlocks(seconds)` (the node lane, a283f5f0d364ceef0; the engine computes miners_10m, blocks_10m, blocks_per_minute, the 90 s blocks and the miners list from it and takes the DAG numbers from its node state), else `"source": "site"` (the public reply, fetched by curl, cached 60 s) with `age_s`; `pending: true` before the first fetch | src/live.rs (`shape_from_blocks`, `parse_recent` + tests), server.rs |
|
||||
|
||||
For Miner UI 4's Cards tab (Ember as its second layer), 0.3.17, on ember-tune:
|
||||
|
||||
| Commit | What | Where |
|
||||
|---|---|---|
|
||||
| 5429e82 (in 0.3.15), 995530b (`tune_floor`, next cut) | a per-card goal: `POST /api/tune/goal {key, goal}` (efficiency, balanced, rate; "" or "global" clears), persisted as `CardPref.tune_goal`, echoed as `card.tune_goal` ("" = the global goal), resolved at the tune's start (`ember::goal_for`); `card.tune_floor` (the chosen clock is the ladder's floor; `CardPref.sweep_floor`); the untuned point of the last full plan on the row: `card.tune_before_watts`, `card.tune_before_mhs` (its step 0, kept across confirm plans; `CardPref.sweep_before_*`), so the row can read "saves 84 W, 0.15% of rate" against `sweep_watts` / `sweep_mhs`; Retune = `POST /api/sweep/start {key}` (forced: clears a back-off since 0.3.15) | ember.rs, engine.rs, server.rs, config.rs, state.rs, hotplug.rs + test |
|
||||
|
||||
For the 0.3.15 cut (6 October 2026 evening), on ember-tune (taken at 5429e82, see the correction above):
|
||||
|
||||
| Commit | What | Where |
|
||||
|---|---|---|
|
||||
| a8cab23 | (A) the helper acts only on lines added after its start; the acknowledgement is the helper's log line for the sequence | powertask.rs `commands_after` + test, engine.rs |
|
||||
| (tip) | (B, C) the heartbeat, `ensure_running` before every task-path write; (D) `may_set`; (E) the back-off yields to a forced request, `retry_note`; the window playbook's gate at 0.3.15; `ember-tune-pc1.ps1` deleted (superseded by the installed-tune playbook) | powertask.rs, engine.rs, ember.rs + tests, relay/playbooks |
|
||||
| e70ac5e | every task command goes to the helper's fixed folder | platform.rs, powertask.rs, engine.rs, main.rs + test |
|
||||
|
||||
For the 0.3.14 cut (6 October 2026, after run 6), on ember-tune:
|
||||
|
||||
| Commit | What | Where |
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ name" and "the login" stand for the values the script reads from the history its
|
|||
| The intake key | 6 tracked files, 8 commits (`78df757` to `4c9810f`) | `packaging/mac/packaged-config.sh`, `infra/gpu-bench/upload.sh`, `proving/windows-wsl2/prove-block.sh`, `prove-shard.sh`, `proto-cuda/windows-miner/upload-log.bat`, `proto-cuda/windows-app/upload-log.bat` |
|
||||
| The dl token | 1 tracked file, 1 commit (`c47ff03`) | `docs/plans/morning-2026-10-04.md` |
|
||||
| The `.next` rotations of both | 0 files, 0 commits | `~/.config/igneum/log-intake-key.next`, `dl-token.next` (4 October 19:25) are not in the tree |
|
||||
| After the 5 October rotation (confirmed 5 October 2026, night, ledger G14) | the values IN THE HISTORY are the OLD ones | `~/.config/igneum/log-intake-key.old-2026-10-05` and `dl-token.old-2026-10-05` hold them; the live files hold the new values, which are in 0 tracked files. `replace.txt` must be written from the `.old-2026-10-05` files, not from the live ones, or the pass replaces nothing. The relay key, the relay token, the relay run key and the machine secrets (`relay-machines/`) are in 0 files and 0 commits |
|
||||
| The relay key and token (current and old) | 0 files, 0 commits | |
|
||||
| The review files | `docs/fud-ledger.md` (36 commits from `39c20b7`), `docs/fud-fixes.md` (6 from `e7545d5`), `docs/review/` (4 from `5ab296c`), `site/ledger.html` (5 from `0ec11be`) | tracked, not ignored |
|
||||
| Tracked files carrying the first name (case-insensitive) | 71 at HEAD; 93 commits touch such content; 10 commit messages carry it | `CLAUDE.md`, the agent file, plans, packaging, the app's WSL paths, the Chrome profile rule |
|
||||
|
|
|
|||
|
|
@ -275,3 +275,35 @@ the quit ask, paused, syncing, the clock, the update card, the job strip, the lo
|
|||
| A Windows and a PC screenshot of the row with a real NVIDIA card (the telemetry cells, the cap slider, Retry) | the 0.3.14 cut, first thing to look at |
|
||||
| The row's state word while tuning carries the meta after it ("tuning: step 7 of 9 · measuring 441 W · 32 GB · ...") and is cut on a narrow row | a later pass: drop the meta while tuning |
|
||||
| The merge with `ember-tune`: `app.js` takes this branch's file (it already carries ember-tune's UI words and test); the engine files merge by themselves | the 0.3.14 shipper |
|
||||
|
||||
## 14. Polish round (6 October 2026, after the 0.3.14 cut)
|
||||
|
||||
the project lead, on the live Mac app: "I do think this could look better", at the job strip reading "Job: 0.3.14 is published:
|
||||
re-read the manifest and install now (...) done after 0 min. Report uploaded." Branch rebased on master 630b537.
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| No raw job, manifest or relay text on a user surface. The strip reads "A job from the team ran: update check, 0 min." / "A job from the team is running: shard benchmark, 6 min. Proving shard 3 of 8." / "A job from the team failed: build, after 32 min." / "Updated to 0.3.14 at 18:52."; the technical line (the job's title, summary, RESULT line, exit code) sits behind a chevron; the × stays. `Notices.jobWord` maps a kind or an id prefix to a user's word (update check, restart, log collection, shard benchmark, build, tuning check, a script). | app.js Notices |
|
||||
| The Activity feed speaks in the app's voice: `View.plainEvent` maps every engine event string (`src/engine.rs` et al., 80 patterns) to a sentence ("Block found by Apple M5 Max", "Rule settings read from the team", "The node stopped (code 101); restarting", "A job from the team ran: update check, 0 min"); the engine's own line is the row's tooltip; the kind word becomes a coloured dot. Unknown lines keep their words with a JSON tail cut. | app.js View, renderMine |
|
||||
| The header: title, grey subtitle and the pill share one baseline; the pill is a sentence with a coloured dot: "Mining · 511 MH/s" (molten), "Tuning" (molten), "Paused", "Syncing the node", "Starting the worker", "Not mining" (grey), "Node restarting", "Node failed", "Engine not answering" (ember), "Stopping", "Detecting cards", "Setting up". `View.pill`. | app.js, app.css |
|
||||
| Three Mine layouts were offered behind `?variant=a|b|c` (quieter, denser, hero number); the project lead picked C, then ruled the page stays as shipped (section 15). The switch is gone. | app.css |
|
||||
| 390 px: the strip's text, chevron and × on one line. Light and dark checked. | app.css |
|
||||
|
||||
Screenshots: `p04`/`p05` the plain job strip closed and open, `p06` the updated strip,
|
||||
`p07` the header paused, `p08` the activity feed, `p09`/`p12` light, `p10`/`p11` 390 px. Tests: 36 pass.
|
||||
|
||||
## 15. the project lead's ruling: the banner only (6 October 2026, evening)
|
||||
|
||||
the project lead: "lets not actually change the miner page just sort the banner". Variant C and the Earnings reshaping are
|
||||
reverted; the Mine and Earnings screens are exactly as shipped in 0.3.14. The three variant screenshots are gone with
|
||||
them. What stays from the polish round:
|
||||
|
||||
| Kept | Where |
|
||||
|---|---|
|
||||
| The strip's look: the row surface (one step lighter than the page), a 1 px ember hairline along the top, ash text, ember only on the state dot and the chevron; the update strip and the job strip share the one `.notice`; the asks and the clock card carry the same hairline. No filled brown or mustard field anywhere (the rail's active item, the pill, the ghost-on button, the option, the stop ring, the log hit and `mark` lost their tints; only `::selection` keeps a colour). | app.css |
|
||||
| The strip's words: "A job from the team ran: update check, 0 min." / "Updated to 0.3.14 at 18:52.", the technical line behind the chevron, the × kept | app.js Notices |
|
||||
| The Activity feed in the app's voice (`View.plainEvent`, the engine line as the tooltip, a kind dot) | app.js View, renderMine |
|
||||
| The header: title, subtitle and pill on one baseline; the pill a sentence with a coloured dot (`View.pill`) | app.js, app.css |
|
||||
|
||||
Screenshots: `r01-mine-strip-dark.png`, `r02-mine-strip-light.png` (Mine as shipped, the job strip showing),
|
||||
`r03-earnings-dark.png`. Tests: 36 pass.
|
||||
|
|
|
|||
BIN
docs/plans/miner-ui-3/p04-strip-job-done-plain.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/plans/miner-ui-3/p05-strip-job-done-open.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
docs/plans/miner-ui-3/p06-strip-updated.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
docs/plans/miner-ui-3/p07-header-paused.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
docs/plans/miner-ui-3/p08-activity-plain.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
docs/plans/miner-ui-3/p09-light-mine.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
docs/plans/miner-ui-3/p10-phone-mine.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
docs/plans/miner-ui-3/p11-phone-earnings.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/plans/miner-ui-3/p12-light-earnings.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/plans/miner-ui-3/r01-mine-strip-dark.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
docs/plans/miner-ui-3/r02-mine-strip-light.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
docs/plans/miner-ui-3/r03-earnings-dark.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
190
docs/plans/miner-ui-4-copy.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Igneum Miner UI 4: the copy sweep (6 October 2026, 20:0x UK)
|
||||
|
||||
the project lead, on the Cards screenshot: "'As the fleet' might need some work, check all UI copy for simplicity." Every string in
|
||||
`index.html` and `app.js` was read against the rules: no word a home miner would not use (fleet, lane, identities,
|
||||
prior, hill climb, sweep, DAA, epoch, vote key, digest, manifest, shard, segment, aggregator, verifier, worker, engine
|
||||
become plain words or carry one grey explanation), every state line one short sentence with the number first, labels
|
||||
are nouns and buttons are verbs, no colon where a sentence will do, no two-beat lines, one word for one thing (tune,
|
||||
card). Main's correction (20:0x UK): the chain's own objects keep the words of the site, the litepaper and the ledger,
|
||||
with a grey explanation where they first appear: shard (not piece), segment (not run of blocks), checkpoint and
|
||||
"locked checkpoint" (not lock point), aggregator (not combiner), verifying (not checking proofs); "network blocks (the
|
||||
DAA score)" stays. The rows below that renamed those words are superseded by this correction. the project lead's rulings folded in: the per-card inherit option is "Default · <shared goal>" in grey, updating when the
|
||||
shared goal changes; "Default" wherever a per-card setting follows the shared one (the cap's "Back to Default"); the
|
||||
Cards strip is titled "Ember Tune"; every goal option carries its meaning as a tooltip: Efficiency "most hashes per
|
||||
watt, a little rate traded", Balanced "a little rate for most of the saving", Maximum "most hashes, watts second",
|
||||
Default "the goal set for all cards". 150 strings changed.
|
||||
|
||||
## The ten most visible
|
||||
|
||||
| Where | Old | New |
|
||||
|---|---|---|
|
||||
| Cards, the per-card goal | As the fleet | Default · Balanced (grey, follows the shared goal) |
|
||||
| Cards, the strip | (no title) | Ember Tune, with each goal's meaning on hover |
|
||||
| Cards, the Ember line from a prior | Tuned 2 min ago from the fleet prior | Tuned 2 min ago from what other miners found |
|
||||
| Cards, the details label | Identities | Miners on this card |
|
||||
| Cards, the cap lines | cap applied: 460 W / cap not applied: Windows needs the administrator prompt / Unpin | Cap in force: 460 W / Cap not set yet. Windows needs the administrator prompt. / Back to Default |
|
||||
| Cards, the row's state word | held for a remote job / stopped after repeated faults | paused for a job from the team / stopped after repeated errors |
|
||||
| Overview, the big button | switch a GPU on below first / no GPU this app can drive / worker starting | switch a card on in Cards first / no card this app can use / card starting |
|
||||
| Prove | off: relay only / Every block is turned into a short proof. The chain hands pieces to your cards | off, passing proofs along / Every block is turned into a short proof, in pieces called shards. Your cards prove shards and earn IGN for each one (shard, segment and aggregator keep their names, each explained once) |
|
||||
| Node details | DAA score / rules digest / next rule switch / last lock | network blocks (the DAA score) / rules fingerprint / next rule change / last checkpoint (a point the miners agree can never be undone) |
|
||||
| Settings | Vote on finality checkpoints / Trust proof records without verifying them / Hill climb | Vote on checkpoints / Trust proofs without checking them / Fine tuning |
|
||||
|
||||
Every toast and event sentence lost its semicolon or colon joint for a full stop ("Mining stops. The node keeps
|
||||
running"). "Worker" (the engine's word for a card's mining process) became "card" wherever a user reads it; "engine"
|
||||
became "app"; "fleet prior" became "what other miners found". Shard, segment, checkpoint and aggregator stay (the
|
||||
chain's own words), each with a grey explanation where it first appears. The engine's own log strings are untouched: `View.plainEvent` maps them.
|
||||
|
||||
## Every string changed
|
||||
|
||||
| File | Where | Old | New |
|
||||
|---|---|---|---|
|
||||
| index.html | The chain scene canvas label | The last two minutes of blocks: this machine's lane and the others, the selected chain as a lit path, checkpoints as bands | The last two minutes of blocks: your row and the other miners, the main chain as a lit path, locks as bands |
|
||||
| index.html | Node details: DAA score row | <span class="k">DAA score</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made</span> | <span class="k">network blocks</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made (the DAA score)</span> |
|
||||
| index.html | Node details: votes row | this machine signs a checkpoint every 30 s | this machine signs a lock point every 30 s |
|
||||
| index.html | Node details: digest label | <div class="k">rules digest</div> | <div class="k">rules fingerprint</div> |
|
||||
| index.html | Node details: tips row | open ends of the block DAG right now | open ends of the chain right now |
|
||||
| index.html | Node details: blue score row | blocks on the agreed main chain</span> | blocks on the agreed main chain (the blue score)</span> |
|
||||
| index.html | Node details: next switch label | <div class="k">next rule switch</div> | <div class="k">next rule change</div> |
|
||||
| index.html | Prove: segments row | whole segments this machine proved | whole runs of blocks this machine proved |
|
||||
| index.html | Prove: shard program id label | <div class="k">shard program id</div> | <div class="k">proof program id</div> |
|
||||
| index.html | Prove: aggregator id label | <div class="k">aggregator id</div> | <div class="k">combiner program id</div> |
|
||||
| index.html | Prove: lead sentence | Every block is turned into a short proof. The chain hands pieces to your cards; each piece proven pays IGN. | Every block is turned into a short proof. Your cards prove pieces of it and earn IGN for each one. |
|
||||
| index.html | Settings: Ember Tune switch | Tunes every card for hashes per watt: once after install, then every 7 days, and after a driver or program change. | Tunes every card for the most hashes per watt. Once after install, then every 7 days, and after a driver or program change. |
|
||||
| index.html | Settings: Hill climb switch | <b>Hill climb</b> <span class="dim">Searches around the best point instead of walking the ladders.</span> | <b>Fine tuning</b> <span class="dim">Searches around the best point instead of stepping down a ladder.</span> |
|
||||
| index.html | Settings: vote switch | <b>Vote on finality checkpoints</b> <span class="dim">Signs a checkpoint every 30 s. Votes lock the chain; leave it on.</span> | <b>Vote on lock points</b> <span class="dim">Signs a lock point every 30 s. Votes are what lock the chain. Leave it on.</span> |
|
||||
| index.html | Settings: trust switch | <b>Trust proof records without verifying them</b> <span class="dim">Devnet only. Without a verifier the node includes records it never checked. Changing this restarts the | <b>Trust proofs without checking them</b> <span class="dim">Devnet only. The node includes proofs it never checked. Changing this restarts the node.</span> |
|
||||
| index.html | Settings: remote jobs switch | Signed jobs (a benchmark, a script, logs to collect) run here once and report back. | Signed jobs from the team (a benchmark, a script, logs to collect) run here once and report back. |
|
||||
| index.html | Settings: Power control | Off, NVIDIA cards are measured only. | Off, NVIDIA cards are only measured. |
|
||||
| index.html | Earnings: dev fee switch | Dev fee: 1 block in 100 pays the miner software's author | Dev fee: 1 block in 100 pays the people who make this app |
|
||||
| index.html | Setup: address lead | Paste an Ethereum-style address you control: 0x and 40 characters. | Paste an address you control. It starts with 0x and has 40 characters after it. |
|
||||
| index.html | Setup: GPU note | An integrated GPU is off by default: it is slow and shares the machine's memory. | A built-in GPU starts off. It is slow and shares the machine's memory. |
|
||||
| index.html | Settings: price | pence per kWh. Every £ figure uses it. | pence per kWh. Every £ figure uses this. |
|
||||
| index.html | Settings: name help | A label for you only. | A name for you only. |
|
||||
| index.html | Prove: setup note | About 20 minutes, once. | Takes about 20 minutes, once. |
|
||||
| index.html | Cards: the strip title | <div class="card tune-strip" id="tune-card"> <div class="ts-row"> | <div class="card tune-strip" id="tune-card"> <div class="card-head"><h3>Ember Tune</h3><span class="eyebrow">tunes every card for hashes per watt</span></di |
|
||||
| index.html | Cards: the shared goal options with their meanings | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button> <button class="seg-b" data-goal="balanced" role | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button> |
|
||||
| index.html | Settings: the shared goal options with their meanings | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button> <button class="seg-b" data-goal="balanced" role | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button> |
|
||||
| app.js | Cards: the per-card goal options | [['', 'As the fleet'], ['efficiency', 'Efficiency'], ['balanced', 'Balanced'], ['rate', 'Maximum']].map(function (o) { return '<button class="seg-b' + (g === o[0] ? ' on' | [['', 'Default', 'the goal set for all cards'], ['efficiency', 'Efficiency', 'most hashes per watt, a little rate traded'], ['balanced', 'Balanced', 'a little rate for mo |
|
||||
| app.js | Cards: the fleet saving line | ' over ' + n + ' tuned card' | ' across ' + n + ' tuned card' |
|
||||
| app.js | Cards: the Ember line from a prior | ' from the fleet prior' | ' from what other miners found' |
|
||||
| app.js | Cards: the Ember line, measured sub | 'Apple silicon sets its own clocks' | 'Apple silicon sets its own power' |
|
||||
| app.js | Cards: the tune line, prior | ' from the fleet prior, confirmed' | ' from what other miners found, confirmed' |
|
||||
| app.js | Cards: the schedule, off | 'Ember Tune is off. Tune and Tune all still work by hand.' | 'Ember Tune is off. Tune and Tune all still work when you press them.' |
|
||||
| app.js | Cards: the floor note | ' (the ladder’s floor)' | ' (the lowest clock Ember will set)' |
|
||||
| app.js | Cards: identities label | <span class="k">Identities</span><p class="help"> | <span class="k">Miners on this card</span><p class="help"> |
|
||||
| app.js | Cards: identities toast | 'Identities set; that card’s worker restarts' | 'Set. The card restarts' |
|
||||
| app.js | Cards: cap help, Ember may move it | 'Ember Tune may move it.' | 'Default: Ember may move it.' |
|
||||
| app.js | Cards: cap help, pinned | 'Set by hand, so the tune leaves it. <button class="btn tiny ghost" data-pin="0">Unpin</button>' | 'Set by hand, so Ember leaves it. <button class="btn tiny ghost" data-pin="0">Back to Default</button>' |
|
||||
| app.js | Cards: cap applied | '<div class="line ok">cap applied: <b>' + want + ' W</b>' | '<div class="line ok">Cap in force: <b>' + want + ' W</b>' |
|
||||
| app.js | Cards: cap not applied | '<div class="line hot">cap not applied: Windows needs the administrator prompt <button class="btn tiny" data-power-retry="1">Retry</button></div>' | '<div class="line hot">Cap not set yet. Windows needs the administrator prompt. <button class="btn tiny" data-power-retry="1">Retry</button></div>' |
|
||||
| app.js | Cards: chosen by the tune pin | ' <span class="pin">chosen by the tune</span>' | ' <span class="pin">Ember’s choice</span>' |
|
||||
| app.js | Cards: Apple cap help | '<p class="help">Apple silicon manages its own power; there is no cap to set.</p>' | '<p class="help">Apple silicon manages its own power. There is no cap to set.</p>' |
|
||||
| app.js | Cards: facts line worker | cd.worker ? 'worker ' + cd.worker : '' | cd.worker ? 'runs on ' + cd.worker : '' |
|
||||
| app.js | Row word: held | 'held for a remote job' | 'paused for a job from the team' |
|
||||
| app.js | Row word: faulted | 'stopped after repeated faults' | 'stopped after repeated errors' |
|
||||
| app.js | Big button: held | 'a remote job holds the cards; mining restarts when it ends' | 'a job from the team is using the cards. Mining restarts when it ends' |
|
||||
| app.js | Hero sub: no GPU worker | m.state === 'idle' ? 'no GPU worker' | m.state === 'idle' ? 'no card mining' |
|
||||
| app.js | Hero sub: worker starting | (n.synced ? 'worker starting' : 'waiting for the node') | (n.synced ? 'card starting' : 'waiting for the node') |
|
||||
| app.js | Big button: worker starting | sub: n && n.synced ? 'worker starting' : 'waiting for the node to sync' | sub: n && n.synced ? 'card starting' : 'waiting for the node to sync' |
|
||||
| app.js | Big button: switch a GPU on | 'switch a GPU on below first' | 'switch a card on in Cards first' |
|
||||
| app.js | Big button: no GPU | 'no GPU this app can drive' | 'no card this app can use' |
|
||||
| app.js | Strip fallback: mining | 'mining: a block this machine finds appears here' | 'Mining. A block this machine finds appears here' |
|
||||
| app.js | Strip fallback: paused | 'paused: no blocks while mining is stopped' | 'Paused. No blocks while mining is stopped' |
|
||||
| app.js | Strip fallback: worker | state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync' | state.node.synced ? 'waiting for the card' : 'waiting for the node to sync' |
|
||||
| app.js | Chain scene: the note | 'The full chain view needs the next engine update; this is the last 10 minutes of your own blocks.' | 'The full chain view needs the next update. This is the last 10 minutes of your own blocks.' |
|
||||
| app.js | Chain scene: the state word | st === 'failed' ? 'your blocks' | st === 'failed' ? 'your blocks only' |
|
||||
| app.js | Node: next program sub | 'until the next hourly mining program' | 'until the next hourly program' |
|
||||
| app.js | Node: next program unknown | 'known once the node runs' | 'known once the node is running' |
|
||||
| app.js | Node: finality note | 'a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight' | 'a point the miners agreed can never be undone. It locks at two thirds of the last 30 days of mining' |
|
||||
| app.js | Node: finality none | 'locks appear once the miner votes on checkpoints' | 'locks appear once this machine votes' |
|
||||
| app.js | Node: switch line | 'A switch is a planned rule change. The node applies it by itself when the chain reaches that height.' | 'A rule change is planned ahead. The node applies it by itself when the chain reaches that height.' |
|
||||
| app.js | Node: switch line, all behind | 'Every planned switch is behind this node. ' | 'Every planned rule change is behind this node. ' |
|
||||
| app.js | Node: switch line, at DAA | sw.name + ' at DAA ' + withCommas(sw.daa) + ': ' + blocks | sw.name + ' at block ' + withCommas(sw.daa) + ': ' + blocks |
|
||||
| app.js | Node: manifest switch line | 'The update manifest announces a switch at DAA ' + withCommas(n.consensus_switch_daa) | 'A rule change is announced at block ' + withCommas(n.consensus_switch_daa) |
|
||||
| app.js | Node: digest not printed | 'not printed yet' | 'not read yet' |
|
||||
| app.js | Prove: the words off | 'Off. Switch it on and this machine proves the pieces the chain assigns to its keys.' | 'Off. Switch it on and this machine proves the pieces it is given.' |
|
||||
| app.js | Prove: proving sub | sub: pv.current \|\| 'a shard' | sub: pv.current \|\| 'a piece' |
|
||||
| app.js | Prove: idle note | 'On. Nothing is assigned to your keys right now; the next shard arrives within a minute.' | 'On. Nothing to prove right now. The next piece arrives within a minute.' |
|
||||
| app.js | Prove: proving note | 'A proof takes minutes on a CPU and seconds on a CUDA card.' | 'A proof takes minutes on a CPU and seconds on an NVIDIA card.' |
|
||||
| app.js | Prove: tier, cannot prove yet | ': cannot prove yet (the prover is CUDA only).' | ': cannot prove yet (proving needs an NVIDIA card).' |
|
||||
| app.js | Prove: tier, proves while it mines | ': proves while it mines (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' | ': proves while it mines (' + gb + ' GB; a full piece needs ' + SHARD_GB + ' GB).' |
|
||||
| app.js | Prove: tier, cannot prove a full shard | ': cannot prove a full shard (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' | ': cannot prove a full piece (' + gb + ' GB; a full piece needs ' + SHARD_GB + ' GB).' |
|
||||
| app.js | Prove: toast on | 'Proving on; the first piece arrives within a minute' | 'Proving on. The first piece arrives within a minute' |
|
||||
| app.js | Prove: verifier words | { command: 'verifying', trust: 'trusting (devnet only)', off: 'off: relay only', unknown: 'not read yet' } | { command: 'checking proofs', trust: 'trusting without checking (devnet only)', off: 'off, passing proofs along', unknown: 'not read yet' } |
|
||||
| app.js | Prove: segments line | 'whole segments this machine proved' | 'whole runs of blocks this machine proved' |
|
||||
| app.js | Prove: segments held | ', ' + pv.segments_held + ' held for the chain rule' | ', ' + pv.segments_held + ' waiting for the chain' |
|
||||
| app.js | Prove: segments last | (pv.segment_last_s ? ', the last in ' | (pv.segment_last_s ? ', the last took ' |
|
||||
| app.js | Earnings: dev fee off | 'Dev fee off: every block pays your address' | 'Dev fee off. Every block pays your address' |
|
||||
| app.js | Earnings: dev fee on | 'Dev fee: 1 block in 100 pays the miner software’s author' | 'Dev fee: 1 block in 100 pays the people who make this app' |
|
||||
| app.js | Earnings: dev fee line | 'The protocol takes nothing; this is the miner software’s fee, the same way every GPU miner takes one' | 'The network itself takes nothing. This is the app’s fee, the same way every mining app takes one' |
|
||||
| app.js | Earnings: dev fee line off | 'The protocol takes nothing either.' | 'The network itself takes nothing either.' |
|
||||
| app.js | Earnings: key help | 'The key for this address was made on this machine and is stored in the app folder, readable by your user only. Keep a copy somewhere safe.' | 'The key for this address was made on this machine and is stored in the app folder. Only your user can read it. Keep a copy somewhere safe.' |
|
||||
| app.js | Settings: jobs note off | 'Off: nothing runs here until the switch above allows remote jobs.' | 'Off. Nothing runs here until the switch above allows jobs from the team.' |
|
||||
| app.js | Settings: jobs note no address | 'No jobs address in this build.' | 'This build has no jobs address.' |
|
||||
| app.js | Settings: jobs note running | 'Running ' + jobTitle(j) + ' (' + j.id + ').' | 'Running: ' + jobTitle(j) + '.' |
|
||||
| app.js | Settings: update note current | 'Up to date' + (u.checked_at ? ', checked ' + rel(state.now - u.checked_at) : '') + '.' | 'Up to date' + (u.checked_at ? '. Checked ' + rel(state.now - u.checked_at) : '') + '.' |
|
||||
| app.js | Settings: update note never | 'Not checked yet. The engine checks by itself every hour.' | 'Not checked yet. The app checks by itself every hour.' |
|
||||
| app.js | Settings: update note rule change | 'A rule switch comes at height ' + withCommas(u.activation_height) + '.' | 'A rule change comes at block ' + withCommas(u.activation_height) + '.' |
|
||||
| app.js | Settings: price toast | 'A price in pence per kWh, for example 28' | 'Type a price in pence per kWh, for example 28' |
|
||||
| app.js | Settings: Ember toast | 'Ember Tune on: once after install, then every 7 days' | 'Ember Tune on. Once after install, then every 7 days' |
|
||||
| app.js | Settings: Power control on toast | 'Power control on: Windows asks for administrator rights once' | 'Power control on. Windows asks for administrator rights once' |
|
||||
| app.js | Settings: Power control off toast | 'Power control off; nothing asks' | 'Power control off. Nothing asks' |
|
||||
| app.js | Settings: trust on toast | 'Trust mode on (devnet only); the node restarts' | 'Trust mode on (devnet only). The node restarts' |
|
||||
| app.js | Settings: trust off toast | 'Trust mode off; the node restarts' | 'Trust mode off. The node restarts' |
|
||||
| app.js | Settings: applied toasts | 'Applied; the miner restarts' | 'Applied. Mining restarts' |
|
||||
| app.js | Cards: switch on toast | 'Worker starting on that card' | 'Starting that card' |
|
||||
| app.js | Cards: switch off toast | 'Worker stopped on that card' | 'That card is off' |
|
||||
| app.js | Cards: cap toast | 'Power cap set; pinned until you unpin it' | 'Power cap set. It stays until you choose Default again' |
|
||||
| app.js | Cards: sendCards default toast | 'Applied; only the changed worker restarts' | 'Applied. Only that card restarts' |
|
||||
| app.js | Cards: stop toast | 'Tune stopped; the card is back where it was' | 'Tune stopped. The card is back where it was' |
|
||||
| app.js | Cards: pin toast | 'The tune chooses the cap again' | 'Default: Ember chooses the cap again' |
|
||||
| app.js | Cards: retry toast | 'Administrator prompt: allow it to set the cap' | 'Allow the administrator prompt to set the cap' |
|
||||
| app.js | Cards: Tune all toast | 'Tune queued for ' + n + ' cards, one at a time' | 'Tune queued for ' + n + ' cards, one after another' |
|
||||
| app.js | Overview: toggle toast | 'Mining stops; the node keeps running' | 'Mining stops. The node keeps running' |
|
||||
| app.js | Address ask | ' from the next block? The miner restarts.' | ' from the next block? Mining restarts.' |
|
||||
| app.js | Trust ask on | 'Include proof records the node never checked? Devnet only; the node restarts.' | 'Include proofs the node never checked? Devnet only. The node restarts.' |
|
||||
| app.js | Trust ask off | 'Verify proof records again? The node restarts.' | 'Check proofs again? The node restarts.' |
|
||||
| app.js | Pill: engine away | 'Engine not answering' | 'App not answering' |
|
||||
| app.js | Node line: engine away | 'the engine is not answering; the window reconnects by itself' | 'the app is not answering. The window reconnects by itself' |
|
||||
| app.js | Button: engine away | 'the engine is not answering' | 'the app is not answering' |
|
||||
| app.js | Hero: engine away | 'no answer from the engine' | 'no answer from the app' |
|
||||
| app.js | Toast: engine did not answer | 'The engine did not answer' | 'The app did not answer' |
|
||||
| app.js | Settings: goal toast | 'The goal needs the Ember Tune update' | 'The goal needs the next Ember Tune update' |
|
||||
| app.js | Cards: card goal toast | 'A per-card goal needs the Ember Tune update' | 'A goal for one card needs the next Ember Tune update' |
|
||||
| app.js | Cards: card goal set toast | 'This card follows the fleet goal' | 'Default: this card follows the goal set for all cards' |
|
||||
| app.js | Activity: identity events | m[1] + ': ' + m[2] + ' identit' + m[3] + '; its worker restarts' | m[1] + ': ' + m[2] + ' miner' + (m[2] === '1' ? '' : 's') + ' on it. The card restarts' |
|
||||
| app.js | Activity: worker ready | m[1] + ': worker ready' | m[1] + ': ready' |
|
||||
| app.js | Activity: worker fault | m[1] + ': worker fault; restarting it' | m[1] + ': an error. Restarting it' |
|
||||
| app.js | Activity: worker stopped | 'A worker stopped (code ' + m[2] + '); restarting in ' + m[3] + ' s' | 'A card stopped (code ' + m[2] + '). Restarting in ' + m[3] + ' s' |
|
||||
| app.js | Activity: worker built | 'Worker built' | 'Mining program built' |
|
||||
| app.js | Activity: program variant | m[1] + ': program variant chosen, ' + m[3] + ' MH/s' | m[1] + ': fastest program chosen, ' + m[3] + ' MH/s' |
|
||||
| app.js | Activity: segment proven pieces | 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s' | 'Run of blocks proven: ' + m[3] + ' pieces in ' + m[4] + ' s' |
|
||||
| app.js | Activity: segment proven | 'Segment proven in ' + m[3] + ' s' | 'Run of blocks proven in ' + m[3] + ' s' |
|
||||
| app.js | Activity: segment paid | 'Segment paid: ' + m[1] + ' IGN' | 'Run of blocks paid: ' + m[1] + ' IGN' |
|
||||
| app.js | Activity: segment held | 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s; its record waits for the chain rule' | 'Run of blocks proven: ' + m[3] + ' pieces in ' + m[4] + ' s. Its record waits for the chain' |
|
||||
| app.js | Activity: segment accepted | 'Segment record accepted' | 'Run of blocks accepted' |
|
||||
| app.js | Activity: node external | 'The team asked for a node restart; this node is not the app’s, so nothing was done' | 'The team asked for a node restart. This node is not the app’s, so nothing was done' |
|
||||
| app.js | Activity: no peer | 'No other node found after 90 s; still looking' | 'No other node found after 90 s. Still looking' |
|
||||
| app.js | Activity: node stopped restarting | 'The node stopped (code ' + m[1] + '); restarting' | 'The node stopped (code ' + m[1] + '). Restarting' |
|
||||
| app.js | Activity: no reading | 'No reading from the node for ' + m[1] + ' s; restarting it' | 'No reading from the node for ' + m[1] + ' s. Restarting it' |
|
||||
| app.js | Activity: mining paused | 'Mining paused; the node keeps running' | 'Mining paused. The node keeps running' |
|
||||
| app.js | Activity: settings changed | 'Settings changed (' + m[1] + '); mining restarts' | 'Settings changed (' + m[1] + '). Mining restarts' |
|
||||
| app.js | Activity: tune best point | m[1] + ': best point ' + m[2] + ' (' + m[3] + '); your cap stays pinned' | m[1] + ': best point ' + m[2] + ' (' + m[3] + '). Your cap stays pinned' |
|
||||
| app.js | Activity: next program compiles | m[1] + ': the next program compiles at the hour boundary instead' | m[1] + ': the next program is built at the top of the hour instead' |
|
||||
| app.js | Activity: new program worker | 'New hourly program; the worker restarts on it' | 'New hourly program. The card restarts on it' |
|
||||
| app.js | Activity: pack | 'Rebuilding the program pack' | 'Rebuilding the mining programs' |
|
||||
| app.js | Activity: trust | 'Devnet trust mode: proof records are not verified (Settings)' | 'Devnet trust mode: proofs are not checked (Settings)' |
|
||||
| app.js | Activity: sweep caps | 'A tune is setting the caps now; retry when it is done' | 'A tune is setting the caps now. Retry when it is done' |
|
||||
| app.js | Activity: updates off engine | 'Updates are off for this engine (a measurement run)' | 'Updates are off for this copy (a measurement run)' |
|
||||
| app.js | Activity: power cap not applied | 'Power cap not applied: the administrator prompt was refused (Retry on the card)' | 'Power cap not set. The administrator prompt was refused (Retry on the card)' |
|
||||
| app.js | Activity: node too old | 'This node is too old for the team’s rule settings; an app update carries the newer node' | 'This node is too old for the team’s rule settings. An app update carries the newer node' |
|
||||
| app.js | Row word: unplugged | 'unplugged; its worker stopped. The row goes in five minutes.' | 'unplugged. Its row goes in five minutes.' |
|
||||
| app.js | Details: removed | '<p class="help">The card was unplugged; its worker stopped. The row goes in five minutes.</p>' | '<p class="help">The card was unplugged. Its row goes in five minutes.</p>' |
|
||||
| app.js | Details: problem | 'No worker can drive this card.' | 'This app cannot use this card.' |
|
||||
| app.js | Setup: no cards | 'No graphics card this app can drive was found.' | 'No graphics card this app can use was found.' |
|
||||
| app.js | Overview: no cards | 'No graphics card this app can drive was found. The node runs on its own.' | 'No graphics card this app can use was found. The node runs on its own.' |
|
||||
| app.js | Pages: overview sub | sub: 'the rate, what it costs, the chain as it grows' | sub: 'your rate, what it costs, the chain as it grows' |
|
||||
| app.js | Pages: prove sub | sub: 'turn blocks into proofs, paid in IGN' | sub: 'prove blocks, earn IGN' |
|
||||
| app.js | Pages: earnings sub | sub: 'what it made, where it goes' | sub: 'what you made, where it goes' |
|
||||
| app.js | Goal names for the Default option | var GOALS = { efficiency: 'the most hashes per watt', balanced: 'hashes per watt within 1% of the top rate', rate: 'the top rate, whatever the watts' }; | var GOALS = { efficiency: 'the most hashes per watt', balanced: 'hashes per watt within 1% of the top rate', rate: 'the top rate, whatever the watts' }; var GOAL_NAME |
|
||||
| app.js | Export GOAL_NAMES | GOALS: GOALS, goalWords: goalWords, | GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, |
|
||||
101
docs/plans/miner-ui-4.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Igneum Miner UI 4: Overview, Cards, Ember on the row
|
||||
|
||||
6 October 2026, evening. the project lead: "add Cards under its own tab, and have Overview and the cool mining demo on the main
|
||||
page with the revamped C option", then "incorporate Ember into the Cards tab beautifully", then "I would like the new
|
||||
miner UI to go into .15". Branch `miner-ui-4` from the 0.3.15 tip `6ea9497` (untouched), worktree
|
||||
`../igneum-wt-miner-ui-4`. Rust: one route (`GET /live-dag.js`, `server.rs`), nothing else.
|
||||
|
||||
## 1. The rail
|
||||
|
||||
Overview, Cards, Earnings, Prove, Settings; Logs and Quit in the foot; a bottom tab bar under 720 px.
|
||||
|
||||
## 2. Overview
|
||||
|
||||
| Block | What |
|
||||
|---|---|
|
||||
| The hero (the project lead's pick, C) | the fleet rate at 84 px on a graphite-to-obsidian fade, W with MH/W, £ a day at the user's price, blocks (this run, lifetime) beside it; Start/Stop as the full-width bar under it with its state sentence at the right |
|
||||
| The chain, live | the site's `live-dag.js` (a byte copy at `app/igneum-app/ui/live-dag.js`, served by the engine at `/live-dag.js`), mounted on the engine's own `api/live` with `mine: 'you'` so this machine's lane reads "you" and its blocks take a molten ring; the legend words in HTML under the canvas: yours, pending, included, excluded, proven, locked; Pause; a tooltip per block. The engine's `/api/live` is OWED (it maps the local node's RPC into the site's reply shape: `{ok, state, blocks[{hash, ts, blue_score, daa, parents, chain, color, miner, locked, final, shards, proven}], finality.checkpoints}`); until it exists the module reports `failed` and the app's own 10-minute blocks strip draws in its place with the note "The full chain view needs the next engine update". |
|
||||
| The node line | as 0.3.15, Details under it |
|
||||
| Activity | the event feed in the app's voice (the strip canvas moved into the chain card as the fallback) |
|
||||
|
||||
## 3. Cards
|
||||
|
||||
| Block | What |
|
||||
|---|---|
|
||||
| The Tuning strip | the goal control (Efficiency, Balanced, Maximum) with "about £X a day at Y W"; the fleet saving ("Ember saves 84 W, £0.56 a day over 1 tuned card", `View.fleetSaving`); Tune all; the schedule line; Power control only when an NVIDIA card has it off |
|
||||
| The rows | as 0.3.15 (state word with its reason, MH/s, W with MH/W, £ a day, °C, the switch, the chevron) with Ember as the second layer |
|
||||
| The flame mark | the brand mark's two polygons, 16 px, outline when not tuned, filling bottom-up with step k of n while tuning (molten), solid ember when tuned, half grey when measured only |
|
||||
| The Ember line | "Tuned 2 h ago · 2470 MHz at 80% · next check Sunday · saves 84 W, 0.16% of rate"; "Tuning: step 4 of 9 · about 7 min left" with the bar; "Measured, not tuned · Power control is off"; "Not tuned yet · starts after 2 min of steady mining"; the fleet pause and a stop in the same place |
|
||||
| The W cell | before → after when tuned (`tune_before_watts` → the live draw) |
|
||||
| The MH/W cell | a 44 px sparkline of the ladder's MH/W per step, the chosen step as a dot |
|
||||
| Tune / Retune / Stop | the row's button; Retune once tuned |
|
||||
| The chevron | the cap slider, the applied or Retry line, memory and clocks, the card's own goal (As the fleet, Efficiency, Balanced, Maximum: `POST api/tune/goal {key, goal}`, "" clears; a build without it gets a toast), Retune, the Tuned line, the curve (W across, MH/s up, every step a dot, the chosen ringed, a rejected one hollow), identities, the facts |
|
||||
|
||||
Fields read (Ember Tune 5429e82, on the 0.3.16 lane): `tune_before_watts`, `tune_before_mhs` (step 0 of the last full
|
||||
plan; 0 = never; the UI falls back to `tune_curve[0]` only for a full plan on an older build and claims no saving for a
|
||||
confirm or baseline plan), `sweep_watts`, `sweep_mhs`, `sweep_eff`, `tune_curve`, `tune_goal`, `tune_step`,
|
||||
`tune_steps`, `tune_eta_s`, `sweep_state`, `sweep_note`, `tune_floor` (asked for). `View.ember` and `View.beforeOf`
|
||||
carry the rules; `view.test.mjs` covers every state.
|
||||
|
||||
## 4. Screenshots (the mock on port 4318 with the rig state, the 5090 tuned, the 4070 at step 4 of 9)
|
||||
|
||||
`docs/plans/miner-ui-4/u01-overview-dark-1440.png`, `u02-overview-light-1440.png`, `u03-cards-dark-1440.png` (the
|
||||
5090's details open: cap, goal, Retune, the curve), `u04-cards-light-1440.png`, `u05-overview-dark-390.png`,
|
||||
`u06-cards-dark-390.png`, `u07-overview-light-390.png`, `u08-cards-light-390.png`, `u09-overview-mac-dark.png`.
|
||||
The chain scene in the shots is the mock's `/api/live` (four lanes, one block a second, "you" ringed); on a real
|
||||
0.3.15 engine the fallback strip shows until `/api/live` lands.
|
||||
|
||||
## 5. Owed
|
||||
|
||||
| What | Who |
|
||||
|---|---|
|
||||
| `GET /api/live` on the engine: the local node's blocks over the window, mapped into the site's shape, this machine's vote keys as the lane "you" | engine |
|
||||
| `card.tune_floor` | Ember (said yes) |
|
||||
| `watts_total`, `pounds_per_day` for the tray tip; `address.balance_wei`; `price_gbp_per_ign` | engine (from miner-ui-3) |
|
||||
| Re-copy `site/live-dag.js` over `app/igneum-app/ui/live-dag.js` when the site agent's audit commit lands, so the two stay identical | the shipper at merge |
|
||||
|
||||
Tests: `node --test app/igneum-app/ui/*.test.mjs`: 37 pass.
|
||||
|
||||
## 6. The second tip (20:0x UK): /api/live on the engine, the copy sweep, the project lead's goal rulings
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| `GET /api/live` on the engine: the observer's `/api/live` (the URL `ota.rs` already polls for the identity count) through curl, cached 2 s per window, this machine's blocks marked as the lane "you" by matching the block's miner id (the first 8 hex of its vote key hash) against every card's `ids`; `state.you_blocks` and `state.source: "observer"` added; `{ok:false, error}` when the observer is unreachable or the build has no observer address, which the UI meets with its own blocks strip. Four unit tests on the shaping from a fixture in the observer's shape (`live::tests`). The local node speaks gRPC and wRPC only and the engine carries no client for either (no TLS, no websocket stack), so a local-node source is OWED (a JSON endpoint on igneumd, or a wRPC client here); the reply shape stays the same when it comes. | `src/live.rs`, `src/server.rs`, `src/main.rs`, `ota::live_api_from` made `pub` |
|
||||
| The copy sweep: 150 strings, every one listed in `docs/plans/miner-ui-4-copy.md` | `ui/index.html`, `ui/app.js`, the tests |
|
||||
| the project lead's rulings: the per-card inherit option is "Default · Balanced" (grey, following the shared goal); "Default" wherever a per-card setting follows the shared one (the cap's "Back to Default"); the Cards strip is titled "Ember Tune"; every goal option carries its meaning as a tooltip (Efficiency "most hashes per watt, a little rate traded", Balanced "a little rate for most of the saving", Maximum "most hashes, watts second", Default "the goal set for all cards") | `ui/index.html`, `ui/app.js` |
|
||||
|
||||
The Rust tests did not run on this Mac (its cargo predates the lock file's version 4); the box runs them at the
|
||||
merge, as the rule says. UI tests: 37 pass. Screenshots `u01`, `u03`, `u04`, `u06` reshot after the sweep.
|
||||
|
||||
## 7. Pinned to the next engine (ember-tune fd03f35, then b55bc1f), 20:5x UK
|
||||
|
||||
Numbering (the shipper, 21:3x UK): 0.3.16 is tonight's corrective cut, the same release as 0.3.15 (this branch's ui/
|
||||
at 872f1c9) under a new number; the engine fields and the pinning below ship as 0.3.17 with miner-ui-4's tip e31acab.
|
||||
|
||||
| Field | Where the UI reads it |
|
||||
|---|---|
|
||||
| `mining.watts_total`, `mining.pounds_per_day` | the Overview hero and Earnings use the engine's figures when present (`View.fleetWatts`, `View.fleetPounds`), else the UI's own sums as before |
|
||||
| `address.balance_wei` (string or null), `address.balance_age_s` (-1 until read), `address.balance_note`, `address.price_gbp_per_ign` (null until a signed manifest carries a price) | Earnings' first row: "12.3456 IGN · in the wallet · read 20 s ago" (`View.balanceWords`); a priced balance shows £ with "at the signed price"; an older engine without the field keeps the row as it was |
|
||||
| `card.tune_floor` | the Ember line's "(the lowest clock Ember will set)" |
|
||||
| `/api/live` reply `source` ("node" when igneumd answers `igneum_getRecentBlocks`, "site" until then; `pending: true` before the first body) | the chain scene's note (`View.liveNote`): "From your node. Your blocks are ringed." or "From Igneum’s observer. Your blocks are ringed."; "you" is matched in the UI from every card's vote-key ids whatever the source names the lane |
|
||||
|
||||
At the 0.3.17 merge `src/live.rs` is ember-tune's (the fuller one, node first, site second); this branch's 0.3.15
|
||||
file is superseded and its `source` word is "site" too, so nothing in the UI changes between the two.
|
||||
|
||||
## 8. N4 (the frozen tip), the capture route, and PC 2's light render (6 October 2026, 23:0x to 23:4x UK)
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| N4, the app half: `sync_decision_v2` (the old rule AND the watch line's `tip_age_s` at or under 120 s AND peers >= 1; the cause word frozen / no peers / syncing / behind on `node.sync_cause`; the state word "behind" or "no peers", never "synced" on a frozen tip); `is_stall_exit` (code 45 or a "STALLED '" tail line; the first restarts the miner, the second since the node started restarts the node and re-dials its peers); `node.tip_age_s`, `node.stall_exits`. The known-failed case is the first test. UI: "Node behind · 1 peer · 133,000 blocks · last block 53 min ago" with the cause line, the pill "Node behind", the big button's "waiting: the node is behind the chain". The node lane's miner half is ca3-v4-0316. | `src/engine.rs`, `src/state.rs`, `ui/app.js`, `view.test.mjs` |
|
||||
| (b) a screenshot from the machine itself: `POST /api/shot {name}` writes `SHOT <path>` on the host protocol (beside URL, STATE, EXIT); the Mac host answers with `WKWebView.takeSnapshot`, the Windows host with WebView2 `CapturePreview` (PNG), to `<data root>/shots/igneum-<name>-<unix>.png` (`live::shot_path`, the name reduced to [a-z0-9-]); `relay/playbooks/app-shot.ps1` asks and waits, a collect job's glob `shots/*.png` brings the file back. Read-only. The Swift parses (`swiftc -parse`); the C++ builds on the GitHub runner at the cut (`BUILD-APP.bat` already links shlwapi.lib). | `src/server.rs`, `src/live.rs`, `app/mac/IgneumMiner.swift`, `app/windows/host.cpp`, `relay/playbooks/app-shot.ps1` |
|
||||
| The light-theme showcase capture: Edge headless on PC 2 wrote nothing in three takes (154.0.4258.53, exit 0, no stderr, in the app's elevated session); PC 2's `api/state` and `api/live` were fetched by `relay/playbooks/app-state-grab.ps1` into the data root and collected, and the ui-mock replayed them (`IGNEUM_MOCK_STATE`, `IGNEUM_MOCK_LIVE`) through the real 0.3.16 UI on the Mac: `docs/plans/miner-ui-4/pc2-0.3.16/pc2-overview-light-1440.png`, `pc2-cards-light-1440.png`, `pc2-overview-dark-1440.png`, captioned "rendered from PC 2's live state at 23:33 UK" (DESKTOP-KMCV30N, RTX 5090 at 103 MH/s and 317 W, node synced, 5 peers). Handed to the site lane. | `tools/ui-mock/server.mjs`, `relay/playbooks/app-state-grab.ps1` |
|
||||
| The fleet watts count mining and tuning cards only (PC 2's idle integrated card drew 48 W and was summed); the Ember off-state line no longer promises a start. | `ui/app.js` |
|
||||
|
||||
Tests: UI 39; the app crate on the box 152 passed (the N4 pair, the shot path, the four live tests among them).
|
||||
|
||||
## 9. 0.3.17 fold (7 October 2026, 23:20 UK)
|
||||
|
||||
- Rebased onto a9eb58f1 (origin/release-0.3.15). ui/ comes wholly from this branch; Ember's finality-paused hunk (ember-tune 1357d280) is folded in as `View.finalityWords(f)`: while finality is paused the node sub-line and the lock row say "paused" and nothing calls a lock final. Test in view.test.mjs (41 UI tests).
|
||||
- src/extnode.rs reads the node lane's `igneum_getNodeInfo` shape: `params` (every resolved consensus field, compared value by value against the manifest's keys), `network` (must be equal), `powEngine` ("stub" = refused on the port check, and a fault on the app's own node: node state `stub`, pill "Node fault", one error event). 168 box tests.
|
||||
- src/live.rs is Ember's merged module at the merge (nothing of this branch's lives there any more).
|
||||
- External node re-decided (the Mac, 7 October 2026 00:18 UK: the hand node left and the app sat on "node stopped" all night). While the app reads another node, or refused one, it checks the port every 5 s; gone for 60 s (`extnode::TAKEOVER_WAIT_S`) it starts its own node on the freed ports and says so in Activity; a node back inside the wait is kept. api/state node.mode own | external | none with node.mode_reason, shown under the Node details. Test `an_external_node_that_goes_away_hands_the_ports_to_the_app_after_the_wait` (169 box tests).
|
||||
BIN
docs/plans/miner-ui-4/live-0.3.16/live-cards-dark-1440.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
docs/plans/miner-ui-4/live-0.3.16/live-cards-dark-390.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/plans/miner-ui-4/live-0.3.16/live-cards-light-1440.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
docs/plans/miner-ui-4/live-0.3.16/live-cards-open-dark.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
docs/plans/miner-ui-4/live-0.3.16/live-earnings-dark-1440.png
Normal file
|
After Width: | Height: | Size: 206 KiB |