Finality pause and /api/live pinned to the node's landed shape (ca3-v4-0316 b2e21447, eec34ac3): finalityActive decides paused when present, the node's "paused: " prefix dropped from the sentence, heldBy not repeated when the reason names the table, timestamp_source header | chain_block, a null vote_key_hash reads as empty; tests on the node's exact words
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a7b5ecccf0
commit
b43d329d10
4 changed files with 40 additions and 13 deletions
|
|
@ -929,6 +929,8 @@ pub fn finality_paused_line(since_unix: f64, reason: &str, held_by: &str) -> Str
|
||||||
/// (empty when live), "finalityProvisional", "heldBy" {tableIndex, stayersShareBps, expiresDaa} or null,
|
/// (empty when live), "finalityProvisional", "heldBy" {tableIndex, stayersShareBps, expiresDaa} or null,
|
||||||
/// "pausedSinceMs" or null. Returns (reason, held-by words, provisional, paused_since unix seconds).
|
/// "pausedSinceMs" or null. Returns (reason, held-by words, provisional, paused_since unix seconds).
|
||||||
pub struct FinalityStatus {
|
pub struct FinalityStatus {
|
||||||
|
/// the node's own word on whether finality is live (b2e21447: finalityActive); None on an older node
|
||||||
|
pub active: Option<bool>,
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
pub held_by: String,
|
pub held_by: String,
|
||||||
pub provisional: bool,
|
pub provisional: bool,
|
||||||
|
|
@ -936,7 +938,10 @@ pub struct FinalityStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_finality_status(v: &serde_json::Value) -> Option<FinalityStatus> {
|
pub fn parse_finality_status(v: &serde_json::Value) -> Option<FinalityStatus> {
|
||||||
let reason = v.get("finalityReason")?.as_str().unwrap_or("").trim().to_string();
|
// the node's words begin "paused: ..." (b2e21447); the app's sentence already says "Finality paused since", so
|
||||||
|
// that prefix goes; a reason that already names who holds it ("held by weight table 7, ...") is not repeated
|
||||||
|
let reason = v.get("finalityReason")?.as_str().unwrap_or("").trim().trim_start_matches("paused:").trim().to_string();
|
||||||
|
let active = v.get("finalityActive").and_then(|b| b.as_bool());
|
||||||
let provisional = v.get("finalityProvisional").and_then(|b| b.as_bool()).unwrap_or(false);
|
let provisional = v.get("finalityProvisional").and_then(|b| b.as_bool()).unwrap_or(false);
|
||||||
let held_by = match v.get("heldBy") {
|
let held_by = match v.get("heldBy") {
|
||||||
Some(h) if h.is_object() => {
|
Some(h) if h.is_object() => {
|
||||||
|
|
@ -947,8 +952,9 @@ pub fn parse_finality_status(v: &serde_json::Value) -> Option<FinalityStatus> {
|
||||||
}
|
}
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
|
let held_by = if reason.contains("held by") { String::new() } else { held_by };
|
||||||
let paused_since = v.get("pausedSinceMs").and_then(|x| x.as_u64()).filter(|ms| *ms > 0).map(|ms| ms as f64 / 1000.0);
|
let paused_since = v.get("pausedSinceMs").and_then(|x| x.as_u64()).filter(|ms| *ms > 0).map(|ms| ms as f64 / 1000.0);
|
||||||
Some(FinalityStatus { reason, held_by, provisional, paused_since })
|
Some(FinalityStatus { active, reason, held_by, provisional, paused_since })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The node's pause line, when it carries one: `... finality_reason=<words or "quoted words"> held_by=<id>`.
|
/// The node's pause line, when it carries one: `... finality_reason=<words or "quoted words"> held_by=<id>`.
|
||||||
|
|
@ -1137,15 +1143,23 @@ mod tests {
|
||||||
assert_eq!(parse_finality_line("x finality_reason=under_two_thirds"), Some(("under two thirds".into(), String::new())));
|
assert_eq!(parse_finality_line("x finality_reason=under_two_thirds"), Some(("under two thirds".into(), String::new())));
|
||||||
assert_eq!(parse_finality_line("status: accepted 3 blocks"), None);
|
assert_eq!(parse_finality_line("status: accepted 3 blocks"), None);
|
||||||
// the structured carrier on igneum_getProvingStatus (the node lane's field names)
|
// the structured carrier on igneum_getProvingStatus (the node lane's field names)
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{"v1":{"active":true},"finalityReason":"under two thirds of the weight is signing","finalityProvisional":true,"heldBy":{"tableIndex":7,"stayersShareBps":6150,"expiresDaa":205000},"pausedSinceMs":1791311940000}"#).unwrap();
|
// the node's own words (b2e21447): "paused: under two thirds ... ({p}%; the table frozen at lock {j} has {q}% signing)"
|
||||||
|
let v: serde_json::Value = serde_json::from_str(r#"{"v1":{"active":true},"finalityActive":false,"finalityReason":"paused: under two thirds of the weight is signing (61%; the table frozen at lock 7 has 58% signing)","finalityProvisional":false,"heldBy":null,"pausedSinceMs":1791311940000}"#).unwrap();
|
||||||
let f = parse_finality_status(&v).unwrap();
|
let f = parse_finality_status(&v).unwrap();
|
||||||
assert_eq!((f.reason.as_str(), f.provisional, f.paused_since), ("under two thirds of the weight is signing", true, Some(1_791_311_940.0)));
|
assert_eq!((f.active, f.provisional, f.paused_since), (Some(false), false, Some(1_791_311_940.0)));
|
||||||
assert_eq!(f.held_by, "weight table 7, 61.50% still signing, expires at DAA 205000");
|
assert_eq!(finality_paused_line(f.paused_since.unwrap(), &f.reason, &f.held_by), "Finality paused since 18:39 UTC: under two thirds of the weight is signing (61%; the table frozen at lock 7 has 58% signing)");
|
||||||
assert_eq!(finality_paused_line(f.paused_since.unwrap(), &f.reason, &f.held_by), "Finality paused since 18:39 UTC: under two thirds of the weight is signing (held by weight table 7, 61.50% still signing, expires at DAA 205000)");
|
// the held-by form: the reason names the table itself, so heldBy is not repeated
|
||||||
|
let v2: serde_json::Value = serde_json::from_str(r#"{"finalityActive":false,"finalityReason":"paused: held by weight table 7, 61% of it signing, expires at DAA 205000","finalityProvisional":true,"heldBy":{"tableIndex":7,"stayersShareBps":6150,"expiresDaa":205000},"pausedSinceMs":1791311940000}"#).unwrap();
|
||||||
|
let g = parse_finality_status(&v2).unwrap();
|
||||||
|
assert!(g.provisional && g.held_by.is_empty());
|
||||||
|
assert_eq!(finality_paused_line(g.paused_since.unwrap(), &g.reason, &g.held_by), "Finality paused since 18:39 UTC: held by weight table 7, 61% of it signing, expires at DAA 205000");
|
||||||
|
// a reason without the table words keeps the heldBy suffix
|
||||||
|
let v3: serde_json::Value = serde_json::from_str(r#"{"finalityActive":false,"finalityReason":"paused: no checkpoint determined above the latest lock","heldBy":{"tableIndex":7,"stayersShareBps":6150,"expiresDaa":205000}}"#).unwrap();
|
||||||
|
assert_eq!(parse_finality_status(&v3).unwrap().held_by, "weight table 7, 61.50% still signing, expires at DAA 205000");
|
||||||
// live finality: an empty reason, null heldBy, null pausedSinceMs
|
// live finality: an empty reason, null heldBy, null pausedSinceMs
|
||||||
let live: serde_json::Value = serde_json::from_str(r#"{"finalityReason":"","finalityProvisional":false,"heldBy":null,"pausedSinceMs":null}"#).unwrap();
|
let live: serde_json::Value = serde_json::from_str(r#"{"finalityReason":"","finalityProvisional":false,"heldBy":null,"pausedSinceMs":null}"#).unwrap();
|
||||||
let l = parse_finality_status(&live).unwrap();
|
let l = parse_finality_status(&live).unwrap();
|
||||||
assert!(l.reason.is_empty() && l.held_by.is_empty() && !l.provisional && l.paused_since.is_none());
|
assert!(l.reason.is_empty() && l.held_by.is_empty() && !l.provisional && l.paused_since.is_none() && l.active.is_none());
|
||||||
// a node before 0.3.16 carries none of it
|
// a node before 0.3.16 carries none of it
|
||||||
assert!(parse_finality_status(&serde_json::json!({"v1":{"active":true}})).is_none());
|
assert!(parse_finality_status(&serde_json::json!({"v1":{"active":true}})).is_none());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1168,6 +1168,7 @@ impl Engine {
|
||||||
let mut st = self.st();
|
let mut st = self.st();
|
||||||
match f {
|
match f {
|
||||||
Some(f) => {
|
Some(f) => {
|
||||||
|
st.finality.node_active = f.active;
|
||||||
st.finality.reason = f.reason;
|
st.finality.reason = f.reason;
|
||||||
st.finality.held_by = f.held_by;
|
st.finality.held_by = f.held_by;
|
||||||
st.finality.provisional = f.provisional;
|
st.finality.provisional = f.provisional;
|
||||||
|
|
@ -1178,6 +1179,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// a node without the fields: the log line (parse_finality_line) may still have set a cause
|
// a node without the fields: the log line (parse_finality_line) may still have set a cause
|
||||||
|
st.finality.node_active = None;
|
||||||
if st.finality.cause_source != "node-line" {
|
if st.finality.cause_source != "node-line" {
|
||||||
st.finality.cause_source.clear();
|
st.finality.cause_source.clear();
|
||||||
}
|
}
|
||||||
|
|
@ -3653,7 +3655,11 @@ impl Engine {
|
||||||
// Horizon polish Q83/Q84: finality paused = a synced node with no checkpoint lock for FINALITY_PAUSE_S (the
|
// Horizon polish Q83/Q84: finality paused = a synced node with no checkpoint lock for FINALITY_PAUSE_S (the
|
||||||
// last lock's age, else the engine's uptime); since the last lock (else the start); one sentence everywhere
|
// last lock's age, else the engine's uptime); since the last lock (else the start); one sentence everywhere
|
||||||
let gap = if st.finality.last_lock > 0 { st.finality.age_s } else { st.uptime_s as f64 };
|
let gap = if st.finality.last_lock > 0 { st.finality.age_s } else { st.uptime_s as f64 };
|
||||||
let paused = st.node.synced && gap > crate::manifest::FINALITY_PAUSE_S;
|
// the node's own word wins when it carries one (0.3.16, finalityActive); else the engine's 15-minute rule
|
||||||
|
let paused = match st.finality.node_active {
|
||||||
|
Some(active) => !active,
|
||||||
|
None => st.node.synced && gap > crate::manifest::FINALITY_PAUSE_S,
|
||||||
|
};
|
||||||
if paused && !st.finality.paused && st.finality.cause_source != "node" {
|
if paused && !st.finality.paused && st.finality.cause_source != "node" {
|
||||||
st.finality.paused_since = if st.finality.last_lock > 0 { st.finality.last_lock_at } else { unix - st.uptime_s as f64 };
|
st.finality.paused_since = if st.finality.last_lock > 0 { st.finality.last_lock_at } else { unix - st.uptime_s as f64 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ pub struct RecentBlock {
|
||||||
pub vote_key_hash: String,
|
pub vote_key_hash: String,
|
||||||
pub is_chain_block: bool,
|
pub is_chain_block: bool,
|
||||||
pub color: String,
|
pub color: String,
|
||||||
/// "" or "merging_block" (the node lane: a merged non-chain block whose own header time was not at hand
|
/// "header" or "chain_block" (eec34ac3: a merged block whose own header was not at hand carries its merging
|
||||||
/// carries its merging chain block's time, marked so blocks_per_minute stays honest)
|
/// chain block's time and blue score, marked so blocks_per_minute stays honest); "" on an older row = header
|
||||||
pub timestamp_source: String,
|
pub timestamp_source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,10 +222,13 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recent_blocks_parse_from_the_node_reply_and_an_empty_reply_is_empty() {
|
fn recent_blocks_parse_from_the_node_reply_and_an_empty_reply_is_empty() {
|
||||||
let v: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"cd","is_chain_block":true,"color":"blue"},{"hash":"0xcd","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"ef","is_chain_block":false,"color":"red","timestamp_source":"merging_block"}]"#).unwrap();
|
let v: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"cd","is_chain_block":true,"color":"blue"},{"hash":"0xcd","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"ef","is_chain_block":false,"color":"red","timestamp_source":"chain_block"}]"#).unwrap();
|
||||||
let p = parse_recent(&v);
|
let p = parse_recent(&v);
|
||||||
assert_eq!(p.len(), 2);
|
assert_eq!(p.len(), 2);
|
||||||
assert_eq!((p[1].color.as_str(), p[1].timestamp_source.as_str()), ("red", "merging_block"));
|
assert_eq!((p[1].color.as_str(), p[1].timestamp_source.as_str()), ("red", "chain_block"));
|
||||||
|
// a null vote_key_hash (a chain block with no mergeset entry, which the executor never produces) reads as ""
|
||||||
|
let n: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":null,"is_chain_block":true,"color":"blue","timestamp_source":"header"}]"#).unwrap();
|
||||||
|
assert_eq!(parse_recent(&n)[0].vote_key_hash, "");
|
||||||
assert_eq!((p[0].blue_score, p[0].is_chain_block, p[0].color.as_str()), (5, true, "blue"));
|
assert_eq!((p[0].blue_score, p[0].is_chain_block, p[0].color.as_str()), (5, true, "blue"));
|
||||||
assert!(parse_recent(&json!(null)).is_empty());
|
assert!(parse_recent(&json!(null)).is_empty());
|
||||||
assert_eq!(short("0x1234567890abcdef00"), "1234567890abcdef", "the site's 16-hex short");
|
assert_eq!(short("0x1234567890abcdef00"), "1234567890abcdef", "the site's 16-hex short");
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,12 @@ pub struct FinalityState {
|
||||||
pub line: String,
|
pub line: String,
|
||||||
/// the node's word (igneum_getProvingStatus finalityProvisional, 0.3.16): locks are provisional right now
|
/// the node's word (igneum_getProvingStatus finalityProvisional, 0.3.16): locks are provisional right now
|
||||||
pub provisional: bool,
|
pub provisional: bool,
|
||||||
/// where the cause came from: "node" (the RPC's structured fields or its log line) or "" (the engine's own rule)
|
/// where the cause came from: "node" (the RPC's structured fields), "node-line" (its log line) or "" (the
|
||||||
|
/// engine's own rule)
|
||||||
pub cause_source: String,
|
pub cause_source: String,
|
||||||
|
/// the node's own finalityActive (b2e21447), when it carries it: then the node decides `paused`, not the
|
||||||
|
/// engine's 15-minute rule
|
||||||
|
pub node_active: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clock skew against the network. skew_s = local time minus network time (negative = this machine is behind).
|
/// Clock skew against the network. skew_s = local time minus network time (negative = this machine is behind).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue