Pool: the class v5 day state from the node's exec RPC (state_provider.rs, --exec-rpc defaulting to --evm-rpc), installed before the engine's first epoch; without it the daemon issued no job on devnet-4 (class v5 needs the execution state)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 19:30:35 +00:00
parent a4c9958cc8
commit c24d508083
3 changed files with 63 additions and 0 deletions

View file

@ -8,6 +8,8 @@ pub struct Config {
pub nodes: Vec<String>,
/// The first node's EVM JSON-RPC (payouts, chain id)
pub evm_rpc: String,
/// The node's exec RPC for the class v5 day state (`igneum_getPowStateLeaves`); empty means `--evm-rpc`
pub exec_rpc: String,
/// Where members connect (spec 9.3: 4461 mainnet, 4462 testnet, 4463 devnet)
pub listen: String,
/// The stats API and the web page
@ -91,6 +93,7 @@ impl Config {
let mut c = Config {
nodes: Vec::new(),
evm_rpc: String::new(),
exec_rpc: String::new(),
listen: String::new(),
http: "127.0.0.1:4480".into(),
data_dir: PathBuf::from("data"),
@ -139,6 +142,7 @@ impl Config {
match a {
"--node" => c.nodes.push(val()),
"--evm-rpc" => c.evm_rpc = val(),
"--exec-rpc" => c.exec_rpc = val(),
"--listen" => c.listen = val(),
"--http" => c.http = val(),
"--data-dir" => c.data_dir = PathBuf::from(val()),
@ -197,6 +201,9 @@ impl Config {
if c.evm_rpc.is_empty() {
c.evm_rpc = format!("http://127.0.0.1:{evm_port}");
}
if c.exec_rpc.is_empty() {
c.exec_rpc = c.evm_rpc.clone();
}
if c.payout_key.as_os_str().is_empty() {
c.payout_key = c.data_dir.join("payout-key.json");
}

View file

@ -11,6 +11,7 @@
//! --data-dir /var/lib/igneum-pool --fee-percent 1 --min-payout 1 [--dry-run]
//! ```
mod state_provider;
mod api;
mod config;
mod node;
@ -171,6 +172,9 @@ async fn main() {
Address::new(prefix, Version::PubKey, &h.finalize().as_bytes())
};
let state_path = cfg.data_dir.join("state.json");
// class v5: the engine's day state comes from the node's exec RPC (the pool's own provider), installed before the first epoch
kaspa_pow::igneum::install_day_state_provider(Arc::new(crate::state_provider::RpcStateProvider::new(&cfg.exec_rpc)));
eprintln!("class v5 day state: {} ({})", cfg.exec_rpc, "igneum_getPowStateLeaves");
let engine = Arc::new(IgneumEngine::new());
let walker_for_open = walker.clone();
let pool = Arc::new(Pool {

View file

@ -0,0 +1,52 @@
//! The class v5 day state for the pool's own engine (8 October 2026, the devnet-4 pair): the pool verifies shares and
//! builds its share verifier's epoch with `IgneumEngine`, and under class v5 every epoch's dataset is keyed by the chain's
//! execution state after the epoch's seed block (spec 01 section 1.8.6). The engine asks the installed
//! `DayStateProvider`; this one fetches the stream from the node's exec RPC (`igneum_getPowStateLeaves [blockHash]`)
//! over a plain HTTP/1.1 POST on a blocking socket, once per epoch, as the miner's `RpcStateProvider` does
//! (`igneum/miner/src/main.rs`). `--exec-rpc` names the URL; its default is the pool's `--evm-rpc` (the same node port).
use kaspa_hashes::Hash;
use std::io::{Read, Write};
use std::time::Duration;
pub struct RpcStateProvider {
url: String,
}
impl RpcStateProvider {
pub fn new(url: &str) -> Self {
Self { url: url.to_string() }
}
fn post(&self, body: &str) -> Result<String, String> {
let rest = self.url.trim_start_matches("http://");
let (hostport, _) = rest.split_once('/').unwrap_or((rest, ""));
let mut stream = std::net::TcpStream::connect(hostport).map_err(|e| format!("connect {hostport}: {e} (the node's exec RPC; --exec-rpc http://<host>:<port> names another)"))?;
stream.set_read_timeout(Some(Duration::from_secs(20))).ok();
let req = format!("POST / HTTP/1.1\r\nHost: {hostport}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
stream.write_all(req.as_bytes()).map_err(|e| format!("write {hostport}: {e}"))?;
let mut resp = Vec::new();
stream.read_to_end(&mut resp).map_err(|e| format!("read {hostport}: {e}"))?;
let text = String::from_utf8_lossy(&resp);
let (_, body) = text.split_once("\r\n\r\n").ok_or_else(|| "no HTTP body".to_string())?;
Ok(body.to_string())
}
}
impl kaspa_pow::igneum::DayStateProvider for RpcStateProvider {
fn state_stream(&self, block: Hash) -> Result<Vec<u8>, String> {
let body = format!("{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"igneum_getPowStateLeaves\",\"params\":[\"{block}\"]}}");
let resp = self.post(&body)?;
const KEY: &str = "\"streamHex\":\"0x";
let hex_at = resp.find(KEY).ok_or_else(|| {
let err = resp.find("\"message\":\"").map(|i| resp[i + 11..].split('"').next().unwrap_or("").to_string()).unwrap_or_else(|| resp.chars().take(200).collect());
format!("exec RPC {} gave no stream for block {block}: {err}", self.url)
})?;
let rest = &resp[hex_at + KEY.len()..];
let hex_str = rest.split('"').next().unwrap_or("");
kaspa_pow::igneum::unhex_bytes(hex_str).ok_or_else(|| "malformed streamHex".to_string())
}
fn name(&self) -> &'static str {
"the node's exec RPC (the pool's provider)"
}
}