diff --git a/pool/src/config.rs b/pool/src/config.rs index fd14d7cdd..ea436ddfb 100644 --- a/pool/src/config.rs +++ b/pool/src/config.rs @@ -8,6 +8,8 @@ pub struct Config { pub nodes: Vec, /// 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"); } diff --git a/pool/src/main.rs b/pool/src/main.rs index 713bc5e24..1ec67941e 100644 --- a/pool/src/main.rs +++ b/pool/src/main.rs @@ -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 { diff --git a/pool/src/state_provider.rs b/pool/src/state_provider.rs new file mode 100644 index 000000000..d5a5074ca --- /dev/null +++ b/pool/src/state_provider.rs @@ -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 { + 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://: 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, 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)" + } +}