• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Neptune-Crypto / neptune-core / 13934460366

18 Mar 2025 10:10PM UTC coverage: 84.31% (+0.03%) from 84.279%
13934460366

Pull #512

github

web-flow
Merge 9c4947ba6 into ee3795966
Pull Request #512: `proptest_state_machine` over `PeerMessage`

9 of 26 new or added lines in 7 files covered. (34.62%)

42 existing lines in 5 files now uncovered.

50796 of 60249 relevant lines covered (84.31%)

178560.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.57
/src/models/blockchain/block/difficulty_control.rs
1
use std::cmp::Ordering;
2
use std::fmt::Display;
3
use std::ops::Add;
4
use std::ops::Shr;
5
use std::ops::ShrAssign;
6

7
use anyhow::ensure;
8
#[cfg(any(test, feature = "arbitrary-impls"))]
9
use arbitrary::Arbitrary;
10
use get_size2::GetSize;
11
use itertools::Itertools;
12
use num_bigint::BigUint;
13
use num_traits::FromPrimitive;
14
use num_traits::ToPrimitive;
15
use num_traits::Zero;
16
use serde::Deserialize;
17
use serde::Serialize;
18
use tasm_lib::triton_vm::prelude::BFieldCodec;
19
use tasm_lib::triton_vm::prelude::BFieldElement;
20
use tasm_lib::triton_vm::prelude::Digest;
21

22
use super::block_height::BlockHeight;
23
use super::MINIMUM_BLOCK_TIME;
24
use crate::models::blockchain::block::block_header::ADVANCE_DIFFICULTY_CORRECTION_FACTOR;
25
use crate::models::blockchain::block::block_header::ADVANCE_DIFFICULTY_CORRECTION_WAIT;
26
use crate::models::blockchain::block::block_header::TARGET_BLOCK_INTERVAL;
27
use crate::models::proof_abstractions::timestamp::Timestamp;
28

29
const DIFFICULTY_NUM_LIMBS: usize = 5;
30

31
/// Estimated number of hashes required to find a block.
32
///
33
/// Every `Difficulty` determines a *target*, which is a hash digest. A block
34
/// has proof-of-work if its hash is smaller than the difficulty of its
35
/// predecessor.
36
///
37
/// The `Difficulty` is set by the `difficulty_control` mechanism such that the
38
/// target block interval is by actual block times.
39
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, BFieldCodec, GetSize)]
394✔
40
#[cfg_attr(any(test, feature = "arbitrary-impls"), derive(Arbitrary))]
×
41
pub struct Difficulty([u32; DIFFICULTY_NUM_LIMBS]);
42

43
impl Difficulty {
44
    pub const NUM_LIMBS: usize = DIFFICULTY_NUM_LIMBS;
45
    const LIMBS_FOR_MINIMUM: [u32; Self::NUM_LIMBS] = [6000, 0, 0, 0, 0];
46
    pub const MINIMUM: Self = Self::new(Self::LIMBS_FOR_MINIMUM);
47
    pub const MAXIMUM: Self = Self::new([u32::MAX; Self::NUM_LIMBS]);
48

49
    pub(crate) const fn new(difficulty: [u32; DIFFICULTY_NUM_LIMBS]) -> Self {
973,903✔
50
        let mut lte_minimum = true;
973,903✔
51
        let mut i = 0;
973,903✔
52
        while i < Self::NUM_LIMBS {
5,843,418✔
53
            if difficulty[i] > Self::LIMBS_FOR_MINIMUM[i] {
4,869,515✔
54
                lte_minimum = false;
458,104✔
55
            }
4,411,411✔
56
            i += 1;
4,869,515✔
57
        }
58
        if lte_minimum {
973,903✔
59
            Self(Self::LIMBS_FOR_MINIMUM)
829,175✔
60
        } else {
61
            Self(difficulty)
144,728✔
62
        }
63
    }
973,903✔
64

65
    /// Convert a difficulty to a target threshold so as to test a block's
66
    /// proof-of-work.
67
    pub(crate) fn target(&self) -> Digest {
1,622,894✔
68
        let difficulty_as_bui: BigUint = BigUint::from(*self);
1,622,894✔
69
        let max_threshold_as_bui: BigUint =
1,622,894✔
70
            Digest([BFieldElement::new(BFieldElement::MAX); Digest::LEN]).into();
1,622,894✔
71
        let threshold_as_bui: BigUint = max_threshold_as_bui / difficulty_as_bui;
1,622,894✔
72

1,622,894✔
73
        threshold_as_bui.try_into().unwrap()
1,622,894✔
74
    }
1,622,894✔
75

76
    /// Multiply the `Difficulty` with a positive fixed point rational number
77
    /// consisting of two u32s as limbs separated by the point. Returns the
78
    /// (wrapping) result and the out-of-bounds limb containing the overflow, if
79
    /// any.
80
    fn safe_mul_fixed_point_rational(&self, lo: u32, hi: u32) -> (Self, u32) {
151,822✔
81
        let mut new_difficulty = [0; Self::NUM_LIMBS + 1];
151,822✔
82
        let mut carry = 0;
151,822✔
83
        for (old_difficulty_i, new_difficulty_i) in self
759,110✔
84
            .0
151,822✔
85
            .iter()
151,822✔
86
            .zip(new_difficulty.iter_mut().take(Self::NUM_LIMBS))
151,822✔
87
        {
759,110✔
88
            let sum = u64::from(carry) + u64::from(*old_difficulty_i) * u64::from(lo);
759,110✔
89
            *new_difficulty_i = sum as u32;
759,110✔
90
            carry = (sum >> 32) as u32;
759,110✔
91
        }
759,110✔
92
        new_difficulty[Self::NUM_LIMBS] = carry;
151,822✔
93
        carry = 0;
151,822✔
94
        for (old_difficulty_i, new_difficulty_i_plus_one) in
759,110✔
95
            self.0.iter().zip(new_difficulty.iter_mut().skip(1))
151,822✔
96
        {
759,110✔
97
            let sum = u64::from(carry) + u64::from(*old_difficulty_i) * u64::from(hi);
759,110✔
98
            let (digit, carry_bit) = new_difficulty_i_plus_one.overflowing_add(sum as u32);
759,110✔
99
            *new_difficulty_i_plus_one = digit;
759,110✔
100
            carry = ((sum >> 32) as u32) + u32::from(carry_bit);
759,110✔
101
        }
759,110✔
102

103
        (
151,822✔
104
            Difficulty::new(new_difficulty[1..].to_owned().try_into().unwrap()),
151,822✔
105
            carry,
151,822✔
106
        )
151,822✔
107
    }
151,822✔
108
}
109

110
impl IntoIterator for Difficulty {
111
    type Item = u32;
112
    type IntoIter = std::array::IntoIter<Self::Item, { Self::NUM_LIMBS }>;
113

114
    fn into_iter(self) -> Self::IntoIter {
132,341✔
115
        self.0.into_iter()
132,341✔
116
    }
132,341✔
117
}
118

119
impl From<Difficulty> for BigUint {
120
    fn from(value: Difficulty) -> Self {
1,634,476✔
121
        let mut bi = BigUint::zero();
1,634,476✔
122
        for &limb in value.0.iter().rev() {
8,172,380✔
123
            bi = (bi << 32) + limb;
8,172,380✔
124
        }
8,172,380✔
125
        bi
1,634,476✔
126
    }
1,634,476✔
127
}
128

129
impl PartialOrd for Difficulty {
130
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
×
131
        Some(self.cmp(other))
×
132
    }
×
133
}
134

135
impl Ord for Difficulty {
136
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
×
137
        self.0
×
138
            .into_iter()
×
139
            .rev()
×
140
            .zip(other.0.into_iter().rev())
×
141
            .map(|(lhs, rhs)| lhs.cmp(&rhs))
×
142
            .fold(Ordering::Equal, |acc, new| match acc {
×
143
                Ordering::Less => acc,
×
144
                Ordering::Equal => new,
×
145
                Ordering::Greater => acc,
×
146
            })
×
147
    }
×
148
}
149

150
impl Shr<usize> for Difficulty {
151
    type Output = Self;
152

153
    fn shr(self, rhs: usize) -> Self::Output {
822,077✔
154
        let limb_offset = rhs / 32;
822,077✔
155
        let mut array = self
822,077✔
156
            .0
822,077✔
157
            .into_iter()
822,077✔
158
            .skip(limb_offset)
822,077✔
159
            .pad_using(Self::NUM_LIMBS, |_| 0u32)
1,206,593✔
160
            .collect_vec();
822,077✔
161

822,077✔
162
        let remainder = rhs % 32;
822,077✔
163
        if remainder.is_zero() {
822,077✔
164
            return Difficulty::new(array.try_into().unwrap());
579,671✔
165
        }
242,406✔
166

242,406✔
167
        let mut borrow = 0u32;
242,406✔
168
        for i in (0..Self::NUM_LIMBS).rev() {
1,212,030✔
169
            let new_borrow = array[i] & ((1 << remainder) - 1);
1,212,030✔
170
            array[i] = (array[i] >> remainder) | (borrow << (32 - remainder));
1,212,030✔
171
            borrow = new_borrow;
1,212,030✔
172
        }
1,212,030✔
173

174
        Difficulty::new(array.try_into().unwrap())
242,406✔
175
    }
822,077✔
176
}
177

178
impl ShrAssign<usize> for Difficulty {
179
    fn shr_assign(&mut self, rhs: usize) {
8,674✔
180
        *self = *self >> rhs;
8,674✔
181
    }
8,674✔
182
}
183

184
impl Display for Difficulty {
185
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7✔
186
        write!(f, "{}", BigUint::from(*self))
7✔
187
    }
7✔
188
}
189

190
impl<T> From<T> for Difficulty
191
where
192
    T: Into<u32>,
193
{
194
    fn from(value: T) -> Self {
2✔
195
        let mut array = [0u32; Self::NUM_LIMBS];
2✔
196
        array[0] = value.into();
2✔
197
        Self(array)
2✔
198
    }
2✔
199
}
200

201
const POW_NUM_LIMBS: usize = 6;
202

203
/// Estimates how many hashes were used to produce the data object.
204
///
205
/// Proof-of-work is used in the fork choice rule: when presented with
206
/// two forks of different height, a node will choose the one with the greater
207
/// amount of proof-of-work.
208
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, BFieldCodec, GetSize)]
394✔
209
#[cfg_attr(any(test, feature = "arbitrary-impls"), derive(Arbitrary))]
×
210
pub struct ProofOfWork([u32; POW_NUM_LIMBS]);
211

212
impl ProofOfWork {
213
    pub(crate) const MAXIMUM: Self = ProofOfWork([u32::MAX; POW_NUM_LIMBS]);
214
    pub(crate) const MINIMUM: Self = ProofOfWork([0u32; POW_NUM_LIMBS]);
215
    pub(crate) const NUM_LIMBS: usize = POW_NUM_LIMBS;
216
    pub(crate) const fn new(amount: [u32; Self::NUM_LIMBS]) -> Self {
1,605✔
217
        Self(amount)
1,605✔
218
    }
1,605✔
219
}
220

221
impl IntoIterator for ProofOfWork {
222
    type Item = u32;
223
    type IntoIter = std::array::IntoIter<Self::Item, { Self::NUM_LIMBS }>;
224

225
    fn into_iter(self) -> Self::IntoIter {
132,071✔
226
        self.0.into_iter()
132,071✔
227
    }
132,071✔
228
}
229

