miner-ui-4: carry the clock sample's exec-record gate (0f6b4650, ledger N7); version words follow the 0.3.19 renumbering

Only update.rs from 0f6b4650; its version bumps stay with the shipper's cut.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 032e1f8717)
This commit is contained in:
igneum-labs 2026-10-07 08:32:03 +00:00
parent da100ddd2e
commit f4ddfe5b0a
6 changed files with 41 additions and 4 deletions

View file

@ -4056,7 +4056,7 @@ impl Engine {
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 (mut synced, mut cause) = sync_decision_v2(&Reading { blocks, headers, peers, flag }, caught_up_moving, stable_s, accepted_recent, private, tip_age_s);
// 0.3.18: a synced-looking node whose blocks never merge into the network reads "behind" and holds the miner
// 7 October 2026: a synced-looking node whose blocks never merge into the network reads "behind" and holds the miner
if synced && !private {
let since = *self.merge_mining_since.get_or_insert(now);
let check = crate::merge::MergeCheck {

View file

@ -22,7 +22,7 @@ pub struct Check {
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 network's compiled merge depth in blue score (`blockrate.mergeDepth`, the node lane's 0.3.18 addition);
/// the network's compiled merge depth in blue score (`blockrate.mergeDepth`, the node lane's addition of 7 October 2026);
/// None on a node without the object, and the app assumes 3,600 (1 bps)
pub merge_depth: Option<u64>,
}

View file

@ -1,4 +1,4 @@
//! Is this node's work merging into the network? (design note for 0.3.18, 7 October 2026, from the node lane's 30-s
//! Is this node's work merging into the network? (design note, 7 October 2026, from the node lane's 30-s
//! propagation reading: a node behind a slow link keeps mining its own chain, its relayed blocks never merge, and the
//! N4 "synced" test (tip moving, a peer present) cannot see it.)
//!

View file

@ -40,9 +40,36 @@ fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
era * 146_097 + doe - 719_468
}
/// True once the node's exec follower holds a record (igneum_getExecStatus's executedTipHash is set). Any error or an
/// unreachable node reads false: the block sample waits rather than asks.
fn exec_has_record(evm_port: u16) -> bool {
let body = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"igneum_getExecStatus\",\"params\":[]}";
let out = crate::detect::run_timeout(
Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", "5", "-X", "POST", &format!("http://127.0.0.1:{evm_port}"), "-H", "Content-Type: application/json", "-d", body]),
None,
Duration::from_secs(7),
);
out.as_deref().map(exec_status_has_record).unwrap_or(false)
}
/// The reading of an igneum_getExecStatus reply: a record is held when executedTipHash is a non-null string.
pub fn exec_status_has_record(reply: &str) -> bool {
serde_json::from_str::<serde_json::Value>(reply)
.ok()
.and_then(|v| v.get("result")?.get("executedTipHash")?.as_str().map(|h| !h.is_empty()))
.unwrap_or(false)
}
/// The latest block's timestamp (unix seconds) from the node's Ethereum JSON-RPC (the execution layer mirrors the
/// consensus block times). The first clock source: local time against what the peers produced.
pub fn latest_block_time(evm_port: u16) -> Option<f64> {
// 0.3.18 (ledger N7, 7 October 2026): a node whose exec follower holds no record yet dies on eth_getBlockByNumber
// (rpc.rs indexes records[0] on an empty vector and the panic hook exits the process). The nodes before 0.3.18's
// fix are live on every machine, and this sample runs every 9 s once blocks arrive, so it asks igneum_getExecStatus
// first (it indexes nothing) and takes no block until the follower reports an executed tip.
if !exec_has_record(evm_port) {
return None;
}
let body = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\",false]}";
let out = crate::detect::run_timeout(
Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", "5", "-X", "POST", &format!("http://127.0.0.1:{evm_port}"), "-H", "Content-Type: application/json", "-d", body]),
@ -56,6 +83,15 @@ pub fn latest_block_time(evm_port: u16) -> Option<f64> {
#[cfg(test)]
mod tests {
/// Ledger N7: the clock sample must not ask eth_getBlockByNumber of a follower with no record
#[test]
fn exec_status_gate() {
assert!(super::exec_status_has_record(r#"{"jsonrpc":"2.0","id":1,"result":{"executedTip":"0x38a5","executedTipHash":"0xa3ae37ec31b9227c3855a1b25ba50ee9f6815bf00b0befcc8836b430baf04343"}}"#));
assert!(!super::exec_status_has_record(r#"{"jsonrpc":"2.0","id":1,"result":{"executedTip":"0x0","executedTipHash":null}}"#));
assert!(!super::exec_status_has_record(r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}"#));
assert!(!super::exec_status_has_record(""));
assert!(!super::exec_status_has_record("not json"));
}
#[test]
fn http_date() {
assert_eq!(super::parse_http_date("Sun, 04 Oct 2026 11:17:47 GMT"), Some(1_791_112_667.0));

View file

@ -465,7 +465,7 @@ var View = (function () {
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':
// 0.3.18: a node whose blocks never merge into the network (src/merge.rs) is behind with a tip that still moves
// 7 October 2026: a node whose blocks never merge into the network (src/merge.rs) is behind with a tip that still moves
if (n.sync_cause === 'not merging') return { word: 'behind', line: 'Your blocks are not merging into the network: every other node is more than the merge depth ahead. Mining pauses until this node merges again; a slow link is the usual cause.', tone: 'bad' };
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' };

View file

@ -114,3 +114,4 @@ Tests: UI 39; the app crate on the box 152 passed (the N4 pair, the shot path, t
- Fix (src/watchdog.rs, src/engine.rs): a miner is judged only while the node is synced (the start, no-status and zero-rate clocks hold at now while it is not); a worker silent from its start with the node synced is one restart then a fault at 60 s (`START_S`), 90 s stays for a running one; a fault the node's readiness explains (`faulted_by_node`) is released by `Event::NodeSynced` when the node syncs, the card starts again with an Activity line; one Activity line per card at its first worker start. Test `a_worker_started_while_the_node_syncs_is_not_faulted_and_a_faulted_one_returns_at_sync` (recorded state), 173 box tests.
- The pre-apply card set already persists (cards' enabled flags live in settings and the relaunch planned every enabled card); the fault, not the plan, was the failure.
- Read-back changed the reading (relaunch-engine-and-workers.txt, 05:40 UK): PC 1's node reported synced=true from the first second; the miners started at +14, +25 and +38 s, subscribed, then every template fetch timed out at 5 s for 180 s while the node ran its finality catch-up (checkpoints 8053 to 8055 locking), so no status line was printed; the watchdog restarted each at 90 s and faulted each at 180 s. Rule added: "template fetch timed out" is the miner's heartbeat (`Event::TemplateTimeout`): the silence clocks start over from the line, the card reads "waiting for the node to answer block templates", one Activity line per episode, and the miner is judged again from its last line (60 s of nothing at all after it is still a restart). The synced-gate half stays. Test extended with the recorded sequence; 173 box tests. The node-side half (the template RPC blocking under the finality replay after a restart) is with the node lane.
- Renumbered again (main, 7 October 2026, 09:2x UK): 0.3.18 is an app-only cut on the 0.3.17 tree (the clock sample gated on igneum_getExecStatus, ledger N7, commit 0e6c1248); this tree ships as 0.3.19 (release-0.3.19, node release-0.3.19-node); decimals is 0.3.20. 0e6c1248's update.rs hunk is carried here so the branch is whole; the Mac check comes on the 0.3.19 apply.