229 lines
9 KiB
Rust
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");
|
|
}
|
|
}
|