From c7309dcdf3678b877be355669487cac8a5726878 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:17:38 +0000 Subject: [PATCH] Open pool: the retarget clamps against the window's mean (never compounded share by share), the ceiling in u128 (a 2^49 first target shifted by 20 wrapped to 0 on the box), a deeper reorg depth and a progressive sync when parents are missing; the fast-time file carries pool_split_activation_daa (never); the gate runs on build-1 Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 093f052c7ee7912250556ec358841714a6f2af79) --- docs/plans/pool.md | 5 +-- infra/fast-time/override-60x.json | 1 + pool/README.md | 4 ++- pool/src/open.rs | 2 +- pool/src/p2p.rs | 19 ++++++++--- pool/src/sidechain.rs | 53 ++++++++++++++++++++++++------- 6 files changed, 64 insertions(+), 20 deletions(-) diff --git a/docs/plans/pool.md b/docs/plans/pool.md index eced7cfa..9ab1bd86 100644 --- a/docs/plans/pool.md +++ b/docs/plans/pool.md @@ -409,8 +409,9 @@ until a cut sets it through the P2 mechanism or the override, as the fee switch ### 10.5 The gate -Run: `tools/lock/with-lock.sh run node pool/tools/open-gate.mjs` on this Mac (nothing the network depends on runs here; -the box was at load 83 on 96 threads with 52 GB free, so the run stayed on the Mac): 4 nodes on the 60x fast-time profile +Run: `node pool/tools/open-gate.mjs` on igneum-build-1 from `/srv/builds/igneum-wt-pool-finish` with the box-built +binaries (the standing rule of 7 October 2026 afternoon: nothing builds or runs on the Mac; the first attempt ran on +the Mac at load 113 and the Mac crashed under it, section 10.4's retarget note): 4 nodes on the 60x fast-time profile with real proof of work at `genesis_bits 0x1e400000`, the genesis dataset at 2^24 words (64 MiB per process against the devnet's 1 GiB, so 100 CPU members, 10 daemons and 4 nodes fit 64 GB), `pool_split_activation_daa 0`; 10 open daemons (one per node in turn, peered in a ring with two chords, the last one withholding its members' shares); 100 CPU members diff --git a/infra/fast-time/override-60x.json b/infra/fast-time/override-60x.json index 75a0e2cc..c6d7cce4 100644 --- a/infra/fast-time/override-60x.json +++ b/infra/fast-time/override-60x.json @@ -63,6 +63,7 @@ "latency_ladder_activation_daa": 18446744073709551615, "latency_ladder_window_daa": 120, "proving_v1_fresh_rule_daa": 18446744073709551615, + "pool_split_activation_daa": 18446744073709551615, "exec_restart_number": 18446744073709551615, "exec_restart_hash": "", "exec_restart_state_root": "", diff --git a/pool/README.md b/pool/README.md index a85a2a62..387048bf 100644 --- a/pool/README.md +++ b/pool/README.md @@ -225,7 +225,9 @@ Captured responses from the measured run are the fixtures. Every Linux build and suite runs on the box through `tools/build-remote.sh` from this directory (`IGNEUM_AGENT=pool`). The crate reads the fork through the `vendor/igneum-node` symlink; the build library syncs the fork worktree it points at as a -whole repository, and the symlink is made once on the box by hand: `ln -s /srv/builds//vendor/igneum-node`. +whole repository, and the symlink is made once on the box by hand: `ln -s /srv/builds//vendor/igneum-node`, +with the line `vendor/igneum-node` in `/srv/builds//.igneum-scratch-spare` so the mirror's clean before every build +keeps it (7 October 2026: without the line the first box build of the pool crate removed it and cargo found no fork). Artefacts land in `pool/target-remote/release/igneum-pool` (x86_64 Linux); the Mac builds no Linux binary. Start-up contract (7 October 2026): the daemon exits 2 when `--listen` or `--http` cannot be bound, exits 3 when the node diff --git a/pool/src/open.rs b/pool/src/open.rs index fa38f818..27fe20eb 100644 --- a/pool/src/open.rs +++ b/pool/src/open.rs @@ -211,7 +211,7 @@ impl Open { c.check_structure(&share).map_err(|e| { if e.contains("parent") && e.contains("unknown") { let mut o = self.orphans.lock().unwrap(); - if o.values().map(|v| v.len()).sum::() < 2000 { + if o.values().map(|v| v.len()).sum::() < 20_000 { o.entry(share.parent32()).or_default().push(share.clone()); } } diff --git a/pool/src/p2p.rs b/pool/src/p2p.rs index 737649f6..aaf20031 100644 --- a/pool/src/p2p.rs +++ b/pool/src/p2p.rs @@ -20,7 +20,8 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::TcpStream; static NEXT_CONN: AtomicU64 = AtomicU64::new(1); -/// Shares asked for below our height when a peer is ahead (a reorg deeper than this needs a restart). +/// Shares asked for below our height when a peer is ahead; each further round of unknown parents asks this much +/// deeper, down to the chain's reorg depth. pub const SYNC_BACK: u64 = 64; pub async fn run(pool: Arc, open: Arc) { @@ -105,6 +106,7 @@ async fn session(pool: Arc, open: Arc, sock: TcpStream, remote: Stri let mut gossip = open.gossip.subscribe(); let mut lines = BufReader::with_capacity(256 << 10, rd).lines(); let mut asked_at = 0u64; + let mut asked_from: Option = None; let mut relayed = 0u64; let mut received = 0u64; let mut last_ping = tokio::time::Instant::now(); @@ -137,6 +139,7 @@ async fn session(pool: Arc, open: Arc, sock: TcpStream, remote: Stri println!("{} p2p {remote}: hello, height {peer_height} (ours {height}){}", unix_ms(), if peer_work > ours { ", ahead: asking for shares" } else { "" }); if peer_work > ours { asked_at = unix_ms(); + asked_from = Some(height.saturating_sub(SYNC_BACK)); send(json!({"t": "get_shares", "from": height.saturating_sub(SYNC_BACK)})); } } @@ -165,16 +168,24 @@ async fn session(pool: Arc, open: Arc, sock: TcpStream, remote: Stri *open.last_reject.lock().unwrap() = format!("{remote}: {e}"); open.rejected.fetch_add(1, Ordering::Relaxed); if e.contains("unknown") && unix_ms().saturating_sub(asked_at) > 2000 { - // the parent is missing: ask for the stretch below it + // the parent is missing: ask for the stretch below it, and each time it is still + // missing a stretch deeper (a fork older than the last request), down to the + // chain's reorg depth + let floor = { open.chain.lock().unwrap().height() }.saturating_sub(crate::sidechain::MAX_REORG_DEPTH); + let from = match asked_from { + Some(f) if f < share.height => f.saturating_sub(SYNC_BACK).max(floor), + _ => share.height.saturating_sub(SYNC_BACK).max(floor), + }; asked_at = unix_ms(); - send(json!({"t": "get_shares", "from": share.height.saturating_sub(SYNC_BACK)})); + asked_from = Some(from); + send(json!({"t": "get_shares", "from": from})); } else if !e.contains("unknown") { println!("{} p2p {remote}: share {} REFUSED: {e}", unix_ms(), &share.hash[..16.min(share.hash.len())]); } } } } - "synced" => {} + "synced" => { asked_from = None; } "ping" => send(json!({"t": "pong", "id": v["id"]})), "pong" => {} "bye" => return Err(format!("peer said bye: {}", v["reason"].as_str().unwrap_or(""))), diff --git a/pool/src/sidechain.rs b/pool/src/sidechain.rs index e672be40..f4a2fa6c 100644 --- a/pool/src/sidechain.rs +++ b/pool/src/sidechain.rs @@ -32,12 +32,15 @@ pub const RETARGET_CLAMP: u128 = 4; /// The chain's target never goes above this (a saturated target makes every hash a share). pub const MAX_TARGET: u64 = 1 << 62; /// The retarget never moves the target more than this many doublings above the chain's first target (a chain -/// whose hashrate fell a thousandfold is restarted with a new first target, not followed into a flood). -pub const MAX_EASING_DOUBLINGS: u32 = 10; +/// whose hashrate fell a millionfold is restarted with a new first target, not followed into a flood). +pub const MAX_EASING_DOUBLINGS: u32 = 20; +/// Shares behind in the window's ratio before the retarget is trusted at all (a window of two is noise). +pub const RETARGET_MIN_SHARES: usize = 8; /// Heights kept below the tip before a share is pruned from memory. pub const KEEP_DEPTH: u64 = 20_000; -/// How far behind the tip a share may extend the chain (a deeper fork is refused; the chain's finality). -pub const MAX_REORG_DEPTH: u64 = 256; +/// How far behind the tip a share may extend the chain (a deeper fork is refused; the chain's finality). At ten +/// shares a second (the gate's rate) this is 200 s of chain; at the default ten seconds a share, over five hours. +pub const MAX_REORG_DEPTH: u64 = 2_000; pub const GENESIS_PARENT: [u8; 32] = [0u8; 32]; /// The seeds of a share's epoch, on the wire and in the log, with the network's cache rule (genesis day and dataset @@ -234,14 +237,15 @@ impl ShareChain { /// start ran the target up to the saturation cap in thirty shares and every hash became a share.) pub fn target_after(&self, parent: &[u8; 32]) -> Option { let genesis = self.genesis_target?; - let ceiling = genesis.checked_shl(MAX_EASING_DOUBLINGS).unwrap_or(u64::MAX).min(MAX_TARGET) as u128; + // in u128: `checked_shl` only refuses a shift of 64 or more and wraps the value otherwise (the box smoke run of + // 7 October: a 2^49 first target shifted by 20 wrapped to 0, every chain target read 0, no share ever entered the chain) + let ceiling = ((genesis as u128) << MAX_EASING_DOUBLINGS).min(MAX_TARGET as u128); if *parent == GENESIS_PARENT { return Some((genesis as u128).min(ceiling) as u64); } let chain = self.ancestry(parent, RETARGET_SHARES + 1); let last = chain.first()?; - let prev_target = last.target() as u128; - if chain.len() < 3 { + if chain.len() < RETARGET_MIN_SHARES { return Some(last.target()); } let oldest = chain.last().unwrap(); @@ -249,9 +253,11 @@ impl ShareChain { let mean_target: u128 = chain.iter().map(|s| s.target() as u128).sum::() / chain.len() as u128; let actual_ms = last.timestamp.saturating_sub(oldest.timestamp).max(1) as u128; let expected_ms = steps * self.p().share_ms as u128; - // mean target x actual / expected: a slow window gets an easier (larger) target - let mut next = mean_target * actual_ms / expected_ms; - next = next.clamp(prev_target / RETARGET_CLAMP, prev_target.saturating_mul(RETARGET_CLAMP)); + // mean target x actual / expected: a slow window gets an easier (larger) target. The clamp is against the + // WINDOW'S MEAN, never the parent's target: a clamp against the parent compounds share by share (the 100-member + // run of 7 October on a Mac at load 113: thirty slow shares eased the target 4^30 to the ceiling, every hash a + // share, two million "parent unknown" refusals and ten diverged tips) + let next = (mean_target * actual_ms / expected_ms).clamp(mean_target / RETARGET_CLAMP, mean_target.saturating_mul(RETARGET_CLAMP)); Some(next.max(1).min(ceiling) as u64) } @@ -613,10 +619,27 @@ pub mod tests { let after = chain.window_split(&parent, a, tip.target()); assert!(!after.iter().any(|(x, _)| *x == [0xcc; 20])); let _ = withheld; - // the retarget: six shares ten seconds apart keep the target where it is (within the clamp) + // the retarget: six shares ten seconds apart keep the target where it is (under the minimum window it is the + // parent's); a window of shares arriving ten times too slowly eases by four at most, and no further on the + // next share (the clamp is against the window's mean, not compounded) let t0 = chain.genesis_target.unwrap(); let tn = chain.target_after(&parent).unwrap(); - assert!(tn >= t0 / 4 && tn <= t0.saturating_mul(4)); + assert_eq!(tn, t0); + { + let mut slow = ShareChain::new(params()); + slow.genesis_target = Some(1 << 58); + let mut p = GENESIS_PARENT; + let mut ts = 1_000_000u64; + for i in 0..RETARGET_MIN_SHARES as u64 + 4 { + let s = Arc::new(make_share(&slow, &engine, p, "ma", a, ts, i)); + slow.check_structure(&s).unwrap(); + slow.insert(s.clone()); + p = s.hash32(); + ts += 100_000; // ten times the 10-second interval + } + let eased = slow.target_after(&p).unwrap(); + assert!(eased >= (1u64 << 58) * 3 && eased <= (1u64 << 58) * 4, "one window of slow shares eases by four at most: {eased:x}"); + } // a fork: two shares on the same parent; the second is stale, the tip stays let s1 = Arc::new(make_share(&chain, &engine, parent, "ma", a, t, 6)); let s2 = Arc::new(make_share(&chain, &engine, parent, "mb", b, t + 1, 6)); @@ -660,6 +683,12 @@ pub mod tests { let bytes = payout_split_extra_data(&split).unwrap(); assert_eq!(payout_split_in(&bytes), Some(split)); assert!(chain.target_after(&GENESIS_PARENT).unwrap() <= MAX_TARGET); + // a first target whose ceiling overflows u64 keeps the target (the wrap of 7 October) + let mut big = ShareChain::new(params()); + big.genesis_target = Some(1 << 49); + assert_eq!(big.target_after(&GENESIS_PARENT), Some(1 << 49)); + big.genesis_target = Some(MAX_TARGET); + assert_eq!(big.target_after(&GENESIS_PARENT), Some(MAX_TARGET)); assert_eq!(work_of(1 << 63), 2); assert_eq!(work_of(0), u128::MAX >> 64); }