From 6cf648e6aedc5707850ff63ca220249d1c7f1127 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:51:43 +0000 Subject: [PATCH] class v5 harness: the stale miner's fall-off is judged by the block's DAA score (a nonce found on a pre-boundary template and submitted after it is a flip-epoch block, valid; the wall-clock count failed the 20:4x UK run on one such block), nonces matched as the nearest double the JSON RPC carries Co-Authored-By: Claude Fable 5.1 --- infra/fast-time/class-v5-signal.mjs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/infra/fast-time/class-v5-signal.mjs b/infra/fast-time/class-v5-signal.mjs index 9406bacf..d6c98dcc 100644 --- a/infra/fast-time/class-v5-signal.mjs +++ b/infra/fast-time/class-v5-signal.mjs @@ -237,7 +237,7 @@ async function allBlocks(n) { const r = await n.rpc.call('getBlocks', { lowHash: low, includeBlocks: true, includeTransactions: false }); const blocks = r.blocks || []; let added = 0; - for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, chain: !!b.verboseData?.isChainBlock }); added++; } + for (const b of blocks) { const h = b.verboseData?.hash || b.header?.hash; if (seen.has(h)) continue; seen.add(h); out.push({ hash: h, daa: +b.header.daaScore, version: +b.header.version, nonce: Number(b.header.nonce ?? 0), chain: !!b.verboseData?.isChainBlock }); added++; } if (!blocks.length || added === 0) break; low = (r.blockHashes || []).at(-1) || blocks.at(-1).verboseData?.hash; if (!low) break; } @@ -332,9 +332,30 @@ const statelessAcceptedAfterFlip = statelessNode ? acceptedAfter(statelessNode.i const honestAcceptedAfterFlip = SIGNALLING.filter(n => n.i !== STALE).map(n => acceptedAfter(n.i, flipAtMs)); // the refusal reaches the miner as a template error and the node's log as the engine's line; either counts const statelessRefusals = statelessNode ? statelessNode.grepLog(REFUSAL_LINE).length + minerLog(statelessNode.i).filter(l => REFUSAL_LINE.test(l)).length : 0; -// the stale miner: accepted after the FIRST REFRESH after the flip (one epoch later), and its rejections -const refreshWall = flipAtMs == null ? null : flipAtMs + EPOCH * 1000; -const staleAcceptedAfterRefresh = STALE == null ? null : acceptedAfter(STALE, refreshWall); +// the stale miner: accepted blocks IN the first refresh epoch after the flip or later (its frozen leaves are the flip +// epoch's; from the next epoch its blocks fail PoW), and its rejections. Judged by the block's DAA score, not the wall +// clock: a nonce found on a template taken before the boundary and submitted a few seconds after it is a flip-epoch +// block (the 20:4x UK run of 7 October 2026 read one such ACCEPTED line, "template 2 gens old", 4.7 s after the +// boundary's wall time, and the wall-clock form of this count failed the case on a block that was valid). The miner's +// ACCEPTED line carries the nonce; the chain carries the nonce and the DAA score of every block. +const refreshDaa = firstV4 ? (firstV4.epoch + 1) * EPOCH : null; +// the JSON RPC carries the 64-bit nonce as a JSON number, so the node's value arrives as the nearest double; Number(BigInt(hex)) +// of the miner's line is the nearest double to the same integer, so the two keys are equal (660 blocks never share one) +const blockByNonce = new Map(blocks.map(b => [b.nonce, b])); +function acceptedAtOrAboveDaa(i, daa) { + if (daa == null) return 0; + let count = 0, unmatched = 0; + for (const l of minerLog(i)) { + const m = /ACCEPTED block nonce=(0x[0-9a-f]+)/.exec(l); + if (!m) continue; + const b = blockByNonce.get(Number(BigInt(m[1]))); + if (!b) { unmatched++; continue; } + if (b.daa >= daa) count++; + } + if (unmatched) log(`miner ${i}: ${unmatched} ACCEPTED nonce(s) not found on the chain read from n0 (pruned or a side block)`); + return count; +} +const staleAcceptedAfterRefresh = STALE == null ? null : acceptedAtOrAboveDaa(STALE, refreshDaa); const staleRejected = STALE == null ? null : rejectedMiner[STALE].length; const daysSeen = [...new Set([...dayOfEpoch.values()])]; const sinks = dag.map(d => String(d.sink || '?').slice(0, 16));