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 <noreply@anthropic.com>
(cherry picked from commit 8fb1117dd7)
This commit is contained in:
parent
8a89ddf0cd
commit
ebce905895
6 changed files with 64 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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 <fork worktree name> /srv/builds/<worktree>/vendor/igneum-node`.
|
||||
whole repository, and the symlink is made once on the box by hand: `ln -s <fork worktree name> /srv/builds/<worktree>/vendor/igneum-node`,
|
||||
with the line `vendor/igneum-node` in `/srv/builds/<worktree>/.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
|
||||
|
|
|
|||
|
|
@ -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::<usize>() < 2000 {
|
||||
if o.values().map(|v| v.len()).sum::<usize>() < 20_000 {
|
||||
o.entry(share.parent32()).or_default().push(share.clone());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Pool>, open: Arc<Open>) {
|
||||
|
|
@ -105,6 +106,7 @@ async fn session(pool: Arc<Pool>, open: Arc<Open>, 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<u64> = 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<Pool>, open: Arc<Open>, 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<Pool>, open: Arc<Open>, 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(""))),
|
||||
|
|
|
|||
|
|
@ -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<u64> {
|
||||
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::<u128>() / 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue