igneum/pool/src/vardiff.rs

229 lines
9 KiB
Rust

//! Variable difficulty per member (spec 09 section 9.8 item 4): choose the shift `s` so that the member sends about
//! one share per `share_interval_s`. The share target is `target64 << s`; a larger `s` means easier shares.
//!
//! Rules. `s` never saturates the 64-bit target (`s < leading_zeros(target64)`), and stays inside the pool's
//! `[min_shift, max_shift]`. After the first correction, the shift moves by ONE per change and never more than once
//! per `min_change_s` (30 s, Designed), so the share rate is a smooth hashrate estimate. The FIRST correction is
//! sized from the measured rate (up to 8 steps at once) because a new member's hashrate is unknown and a card at
//! 100 MH/s would otherwise flood the verifier for minutes; the plan says so (docs/plans/pool.md).
//! Idle rule: a member that sent no share for three intervals since the last change gets one easier step.
use std::collections::VecDeque;
#[derive(Clone, Debug)]
pub struct Vardiff {
pub shift: u32,
pub min_shift: u32,
pub max_shift: u32,
pub interval_s: f64,
pub min_change_s: f64,
started_s: f64,
last_change_s: f64,
/// Share times since the last change (seconds)
times: VecDeque<f64>,
first_done: bool,
pub changes: u64,
}
impl Vardiff {
pub fn new(initial_shift: u32, min_shift: u32, max_shift: u32, interval_s: f64, now_s: f64) -> Self {
Self {
shift: initial_shift.clamp(min_shift, max_shift),
min_shift,
max_shift,
interval_s,
min_change_s: 30.0,
started_s: now_s,
last_change_s: now_s,
times: VecDeque::new(),
first_done: false,
changes: 0,
}
}
/// The largest shift that keeps `target64 << s` under 2^64 (spec: a saturated target makes every hash a share).
pub fn cap_for(target64: u64) -> u32 {
target64.leading_zeros().saturating_sub(1)
}
/// The initial shift for a member whose hashrate is unknown: shares worth about 2^20 hashes each, so a CPU at
/// 0.1 MH/s sends one per 10 s and a 100 MH/s card sends 100 per second until the first correction.
pub fn initial_shift(target64: u64, min_shift: u32, max_shift: u32) -> u32 {
let log2_target = 63u32.saturating_sub(target64.leading_zeros());
let want = 44u32.saturating_sub(log2_target);
want.clamp(min_shift, max_shift.min(Self::cap_for(target64)))
}
pub fn on_share(&mut self, now_s: f64) {
self.times.push_back(now_s);
while self.times.len() > 1000 {
self.times.pop_front();
}
}
/// Shares per second since the last change (or since the start).
pub fn rate(&self, now_s: f64) -> f64 {
let since = (now_s - self.last_change_s).max(1.0);
self.times.len() as f64 / since
}
/// Call every share and every second. Returns the new shift when it changes.
pub fn retarget(&mut self, now_s: f64, target64: u64) -> Option<u32> {
let since_change = now_s - self.last_change_s;
let n = self.times.len();
let cap = self.max_shift.min(Self::cap_for(target64));
let old = self.shift;
if !self.first_done {
// First correction: after 8 shares, or after one interval with fewer, size the jump from the rate
if n < 8 && (now_s - self.started_s) < self.interval_s {
return None;
}
if n == 0 {
// nothing yet after an interval: wait up to three intervals, then one easier step
if (now_s - self.started_s) < 3.0 * self.interval_s {
return None;
}
self.shift = (self.shift + 1).min(cap);
} else {
let per_interval = self.rate(now_s) * self.interval_s;
let steps = per_interval.log2().abs().ceil().min(8.0) as u32;
if per_interval > 1.5 {
self.shift = self.shift.saturating_sub(steps).max(self.min_shift);
} else if per_interval < 0.66 {
self.shift = (self.shift + steps).min(cap);
}
}
self.first_done = true;
} else {
if since_change < self.min_change_s {
return None;
}
if n == 0 {
if since_change < 3.0 * self.interval_s {
return None;
}
self.shift = (self.shift + 1).min(cap);
} else {
let per_interval = self.rate(now_s) * self.interval_s;
if per_interval > 1.5 {
self.shift = self.shift.saturating_sub(1).max(self.min_shift);
} else if per_interval < 0.66 {
self.shift = (self.shift + 1).min(cap);
}
}
}
// the cap can fall when the network target tightens: always honour it
self.shift = self.shift.min(cap);
if self.shift != old {
self.last_change_s = now_s;
self.times.clear();
self.changes += 1;
Some(self.shift)
} else {
self.times.retain(|t| now_s - *t <= 120.0);
None
}
}
}
/// `min(2^64 - 1, target64 << s)`, never saturated by construction of the cap.
pub fn share_target(target64: u64, shift: u32) -> u64 {
if shift >= 64 {
return u64::MAX;
}
target64.checked_shl(shift).filter(|t| (*t >> shift) == target64).unwrap_or(u64::MAX)
}
/// The weight of a share at shift `s`: `2^-s` of a block (spec 9.8 item 3).
pub fn share_weight(shift: u32) -> f64 {
2f64.powi(-(shift as i32))
}
#[cfg(test)]
mod tests {
use super::*;
const T64: u64 = 1 << 40; // a block every 2^24 hashes
#[test]
fn the_cap_never_saturates_the_target() {
assert_eq!(Vardiff::cap_for(T64), 22);
assert_eq!(share_target(T64, 22), 1 << 62);
assert_eq!(share_target(T64, 23), 1 << 63, "one past the cap still fits");
assert_eq!(share_target(T64, 24), u64::MAX, "an overflowing shift saturates, which the cap prevents");
assert_eq!(Vardiff::cap_for(u64::MAX), 0);
assert_eq!(share_weight(3), 0.125);
}
#[test]
fn initial_shift_targets_a_million_hashes_per_share() {
// target 2^40: 2^24 hashes per block, wanted 2^20 per share: shift 4
assert_eq!(Vardiff::initial_shift(T64, 0, 60), 4);
// target 2^20: 2^44 hashes per block: shift 24
assert_eq!(Vardiff::initial_shift(1 << 20, 0, 60), 24);
// a target so easy that 2^44 is above it: clamped to the floor
assert_eq!(Vardiff::initial_shift(1 << 50, 0, 60), 0);
// clamped to the cap
assert_eq!(Vardiff::initial_shift(1 << 20, 0, 10), 10);
}
/// A fast member: 100 shares per second at the initial shift. The first correction jumps by up to 8 steps,
/// afterwards one step per 30 s, until the rate is about one per 10 s.
#[test]
fn a_fast_member_is_brought_down_and_then_stepped() {
let mut v = Vardiff::new(20, 0, 60, 10.0, 0.0);
let mut t = 0.0;
for _ in 0..8 {
t += 0.01;
v.on_share(t);
}
// eight shares in 80 ms: rate 8/s (since is floored to 1 s), 80 per interval: log2(80) = 6.3 -> 7 steps
assert_eq!(v.retarget(t, 1 << 20), Some(13));
// the next change is refused inside 30 s whatever the rate
for _ in 0..50 {
t += 0.1;
v.on_share(t);
}
assert_eq!(v.retarget(t, 1 << 20), None);
t += 30.0;
assert_eq!(v.retarget(t, 1 << 20), Some(12), "one step after 30 s");
}
/// A slow member sends nothing: after three intervals the shift eases by one, then one per 30 s.
#[test]
fn a_silent_member_gets_easier_shares_slowly() {
let mut v = Vardiff::new(4, 0, 60, 10.0, 0.0);
assert_eq!(v.retarget(20.0, 1 << 20), None);
assert_eq!(v.retarget(31.0, 1 << 20), Some(5));
assert_eq!(v.retarget(50.0, 1 << 20), None, "inside 30 s of the change");
assert_eq!(v.retarget(62.0, 1 << 20), Some(6));
}
/// At the right rate nothing changes.
#[test]
fn a_member_on_rate_is_left_alone() {
let mut v = Vardiff::new(10, 0, 60, 10.0, 0.0);
let mut t = 0.0;
for _ in 0..12 {
t += 10.0;
v.on_share(t);
assert_eq!(v.retarget(t, 1 << 20), None, "t={t}");
}
assert_eq!(v.changes, 0);
}
/// The shift respects the pool's bounds and the saturation cap when the network target eases.
#[test]
fn bounds_and_cap_hold() {
let mut v = Vardiff::new(30, 2, 40, 10.0, 0.0);
// a slow member: the cap of a very easy target (2^60 -> leading zeros 3 -> cap 2) wins over its shift
assert_eq!(v.retarget(31.0, 1 << 60), Some(2), "cap wins over the member's shift");
let mut v = Vardiff::new(3, 2, 40, 10.0, 0.0);
let mut t = 0.0;
for _ in 0..100 {
t += 0.01;
v.on_share(t);
}
assert_eq!(v.retarget(t, 1 << 20), Some(2), "floor at min_shift");
}
}