first-block-21 (the project lead, 7 October 2026: "the 'you got your first block' situation only shows for the miner's first ever block"): the first-block card once in the app's life. Engine: ladder.rs carries a persisted first_block_shown flag set when the lifetime count settings.json holds crosses from 0 to 1 (survives restarts, updates and a kept data directory; never touched by a key changing identities or a card swap; a count that starts over on a shown record raises nothing); Ladder::start_run at engine start drops a milestone an earlier run persisted (a card is for the run that raised it) and takes the flag on a 0.3.20 record whose first block already came; the new-card card never fires at a count of 1; the state carries started_at (the run's wall-clock start). View: blockCard refuses a milestone raised before this run (staleCard, a minute of slack, uptime_s when the engine has no started_at), firstWait keeps the count-up off once the ladder records a first block, the first rung reads the flag. Tests: seven in ladder.rs (the known-failed second run first: a 0.3.20 record with the card persisted and the count at 1 showed it again; the first ever block; a restart at 1; an update at 1 over the older shape; a kept directory at 0 with the flag unset; a count that starts over; block 2 ordinary) and one view test in the same order. Mock: the secondblock scenario and started_at. Lines: build-2 app gate 254 + 32 + 8 (test --release); UI 67; pre-push 56 green; captures in ~/Desktop/igneum-previews-2026-10-07/first-block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 14:57:34 +00:00
parent d45820cfe6
commit b3bca507a4
6 changed files with 237 additions and 10 deletions

View file

