--- a/igneum/miner/src/main.rs +++ b/igneum/miner/src/main.rs @@ -366,8 +366,15 @@ /// selected parent, which is the selected parent of any block built on the current template). struct Seeder { genesis: Hash, - /// (epoch index, sink the walk started from) -> seed - memo: HashMap<(u64, Hash), Hash>, + /// epoch index -> (selected-chain block -> that epoch's seed) for every block a walk has visited. A block's seed + /// is a function of the block alone (its selected parent chain is fixed), so a walk from a new sink stops at the + /// first block already in the map: a job costs as many `get_block` calls as the sink advanced, not the DAA + /// position in the epoch (the old `(epoch, sink)` key missed on every new block and walked back to the epoch's + /// first block, up to 3,600 calls, measured on the RTX 5090 on 3 October 2026 as a gap between jobs that grew + /// from 0.10 s to 0.33 s across epoch 2; docs/analysis/hashrate-decay-2026-10-03.md). + memo: HashMap>, + /// `get_block` calls made by seed walks, for the STATUS line + walk_calls: u64, } impl Seeder { @@ -392,7 +399,7 @@ if genesis != DEVNET_PARAMS.genesis.hash { println!("{} node genesis {} differs from the shared devnet genesis: a private test network", now(), genesis); } - Self { genesis, memo: HashMap::new() } + Self { genesis, memo: HashMap::new(), walk_calls: 0 } } async fn seeds_for(&mut self, client: &GrpcClient, header: &Header) -> EpochSeeds { @@ -402,25 +409,32 @@ return EpochSeeds { epoch: self.genesis, day }; } let sink = client.get_block_dag_info().await.expect("dag info").sink; - if let Some(seed) = self.memo.get(&(epoch, sink)) { - return EpochSeeds { epoch: *seed, day }; - } + // This epoch's map and the previous one stay (a template can still land in the previous epoch at the + // boundary); older ones go. A map holds at most one entry per selected-chain block of its epoch (3,600). + self.memo.retain(|e, _| *e + 1 >= epoch); + let known = self.memo.entry(epoch).or_default(); let epoch_start = epoch * POW_EPOCH_BLOCKS; let mut cur = sink; + let mut path = Vec::new(); let seed = loop { + if let Some(seed) = known.get(&cur) { + break *seed; + } if cur == self.genesis { break self.genesis; } let b = client.get_block(cur, false).await.expect("get block"); + self.walk_calls += 1; if b.header.daa_score < epoch_start { break cur; } + path.push(cur); cur = b.verbose_data.expect("verbose data").selected_parent_hash; }; - if self.memo.len() > 10_000 { - self.memo.clear(); + for h in path { + known.insert(h, seed); } - self.memo.insert((epoch, sink), seed); + known.insert(sink, seed); EpochSeeds { epoch: seed, day } } } @@ -707,6 +721,9 @@ let mut gpu_ms_total = 0.0f64; let mut jobs = 0u64; let mut last_report = Instant::now(); + // The previous STATUS line's counters, for the per-interval rates (the cumulative ones decay by construction + // after a fast first interval, which read as a hash-rate decay on 3 October 2026) + let (mut last_hashes, mut last_gpu_ms, mut last_jobs, mut last_walk) = (0u64, 0.0f64, 0u64, 0u64); let mut last_seeds: Option = None; let mut rng = rand::thread_rng(); while start.elapsed() < Duration::from_secs(o.secs) { @@ -813,8 +830,9 @@ } if last_report.elapsed() > Duration::from_secs(o.status_secs) { let el = start.elapsed().as_secs_f64(); + let (dt, dh, dg) = (last_report.elapsed().as_secs_f64(), (hashes_total - last_hashes) as f64, gpu_ms_total - last_gpu_ms); println!( - "{} STATUS '{}' [worker]: {:.0}s jobs={} accepted={} rejected={} mismatched={} extra={} rate={:.2} blocks/s hash={:.2} MH/s wall ({:.2} MH/s inside jobs) template_age={:.2}s synced={}", + "{} STATUS '{}' [worker]: {:.0}s jobs={} accepted={} rejected={} mismatched={} extra={} rate={:.2} blocks/s hash={:.2} MH/s wall ({:.2} MH/s inside jobs) now={:.2} MH/s wall ({:.2} MH/s inside jobs, {} jobs, seed walk {} calls) template_age={:.2}s synced={}", now(), o.vote_label, el, @@ -826,9 +844,14 @@ found as f64 / el, hashes_total as f64 / el / 1e6, if gpu_ms_total > 0.0 { hashes_total as f64 / gpu_ms_total / 1e3 } else { 0.0 }, + if dt > 0.0 { dh / dt / 1e6 } else { 0.0 }, + if dg > 0.0 { dh / dg / 1e3 } else { 0.0 }, + jobs - last_jobs, + seeder.walk_calls - last_walk, template_at.elapsed().as_secs_f64(), synced ); + (last_hashes, last_gpu_ms, last_jobs, last_walk) = (hashes_total, gpu_ms_total, jobs, seeder.walk_calls); last_report = Instant::now(); } }