230
impl<T> Add<T> for ProofOfWork
231
where
232
    T: IntoIterator<Item = u32>,
233
{
234
    type Output = ProofOfWork;
235

236
    fn add(self, rhs: T) -> Self::Output {
132,071✔
237
        let mut result = [0; Self::NUM_LIMBS];
132,071✔
238
        let mut carry = 0;
132,071✔
239
        let mut n = 0;
132,071✔
240
        for (i, (difficulty_digit, pow_digit)) in
660,355✔
241
            rhs.into_iter().zip(self.0.into_iter()).enumerate()
132,071✔
242
        {
660,355✔
243
            let sum = u64::from(carry) + u64::from(difficulty_digit) + u64::from(pow_digit);
660,355✔
244
            result[i] = sum as u32;
660,355✔
245
            carry = (sum >> 32) as u32;
660,355✔
246
            n += 1;
660,355✔
247
        }
660,355✔
248
        for (self_i, result_i) in self.into_iter().zip(result.iter_mut()).skip(n) {
132,071✔
249
            let sum = u64::from(carry) + u64::from(self_i);
132,071✔
250
            *result_i = sum as u32;
132,071✔
251
            carry = (sum >> 32) as u32;
132,071✔
252
        }
132,071✔
253
        Self(result)
132,071✔
254
    }
132,071✔
255
}
256

257
impl Zero for ProofOfWork {
258
    fn zero() -> Self {
1,601✔
259
        Self::new([0u32; Self::NUM_LIMBS])
1,601✔
260
    }
1,601✔
261

262
    fn is_zero(&self) -> bool {
×
263
        *self == Self::zero()
×
264
    }
×
265
}
266

267
impl From<ProofOfWork> for BigUint {
268
    fn from(value: ProofOfWork) -> Self {
2,082✔
269
        let mut bi = BigUint::zero();
2,082✔
270
        for &limb in value.0.iter().rev() {
12,492✔
271
            bi = (bi << 32) + limb;
12,492✔
272
        }
12,492✔
273
        bi
2,082✔
274
    }
2,082✔
275
}
276

277
impl TryFrom<f64> for ProofOfWork {
278
    type Error = anyhow::Error;
279

280
    fn try_from(value: f64) -> Result<Self, Self::Error> {
525✔
281
        ensure!(!value.is_nan(), "cannot convert NaN to ProofOfWork value");
525✔
282

283
        if value < 0_f64 {
525✔
284
            return Ok(ProofOfWork::MINIMUM);
×
285
        }
525✔
286
        if value.is_infinite() {
525✔
287
            return Ok(ProofOfWork::MAXIMUM);
×
288
        }
525✔
289
        let digits = BigUint::from_f64(value).unwrap().to_u32_digits();
525✔
290
        if digits.len() > POW_NUM_LIMBS && digits.iter().skip(POW_NUM_LIMBS).any(|&d| d != 0) {
525✔
291
            return Ok(ProofOfWork::MAXIMUM);
×
292
        }
525✔
293
        Ok(ProofOfWork(
525✔
294
            digits
525✔
295
                .into_iter()
525✔
296
                .pad_using(POW_NUM_LIMBS, |_| 0u32)
613✔
297
                .take(POW_NUM_LIMBS)
525✔
298
                .collect_vec()
525✔
299
                .try_into()
525✔
300
                .unwrap(),
525✔
301
        ))
525✔
302
    }
525✔
303
}
304

305
impl PartialOrd for ProofOfWork {
306
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
590✔
307
        Some(self.cmp(other))
590✔
308
    }
590✔
309
}
310

311
impl Ord for ProofOfWork {
312
    fn cmp(&self, other: &Self) -> Ordering {
590✔
313
        self.0
590✔
314
            .into_iter()
590✔
315
            .rev()
590✔
316
            .zip(other.0.into_iter().rev())
590✔
317
            .map(|(lhs, rhs)| lhs.cmp(&rhs))
3,540✔
318
            .fold(Ordering::Equal, |acc, new| match acc {
3,540✔
319
                Ordering::Less => acc,
1,285✔
320
                Ordering::Equal => new,
965✔
321
                Ordering::Greater => acc,
1,290✔
322
            })
3,540✔
323
    }
590✔
324
}
325

326
impl Display for ProofOfWork {
327
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
512✔
328
        write!(f, "{}", BigUint::from(*self))
512✔
329
    }
512✔
330
}
331