@ -147,6 +147,9 @@ pub struct Shared {
/// ui-ota: the folder the server serves the dashboard from (None = the files embedded in the binary) /// ui-ota: the folder the server serves the dashboard from (None = the files embedded in the binary)
ui_dir: Mutex<Option<PathBuf>>, ui_dir: Mutex<Option<PathBuf>>,
pub started: Instant, pub started: Instant,
/// first-block-21: the run's start on the wall clock (unix s), for the dashboard's "a card is for this run" rule;
/// `started` is monotonic and stops across a sleep, so the two are kept apart
pub started_unix: f64,
engine_log: Mutex<Option<std::fs::File>>, engine_log: Mutex<Option<std::fs::File>>,
/// the app log's path: the one the uploader sends (main.rs names it; the engine must not guess the stamp) /// the app log's path: the one the uploader sends (main.rs names it; the engine must not guess the stamp)
pub log_path: PathBuf, pub log_path: PathBuf,
@ -163,7 +166,12 @@ impl Shared {
let settings_path = runtime.app_dir.join("settings.json"); let settings_path = runtime.app_dir.join("settings.json");
let wallet_path = runtime.app_dir.join("wallet.json"); let wallet_path = runtime.app_dir.join("wallet.json");
let ladder_path = runtime.app_dir.join("ladder.json"); let ladder_path = runtime.app_dir.join("ladder.json");
let ladder = crate::ladder::Ladder::load(&ladder_path); // first-block-21: a block card is for the run that raised it, and the first-block flag is reconciled with
// the persisted lifetime count (a record from before the flag whose first block already came takes it)
let mut ladder = crate::ladder::Ladder::load(&ladder_path);
if ladder.start_run(settings.accepted_total) {
ladder.save(&ladder_path);
}
let mut st = State { version: VERSION.into(), ..Default::default() }; let mut st = State { version: VERSION.into(), ..Default::default() };
st.network = runtime.network.clone(); st.network = runtime.network.clone();
st.chain = if runtime.network == "devnet" { "devnet v4".into() } else { format!("{} (private)", runtime.network) }; st.chain = if runtime.network == "devnet" { "devnet v4".into() } else { format!("{} (private)", runtime.network) };
@ -201,6 +209,7 @@ impl Shared {
cmd_tx: Mutex::new(cmd_tx), cmd_tx: Mutex::new(cmd_tx),
ui_dir: Mutex::new(None), ui_dir: Mutex::new(None),
started: Instant::now(), started: Instant::now(),
started_unix: crate::platform::unix_now_f(),
engine_log: Mutex::new(engine_log), engine_log: Mutex::new(engine_log),
log_path, log_path,
port: std::sync::atomic::AtomicU16::new(0), port: std::sync::atomic::AtomicU16::new(0),
@ -274,6 +283,7 @@ impl Shared {
let mut st = self.state.lock().unwrap().clone(); let mut st = self.state.lock().unwrap().clone();
st.now = crate::platform::unix_now_f(); st.now = crate::platform::unix_now_f();
st.uptime_s = self.started.elapsed().as_secs(); st.uptime_s = self.started.elapsed().as_secs();
st.started_at = self.started_unix;
st.events = self.rings.lock().unwrap().events.iter().cloned().collect(); st.events = self.rings.lock().unwrap().events.iter().cloned().collect();
st.ladder = self.ladder.lock().unwrap().state(st.now, LADDER_GAP_S); st.ladder = self.ladder.lock().unwrap().state(st.now, LADDER_GAP_S);
match serde_json::to_value(st) { match serde_json::to_value(st) {

View file

@ -106,6 +106,15 @@ pub struct Ladder {
/// node-accepted blocks waiting for the miner's own ACCEPTED line (nonce -> hash, daa), and the reverse case /// node-accepted blocks waiting for the miner's own ACCEPTED line (nonce -> hash, daa), and the reverse case
#[serde(default)] #[serde(default)]
pending: Vec<(String, String, u64)>, pending: Vec<(String, String, u64)>,
/// first-block-21 (the project lead, 7 October 2026: "the 'you got your first block' situation only shows for the miner's
/// first ever block"): set the moment the first-block card is raised, when the lifetime count the engine
/// persists (settings.json accepted_total) crosses from 0 to 1. Kept across restarts, updates and a kept data
/// directory; never cleared by a key changing identities or a card being swapped (the count is the machine's,
/// not a key's or a card's). A count that starts over on a record carrying the flag (settings.json lost, the
/// ladder kept) raises no first card again; a record without the flag whose first block is older than this
/// version (count already 1 or more, or first_block_at set) takes the flag at `start_run`.
#[serde(default)]
pub first_block_shown: bool,
} }
/// What the dashboard reads (api/state.ladder): the record above summarised at `now`. /// What the dashboard reads (api/state.ladder): the record above summarised at `now`.
@ -133,6 +142,8 @@ pub struct LadderState {
pub milestone: Option<Milestone>, pub milestone: Option<Milestone>,
pub first_synced_at: f64, pub first_synced_at: f64,
pub first_mining_at: f64, pub first_mining_at: f64,
/// first-block-21: the first-block card has been raised once on this machine (see `Ladder::first_block_shown`)
pub first_block_shown: bool,
} }
pub fn day_of(unix: f64) -> u64 { pub fn day_of(unix: f64) -> u64 {
@ -152,6 +163,25 @@ impl Ladder {
} }
} }
/// The engine's run begins over this record (first-block-21, 7 October 2026). `accepted_total` is the lifetime
/// count settings.json holds. A block card is for the run that raised it: a milestone persisted by an earlier run
/// is dropped here, so a restart or an update never shows "your first block" (or block 100) a second time. A
/// record from before the flag existed whose first block already came (the count at 1 or more, or first_block_at
/// set) takes the flag now, since that card was shown by the version that raised it. Returns true when the
/// record changed and wants saving.
pub fn start_run(&mut self, accepted_total: u64) -> bool {
let mut dirty = false;
if self.milestone.is_some() {
self.milestone = None;
dirty = true;
}
if !self.first_block_shown && (accepted_total >= 1 || self.first_block_at > 0.0) {
self.first_block_shown = true;
dirty = true;
}
dirty
}
/// The node validated a block (its own log line): remembered by nonce until the miner's line claims it. /// The node validated a block (its own log line): remembered by nonce until the miner's line claims it.
pub fn on_node_accepted(&mut self, hash: &str, daa: u64, nonce: &str) { pub fn on_node_accepted(&mut self, hash: &str, daa: u64, nonce: &str) {
let nonce = norm_nonce(nonce); let nonce = norm_nonce(nonce);
@ -214,7 +244,13 @@ impl Ladder {
} }
self.blocks.insert(0, BlockFound { hash: hash.clone(), daa, nonce, at: now, card: card.to_string(), card_name: card_name.to_string(), count: total }); self.blocks.insert(0, BlockFound { hash: hash.clone(), daa, nonce, at: now, card: card.to_string(), card_name: card_name.to_string(), count: total });
self.blocks.truncate(KEEP_BLOCKS); self.blocks.truncate(KEEP_BLOCKS);
let kind = if total == 1 { "first" } else if total == 100 { "hundred" } else if total == 1_000 { "thousand" } else if total >= 10_000 && total % 10_000 == 0 { "ten_thousand" } else if new_card { "card" } else { "" }; // the first-block card: once in the app's life, when the persisted lifetime count crosses from 0 to 1 and the
// flag is unset (first-block-21); a count that starts over on a record already shown raises nothing here
let first = total == 1 && !self.first_block_shown;
if first {
self.first_block_shown = true;
}
let kind = if first { "first" } else if total == 100 { "hundred" } else if total == 1_000 { "thousand" } else if total >= 10_000 && total % 10_000 == 0 { "ten_thousand" } else if new_card && total > 1 { "card" } else { "" };
if kind.is_empty() { if kind.is_empty() {
return None; return None;
} }
@ -308,6 +344,7 @@ impl Ladder {
milestone: self.milestone.clone(), milestone: self.milestone.clone(),
first_synced_at: self.first_synced_at, first_synced_at: self.first_synced_at,
first_mining_at: self.first_mining_at, first_mining_at: self.first_mining_at,
first_block_shown: self.first_block_shown,
} }
} }
} }
@ -511,4 +548,104 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
assert_eq!(Ladder::load(&dir.join("missing.json")).votes_signed, 0); assert_eq!(Ladder::load(&dir.join("missing.json")).votes_signed, 0);
} }
// ---- first-block-21 (the project lead, 7 October 2026): the first-block card once in the app's life ----
/// The record as 0.3.20 wrote it after the first block: the milestone persisted, no flag field.
const RECORD_0320: &str = r#"{"first_block_at":1791140918.5,"first_block_card":"nvidia:0:RTX 4070","first_block_hash":"28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce","first_block_daa":38637,"days":[{"day":20730,"blocks":1}],"cards_first":{"nvidia:0:RTX 4070":1791140918.5},"blocks":[{"hash":"28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce","daa":38637,"nonce":"267dd27200a91c80","at":1791140918.5,"card":"nvidia:0:RTX 4070","card_name":"NVIDIA GeForce RTX 4070","count":1}],"votes_signed":2,"last_vote_index":8496,"last_vote_at":1791140950.0,"signed_open":[8496],"last_signed_locked":0,"signed_locked_count":0,"streak_since":1791140930.0,"shards":[],"milestone":{"kind":"first","count":1,"card":"nvidia:0:RTX 4070","card_name":"NVIDIA GeForce RTX 4070","hash":"28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce","daa":38637,"at":1791140918.5},"first_synced_at":1791140000.0,"first_mining_at":1791140100.0,"pending":[]}"#;
#[test]
fn known_failed_a_second_run_with_the_lifetime_count_at_1_showed_the_first_card_again() {
// the known-failed case: before this change a second run loaded the persisted milestone and the dashboard
// showed "your first block" again (every restart, every update, until the dismiss in that window's storage)
let mut l: Ladder = serde_json::from_str(RECORD_0320).unwrap();
assert_eq!(l.milestone.as_ref().map(|m| m.kind.as_str()), Some("first"), "the fixture carries the persisted card");
assert!(!l.first_block_shown, "the 0.3.20 record has no flag");
assert!(l.start_run(1), "the record changes and wants saving");
let s = l.state(1791150000.0, 600.0);
assert!(s.milestone.is_none(), "a card raised by an earlier run is never shown again");
assert!(s.first_block_shown);
assert_eq!(s.first_block_at, 1791140918.5, "the ladder's first-block facts stay");
}
#[test]
fn the_first_ever_block_raises_the_card_once_and_sets_the_flag() {
let mut l = Ladder::default();
assert!(!l.start_run(0), "a fresh record changes nothing at start");
let m = l.on_block(100.0, "a", "RTX 4070", "0x1", 1).unwrap();
assert_eq!(m.kind, "first");
assert!(l.first_block_shown);
let s = l.state(101.0, 600.0);
assert!(s.first_block_shown);
assert_eq!(s.milestone.as_ref().unwrap().kind, "first");
}
#[test]
fn a_restart_with_the_count_at_1_shows_nothing_and_block_2_raises_no_card() {
let mut run1 = Ladder::default();
run1.on_block(100.0, "a", "RTX 4070", "0x1", 1);
let dir = std::env::temp_dir().join(format!("igneum-ladder-first-{}", std::process::id()));
let path = dir.join("ladder.json");
run1.save(&path);
let mut run2 = Ladder::load(&path);
assert!(run2.first_block_shown, "the flag survives the save");
assert!(run2.start_run(1), "the persisted card is dropped");
assert!(run2.state(200.0, 600.0).milestone.is_none());
assert!(run2.on_block(300.0, "a", "RTX 4070", "0x2", 2).is_none(), "block 2 is an ordinary block");
assert_eq!(run2.blocks[0].count, 2);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_update_with_the_count_at_1_takes_the_flag_from_the_older_record_shape() {
// the new version opens a 0.3.20 record: the flag is set from the count, the card dropped, and the flag
// survives the next save in the new shape
let mut l: Ladder = serde_json::from_str(RECORD_0320).unwrap();
assert!(l.start_run(1));
assert!(l.first_block_shown);
assert!(l.milestone.is_none());
let mut back: Ladder = serde_json::from_str(&serde_json::to_string(&l).unwrap()).unwrap();
assert!(back.first_block_shown);
assert!(back.on_block(1791150000.0, "nvidia:0:RTX 4070", "NVIDIA GeForce RTX 4070", "0x3", 2).is_none());
// a 0.3.20 record whose count is still 0 but whose first_block_at is set (settings.json rewritten) is shown too
let mut l2: Ladder = serde_json::from_str(RECORD_0320).unwrap();
assert!(l2.start_run(0));
assert!(l2.first_block_shown);
}
#[test]
fn a_kept_data_directory_with_the_count_at_0_and_the_flag_unset_shows_it_on_the_first_block() {
// a kept directory from a machine that synced and voted but never found a block
let mut l = Ladder::default();
l.mark("synced", 10.0);
l.mark("mining", 20.0);
l.on_vote(30.0, 5, 600.0);
assert!(!l.start_run(0), "nothing to reconcile: no block yet");
assert!(!l.first_block_shown);
let m = l.on_block(100.0, "a", "RTX 4070", "0x1", 1).unwrap();
assert_eq!(m.kind, "first");
assert!(l.first_block_shown);
}
#[test]
fn a_count_that_starts_over_on_a_record_already_shown_raises_no_first_card() {
// settings.json lost (the count back at 0), the ladder kept with the flag: the next block is total 1 again
let mut l = Ladder::default();
l.on_block(100.0, "a", "RTX 4070", "0x1", 1);
assert!(l.on_block(200.0, "a", "RTX 4070", "0x2", 1).is_none(), "no second first-block card");
assert!(l.on_block(300.0, "b", "RTX 5090", "0x3", 1).is_none(), "nor a new-card card at a count of 1");
assert!(l.first_block_shown);
assert_eq!(l.state(301.0, 600.0).blocks.len(), 3, "the blocks are still recorded");
}
#[test]
fn block_2_is_an_ordinary_block_with_no_card() {
let mut l = Ladder::default();
l.on_block(100.0, "a", "RTX 4070", "0x1", 1);
assert!(l.on_block(130.0, "a", "RTX 4070", "0x2", 2).is_none());
let s = l.state(131.0, 600.0);
assert_eq!(s.blocks[0].count, 2);
assert_eq!(s.blocks_today, 2);
assert_eq!(s.milestone.as_ref().unwrap().count, 1, "the first card of this run stays until the run ends");
}
} }

