Finality pause (0.3.16): the node lane's structured carrier on igneum_getProvingStatus (finalityReason, finalityProvisional, heldBy {tableIndex, stayersShareBps, expiresDaa}, pausedSinceMs) read on the 30-s RPC cadence; the node's pausedSince wins over the engine's own; state.finality.provisional and cause_source; the log line and "finality resumed" kept; tests with the node lane's field names
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e54a87bc44
commit
a7b5ecccf0
3 changed files with 74 additions and 1 deletions
|
|
@ -925,6 +925,32 @@ pub fn finality_paused_line(since_unix: f64, reason: &str, held_by: &str) -> Str
|
||||||
format!("Finality paused since {} UTC: {cause}", hhmm_utc(since_unix))
|
format!("Finality paused since {} UTC: {cause}", hhmm_utc(since_unix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The node's structured finality fields on igneum_getProvingStatus (the node lane, 0.3.16): "finalityReason"
|
||||||
|
/// (empty when live), "finalityProvisional", "heldBy" {tableIndex, stayersShareBps, expiresDaa} or null,
|
||||||
|
/// "pausedSinceMs" or null. Returns (reason, held-by words, provisional, paused_since unix seconds).
|
||||||
|
pub struct FinalityStatus {
|
||||||
|
pub reason: String,
|
||||||
|
pub held_by: String,
|
||||||
|
pub provisional: bool,
|
||||||
|
pub paused_since: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_finality_status(v: &serde_json::Value) -> Option<FinalityStatus> {
|
||||||
|
let reason = v.get("finalityReason")?.as_str().unwrap_or("").trim().to_string();
|
||||||
|
let provisional = v.get("finalityProvisional").and_then(|b| b.as_bool()).unwrap_or(false);
|
||||||
|
let held_by = match v.get("heldBy") {
|
||||||
|
Some(h) if h.is_object() => {
|
||||||
|
let idx = h.get("tableIndex").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||||
|
let bps = h.get("stayersShareBps").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||||
|
let exp = h.get("expiresDaa").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||||
|
format!("weight table {idx}, {}.{:02}% still signing, expires at DAA {exp}", bps / 100, bps % 100)
|
||||||
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
};
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
|
||||||
/// 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>`.
|
||||||
/// Returns (reason, held_by); None when the line is not one.
|
/// Returns (reason, held_by); None when the line is not one.
|
||||||
pub fn parse_finality_line(text: &str) -> Option<(String, String)> {
|
pub fn parse_finality_line(text: &str) -> Option<(String, String)> {
|
||||||
|
|
@ -1110,6 +1136,18 @@ mod tests {
|
||||||
assert_eq!(parse_finality_line("1791311940 finality paused finality_reason=\"vote split at index 412\" held_by=ae432dc7"), Some(("vote split at index 412".into(), "ae432dc7".into())));
|
assert_eq!(parse_finality_line("1791311940 finality paused finality_reason=\"vote split at index 412\" held_by=ae432dc7"), Some(("vote split at index 412".into(), "ae432dc7".into())));
|
||||||
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)
|
||||||
|
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();
|
||||||
|
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.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 (held by weight table 7, 61.50% still signing, expires at DAA 205000)");
|
||||||
|
// 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 l = parse_finality_status(&live).unwrap();
|
||||||
|
assert!(l.reason.is_empty() && l.held_by.is_empty() && !l.provisional && l.paused_since.is_none());
|
||||||
|
// a node before 0.3.16 carries none of it
|
||||||
|
assert!(parse_finality_status(&serde_json::json!({"v1":{"active":true}})).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ pub enum Cmd {
|
||||||
Detect,
|
Detect,
|
||||||
/// the payout address's balance (eth_getBalance, hex) or why it could not be read
|
/// the payout address's balance (eth_getBalance, hex) or why it could not be read
|
||||||
BalanceRead(Result<String, String>),
|
BalanceRead(Result<String, String>),
|
||||||
|
/// the node's finality fields from igneum_getProvingStatus (None: the node carries none, or did not answer)
|
||||||
|
FinalityStatus(Option<crate::ember::FinalityStatus>),
|
||||||
ApplyCards(Vec<CardChoice>),
|
ApplyCards(Vec<CardChoice>),
|
||||||
/// one enumeration of the cards finished (the first, or a re-detection: src/hotplug.rs)
|
/// one enumeration of the cards finished (the first, or a re-detection: src/hotplug.rs)
|
||||||
Detected(crate::detect::Detection),
|
Detected(crate::detect::Detection),
|
||||||
|
|
@ -1162,6 +1164,26 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Cmd::TuneProgress(v) => self.tune_progress(&v),
|
Cmd::TuneProgress(v) => self.tune_progress(&v),
|
||||||
|
Cmd::FinalityStatus(f) => {
|
||||||
|
let mut st = self.st();
|
||||||
|
match f {
|
||||||
|
Some(f) => {
|
||||||
|
st.finality.reason = f.reason;
|
||||||
|
st.finality.held_by = f.held_by;
|
||||||
|
st.finality.provisional = f.provisional;
|
||||||
|
st.finality.cause_source = "node".into();
|
||||||
|
if let Some(since) = f.paused_since {
|
||||||
|
st.finality.paused_since = since;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// a node without the fields: the log line (parse_finality_line) may still have set a cause
|
||||||
|
if st.finality.cause_source != "node-line" {
|
||||||
|
st.finality.cause_source.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Cmd::BalanceRead(r) => {
|
Cmd::BalanceRead(r) => {
|
||||||
self.balance_busy = false;
|
self.balance_busy = false;
|
||||||
match r {
|
match r {
|
||||||
|
|
@ -3582,6 +3604,10 @@ impl Engine {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let r = crate::prover::evm_rpc(&shared, "eth_getBalance", serde_json::json!([addr, "latest"]), Duration::from_secs(8)).and_then(|v| v.as_str().map(|s| s.to_string()).ok_or_else(|| "eth_getBalance: not a string".to_string()));
|
let r = crate::prover::evm_rpc(&shared, "eth_getBalance", serde_json::json!([addr, "latest"]), Duration::from_secs(8)).and_then(|v| v.as_str().map(|s| s.to_string()).ok_or_else(|| "eth_getBalance: not a string".to_string()));
|
||||||
shared.send(Cmd::BalanceRead(r));
|
shared.send(Cmd::BalanceRead(r));
|
||||||
|
// the same 30-s cadence carries the node's finality fields (the node lane, 0.3.16); a node without them
|
||||||
|
// answers without the keys and the engine's own rule stands
|
||||||
|
let f = crate::prover::evm_rpc(&shared, "igneum_getProvingStatus", serde_json::json!([]), Duration::from_secs(8)).ok().and_then(|v| crate::ember::parse_finality_status(&v));
|
||||||
|
shared.send(Cmd::FinalityStatus(f));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3628,7 +3654,7 @@ impl Engine {
|
||||||
// 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;
|
let paused = st.node.synced && gap > crate::manifest::FINALITY_PAUSE_S;
|
||||||
if paused && !st.finality.paused {
|
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 };
|
||||||
}
|
}
|
||||||
st.finality.paused = paused;
|
st.finality.paused = paused;
|
||||||
|
|
@ -4047,7 +4073,12 @@ impl Engine {
|
||||||
let mut st = self.st();
|
let mut st = self.st();
|
||||||
st.finality.reason = reason;
|
st.finality.reason = reason;
|
||||||
st.finality.held_by = held_by;
|
st.finality.held_by = held_by;
|
||||||
|
st.finality.cause_source = "node-line".into();
|
||||||
}
|
}
|
||||||
|
} else if text.contains("finality resumed") {
|
||||||
|
let mut st = self.st();
|
||||||
|
st.finality.reason.clear();
|
||||||
|
st.finality.held_by.clear();
|
||||||
} else if text.contains(" VOTE index=") {
|
} else if text.contains(" VOTE index=") {
|
||||||
self.st().finality.votes += 1;
|
self.st().finality.votes += 1;
|
||||||
} else if text.contains("worker could not prepare") || text.contains(" prepare-failed ") {
|
} else if text.contains("worker could not prepare") || text.contains(" prepare-failed ") {
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,10 @@ pub struct FinalityState {
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
pub held_by: String,
|
pub held_by: String,
|
||||||
pub line: String,
|
pub line: String,
|
||||||
|
/// the node's word (igneum_getProvingStatus finalityProvisional, 0.3.16): locks are provisional right now
|
||||||
|
pub provisional: bool,
|
||||||
|
/// where the cause came from: "node" (the RPC's structured fields or its log line) or "" (the engine's own rule)
|
||||||
|
pub cause_source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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