Pool on the 2.0.2 release branch: pool/ and docs/plans/pool.md from the pool line's tip pool-2.0 e063fdcd (pool-review-b 786e09cc: review B F12 durable payment intents, F13 admission bounds, INT-04 the verified ledger, INT-15 the challenge-bound authorize; merged by the pool seat on 8ff7a0f4 the class v5 day-state source via --exec-rpc and 8106ba27 the base unit from the node's network, the two fixes the devnet-4 pair needs; suite 51 of 51 on build-2 against successor-2.0.1 2b1a247a). The spec stays the hand-merged copy this branch carries. Rule 24's crate gate from this tree on build-6 against the pinned node 7cfa422a is the read-back below

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 20:03:01 +00:00
parent 552683e034
commit 0dfb3b05d3
3 changed files with 74 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
@ -103,6 +105,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"),
@ -155,6 +158,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()),
@ -224,6 +228,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

@ -12,6 +12,7 @@
//! ```
mod admission;
mod state_provider;
mod api;
mod config;
mod frame;
@ -122,6 +123,17 @@ async fn main() {
}
}
let node = clients.remove(0);
// Igneum (O-2.6): the node's base unit in this process from its network (the dag info's network id), before any
// amount is read: at 18 decimals a coinbase amount carries a high word that the 8-decimal reading refuses as
// malformed (the devnet-4 pair, 8 October 2026: every template refused with "amount high word 1 ... at 8 decimals")
match tokio::time::timeout(Duration::from_secs(10), node.get_block_dag_info()).await {
Ok(Ok(info)) => {
let params = kaspa_consensus_core::config::params::Params::from(info.network);
kaspa_consensus_core::unit::install_base_unit(params.base_unit_decimals);
eprintln!("base unit: {} decimals (network {})", params.base_unit_decimals, info.network);
}
other => eprintln!("base unit: the node's dag info did not answer ({other:?}); amounts read at the default unit"),
}
let walker = match tokio::time::timeout(Duration::from_secs(10), connect_reconnecting(cfg.nodes[0].clone())).await {
Ok(Ok(c)) => Arc::new(c),
Ok(Err(e)) => {
@ -174,6 +186,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)"
}
}