View file

@ -556,6 +556,8 @@ pub struct State {
pub jobs: JobsState, pub jobs: JobsState,
pub events: Vec<Event>, pub events: Vec<Event>,
pub uptime_s: u64, pub uptime_s: u64,
/// first-block-21: when this engine run began (unix s, the wall clock; uptime_s is monotonic and stops across a sleep)
pub started_at: f64,
pub now: f64, pub now: f64,
pub quitting: bool, pub quitting: bool,
pub live_page: String, pub live_page: String,

View file

@ -1046,8 +1046,9 @@ var View = (function () {
function ladderRungs(s, chain, now) { function ladderRungs(s, chain, now) {
s = s || {}; var m = s.mining || {}, ld = s.ladder || {}, pv = s.proving || {}, c = chainNums(chain), rungs = []; s = s || {}; var m = s.mining || {}, ld = s.ladder || {}, pv = s.proving || {}, c = chainNums(chain), rungs = [];
var best = c.best, inTable = !!best, blocks = inTable ? best.blocks : 0, found = m.accepted_total || 0; var best = c.best, inTable = !!best, blocks = inTable ? best.blocks : 0, found = m.accepted_total || 0;
// 0. first block // 0. first block (first-block-21: a machine whose first block came, on any run, never reads "waiting" again,
var first = found > 0 || blocks > 0; // whatever the lifetime count says after a settings reset)
var first = found > 0 || blocks > 0 || ld.first_block_at > 0 || !!ld.first_block_shown;
rungs.push({ id: 'first', name: 'First block', state: first ? 'done' : 'now', line: first ? 'First block' + (ld.first_block_at > 0 && now ? ' ' + ago(now - ld.first_block_at) : '') + (ld.first_block_card ? ', ' + cardName(s, ld.first_block_card) : '') + '.' : 'Waiting for your first block.', sub: inTable ? withCommas(blocks) + ' block' + (blocks === 1 ? '' : 's') + ' with your key in the window' : (found ? withCommas(found) + ' found by this machine' : ''), field: inTable ? FIELDS.blocks : FIELDS.found, progress: -1 }); rungs.push({ id: 'first', name: 'First block', state: first ? 'done' : 'now', line: first ? 'First block' + (ld.first_block_at > 0 && now ? ' ' + ago(now - ld.first_block_at) : '') + (ld.first_block_card ? ', ' + cardName(s, ld.first_block_card) : '') + '.' : 'Waiting for your first block.', sub: inTable ? withCommas(blocks) + ' block' + (blocks === 1 ? '' : 's') + ' with your key in the window' : (found ? withCommas(found) + ' found by this machine' : ''), field: inTable ? FIELDS.blocks : FIELDS.found, progress: -1 });
// 1. the vote // 1. the vote
var dust = c.dust, voter = inTable && best.voter; var dust = c.dust, voter = inTable && best.voter;
@ -1074,8 +1075,25 @@ var View = (function () {
function currentRung(rungs) { for (var i = 0; i < rungs.length; i++) if (rungs[i].state === 'now') return rungs[i]; for (var j = rungs.length - 1; j >= 0; j--) if (rungs[j].state === 'done') return rungs[j]; return rungs[0]; } function currentRung(rungs) { for (var i = 0; i < rungs.length; i++) if (rungs[i].state === 'now') return rungs[i]; for (var j = rungs.length - 1; j >= 0; j--) if (rungs[j].state === 'done') return rungs[j]; return rungs[0]; }
// the block card: m = state.ladder.milestone, s = state, chain, now. {kind, eyebrow, title, when, cardLine, ignLine, // the block card: m = state.ladder.milestone, s = state, chain, now. {kind, eyebrow, title, when, cardLine, ignLine,
// rankLine, hash, url, fields} // rankLine, hash, url, fields}
// the Poisson count-up card shows only while this machine waits for its first ever block (first-block-21): never
// once the ladder records one, whatever the lifetime count says after a settings reset
function firstWait(s) {
s = s || {}; var m = s.mining || {}, ld = s.ladder || {};
return m.state === 'mining' && !(m.accepted_total > 0) && !(ld.first_block_at > 0) && !ld.first_block_shown;
}
// a card is for the run that raised it (first-block-21, the project lead, 7 October 2026: "the 'you got your first block'
// situation only shows for the miner's first ever block"). The engine drops a persisted milestone at start
// (ladder.rs start_run) and reports the run's start as started_at; against an older engine that still sends one, a
// milestone raised before this run began is refused here (a minute of slack; uptime_s stands in when the engine
// has no started_at). So a restart, an update or a reinstall over a kept state never shows the card twice.
function staleCard(m, s, now) {
if (!m || !(m.at > 0) || !s) return false;
var started = s.started_at > 0 ? s.started_at : (s.uptime_s >= 0 && now > 0 ? now - s.uptime_s : 0);
return started > 0 && m.at + 60 < started;
}
function blockCard(m, s, chain, now) { function blockCard(m, s, chain, now) {
if (!m || !(m.count > 0)) return null; if (!m || !(m.count > 0)) return null;
if (staleCard(m, s, now)) return null;
var c = chainNums(chain), cards = (s.mining || {}).cards || [], cd = cards.filter(function (x) { return x.key === m.card; })[0] || {}; var c = chainNums(chain), cards = (s.mining || {}).cards || [], cd = cards.filter(function (x) { return x.key === m.card; })[0] || {};
var name = m.card_name || cd.name || 'your card', mhs = cd.hash_avg > 0 ? cd.hash_avg : cd.hash_now || 0; var name = m.card_name || cd.name || 'your card', mhs = cd.hash_avg > 0 ? cd.hash_avg : cd.hash_now || 0;
var eyebrow = m.kind === 'first' ? 'your first block' : m.kind === 'hundred' ? 'block 100 of this machine' : m.kind === 'thousand' ? 'block 1,000 of this machine' : m.kind === 'ten_thousand' ? 'block ' + withCommas(m.count) + ' of this machine' : 'first block on ' + name; var eyebrow = m.kind === 'first' ? 'your first block' : m.kind === 'hundred' ? 'block 100 of this machine' : m.kind === 'thousand' ? 'block 1,000 of this machine' : m.kind === 'ten_thousand' ? 'block ' + withCommas(m.count) + ' of this machine' : 'first block on ' + name;
@ -1174,7 +1192,7 @@ var View = (function () {
} }
function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; } 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.'; } function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; }
return { VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, 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, driverWords: driverWords, vendorWord: vendorWord, 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, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords }; return { VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, 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, driverWords: driverWords, vendorWord: vendorWord, 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, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
})(); })();
/* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ---------- /* ---------- 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 One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned
@ -1995,7 +2013,7 @@ if (typeof document !== 'undefined') (function () {
var m = s.mining, now = s.now, ld = s.ladder || {}; var m = s.mining, now = s.now, ld = s.ladder || {};
if (m.state === 'mining' && m.hash_total > 0) { if (!miningSince) miningSince = now; } else miningSince = 0; if (m.state === 'mining' && m.hash_total > 0) { if (!miningSince) miningSince = now; } else miningSince = 0;
var c = View.chainNums(chain), p = View.poisson(m.hash_total, c.hps, miningSince ? now - miningSince : 0, c.bps, (m.cards || []).filter(function (x) { return x.state === 'mining'; }).length > 1); var c = View.chainNums(chain), p = View.poisson(m.hash_total, c.hps, miningSince ? now - miningSince : 0, c.bps, (m.cards || []).filter(function (x) { return x.state === 'mining'; }).length > 1);
var waiting = !(m.accepted_total > 0) && m.state === 'mining'; var waiting = View.firstWait(s);
setHidden('first-wait', !waiting); setHidden('first-wait', !waiting);
if (waiting) { if (waiting) {
if (p) { setText('first-wait-line', p.line); setText('first-wait-sub', p.pool || 'The chance counts up from the first job. The line gives way to the block card.'); setText('first-wait-pct', p.chanceText); $('first-wait-arc').style.strokeDashoffset = (97.4 * (1 - p.chance)).toFixed(1); } if (p) { setText('first-wait-line', p.line); setText('first-wait-sub', p.pool || 'The chance counts up from the first job. The line gives way to the block card.'); setText('first-wait-pct', p.chanceText); $('first-wait-arc').style.strokeDashoffset = (97.4 * (1 - p.chance)).toFixed(1); }

View file

@ -638,6 +638,58 @@ test('a Devnet 2 key walks every rung on screen, and every rung names its RPC fi
assert.equal(V.ladderRungs(stateOf(), chainOf(), T5)[6].line, 'Proving is off.'); assert.equal(V.ladderRungs(stateOf(), chainOf(), T5)[6].line, 'Proving is off.');
}); });
test('first-block-21 (the project lead, 7 October 2026): the first-block card shows once in the app’s life, never on a later run', () => {
const m1 = { kind: 'first', count: 1, card: 'nvidia:0:RTX 4070', card_name: 'NVIDIA GeForce RTX 4070', hash: '9f3a' + 'ab'.repeat(28) + 'c21e', daa: 1284117, at: T5 - 60 };
const c = chainOf({ mine: [key({ blocks: 1, rank: 4 })], best: key({ blocks: 1, rank: 4 }) });
const afterFirst = (over, ladderOver) => stateOf({ mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 0, found: [] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 7200, first_block_card: 'nvidia:0:RTX 4070', first_block_hash: m1.hash, first_block_daa: 1284117, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, first_block_shown: true, ...ladderOver }, ...over });
// KNOWN-FAILED FIRST: a second run (the engine up 30 s, the lifetime count already 1, the card raised two hours ago
// on the first run and still in the record an older engine sends) showed "your first block" again before this change
const stale = { ...m1, at: T5 - 7200 };
let s = afterFirst({ uptime_s: 30, started_at: T5 - 30 }, { milestone: stale });
assert.equal(V.blockCard(stale, s, c, T5), null, 'a second run with the count at 1 shows no first-block card');
assert.equal(V.staleCard(stale, s, T5), true);
// the first ever block: the card, in the run that raised it
s = stateOf({ mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 1, found: [T5 - 60] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 60, first_block_card: 'nvidia:0:RTX 4070', first_block_hash: m1.hash, first_block_daa: 1284117, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, first_block_shown: true, milestone: m1 } });
s.started_at = T5 - 1800;
const bc = V.blockCard(m1, s, c, T5);
assert.equal(bc.eyebrow, 'your first block');
assert.equal(bc.title, 'Block 1,284,117 is yours.');
assert.equal(V.staleCard(m1, s, T5), false);
assert.equal(V.firstWait(s), false, 'the count-up gives way to the card');
// a restart with the count at 1: the engine dropped the milestone (ladder.rs start_run); nothing to show, the
// first rung stays done and the count-up never comes back
s = afterFirst({ uptime_s: 12, started_at: T5 - 12 }, { milestone: null });
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5), null);
assert.equal(V.ladderRungs(s, c, T5)[0].state, 'done');
assert.equal(V.firstWait(s), false);
// an update with the count at 1: a newer version, the same rule; a stale card an older engine still sends is refused
s = afterFirst({ uptime_s: 45, started_at: T5 - 45, version: '0.3.21' }, { milestone: stale });
assert.equal(V.blockCard(stale, s, c, T5), null);
// an engine without started_at (older): uptime_s stands in
s = afterFirst({ uptime_s: 45, started_at: 0 }, { milestone: stale });
assert.equal(V.blockCard(stale, s, c, T5), null);
// a kept data directory with the count at 0 and the flag unset: the count-up shows, then the first block raises the card
s = stateOf({ uptime_s: 300, started_at: T5 - 300, ladder: { ...stateOf().ladder, first_block_shown: false, first_synced_at: T5 - 86400 * 3, first_mining_at: T5 - 86400 * 3, votes_signed: 40 } });
assert.equal(V.firstWait(s), true);
assert.equal(V.ladderRungs(s, chainOf(), T5)[0].line, 'Waiting for your first block.');
s = stateOf({ uptime_s: 400, started_at: T5 - 400, mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 1, found: [T5 - 5] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 5, first_block_shown: true, first_synced_at: T5 - 86400 * 3, first_mining_at: T5 - 86400 * 3, votes_signed: 40, milestone: { ...m1, at: T5 - 5 } } });
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5).eyebrow, 'your first block');
assert.equal(V.firstWait(s), false);
// a count that started over (settings.json lost, the ladder kept with the flag): no "waiting", no card
s = stateOf({ mining: { ...stateOf().mining, accepted_total: 0, found: [] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 86400, first_block_shown: true, milestone: null } });
assert.equal(V.firstWait(s), false);
assert.equal(V.ladderRungs(s, chainOf(), T5)[0].state, 'done');
assert.equal(V.blockCard(null, s, c, T5), null);
// block 2: the ordinary accepted pulse only (the block on the strip's list, the activity line, the run count), no card
s = afterFirst({ uptime_s: 1800, started_at: T5 - 1800, mining: { ...stateOf().mining, accepted_total: 2, accepted_session: 1, found: [T5 - 2] } }, { milestone: null, blocks_30d: 2, blocks_today: 2 });
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5), null);
assert.equal(V.plainEvent('block accepted by the node (NVIDIA GeForce RTX 4070)').text, 'Block found by NVIDIA GeForce RTX 4070');
assert.equal(V.earningsLines(s, c, 28, T5).run.text, '1 block this run');
assert.equal(V.earningsLines(s, c, 28, T5).lifetime.text, '2 blocks');
assert.deepEqual(s.mining.found, [T5 - 2], 'the strip flashes the block that just arrived');
assert.equal(V.ladderRungs(s, c, T5)[0].state, 'done');
});
test('the rungs under mainnet-sized params: 100 blocks to a vote, 30 days to full weight; the relay and the cap are named', () => { test('the rungs under mainnet-sized params: 100 blocks to a vote, 30 days to full weight; the relay and the cap are named', () => {
const s = stateOf({ mining: { ...stateOf().mining, accepted_total: 61, found: [] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 61 } }); const s = stateOf({ mining: { ...stateOf().mining, accepted_total: 61, found: [] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 61 } });
const c = chainOf({ source: 'observer', keys_listed: 64, keys_cap: 64, voters: 1204, mine: [key({ blocks: 61, rank: 41 })], best: key({ blocks: 61, rank: 41 }) }, MAINNET); const c = chainOf({ source: 'observer', keys_listed: 64, keys_cap: 64, voters: 1204, mine: [key({ blocks: 61, rank: 41 })], best: key({ blocks: 61, rank: 41 }) }, MAINNET);

View file

@ -20,6 +20,8 @@
// job a remote job running // job a remote job running
// firstwait miner-ui-5: mining, no block yet (the Poisson count-up), the ladder's first rung // firstwait miner-ui-5: mining, no block yet (the Poisson count-up), the ladder's first rung
// firstblock miner-ui-5: the first block a minute ago (the block card), the key under the dust line // firstblock miner-ui-5: the first block a minute ago (the block card), the key under the dust line
// secondblock first-block-21: block 2 two seconds ago, ten minutes after the first; the ordinary accepted pulse (the
// block on the strip, the activity line, "2 blocks this run"), no card; ladder.first_block_shown true
// ladder miner-ui-5: every rung walked (vote, signature, full window, rank 3, streak, a paid shard) // ladder miner-ui-5: every rung walked (vote, signature, full window, rank 3, streak, a paid shard)
// drivers driver-check: the 5090 fine, the 9070 XT and the Arc with an Install offer (also drivers-running, // drivers driver-check: the 5090 fine, the 9070 XT and the Arc with an Install offer (also drivers-running,
// drivers-reboot, drivers-error, drivers-mac) // drivers-reboot, drivers-error, drivers-mac)
@ -119,10 +121,12 @@ function liveNow(windowS) {
} }
// miner-ui-5: this machine's ladder record (state.ladder, src/ladder.rs) per scenario // miner-ui-5: this machine's ladder record (state.ladder, src/ladder.rs) per scenario
function ladderNow(t) { function ladderNow(t) {
const base = { first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: t0 - 2800, first_mining_at: t0 - 2700 }; const base = { first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: t0 - 2800, first_mining_at: t0 - 2700, first_block_shown: false };
const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e'; const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e';
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 } }; const hash2 = '4c1d9e7a2b5f8c0e3d6a9b1c4e7f0a2d5b8c1e4f7a0b3d6c9e2f5a8b1d4c7e0f3a2';
if (scenario === 'ladder') return { ...base, first_block_at: t - 7300, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 1, blocks_30d: 61, blocks_today: 61, votes_signed: 240, last_vote_index: 8495, last_vote_at: t - 12, last_signed_locked: 8495, signed_locked_count: 238, streak_s: 7100, shards: [{ block: 160390, shard: 3, wei: '1150000000000000000', at: t - 400 }], milestone: { kind: 'hundred', count: 100, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266400, at: t - 50 } }; if (scenario === 'secondblock') return { ...base, first_block_at: t - 602, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 2, blocks_today: 2, cards_first: { [recState.mining.cards[0].key]: t - 602 }, blocks: [{ hash: hash2, daa: 267054, nonce: '3a9c0d11e2f4b5a6', at: t - 2, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 2 }, { hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 602, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 20, last_vote_index: 8516, last_vote_at: t - 8, milestone: null, first_block_shown: true };
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 }, first_block_shown: true };
if (scenario === 'ladder') return { ...base, first_block_at: t - 7300, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 1, blocks_30d: 61, blocks_today: 61, votes_signed: 240, last_vote_index: 8495, last_vote_at: t - 12, last_signed_locked: 8495, signed_locked_count: 238, streak_s: 7100, shards: [{ block: 160390, shard: 3, wei: '1150000000000000000', at: t - 400 }], milestone: { kind: 'hundred', count: 100, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266400, at: t - 50 }, first_block_shown: true };
return base; return base;
} }
// miner-ui-5: GET /api/ladder, in src/chainfacts.rs's shape: the devnet's finality params (api/live, 7 Oct 2026), // miner-ui-5: GET /api/ladder, in src/chainfacts.rs's shape: the devnet's finality params (api/live, 7 Oct 2026),
@ -130,6 +134,7 @@ function ladderNow(t) {
function ladderReply(t) { function ladderReply(t) {
const mine = scenario === 'firstblock' ? { id: '8fafda27', blocks: 1, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4 } const mine = scenario === 'firstblock' ? { id: '8fafda27', blocks: 1, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4 }
: scenario === 'ladder' ? { id: '8fafda27', blocks: 61, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 3 } : scenario === 'ladder' ? { id: '8fafda27', blocks: 61, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 3 }
: scenario === 'secondblock' ? { id: '8fafda27', blocks: 2, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4 }
: scenario === 'firstwait' ? null : { id: '8fafda27', blocks: 251, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 2 }; : scenario === 'firstwait' ? null : { id: '8fafda27', blocks: 251, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 2 };
return { ok: true, source: 'observer', read_at: t, cached_age_s: 0, rpc: { weights: 'getFinalityWeights', checkpoints: 'getFinalityCheckpoints', stats: '/api/stats', relay: 'igneum.network/api/live (the observer\'s copy of the node\'s reply)' }, return { ok: true, source: 'observer', read_at: t, cached_age_s: 0, rpc: { weights: 'getFinalityWeights', checkpoints: 'getFinalityCheckpoints', stats: '/api/stats', relay: 'igneum.network/api/live (the observer\'s copy of the node\'s reply)' },
params: { dust: 5, weight_window: 7200, presence_window: 20, checkpoint_interval: 30, min_daa: 7200 }, checkpoint_index: 8495, daa_score: 266454, latest_locked_index: 8495, next_index: 8496, finality_active: true, params: { dust: 5, weight_window: 7200, presence_window: 20, checkpoint_interval: 30, min_daa: 7200 }, checkpoint_index: 8495, daa_score: 266454, latest_locked_index: 8495, next_index: 8496, finality_active: true,
@ -145,7 +150,9 @@ function stateNow() {
s.installed_at = t0 - 3000; s.installed_at = t0 - 3000;
if (scenario === 'firstwait') { s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.mining.found = []; s.finality = { ...s.finality, last_lock: 0, votes: 0 }; } if (scenario === 'firstwait') { s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.mining.found = []; s.finality = { ...s.finality, last_lock: 0, votes: 0 }; }
if (scenario === 'firstblock') { s.mining.accepted_total = 1; s.mining.accepted_session = 1; s.mining.cards[0].accepted = 1; } if (scenario === 'firstblock') { s.mining.accepted_total = 1; s.mining.accepted_session = 1; s.mining.cards[0].accepted = 1; }
if (scenario === 'secondblock') { s.mining.accepted_total = 2; s.mining.accepted_session = 2; s.mining.cards[0].accepted = 2; }
s.uptime_s = recState.uptime_s + Math.floor(t - t0); s.uptime_s = recState.uptime_s + Math.floor(t - t0);
s.started_at = t0 - recState.uptime_s; // first-block-21: the run's start on the wall clock (every card here is this run's)
s.mining.found = recState.mining.found.map(x => x + shift).filter(x => t - x < 3600); s.mining.found = recState.mining.found.map(x => x + shift).filter(x => t - x < 3600);
s.events = recState.events.map(e => ({ ...e, t: e.t + shift })); s.events = recState.events.map(e => ({ ...e, t: e.t + shift }));
s.finality.last_lock_at += shift; s.finality.age_s = t - s.finality.last_lock_at; s.finality.last_lock_at += shift; s.finality.age_s = t - s.finality.last_lock_at;
@ -159,9 +166,10 @@ function stateNow() {
const wob = Math.sin((t - t0) / 7) * 1.4; const wob = Math.sin((t - t0) / 7) * 1.4;
s.mining.hash_total = +(16.9 + wob).toFixed(2); s.mining.cards[0].hash_now = s.mining.hash_total; s.mining.hash_total = +(16.9 + wob).toFixed(2); s.mining.cards[0].hash_now = s.mining.hash_total;
// a fresh block every ~9 s on average (none before the first block) // a fresh block every ~9 s on average (none before the first block)
const extra = []; if (scenario !== 'firstwait' && scenario !== 'firstblock') for (let k = t0; k < t; k += 9.3) extra.push(k + 0.4); const extra = []; if (scenario !== 'firstwait' && scenario !== 'firstblock' && scenario !== 'secondblock') for (let k = t0; k < t; k += 9.3) extra.push(k + 0.4);
if (scenario === 'firstwait') { s.mining.found = []; s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.events = s.events.filter(e => e.kind !== 'block'); } if (scenario === 'firstwait') { s.mining.found = []; s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.events = s.events.filter(e => e.kind !== 'block'); }
if (scenario === 'firstblock') { s.mining.found = [t - 70]; s.events = [{ kind: 'block', t: t - 70, text: 'first block: found by ' + s.mining.cards[0].name + ' (block card)' }, { kind: 'block', t: t - 70, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }].concat(s.events.filter(e => e.kind !== 'block')).slice(0, 40); } if (scenario === 'firstblock') { s.mining.found = [t - 70]; s.events = [{ kind: 'block', t: t - 70, text: 'first block: found by ' + s.mining.cards[0].name + ' (block card)' }, { kind: 'block', t: t - 70, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }].concat(s.events.filter(e => e.kind !== 'block')).slice(0, 40); }
if (scenario === 'secondblock') { s.mining.found = [t - 602, t - 2]; s.events = [{ kind: 'block', t: t - 2, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }, { kind: 'block', t: t - 602, text: 'first block: found by ' + s.mining.cards[0].name + ' (block card)' }, { kind: 'block', t: t - 602, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }].concat(s.events.filter(e => e.kind !== 'block')).slice(0, 40); }
if (scenario === 'ladder') { s.settings.prove = true; s.proving = { ...s.proving, enabled: true, available: true, status: 'idle', paid: 1, paid_wei: '1150000000000000000', last_paid: 'block 160390 shard 3' }; s.events = [{ kind: 'proving', t: t - 400, text: 'block 160390 shard 3 paid 1.15 IGN' }, { kind: 'block', t: t - 50, text: 'block 100 of this machine: found by ' + s.mining.cards[0].name + ' (block card)' }].concat(s.events).slice(0, 40); } if (scenario === 'ladder') { s.settings.prove = true; s.proving = { ...s.proving, enabled: true, available: true, status: 'idle', paid: 1, paid_wei: '1150000000000000000', last_paid: 'block 160390 shard 3' }; s.events = [{ kind: 'proving', t: t - 400, text: 'block 160390 shard 3 paid 1.15 IGN' }, { kind: 'block', t: t - 50, text: 'block 100 of this machine: found by ' + s.mining.cards[0].name + ' (block card)' }].concat(s.events).slice(0, 40); }
s.mining.found = s.mining.found.concat(extra).filter(x => t - x < 3600).sort((a, b) => a - b); s.mining.found = s.mining.found.concat(extra).filter(x => t - x < 3600).sort((a, b) => a - b);
s.mining.accepted_session += extra.length; s.mining.accepted_total += extra.length; s.mining.cards[0].accepted += extra.length; s.mining.accepted_session += extra.length; s.mining.accepted_total += extra.length; s.mining.cards[0].accepted += extra.length;