miner-ui-4: fold Ember's finality-paused words, read igneum_getNodeInfo (params, network, powEngine stub fault)
View.finalityWords with Ember's test (41 UI tests). extnode reads the node lane's reply shape: params compared value by value, network must match, a stub engine is refused and a fault on the app's own node. 168 box tests green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
98c1f05eff
commit
4f6cfbfba7
5 changed files with 75 additions and 12 deletions
|
|
@ -950,6 +950,15 @@ 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; }
|
||||
|
|
@ -1450,7 +1459,8 @@ impl Engine {
|
|||
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 d = crate::extnode::decide(port, ours.as_ref(), None, &check);
|
||||
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;
|
||||
{
|
||||
let mut st = self.st();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ pub struct Check {
|
|||
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.
|
||||
|
|
@ -33,20 +36,30 @@ pub struct Decision {
|
|||
|
||||
/// 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 {
|
||||
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 !k.ends_with("_daa") && !k.starts_with("exec_restart") {
|
||||
continue;
|
||||
}
|
||||
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})") };
|
||||
|
|
@ -82,7 +95,8 @@ pub fn probe(evm_port: u16) -> Check {
|
|||
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("overrides").cloned().filter(|v| v.is_object());
|
||||
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
|
||||
}
|
||||
|
|
@ -132,10 +146,17 @@ mod tests {
|
|||
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("fees_v1_activation_daa none"));
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -474,6 +474,7 @@ var View = (function () {
|
|||
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: 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: '' };
|
||||
}
|
||||
|
|
@ -664,6 +665,7 @@ var View = (function () {
|
|||
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: '' };
|
||||
|
|
@ -750,6 +752,15 @@ var View = (function () {
|
|||
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 || {};
|
||||
|
|
@ -761,7 +772,7 @@ var View = (function () {
|
|||
// the confirmations' words (in place, never a dialog)
|
||||
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, 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 };
|
||||
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
|
||||
|
|
@ -1602,8 +1613,8 @@ 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 + '.' : ''));
|
||||
|
|
@ -1624,8 +1635,9 @@ if (typeof document !== 'undefined') (function () {
|
|||
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. It locks at two thirds of the last 30 days of mining'); }
|
||||
else { setText('f-lock', 'none yet'); setText('f-note', f.message || 'checkpoints lock once this machine and the others vote'); }
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -432,6 +432,20 @@ test('N4 (6 October 2026): a frozen tip reads behind, never synced; the tip age
|
|||
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' });
|
||||
|
|
|
|||
|
|
@ -92,3 +92,9 @@ file is superseded and its `source` word is "site" too, so nothing in the UI cha
|
|||
| 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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue