The fleet's 10-member load run (6 October 2026, 18:14Z to 18:24Z) accepted 0 shares: the pool fork's miner re-checked GPU shares with a fixed class v2 program while the 0.3.14 workers hashed class v3 (docs/plans/pool.md section 9). pool: node.rs builds the share verifier's seeds from the template's pow_epoch with the class and the era, installs the node's genesis day, dataset size and class v3 activation beside the schedule, and sends program_class, next_program_class, era_seed, era_index, genesis_day_index, genesis_dataset_log2 and program_class_v3_activation_daa in `seeds` and program_class and era_seed in `job` (protocol.rs, additive fields); verify.rs tests through EpochSeeds::v2. Protocol, vardiff, PPLNS, stats API and page otherwise unchanged. Suite 22 of 22 on igneum-build-1. igneum-pow tests/recheck.rs: for class v3 (packs-ca2-mixer/mx8-devnet-epoch0) and class v4 (packs-ca3-v4/v4-devnet-epoch0) the pack's program reproduces the pack's 96 vectors (the worker's reference), the chain seam the node engine calls (Epoch::chain_program, chain_dataset_day) builds the same program, one known nonce hashes equal through both paths, and the fixed-v2 program of the same seed disagrees; the pinned v3 and v4 program ids differ. 2 of 2 on igneum-build-1. packaging/hive: the pool:// mode merged with master's per-card IDENTITIES=auto and OVERRIDE handling (selftest passes). pool/README.md and pool.md: building on igneum-build-1 (the vendor/igneum-node symlink on the box). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
245 lines
7.2 KiB
Rust
245 lines
7.2 KiB
Rust
//! The pool protocol on the wire: spec 09 section 9.5, one JSON object per line, `"t"` names the type.
|
|
//!
|
|
//! v0 encoding notes (docs/plans/pool.md): a `template` carries the node's own `RpcRawBlock` JSON (mode A, the full
|
|
//! block, so a member with a node can submit it there as well); `authorize` carries the member's payout address and
|
|
//! worker name beside the key, because v0 pays by EVM address and the spec's message names the member by key only;
|
|
//! `job` carries the seed pair the worker protocol wants (`epoch_seed`, `day`) and the program class and era seed
|
|
//! (Counter ASIC 2.0) so a member needs no `seeds` lookup
|
|
//! per job. Unknown fields are ignored, as the spec requires.
|
|
|
|
use kaspa_rpc_core::RpcRawBlock;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Protocol version string sent in `hello.versions` and `welcome.version`.
|
|
pub const VERSION: &str = "0.1";
|
|
/// Largest line accepted on either side (spec 9.3: 4 MiB, a template with its transactions).
|
|
pub const MAX_LINE: usize = 4 << 20;
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
#[serde(tag = "t", rename_all = "snake_case")]
|
|
pub enum Msg {
|
|
Hello {
|
|
versions: Vec<String>,
|
|
chain_id: u64,
|
|
client: String,
|
|
#[serde(default)]
|
|
modes: Vec<String>,
|
|
},
|
|
Welcome {
|
|
version: String,
|
|
chain_id: u64,
|
|
network: String,
|
|
pool_address: String,
|
|
modes: Vec<String>,
|
|
vote_mode: String,
|
|
share_scheme: ShareScheme,
|
|
min_shift: u32,
|
|
max_shift: u32,
|
|
share_interval_s: u64,
|
|
stale_grace_ms: u64,
|
|
pool_name: String,
|
|
},
|
|
Authorize {
|
|
/// 48 bytes hex, the member's BLS vote key
|
|
pubkey: String,
|
|
/// 96 bytes hex, the proof of possession (spec 3.10 KeyReveal)
|
|
pop: String,
|
|
/// Worker name shown on the dashboards
|
|
label: String,
|
|
/// EVM payout address, 0x + 40 hex (v0 addition)
|
|
payout: String,
|
|
/// `binding` of spec 9.3 (a signature over the TLS exporter) is absent in v0: no TLS yet
|
|
#[serde(default)]
|
|
binding: String,
|
|
},
|
|
Authorized {
|
|
member_id: u64,
|
|
revealed: bool,
|
|
key_hash: String,
|
|
},
|
|
Seeds {
|
|
epoch_seed: String,
|
|
day: u64,
|
|
day_seed: String,
|
|
seed_source: String,
|
|
next_epoch_seed: Option<String>,
|
|
next_at_daa: u64,
|
|
epoch_blocks: u64,
|
|
epoch_lead: u64,
|
|
day_ms: u64,
|
|
/// Counter ASIC 2.0 (added 6 October 2026, after the fleet's 0-share run): the program class of the current
|
|
/// epoch as the node's `pow_epoch.program_class` reports it (the generator number, 2 or 3), the next epoch's
|
|
/// class, and the era seed class v3 programs are drawn under. A member hashes the program these name.
|
|
program_class: u32,
|
|
next_program_class: u32,
|
|
era_seed: Option<String>,
|
|
era_index: u64,
|
|
/// The node's genesis day index, genesis dataset size (log2 words) and class v3 activation DAA: what a
|
|
/// member without a verifier node installs so its day cache is built at the network's size
|
|
genesis_day_index: u64,
|
|
genesis_dataset_log2: u32,
|
|
program_class_v3_activation_daa: u64,
|
|
},
|
|
SetTarget {
|
|
shift: u32,
|
|
share_target64: String,
|
|
},
|
|
Template {
|
|
template_id: u64,
|
|
mode: String,
|
|
block: Box<RpcRawBlock>,
|
|
daa_score: u64,
|
|
bits: u32,
|
|
#[serde(default)]
|
|
votes_carried: Vec<u64>,
|
|
},
|
|
Job {
|
|
job_id: u64,
|
|
template_id: u64,
|
|
prehash: String,
|
|
target64: String,
|
|
share_target64: String,
|
|
nonce_start: String,
|
|
nonce_count: String,
|
|
epoch_seed: String,
|
|
day: u64,
|
|
/// The program class (generator number) and era seed of the job's epoch: the member's CPU re-check and its
|
|
/// worker lines use exactly these (never a fixed class)
|
|
program_class: u32,
|
|
era_seed: Option<String>,
|
|
clean: bool,
|
|
},
|
|
JobRefused {
|
|
job_id: u64,
|
|
code: String,
|
|
detail: String,
|
|
},
|
|
Share {
|
|
job_id: u64,
|
|
nonce: String,
|
|
hash: String,
|
|
},
|
|
ShareResult {
|
|
job_id: u64,
|
|
nonce: String,
|
|
accepted: bool,
|
|
code: String,
|
|
weight: f64,
|
|
#[serde(default)]
|
|
block: bool,
|
|
},
|
|
Solution {
|
|
job_id: u64,
|
|
nonce: String,
|
|
hash: String,
|
|
block_hash: String,
|
|
},
|
|
Stats {
|
|
#[serde(default)]
|
|
hashrate: f64,
|
|
#[serde(default)]
|
|
workers: u32,
|
|
#[serde(default)]
|
|
refusals: u64,
|
|
#[serde(default)]
|
|
shares: u64,
|
|
#[serde(default)]
|
|
members: u32,
|
|
#[serde(default)]
|
|
blocks_24h: u64,
|
|
#[serde(default)]
|
|
declared_share: f64,
|
|
},
|
|
Ping {
|
|
id: u64,
|
|
},
|
|
Pong {
|
|
id: u64,
|
|
},
|
|
Bye {
|
|
reason: String,
|
|
},
|
|
Error {
|
|
code: String,
|
|
detail: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct ShareScheme {
|
|
pub scheme: String,
|
|
pub fee_percent: f64,
|
|
pub pplns_window_blocks: f64,
|
|
pub min_payout_ign: f64,
|
|
}
|
|
|
|
impl Msg {
|
|
/// One line, newline terminated.
|
|
pub fn line(&self) -> String {
|
|
let mut s = serde_json::to_string(self).expect("message serialises");
|
|
s.push('\n');
|
|
s
|
|
}
|
|
pub fn parse(line: &str) -> Result<Msg, String> {
|
|
serde_json::from_str(line).map_err(|e| e.to_string())
|
|
}
|
|
}
|
|
|
|
pub fn hex_u64(v: u64) -> String {
|
|
format!("{v:016x}")
|
|
}
|
|
|
|
pub fn parse_hex_u64(s: &str) -> Option<u64> {
|
|
u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()
|
|
}
|
|
|
|
pub fn hex_bytes(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
pub fn parse_hex_array<const N: usize>(s: &str) -> Option<[u8; N]> {
|
|
let s = s.trim_start_matches("0x");
|
|
if s.len() != 2 * N {
|
|
return None;
|
|
}
|
|
let mut out = [0u8; N];
|
|
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
|
|
out[i] = u8::from_str_radix(std::str::from_utf8(chunk).ok()?, 16).ok()?;
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn messages_round_trip_with_the_type_tag() {
|
|
let m = Msg::Share { job_id: 7, nonce: hex_u64(0x1234), hash: hex_u64(42) };
|
|
let line = m.line();
|
|
assert!(line.starts_with("{\"t\":\"share\""), "{line}");
|
|
assert!(line.ends_with('\n'));
|
|
match Msg::parse(line.trim()).unwrap() {
|
|
Msg::Share { job_id, nonce, hash } => {
|
|
assert_eq!(job_id, 7);
|
|
assert_eq!(parse_hex_u64(&nonce), Some(0x1234));
|
|
assert_eq!(parse_hex_u64(&hash), Some(42));
|
|
}
|
|
other => panic!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_fields_are_ignored_and_missing_optionals_default() {
|
|
let m = Msg::parse(r#"{"t":"hello","versions":["0.1"],"chain_id":4463,"client":"x","extra":1}"#).unwrap();
|
|
assert!(matches!(m, Msg::Hello { modes, .. } if modes.is_empty()));
|
|
assert!(Msg::parse(r#"{"t":"nope"}"#).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn hex_helpers() {
|
|
assert_eq!(parse_hex_array::<2>("0a0b"), Some([10, 11]));
|
|
assert_eq!(parse_hex_array::<2>("0a0"), None);
|
|
assert_eq!(hex_bytes(&[255, 0]), "ff00");
|
|
}
|
|
}
|