Pool-0: welcome carries software_dev_fee_percent beside the pool fee, the page's Fees row and site/miner.html say pool-0 charges the same 1 percent as the solo dev fee; jobs and seeds carry the latency ladder's rung for 0.3.19.
TLS (pool/src/tls.rs): --tls-cert/--tls-key or --tls-self-signed (a P-256 pair under the data dir, the certificate pin printed at start); the binding is the member's BLS signature over this connection's exporter (label EXPORTER-igneum-pool-binding, context the chain id), refused on any other connection; testnet and mainnet refuse the clear without --allow-plain. HiveOS: pools:// and POOL_PIN.
Page rows: node state in words (Q68), one formatter set and luck in MiningPoolStats' convention (Q69), samples and check costs persisted (Q70), payments under the lookup (Q71), hourly history with a sparkline, --alert-webhook, /metrics, /health on the node (Q72), the network's finality state beside "payouts follow blue confirmation, not finality" (Q73).
The open pool (pool/src/sidechain.rs, open.rs, p2p.rs; igneum-pool --open): the member's own node and daemon, no payout key, no balance; every coinbase carries the member's own address, the share chain's parent (IGNS) and the window's split (IGNP); templates re-stamped on a new chain tip without a node call; shares gossiped and checked by every member (structure, the node's seeds, the PoW); heaviest work wins, a fork's loser is stale; the dev fee as one split entry; shares.log and verify-share for the drop proof; the gate harness pool/tools/open-gate.mjs. The node side (the executor's split from pool_split_activation_daa) is on the fork branch pool-finish-node.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 3e5271ba6a)
271 lines
8.6 KiB
Rust
271 lines
8.6 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,
|
|
/// `operator` (pool-0: the pool's address in every coinbase, PPLNS from its balance) or `open` (the share
|
|
/// sidechain: every coinbase pays the window, no balance, no operator key)
|
|
#[serde(default)]
|
|
pool_mode: String,
|
|
/// Spec 9.3: whether this connection is TLS (the `authorize` binding is then required)
|
|
#[serde(default)]
|
|
tls: bool,
|
|
},
|
|
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,
|
|
/// Spec 9.3, O-9.7: over TLS, the member's BLS signature (96 bytes hex) over this connection's exporter
|
|
/// (`finality::binding_message`); empty over plain TCP
|
|
#[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,
|
|
/// The latency ladder (0.3.19): the shadow pass count of the current and the next epoch, part of the program
|
|
#[serde(default)]
|
|
shadow_reps: u32,
|
|
#[serde(default)]
|
|
next_shadow_reps: u32,
|
|
},
|
|
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>,
|
|
/// The latency ladder's shadow pass count of the job's epoch (0 before the ladder)
|
|
#[serde(default)]
|
|
shadow_reps: u32,
|
|
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,
|
|
/// Published beside the pool fee (reinvent 3.5): the software dev fee a member pays in this mode. Pool-0: 0
|
|
/// (the pool issues the templates, its fee is the only fee); the open pool: the split entry to the software
|
|
/// address, the same 1 percent the solo miner pays, so solo, pool-0 and open are fee-neutral
|
|
#[serde(default)]
|
|
pub software_dev_fee_percent: f64,
|
|
/// The open pool: the window in shares and the chain's seconds per share
|
|
#[serde(default)]
|
|
pub window_shares: u64,
|
|
#[serde(default)]
|
|
pub chain_share_s: 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");
|
|
}
|
|
}
|