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

tari-project / tari / 16990089413

15 Aug 2025 12:36PM UTC coverage: 54.497% (+0.06%) from 54.441%
16990089413

push

github

web-flow
chore: cleanup indexes (#7411)

Description
---
Forces clean indexs

970 of 2919 new or added lines in 369 files covered. (33.23%)

60 existing lines in 33 files now uncovered.

76698 of 140739 relevant lines covered (54.5%)

193749.86 hits per line

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

80.72
/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> {
204✔
43
        if target_time == 0 {
204✔
44
            return Err(
×
45
                "LinearWeightedMovingAverage::new(...) expected `target_time` to be greater than 0, but 0 was given"
×
46
                    .into(),
×
47
            );
×
48
        }
204✔
49
        if block_window == 0 {
204✔
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
        }
203✔
55
        if target_time.checked_mul(LWMA_MAX_BLOCK_TIME_RATIO).is_none() {
203✔
56
            return Err(format!(
×
NEW
57
                "LinearWeightedMovingAverage::new(...) expected `target_time` to be at least \
×
NEW
58
                 {LWMA_MAX_BLOCK_TIME_RATIO} times smaller than `u64::MAX`",
×
UNCOV
59
            ));
×
60
        }
203✔
61
        Ok(Self {
203✔
62
            target_difficulties: VecDeque::with_capacity(block_window + 1),
203✔
63
            block_window,
203✔
64
            target_time: u128::from(target_time),
203✔
65
            max_block_time: target_time * LWMA_MAX_BLOCK_TIME_RATIO,
203✔
66
        })
203✔
67
    }
204✔
68

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

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

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

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

266✔
93
        let ave_difficulty = difficulty_sum / n;
266✔
94

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

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

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

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

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

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

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

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

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

205
    fn get_difficulty(&self) -> Option<Difficulty> {
327✔
206
        self.calculate()
327✔
207
    }
327✔
208
}
209

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

214
    use crate::proof_of_work::{lwma_diff::LinearWeightedMovingAverage, Difficulty, DifficultyAdjustment};
215

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

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

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

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

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

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