332
/// Control system for block difficulty.
333
///
334
/// This function computes the new block's difficulty from the block's
335
/// timestamp, the previous block's difficulty, and the previous block's
336
/// timestamp. It regulates the block interval by tuning the difficulty.
337
/// It assumes that the block timestamp is valid.
338
///
339
/// This mechanism is a PID controller with P = -2^-4 (and I = D = 0) and with
340
/// with a few modifications such as clamping and advance correction.
341
/// The following diagram describes the mechanism.
342
///
343
/// ```notest
344
///                             --------------
345
///                            |              |------ new timestamp ------
346
///  --- new difficulty    --->|  blockchain  |------ old timestamp ----  |
347
/// |   (control signal)       |              |------ old difficulty -  | |
348
/// |                           --------------                        | | |
349
/// |   ---                            ---                            | | |
350
///  --| * |<-------------------------| * |<--------------------------  | |
351
///     ---                            ---                              | |
352
///      ^ PID                          ^  advance                      | |
353
///      | adjustment                   |  correction                 - v v
354
///      |                              |                               ---
355
///      |                           ------                            | + |
356
///     ---                         | 2^-x |                            ---
357
///    | + |<--- 1.0                 ------                   (process   |
358
///     ---                             ^      (setpoint:)    variable:) |
359
///      ^                              |        target         observed |
360
///      |                           -------      block       block time |
361
///      |                          | floor |    interval                v
362
///      |                           -------       |                 -  ---
363
///      |                              ^          |------------------>| + |
364
///      |                              |          |                    ---
365
///      |                          --------       |                     |
366
///      |                         | * 2^-7 |      v                     |
367
///      |   (P =)                  --------     -----                   |
368
///      |   -2^-4                      ^       | 1/x |                  |
369
///      |     |                        |        -----                   |
370
///      |     v                        |          v                     |
371
///      |    ---     ---------------   |         ---     absolute error |
372
///       ---| * |<--| clamp [-1; 4] |<----------| * |<------------------
373
///           ---     ---------------   relative  ---
374
///                                      error
375
///``
376
/// The P-controller (without clamping) does have a systematic error up to -5%
377
/// of the setpoint, whose exact magnitude depends on the relation between
378
/// proving and guessing time. This bias could be eliminated in principle by
379
/// setting I and D; but the resulting controller is more complex (=> difficult
380
/// to implement), generates overshoot (=> bad for liveness), and periodicity
381
/// (=> attack vector). Most importantly, the bias is counteracted to some
382
/// degree by the clamping.
383
/// ```
384
pub(crate) fn difficulty_control(
21,422✔
385
    new_timestamp: Timestamp,
21,422✔
386
    old_timestamp: Timestamp,
21,422✔
387
    mut old_difficulty: Difficulty,
21,422✔
388
    target_block_interval: Option<Timestamp>,
21,422✔
389
    previous_block_height: BlockHeight,
21,422✔
390
) -> Difficulty {
21,422✔
391
    // no adjustment if the previous block is the genesis block
21,422✔
392
    if previous_block_height.is_genesis() {
21,422✔
393
        return old_difficulty;
206✔
394
    }
21,216✔
395

21,216✔
396
    // otherwise, compute PID control signal
21,216✔
397

21,216✔
398
    // target; signal to follow
21,216✔
399
    let target_block_interval = target_block_interval.unwrap_or(TARGET_BLOCK_INTERVAL);
21,216✔
400

21,216✔
401
    // most recent observed block time
21,216✔
402
    let delta_t = new_timestamp - old_timestamp;
21,216✔
403

21,216✔
404
    // distance to target
21,216✔
405
    let absolute_error = (delta_t.0.value() as i64) - (target_block_interval.0.value() as i64);
21,216✔
406
    let relative_error =
21,216✔
407
        i128::from(absolute_error) * ((1 << 32) / i128::from(target_block_interval.0.value()));
21,216✔
408
    let clamped_error = relative_error.clamp(-1 << 32, 4 << 32);
21,216✔
409

21,216✔
410
    // Errors smaller than -1 cannot occur because delta_t >= MINIMUM_BLOCK_TIME > 0.
21,216✔
411
    // Errors greater than 4 can occur but are clamped away because otherwise a
21,216✔
412
    // single arbitrarily large concrete block time can induce an arbitrarily
21,216✔
413
    // large downward adjustment to the difficulty.
21,216✔
414
    // After clamping a `u64` suffices but before clamping we might get overflow
21,216✔
415
    // for very large block times so we use i128 for the `relative_error`.
21,216✔
416

21,216✔
417
    // Every time ADVANCE_DIFFICULTY_CORRECTION_WAIT target block times pass
21,216✔
418
    // between two blocks, the effective difficulty (the thing being compared
21,216✔
419
    // against the new block's hash) drops by a factor
21,216✔
420
    // ADVANCE_DIFFICULTY_CORRECTION_FACTOR, or drops to the minimum difficulty,
21,216✔
421
    // whichever is largest.
21,216✔
422
    let num_advance_reductions =
21,216✔
423
        relative_error >> (32 + ADVANCE_DIFFICULTY_CORRECTION_WAIT.ilog2());
21,216✔
424
    if num_advance_reductions > 0 {
21,216✔
425
        let shift_amount = ((num_advance_reductions as u128)
8,416✔
426
            * u128::from(ADVANCE_DIFFICULTY_CORRECTION_FACTOR.ilog2()))
8,416✔
427
            as usize;
8,416✔
428
        old_difficulty >>= shift_amount;
8,416✔
429
    }
12,800✔
430

431
    // change to control signal
432
    // adjustment_factor = (1 + P * error)
433
    // const P: f64 = -1.0 / 16.0;
434
    let one_plus_p_times_error = (1 << 32) + ((-clamped_error) >> 4);
21,216✔
435
    debug_assert!(one_plus_p_times_error.is_positive());
21,216✔
436

437
    let lo = one_plus_p_times_error as u32;
21,216✔
438
    let hi = (one_plus_p_times_error >> 32) as u32;
21,216✔
439
    let (new_difficulty, overflow) = old_difficulty.safe_mul_fixed_point_rational(lo, hi);
21,216✔
440

21,216✔
441
    if overflow > 0 {
21,216✔
442
        Difficulty::MAXIMUM
114✔
443
    } else {
444
        new_difficulty
21,102✔
445
    }
446
}
21,422✔
447

448
/// Determine an upper bound for the maximum possible cumulative proof-of-work
449
/// after n blocks given the start conditions.
450
pub(crate) fn max_cumulative_pow_after(
527✔
451
    cumulative_pow_start: ProofOfWork,
527✔
452
    difficulty_start: Difficulty,
527✔
453
    num_blocks: usize,
527✔
454
) -> ProofOfWork {
527✔
455
    // If the observed interval between consecutive blocks is the minimum
456
    // allowed by the consensus rules, the clamped relative error is almost -1.
457
    // In this case the PID adjustment factor is
458
    // f =  1 + (MINIMUM_BLOCK_TIME - TARGET_BLOCK_INTERVAL) / TARGET_BLOCK_INTERVAL * P
459
    //   =  1 - (60 - 294) / 294 / 16,
460
    const EPSILON: f64 = 0.000001;
461
    let f = 1.0_f64
527✔
462
        + (TARGET_BLOCK_INTERVAL.to_millis() - MINIMUM_BLOCK_TIME.to_millis()) as f64
527✔
463
            / TARGET_BLOCK_INTERVAL.to_millis() as f64
527✔
464
            / 16.0
527✔
465
        + EPSILON;
527✔
466
    let mut max_difficulty: f64 = BigUint::from(difficulty_start).to_f64().unwrap();
527✔
467
    let mut max_cumpow: f64 =
527✔
468
        BigUint::from(cumulative_pow_start).to_f64().unwrap() * (1.0 + EPSILON);
527✔
469
    let cap = BigUint::from(ProofOfWork::MAXIMUM).to_f64().unwrap();
527✔
470
    for _ in 0..num_blocks {
527✔
471
        max_cumpow += max_difficulty;
134,868✔
472
        max_difficulty *= f;
134,868✔
473

134,868✔
474
        // Avoid spending more time in loop if we've already reached max.
134,868✔
475
        if max_cumpow >= cap {
134,868✔
476
            return ProofOfWork::MAXIMUM;
2✔
477
        }
134,866✔
478
    }
479

480
    // This conversion is safe and cannot panic.
481
    ProofOfWork::try_from(max_cumpow).unwrap_or_else(|_e| {
525✔
482
        panic!("max_cumpow is within bounds where successful conversion should be guaranteed")
×
483
    })
525✔
484
}
527✔
485

486
#[cfg(test)]
487
mod test {
488
    use itertools::Itertools;
489
    use num_bigint::BigInt;
490
    use num_bigint::BigUint;
491
    use num_rational::BigRational;
492
    use num_traits::One;
493
    use num_traits::ToPrimitive;
494
    use num_traits::Zero;
495
    use proptest::prop_assert;
496
    use proptest::prop_assert_eq;
497
    use proptest::test_runner::TestRng;
498
    use proptest_arbitrary_interop::arb;
499
    use rand::Rng;
500
    use rand_distr::Bernoulli;
501
    use rand_distr::Distribution;
502
    use rand_distr::Geometric;
503
    use test_strategy::proptest;
504

505
    use super::*;
506

507
    impl Difficulty {
508
        pub(crate) fn from_biguint(bi: BigUint) -> Self {
×
509
            assert!(
×
510
                bi.iter_u32_digits().count() <= Self::NUM_LIMBS,
×
511
                "BigUint too large to convert to Difficulty"
×
512
            );
513
            Self(
×
514
                bi.iter_u32_digits()
×
515
                    .take(Self::NUM_LIMBS)
×
516
                    .pad_using(Self::NUM_LIMBS, |_| 0u32)
×
517
                    .collect_vec()
×
518
                    .try_into()
×
519
                    .unwrap(),
×
520
            )
×
521
        }
×
522

523
        /// Convert a u64 into a difficulty.
524
        pub(crate) fn from_u64(value: u64) -> Self {
773✔
525
            let mut array = [0u32; Self::NUM_LIMBS];
773✔
526
            array[0] = (value & u64::from(u32::MAX)) as u32;
773✔
527
            array[1] = (value >> 32) as u32;
773✔
528
            Self(array)
773✔
529
        }
773✔
530
    }
531

532
    impl ProofOfWork {
533
        /// Convert a u64 into a proof-of-work value.
534
        pub(crate) fn from_u64(value: u64) -> Self {
515✔
535
            let as_difficulty = Difficulty::from_u64(value);
515✔
536
            ProofOfWork::zero() + as_difficulty
515✔
537
        }
515✔
538
    }
539

540
    fn sample_block_time(
10,000✔
541
        hash_rate: f64,
10,000✔
542
        mut difficulty: Difficulty,
10,000✔
543
        proving_time: f64,
10,000✔
544
        target_block_time: f64,
10,000✔
545
        seed: [u8; 32],
10,000✔
546
    ) -> f64 {
10,000✔
547
        let mut rng = TestRng::from_seed(proptest::test_runner::RngAlgorithm::ChaCha, &seed);
10,000✔
548
        let mut block_time_so_far = proving_time;
10,000✔
549
        let window_duration = target_block_time * (ADVANCE_DIFFICULTY_CORRECTION_WAIT as f64);
10,000✔
550
        let num_hashes_calculated_per_window = hash_rate * window_duration;
10,000✔
551
        for window in 0.. {
10,002✔
552
            // probability of success per Bernoulli trial
553
            let p_rational = BigRational::from_integer(1.into())
10,002✔
554
                / BigRational::from_integer(BigInt::from(BigUint::from(difficulty)));
10,002✔
555
            let p = p_rational.to_f64().expect(
10,002✔
556
                "difficulty-to-target conversion from `BigRational` to `f64` should succeed",
10,002✔
557
            );
10,002✔
558

10,002✔
559
            // determine whether we are successful in this time window
10,002✔
560
            let log_prob_failure = (-p).ln_1p(); // ln (1-p)
10,002✔
561
            let log_prob_collective_failure = log_prob_failure * num_hashes_calculated_per_window;
10,002✔
562
            let prob_collective_success = -log_prob_collective_failure.exp_m1(); // 1-e^x
10,002✔
563

10,002✔
564
            let success = Bernoulli::new(prob_collective_success)
10,002✔
565
                .unwrap()
10,002✔
566
                .sample(&mut rng);
10,002✔
567

10,002✔
568
            // if not, advance-correct difficulty
10,002✔
569
            if !success {
10,002✔
570
                println!(
2✔
571
                    "window {window}: time spent mining so far is {block_time_so_far}; \
2✔
572
                    probability of collective success is \
2✔
573
                    {prob_collective_success} and success was {success}, \
2✔
574
                    so correcting difficulty ...",
2✔
575
                );
2✔
576
                difficulty >>= ADVANCE_DIFFICULTY_CORRECTION_FACTOR
2✔
577
                    .ilog2()
2✔
578
                    .try_into()
2✔
579
                    .unwrap();
2✔
580
                block_time_so_far += window_duration;
2✔
581
                continue;
2✔
582
            }
10,000✔
583

10,000✔
584
            // else, determine time spent hashing
10,000✔
585
            // reject samples that exceed window bounds
10,000✔
586
            let distribution = Geometric::new(p).unwrap();
10,000✔
587
            let mut num_hashes = 1u64 + distribution.sample(&mut rng);
10,000✔
588
            let mut time_spent_guessing = (num_hashes as f64) / hash_rate;
10,000✔
589
            while time_spent_guessing > window_duration {
10,000✔
UNCOV
590
                num_hashes = 1u64 + distribution.sample(&mut rng);
×
UNCOV
591
                time_spent_guessing = (num_hashes as f64) / hash_rate;
×
UNCOV
592
            }
×
593
            block_time_so_far += time_spent_guessing;
10,000✔
594
            break;
10,000✔
595
        }
596

597
        block_time_so_far
10,000✔
598
    }
10,000✔
599

600
    #[derive(Debug, Clone, Copy)]
601
    struct SimulationEpoch {
602
        log_hash_rate: f64,
603
        proving_time: f64,
604
        num_iterations: usize,
605
    }
606

607
    #[test]
608
    fn block_time_tracks_target() {
1✔
609
        // declare epochs
1✔
610
        let epochs = [
1✔
611
            SimulationEpoch {
1✔
612
                log_hash_rate: 2.0,
1✔
613
                proving_time: 300.0,
1✔
614
                num_iterations: 2000,
1✔
615
            },
1✔
616
            SimulationEpoch {
1✔
617
                log_hash_rate: 3.0,
1✔
618
                proving_time: 300.0,
1✔
619
                num_iterations: 2000,
1✔
620
            },
1✔
621
            SimulationEpoch {
1✔
622
                log_hash_rate: 3.0,
1✔
623
                proving_time: 60.0,
1✔
624
                num_iterations: 2000,
1✔
625
            },
1✔
626
            SimulationEpoch {
1✔
627
                log_hash_rate: 5.0,
1✔
628
                proving_time: 60.0,
1✔
629
                num_iterations: 2000,
1✔
630
            },
1✔
631
            SimulationEpoch {
1✔
632
                log_hash_rate: 2.0,
1✔
633
                proving_time: 0.0,
1✔
634
                num_iterations: 2000,
1✔
635
            },
1✔
636
        ];
1✔
637

1✔
638
        // run simulation
1✔
639
        let mut rng = rand::rng();
1✔
640
        let mut block_times = vec![];
1✔
641
        let mut difficulty = Difficulty::MINIMUM;
1✔
642
        let target_block_time = 600f64;
1✔
643
        let target_block_interval = Timestamp::seconds(target_block_time.round() as u64);
1✔
644
        let mut new_timestamp = Timestamp::now();
1✔
645
        let mut block_height = BlockHeight::genesis();
1✔
646
        for SimulationEpoch {
647
            log_hash_rate,
5✔
648
            proving_time,
5✔
649
            num_iterations,
5✔
650
        } in epochs
6✔
651
        {
652
            let hash_rate = 10f64.powf(log_hash_rate);
5✔
653
            for _ in 0..num_iterations {
10,000✔
654
                let block_time = sample_block_time(
10,000✔
655
                    hash_rate,
10,000✔
656
                    difficulty,
10,000✔
657
                    proving_time,
10,000✔
658
                    target_block_time,
10,000✔
659
                    rng.random(),
10,000✔
660
                );
10,000✔
661
                block_times.push(block_time);
10,000✔
662
                let old_timestamp = new_timestamp;
10,000✔
663
                new_timestamp += Timestamp::seconds(block_time.round() as u64);
10,000✔
664

10,000✔
665
                difficulty = difficulty_control(
10,000✔
666
                    new_timestamp,
10,000✔
667
                    old_timestamp,
10,000✔
668
                    difficulty,
10,000✔
669
                    Some(target_block_interval),
10,000✔
670
                    block_height,
10,000✔
671
                );
10,000✔
672
                block_height = block_height.next();
10,000✔
673
            }
10,000✔
674
        }
675

676
        // select monitored block times
677
        let allowed_adjustment_period = 1000usize;
1✔
678
        let mut monitored_block_times = vec![];
1✔
679
        let mut counter = 0;
1✔
680
        for epoch in epochs {
6✔
681
            monitored_block_times.append(
5✔
682
                &mut block_times
5✔
683
                    [counter + allowed_adjustment_period..counter + epoch.num_iterations]
5✔
684
                    .to_owned(),
5✔
685
            );
5✔
686
            counter += epoch.num_iterations;
5✔
687
        }
5✔
688

689
        // perform statistical test on block times
690
        let n = monitored_block_times.len();
1✔
691
        let mean = monitored_block_times.into_iter().sum::<f64>() / (n as f64);
1✔
692
        println!("mean block time: {mean}\ntarget is: {target_block_time}");
1✔
693

1✔
694
        let margin = 0.05;
1✔
695
        assert!(target_block_time * (1.0 - margin) < mean);
1✔
696
        assert!(mean < target_block_time * (1.0 + margin));
1✔
697
    }
1✔
698

699
    #[proptest(cases = 10000)]
30,000✔
700
    fn one_plus_p_times_error_is_never_negative(
701
        #[strategy(arb())] old_timestamp: Timestamp,
1✔
702
        #[strategy(Timestamp::arbitrary_after(#old_timestamp))] new_timestamp: Timestamp,
10,000✔
703
        #[strategy(arb())] old_difficulty: Difficulty,
1✔
704
        #[strategy(Timestamp::arbitrary_between(Timestamp::seconds(0), Timestamp::days(1)))]
705
        target_block_interval: Timestamp,
1✔
706
        #[strategy(arb())] previous_block_height: BlockHeight,
1✔
707
    ) {
708
        // Function `difficulty_control` debug-asserts that the relevant
709
        // quantity is positive; so we just call the function to try to
710
        // trigger the error.
711
        difficulty_control(
712
            new_timestamp,
713
            old_timestamp,
714
            old_difficulty,
715
            Some(target_block_interval),
716
            previous_block_height,
717
        );
718
    }
719

720
    #[cfg(debug_assertions)]
721
    #[test]
722
    #[should_panic]
723
    fn debug_assert_fails() {
1✔
724
        debug_assert!(false);
1✔
725
    }
×
726

727
    #[proptest]
512✔
728
    fn mul_by_fixed_point_rational_distributes(
729
        #[strategy(arb())] a: Difficulty,
1✔
730
        #[strategy(arb())] b: Difficulty,
1✔
731
        #[strategy(arb())] lo: u32,
1✔
732
        #[strategy(arb())] hi: u32,
1✔
733
    ) {
734
        let a_bui = BigUint::from(a);
735
        let b_bui = BigUint::from(b);
736
        let a_plus_b_bui = a_bui + b_bui;
737
        if a_plus_b_bui.clone() >= BigUint::one() << (Difficulty::NUM_LIMBS * 32) {
738
            // a + b generates overflow which is not caught
739
            // so ignore test in this case
740
            return Ok(());
741
        }
742

743
        let r = u64::from(lo) + (u64::from(hi) << 32);
744
        let r_times_a_plus_b_bui: BigUint = (a_plus_b_bui.clone() * r) >> 32;
745

746
        let (ra, ra_overflow) = a.safe_mul_fixed_point_rational(lo, hi);
747
        let (rb, rb_overflow) = b.safe_mul_fixed_point_rational(lo, hi);
748

749
        let r_times_a_bui = BigUint::new(
750
            ra.into_iter()
751
                .pad_using(Difficulty::NUM_LIMBS, |_| 0u32)
×
752
                .chain([ra_overflow].into_iter())
753
                .collect_vec(),
754
        );
755
        let r_times_b_bui = BigUint::new(
756
            rb.into_iter()
757
                .pad_using(Difficulty::NUM_LIMBS, |_| 0u32)
×
758
                .chain([rb_overflow].into_iter())
759
                .collect_vec(),
760
        );
761
        let r_times_a_plus_r_times_b_bui = r_times_a_bui + r_times_b_bui;
762

763
        // ignore least-significant bit because it might differ due to a carry
764
        // from the fractional part
765
        prop_assert!(
766
            r_times_a_plus_r_times_b_bui.clone() == r_times_a_plus_b_bui.clone()
767
                || r_times_a_plus_r_times_b_bui + BigUint::one() == r_times_a_plus_b_bui
768
        );
769
    }
770

771
    #[proptest]
512✔
772
    fn shift_right_accumulates(
773
        #[strategy(arb())] diff: Difficulty,
1✔
774
        #[strategy(0usize..100)] a: usize,
1✔
775
        #[strategy(0usize..100)] b: usize,
1✔
776
    ) {
777
        prop_assert_eq!((diff >> a) >> b, diff >> (a + b));
778
        prop_assert_eq!((diff >> b) >> a, diff >> (a + b));
779
    }
780

781
    #[proptest]
512✔
782
    fn shift_right_matches_biguint(
783
        #[strategy(arb())] diff: Difficulty,
1✔
784
        #[strategy(0usize..100)] a: usize,
1✔
785
    ) {
786
        prop_assert_eq!(BigUint::from(diff) >> a, BigUint::from(diff >> a));
787
    }
788

789
    #[proptest]
512✔
790
    fn shift_right_assign_matches_shift_right(
791
        #[strategy(arb())] diff: Difficulty,
1✔
792
        #[strategy(0usize..100)] a: usize,
1✔
793
    ) {
794
        let mut running_diff = diff;
795
        running_diff >>= a;
796
        prop_assert_eq!(diff >> a, running_diff);
797
    }
798

799
    /// Determine the maximum possible cumulative proof-of-work after n blocks given
800
    /// the start conditions.
801
    fn max_cumulative_pow_after_iterative_test_impl(
257✔
802
        cumulative_pow_start: ProofOfWork,
257✔
803
        difficulty_start: Difficulty,
257✔
804
        num_blocks: usize,
257✔
805
    ) -> ProofOfWork {
257✔
806
        let mut cumulative_pow = cumulative_pow_start;
257✔
807
        let mut difficulty = difficulty_start;
257✔
808

257✔
809
        // f =  1 + (MINIMUM_BLOCK_TIME - TARGET_BLOCK_INTERVAL) / TARGET_BLOCK_INTERVAL * P
257✔
810
        // let f = (1u64 << 32) + (115 << 21);
257✔
811
        // let second_term = (MINIMUM_BLOCK_TIME.to_millis() - TARGET_BLOCK_INTERVAL.to_millis())
257✔
812
        //     as f64
257✔
813
        //     / TARGET_BLOCK_INTERVAL.to_millis() as f64;
257✔
814
        let f = (1.0_f64
257✔
815
            + (TARGET_BLOCK_INTERVAL.to_millis() - MINIMUM_BLOCK_TIME.to_millis()) as f64
257✔
816
                / TARGET_BLOCK_INTERVAL.to_millis() as f64
257✔
817
                / 16.0)
257✔
818
            * (1u64 << 32) as f64;
257✔
819
        let f = f as u64;
257✔
820
        // let second_term_shift_amount = 32 - second_term.ilog2() - 1;
257✔
821
        // let f = (1u64 << 32) + (second_term << second_term_shift_amount);
257✔
822
        let lo = f as u32;
257✔
823
        let hi = (f >> 32) as u32;
257✔
824
        for _ in 0..num_blocks {
257✔
825
            cumulative_pow = cumulative_pow + difficulty;
130,336✔
826
            let (product, overflow) = difficulty.safe_mul_fixed_point_rational(lo, hi);
130,336✔
827
            difficulty = if overflow == 0 {
130,336✔
828
                product
130,336✔
829
            } else {
830
                Difficulty::MAXIMUM
×
831
            };
832
        }
833
        cumulative_pow
257✔
834
    }
257✔
835

836
    #[test]
837
    fn max_pow_after_doesnt_crash() {
1✔
838
        let init_cumpow = ProofOfWork::from_u64(200);
1✔
839
        let init_difficulty = Difficulty::from_u64(1000);
1✔
840
        let _calculated = max_cumulative_pow_after(init_cumpow, init_difficulty, 1_000_000_000);
1✔
841
        let _calculated_again = max_cumulative_pow_after(init_cumpow, init_difficulty, usize::MAX);
1✔
842
    }
1✔
843

844
    #[test]
845
    fn max_pow_after_accepts_zero_num_blocks() {
1✔
846
        let init_cumpow = ProofOfWork::from_u64(200);
1✔
847
        let init_difficulty = Difficulty::from_u64(1000);
1✔
848
        let _calculated = max_cumulative_pow_after(init_cumpow, init_difficulty, 0);
1✔
849
    }
1✔
850

851
    #[proptest]
512✔
852
    fn ensure_no_false_negatives_when_num_blocks_is_zero(
853
        #[strategy(arb())] init_pow: ProofOfWork,
1✔
854
        #[strategy(arb())] init_difficulty: Difficulty,
1✔
855
    ) {
856
        let max = max_cumulative_pow_after(init_pow, init_difficulty, 0);
857
        prop_assert!(
858
            max >= init_pow,
859
            "Max-calculator must upward bound pow-value for zero-blocks input"
860
        );
861
    }
862

863
    #[proptest]
512✔
864
    fn test_sanity_max_pow_after_prop(
865
        #[strategy(arb())] init_difficulty: u64,
1✔
866
        #[strategy(0usize..1000)] num_blocks: usize,
1✔
867
        #[strategy(0u64..(u64::MAX << 1))] init_cumpow: u64,
1✔
868
    ) {
869
        // Captures a potential acceptable impresision when converting to f64 in
870
        // the `max_cumulative_pow_after` function.
871
        let init_cumpow_upper_bound = ProofOfWork::from_u64(init_cumpow + 1_000_000);
872

873
        let init_cumpow = ProofOfWork::from_u64(init_cumpow);
874
        let init_difficulty = Difficulty::from_u64(init_difficulty);
875
        let calculated = max_cumulative_pow_after(init_cumpow, init_difficulty, num_blocks);
876

877
        let approximation = max_cumulative_pow_after_iterative_test_impl(
878
            init_cumpow_upper_bound,
879
            init_difficulty,
880
            num_blocks,
881
        );
882
        println!("upper_bound: {approximation}");
883
        println!("calculated: {calculated}");
884
        println!("num_blocks: {num_blocks}");
885
        let approximation_as_f64 = BigUint::from(approximation).to_f64().unwrap();
886
        let upper_bound = approximation_as_f64 * 1.01;
887
        let lower_bound = approximation_as_f64 * 0.99;
888
        let calculated_as_f64 = BigUint::from(calculated).to_f64().unwrap();
889
        prop_assert!(upper_bound >= calculated_as_f64);
890
        prop_assert!(lower_bound <= calculated_as_f64);
891
        prop_assert!(calculated < ProofOfWork::MAXIMUM);
892
    }
893

894
    #[test]
895
    fn test_sanity_max_pow_after_unit() {
1✔
896
        let init_cumpow = 100u64;
1✔
897
        let init_cumpow = ProofOfWork::from_u64(init_cumpow);
1✔
898
        let init_difficulty = Difficulty::MINIMUM;
1✔
899
        let num_blocks = 1000;
1✔
900
        let calculated = max_cumulative_pow_after(init_cumpow, init_difficulty, num_blocks);
1✔
901
        let approximation =
1✔
902
            max_cumulative_pow_after_iterative_test_impl(init_cumpow, init_difficulty, num_blocks);
1✔
903
        let approximation_as_f64 = BigUint::from(approximation).to_f64().unwrap();
1✔
904
        let upper_bound = approximation_as_f64 * 1.01;
1✔
905
        let lower_bound = approximation_as_f64 * 0.99;
1✔
906
        let calculated_as_f64 = BigUint::from(calculated).to_f64().unwrap();
1✔
907
        assert!(upper_bound >= calculated_as_f64);
1✔
908
        assert!(lower_bound <= calculated_as_f64);
1✔
909
        assert!(calculated < ProofOfWork::MAXIMUM);
1✔
910
    }
1✔
911
}
912

913
#[cfg(test)]
914
pub mod propteststrategy {
915
    use super::ProofOfWork;
916
    use proptest::{prelude::any, prop_compose};
917

918
    prop_compose! {
919
        pub fn random() (limbs in [any::<u32>(); super::POW_NUM_LIMBS]) -> ProofOfWork {ProofOfWork(limbs)}
920
    }
921
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc