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

tari-project / tari / 15423730966

03 Jun 2025 05:20PM UTC coverage: 72.329% (-0.4%) from 72.763%
15423730966

push

github

SWvheerden
update readme

81584 of 112795 relevant lines covered (72.33%)

252196.45 hits per line

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

80.0
/base_layer/core/src/proof_of_work/lwma_diff.rs
1
// Copyright 2022 The Tari Project
2
// SPDX-License-Identifier: BSD-3-Clause
3

4
// Portions of the code:
5

6
// LWMA-1 for BTC & Zcash clones
7
// Copyright (c) 2017-2019 The Bitcoin Gold developers, Zawy, iamstenman (Microbitcoin)
8
// MIT License
9
// Algorithm by Zawy, a modification of WT-144 by Tom Harding
10
// References:
11
// https://github.com/zawy12/difficulty-algorithms/issues/3#issuecomment-442129791
12
// https://github.com/zcash/zcash/issues/4021
13

14
use std::{cmp::min, collections::VecDeque, convert::TryFrom};
15

16
use log::*;
17
use tari_utilities::epoch_time::EpochTime;
18

19
use crate::proof_of_work::{
20
    difficulty::{Difficulty, DifficultyAdjustment},
21
    error::DifficultyAdjustmentError,
22
    DifficultyError,
23
};
24

25
/// This is the recommended maximum block time ratio for LWMA-1
26
pub const LWMA_MAX_BLOCK_TIME_RATIO: u64 = 6;
27

28
/// Log target for `c::pow::lwma_diff`
29
pub const LOG_TARGET: &str = "c::pow::lwma_diff";
30

31
/// Struct for the Linear Weighted Moving Average (LWMA) difficulty adjustment algorithm
32
#[derive(Debug, Clone)]
33
pub struct LinearWeightedMovingAverage {
34
    target_difficulties: VecDeque<(EpochTime, Difficulty)>,
35
    block_window: usize,
36
    target_time: u128,
37
    max_block_time: u64,
38
}
39

40
impl LinearWeightedMovingAverage {
41
    /// Initialize a new `LinearWeightedMovingAverage`
42
    pub fn new(block_window: usize, target_time: u64) -> Result<Self, String> {
287✔
43
        if target_time == 0 {
287✔
44
            return Err(
×
45
                "LinearWeightedMovingAverage::new(...) expected `target_time` to be greater than 0, but 0 was given"
×
46
                    .into(),
×
47
            );
×
48
        }
287✔
49
        if block_window == 0 {
287✔
50
            return Err(
1✔
51
                "LinearWeightedMovingAverage::new(...) expected `block_window` to be greater than 0, but 0 was given"
1✔
52
                    .into(),
1✔
53
            );
1✔
54
        }
286✔
55
        if target_time.checked_mul(LWMA_MAX_BLOCK_TIME_RATIO).is_none() {
286✔
56
            return Err(format!(
×
57
                "LinearWeightedMovingAverage::new(...) expected `target_time` to be at least {} times smaller than \
×
58
                 `u64::MAX`",
×
59
                LWMA_MAX_BLOCK_TIME_RATIO,
×
60
            ));
×
61
        }
286✔
62
        Ok(Self {
286✔
63
            target_difficulties: VecDeque::with_capacity(block_window + 1),
286✔
64
            block_window,
286✔
65
            target_time: u128::from(target_time),
286✔
66
            max_block_time: target_time * LWMA_MAX_BLOCK_TIME_RATIO,
286✔
67
        })
286✔
68
    }
287✔
69

70
    /// Helper function to calculate the maximum block time for a given target time
71
    pub fn max_block_time(target_time: u64) -> Result<u64, DifficultyError> {
×
72
        target_time
×
73
            .checked_mul(LWMA_MAX_BLOCK_TIME_RATIO)
×
74
            .ok_or(DifficultyError::MaxBlockTimeOverflow)
×
75
    }
×
76

77
    fn calculate(&self) -> Option<Difficulty> {
424✔
78
        // This function uses u128 internally for most of the math as its possible to have an overflow with large
424✔
79
        // difficulties and large block windows
424✔
80
        if self.target_difficulties.len() <= 1 {
424✔
81
            return None;
87✔
82
        }
337✔
83

337✔
84
        // Use the array length rather than block_window to include early cases where the no. of pts < block_window
337✔
85
        let n = (self.target_difficulties.len() - 1) as u128;
337✔
86

337✔
87
        let mut weighted_times: u128 = 0;
337✔
88
        let difficulty_sum = self
337✔
89
            .target_difficulties
337✔
90
            .iter()
337✔
91
            .skip(1)
337✔
92
            .fold(0u128, |difficulty, (_, d)| difficulty + u128::from(d.as_u64()));
17,615✔
93

337✔
94
        let ave_difficulty = difficulty_sum / n;
337✔
95

337✔
96
        let (mut previous_timestamp, _) = self.target_difficulties[0];
337✔
97
        let mut this_timestamp;
98
        // Loop through N most recent blocks.
99
        for (i, (timestamp, _)) in self.target_difficulties.iter().skip(1).enumerate() {
17,615✔
100
            // We cannot have if solve_time < 1 then solve_time = 1, this will greatly increase the next timestamp
101
            // difficulty which will lower the difficulty
102
            if *timestamp > previous_timestamp {
17,615✔
103
                this_timestamp = *timestamp;
8,005✔
104
            } else {
8,005✔
105
                this_timestamp = previous_timestamp.checked_add(EpochTime::from(1))?;
9,610✔
106
            }
107
            let solve_time = min(
17,615✔
108
                this_timestamp
17,615✔
109
                    .checked_sub(previous_timestamp)
17,615✔
110
                    .unwrap_or(EpochTime::from(1)) // this should never occur
17,615✔
111
                    .as_u64(),
17,615✔
112
                self.max_block_time,
17,615✔
113
            );
17,615✔
114
            previous_timestamp = this_timestamp;
17,615✔
115

17,615✔
116
            // Give linearly higher weight to more recent solve times.
17,615✔
117
            // Note: This will not overflow for practical values of block_window and solve time.
17,615✔
118
            weighted_times += u128::from(solve_time * (i + 1) as u64);
17,615✔
119
        }
120
        // k is the sum of weights (1+2+..+n) * target_time
121
        let k = n * (n + 1) * self.target_time / 2;
337✔
122
        let target = u64::try_from(ave_difficulty * k / weighted_times).unwrap_or(u64::MAX);
337✔
123
        trace!(
337✔
124
            target: LOG_TARGET,
×
125
            "DiffCalc; t={}; bw={}; n={}; ts[0]={}; ts[n]={}; weighted_ts={}; k={}; diff[0]={}; diff[n]={}; \
×
126
             ave_difficulty={}; target={}",
×
127
            self.target_time,
×
128
            self.block_window,
×
129
            n,
×
130
            self.target_difficulties[0].0,
×
131
            self.target_difficulties[n as usize].0,
×
132
            weighted_times,
×
133
            k,
×
134
            self.target_difficulties[0].1,
×
135
            self.target_difficulties[n as usize].1,
×
136
            ave_difficulty,
137
            target
138
        );
139
        trace!(target: LOG_TARGET, "New target difficulty: {}", target);
337✔
140
        if target < Difficulty::min().as_u64() {
337✔
141
            None
50✔
142
        } else {
143
            Some(Difficulty::from_u64(target).expect("Difficulty is valid"))
287✔
144
        }
145
    }
424✔
146

147
    /// Indicates if the `LinearWeightedMovingAverage` is full
148
    pub fn is_full(&self) -> bool {
7,929✔
149
        self.num_samples() == self.block_window() + 1
7,929✔
150
    }
7,929✔
151

152
    /// Returns the number of samples in the `LinearWeightedMovingAverage`
153
    #[inline]
154
    pub fn num_samples(&self) -> usize {
7,934✔
155
        self.target_difficulties.len()
7,934✔
156
    }
7,934✔
157

158
    /// Returns the block window size
159
    #[inline]
160
    pub(super) fn block_window(&self) -> usize {
7,929✔
161
        self.block_window
7,929✔
162
    }
7,929✔
163

164
    /// Adds a new timestamp and target difficulty in front of the queue
165
    pub fn add_front(&mut self, timestamp: EpochTime, target_difficulty: Difficulty) {
940✔
166
        if self.is_full() {
940✔
167
            self.target_difficulties.pop_back();
1✔
168
        }
939✔
169
        self.target_difficulties.push_front((timestamp, target_difficulty));
940✔
170
    }
940✔
171

172
    /// Adds a new timestamp and target difficulty at the back of the queue
173
    pub fn add_back(&mut self, timestamp: EpochTime, target_difficulty: Difficulty) {
6,256✔
174
        if self.is_full() {
6,256✔
175
            self.target_difficulties.pop_front();
130✔
176
        }
6,126✔
177
        self.target_difficulties.push_back((timestamp, target_difficulty));
6,256✔
178
    }
6,256✔
179

180
    pub fn update_target_time(&mut self, target_time: u64) -> Result<(), String> {
×
181
        if target_time == 0 {
×
182
            return Err(
×
183
                "LinearWeightedMovingAverage::update_target_time(...) expected `target_time` to be greater than 0, \
×
184
                 but 0 was given"
×
185
                    .into(),
×
186
            );
×
187
        }
×
188
        if target_time.checked_mul(LWMA_MAX_BLOCK_TIME_RATIO).is_none() {
×
189
            return Err(format!(
×
190
                "LinearWeightedMovingAverage::update_target_time(...) expected `target_time` to be at least {} times \
×
191
                 smaller than `u64::MAX`",
×
192
                LWMA_MAX_BLOCK_TIME_RATIO,
×
193
            ));
×
194
        }
×
195
        self.target_time = u128::from(target_time);
×
196
        self.max_block_time = target_time * LWMA_MAX_BLOCK_TIME_RATIO;
×
197
        Ok(())
×
198
    }
×
199
}
200

201
impl DifficultyAdjustment for LinearWeightedMovingAverage {
202
    fn add(&mut self, timestamp: EpochTime, target_difficulty: Difficulty) -> Result<(), DifficultyAdjustmentError> {
6,230✔
203
        self.add_back(timestamp, target_difficulty);
6,230✔
204
        Ok(())
6,230✔
205
    }
6,230✔
206

207
    fn get_difficulty(&self) -> Option<Difficulty> {
424✔
208
        self.calculate()
424✔
209
    }
424✔
210
}
211

212
#[cfg(test)]
213
mod test {
214
    use tari_utilities::epoch_time::EpochTime;
215

216
    use crate::proof_of_work::{lwma_diff::LinearWeightedMovingAverage, Difficulty, DifficultyAdjustment};
217

218
    #[test]
219
    fn lwma_zero_len() {
1✔
220
        let dif = LinearWeightedMovingAverage::new(90, 120).unwrap();
1✔
221
        assert_eq!(dif.get_difficulty(), None);
1✔
222
    }
1✔
223

224
    #[test]
225
    fn lwma_is_full() {
1✔
226
        // This is important to check because using a VecDeque can cause bugs unless the following is accounted for
1✔
227
        // let v = VecDeq::with_capacity(10);
1✔
228
        // assert_eq!(v.capacity(), 11);
1✔
229
        // A Vec was chosen because it ended up being simpler to use
1✔
230
        let dif = LinearWeightedMovingAverage::new(0, 120);
1✔
231
        assert!(dif.is_err());
1✔
232
        let mut dif = LinearWeightedMovingAverage::new(1, 120).unwrap();
1✔
233
        dif.add_front(60.into(), Difficulty::from_u64(100).unwrap());
1✔
234
        assert!(!dif.is_full());
1✔
235
        assert_eq!(dif.num_samples(), 1);
1✔
236
        dif.add_front(60.into(), Difficulty::from_u64(100).unwrap());
1✔
237
        assert_eq!(dif.num_samples(), 2);
1✔
238
        assert!(dif.is_full());
1✔
239
        dif.add_front(60.into(), Difficulty::from_u64(100).unwrap());
1✔
240
        assert_eq!(dif.num_samples(), 2);
1✔
241
        assert!(dif.is_full());
1✔
242
    }
1✔
243

244
    #[test]
245
    fn lwma_negative_solve_times() {
1✔
246
        let mut dif = LinearWeightedMovingAverage::new(90, 120).unwrap();
1✔
247
        let mut timestamp = 60.into();
1✔
248
        let cum_diff = Difficulty::from_u64(100).unwrap();
1✔
249
        let _ = dif.add(timestamp, cum_diff);
1✔
250
        timestamp = timestamp.checked_add(EpochTime::from(60)).unwrap();
1✔
251
        let _ = dif.add(timestamp, cum_diff);
1✔
252
        // Lets create a history and populate the vecs
253
        for _i in 0..150 {
151✔
254
            timestamp = timestamp.checked_add(EpochTime::from(60)).unwrap();
150✔
255
            let _ = dif.add(timestamp, cum_diff);
150✔
256
        }
150✔
257
        // lets create chaos by having 60 blocks as negative solve times. This should never be allowed in practice by
258
        // having checks on the block times.
259
        for _i in 0..60 {
61✔
260
            timestamp = (timestamp.as_u64() - 1).into(); // Only choosing -1 here since we are testing negative solve times and we cannot have 0 time
60✔
261
            let diff_before = dif.get_difficulty().unwrap();
60✔
262
            let _ = dif.add(timestamp, cum_diff);
60✔
263
            let diff_after = dif.get_difficulty().unwrap();
60✔
264
            // Algo should handle this as 1sec solve time thus increase the difficulty constantly
60✔
265
            assert!(diff_after > diff_before);
60✔
266
        }
267
    }
1✔
268

269
    #[test]
270
    fn lwma_limit_difficulty_change() {
1✔
271
        let mut dif = LinearWeightedMovingAverage::new(5, 60).unwrap();
1✔
272
        let _ = dif.add(60.into(), Difficulty::from_u64(100).unwrap());
1✔
273
        let _ = dif.add(10_000_000.into(), Difficulty::from_u64(100).unwrap());
1✔
274
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(16).unwrap());
1✔
275
        let _ = dif.add(20_000_000.into(), Difficulty::from_u64(16).unwrap());
1✔
276
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(9).unwrap());
1✔
277
    }
1✔
278

279
    // Data for 5-period moving average
280
    // Timestamp: 60, 120, 180, 240, 300, 350, 380, 445, 515, 615, 975, 976, 977, 978, 979
281
    // Intervals: 60,  60,  60,  60,  60,  50,  30,  65,  70, 100, 360,   1,   1,   1,   1
282
    // Diff:     100, 100, 100, 100, 100, 105, 128, 123, 116,  94,  39,  46,  55,  75, 148
283
    // Acum dif: 100, 200, 300, 400, 500, 605, 733, 856, 972,1066,1105,1151,1206,1281,1429
284
    // Target:     1, 100, 100, 100, 100, 106 134,  128, 119,  93,  35,  38,  46,  65, 173
285
    // These values where calculated in excel to confirm they are correct
286
    #[test]
287
    fn lwma_calculate() {
1✔
288
        let mut dif = LinearWeightedMovingAverage::new(5, 60).unwrap();
1✔
289
        let _ = dif.add(60.into(), Difficulty::from_u64(100).unwrap());
1✔
290
        assert_eq!(dif.get_difficulty(), None);
1✔
291
        let _ = dif.add(120.into(), Difficulty::from_u64(100).unwrap());
1✔
292
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(100).unwrap());
1✔
293
        let _ = dif.add(180.into(), Difficulty::from_u64(100).unwrap());
1✔
294
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(100).unwrap());
1✔
295
        let _ = dif.add(240.into(), Difficulty::from_u64(100).unwrap());
1✔
296
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(100).unwrap());
1✔
297
        let _ = dif.add(300.into(), Difficulty::from_u64(100).unwrap());
1✔
298
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(100).unwrap());
1✔
299
        let _ = dif.add(350.into(), Difficulty::from_u64(105).unwrap());
1✔
300
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(106).unwrap());
1✔
301
        let _ = dif.add(380.into(), Difficulty::from_u64(128).unwrap());
1✔
302
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(134).unwrap());
1✔
303
        let _ = dif.add(445.into(), Difficulty::from_u64(123).unwrap());
1✔
304
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(128).unwrap());
1✔
305
        let _ = dif.add(515.into(), Difficulty::from_u64(116).unwrap());
1✔
306
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(119).unwrap());
1✔
307
        let _ = dif.add(615.into(), Difficulty::from_u64(94).unwrap());
1✔
308
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(93).unwrap());
1✔
309
        let _ = dif.add(975.into(), Difficulty::from_u64(39).unwrap());
1✔
310
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(35).unwrap());
1✔
311
        let _ = dif.add(976.into(), Difficulty::from_u64(46).unwrap());
1✔
312
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(38).unwrap());
1✔
313
        let _ = dif.add(977.into(), Difficulty::from_u64(55).unwrap());
1✔
314
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(46).unwrap());
1✔
315
        let _ = dif.add(978.into(), Difficulty::from_u64(75).unwrap());
1✔
316
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(65).unwrap());
1✔
317
        let _ = dif.add(979.into(), Difficulty::from_u64(148).unwrap());
1✔
318
        assert_eq!(dif.get_difficulty().unwrap(), Difficulty::from_u64(173).unwrap());
1✔
319
    }
1✔
320

321
    #[test]
322
    fn ensure_calculate_does_not_overflow_with_large_block_window() {
1✔
323
        let mut dif = LinearWeightedMovingAverage::new(6000, 60).unwrap();
1✔
324
        for _i in 0..6000 {
6,001✔
325
            let _ = dif.add(60.into(), Difficulty::max());
6,000✔
326
        }
6,000✔
327
        // We don't care about the value, we just want to test that get_difficulty does not panic with an overflow.
328
        dif.get_difficulty().unwrap();
1✔
329
    }
1✔
330
}
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