execrpc: one gate for every execution-layer RPC call the app makes (ledger N7, main's rule for 0.3.19)
A node before the exec RPC bounds fix (every 0.3.17 node) dies when a method that resolves a block number or indexes the record vector is asked while its exec follower holds no record; PC 1 crash-looped on two callers in one night (eth_getBlockByNumber from the clock sample, then igneum_getAssignedShards from the prover loop: 'panicked at igneum/exec/src/rpc.rs:808:35: range start index 1 out of range for slice of length 0'). Every caller (prover.rs's evm_rpc, update.rs's clock sample, extnode's rpc for chainfacts and the external-node probe) now goes through execrpc::call: SAFE_ON_EMPTY methods go out, GATED ones wait for igneum_getExecStatus's executedTipHash, an unclassified method is refused. The test every_caller_goes_through_the_gate scans src/ for JSON-RPC requests built elsewhere and for unclassified exec method names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
089b078a3a
commit
164dc99413
4 changed files with 10 additions and 51 deletions
|
|
@ -8,7 +8,6 @@
|
|||
//! too, read every 30 s instead of parsed from a stdout the app does not own.
|
||||
use serde_json::{json, Value};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
/// What the other node answered: None where it could not answer.
|
||||
|
|
@ -110,14 +109,8 @@ pub fn step(port_open: bool, now_s: f64, gone_since: &mut Option<f64>) -> Step {
|
|||
|
||||
/// One JSON-RPC call to the node's EVM port through curl (the engine carries no HTTP client; update.rs does the same).
|
||||
pub fn rpc(evm_port: u16, method: &str, params: Value, limit: Duration) -> Option<Value> {
|
||||
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
|
||||
let out = crate::detect::run_timeout(
|
||||
Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", &format!("{}", limit.as_secs().max(1)), "-X", "POST", &format!("http://127.0.0.1:{evm_port}"), "-H", "Content-Type: application/json", "-d", &body]),
|
||||
None,
|
||||
limit + Duration::from_secs(2),
|
||||
)?;
|
||||
let v: Value = serde_json::from_str(&out).ok()?;
|
||||
v.get("result").cloned().filter(|r| !r.is_null())
|
||||
// one path (ledger N7): execrpc holds a records-indexing method until the node's exec follower has a record
|
||||
crate::execrpc::call(evm_port, method, params, limit).ok().filter(|r| !r.is_null())
|
||||
}
|
||||
|
||||
/// The other node's answers, with what each method gives: eth_chainId (every node), igneum_getNodeInfo (newer nodes).
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ mod manifest;
|
|||
mod ota;
|
||||
mod uiota;
|
||||
mod update;
|
||||
mod execrpc;
|
||||
mod jobs;
|
||||
mod jobrun;
|
||||
mod jobbuild;
|
||||
|
|
|
|||
|
|
@ -168,22 +168,8 @@ fn exec_boundary(shared: &Shared) -> u64 {
|
|||
}
|
||||
|
||||
pub(crate) fn evm_rpc(shared: &Shared, method: &str, params: Value, timeout: Duration) -> Result<Value, String> {
|
||||
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }).to_string();
|
||||
let tmp = std::env::temp_dir().join(format!("igneum-prover-{}-{}.json", std::process::id(), method));
|
||||
std::fs::write(&tmp, body).map_err(|e| e.to_string())?;
|
||||
let url = format!("http://127.0.0.1:{}", shared.runtime.evm_port());
|
||||
let out = crate::detect::run_timeout(
|
||||
Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", &timeout.as_secs().to_string(), "-X", "POST", &url, "-H", "Content-Type: application/json", "--data-binary", &format!("@{}", tmp.display())]),
|
||||
None,
|
||||
timeout + Duration::from_secs(2),
|
||||
);
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
let out = out.ok_or_else(|| format!("{method}: the node's RPC did not answer"))?;
|
||||
let v: Value = serde_json::from_str(&out).map_err(|e| format!("{method}: {e}"))?;
|
||||
if let Some(err) = v.get("error") {
|
||||
return Err(format!("{method}: {}", err.get("message").and_then(|m| m.as_str()).unwrap_or("error")));
|
||||
}
|
||||
Ok(v.get("result").cloned().unwrap_or(Value::Null))
|
||||
// one path (ledger N7): execrpc holds a records-indexing method until the node's exec follower has a record
|
||||
crate::execrpc::call(shared.runtime.evm_port(), method, params, timeout)
|
||||
}
|
||||
|
||||
fn find_tools(bin_dir: &Path) -> Result<Tools, String> {
|
||||
|
|
|
|||
|
|
@ -43,41 +43,20 @@ fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
|
|||
/// 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.
|
||||
pub 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)
|
||||
crate::execrpc::has_record(evm_port)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
serde_json::from_str::<serde_json::Value>(reply).ok().map(|v| crate::execrpc::status_has_record(v.get("result").unwrap_or(&serde_json::Value::Null))).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]),
|
||||
None,
|
||||
Duration::from_secs(7),
|
||||
)?;
|
||||
let v: serde_json::Value = serde_json::from_str(&out).ok()?;
|
||||
let ts = v.get("result")?.get("timestamp")?.as_str()?;
|
||||
// 0.3.18/0.3.19 (ledger N7): through the one gate, which asks nothing of a follower without a record
|
||||
let block = crate::execrpc::call(evm_port, "eth_getBlockByNumber", serde_json::json!(["latest", false]), Duration::from_secs(5)).ok()?;
|
||||
let ts = block.get("timestamp")?.as_str()?;
|
||||
u64::from_str_radix(ts.trim_start_matches("0x"), 16).ok().map(|t| t as f64)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue