1
//! "Tracked" consensus method
2

            
3
use super::*;
4

            
5
// Let's not have this in the whole crate, so not in internal_prelude.rs.
6
use std::ops::Bound;
7

            
8
/// Consensus method value but which also tracks comparisons that are made
9
///
10
/// Passed to microdescriptor calculation algorithm.
11
/// This allows us to avoid recalculating identical microdescriptors
12
/// for successive consensus methods when we can see that the algorithm didn't care.
13
///
14
/// This type deliberately does not give access to the underlying consensus method.
15
/// Instead, make comparisons with integers (`u32`), or with [`ConsensusMethod`].
16
//
17
// At the time of writing (July 2026) there are four methods still supported by most dirauths:
18
// 32-35 inclusive. which imply only 2 rather than 4 different microdescriptor calculations.
19
#[derive(Debug)] // Should not be Clone: the original wouldn't track tests on the clone
20
pub struct TrackedConsensusMethod {
21
    /// The actual method
22
    ///
23
    /// We provide no access to this other than via `PartialOrd` and `PartialEq`.
24
    /// That prevents untracked inspection of the value,
25
    /// which would allow us to miss ways the algorithm *does* depend on the method.
26
    method: ConsensusMethod,
27

            
28
    /// Range of other methods that are (so far) equivalent to this one.
29
    equivalent: Cell<ConsensusMethodRange>,
30
}
31

            
32
/// Range of consensus methods
33
///
34
/// Normally, use this with [`.contains`](RangeBounds::contains).
35
//
36
// Astonishingly, std doesn't have a sensible type for this!
37
#[derive(Clone, Copy, Eq, PartialEq, Hash, amplify::Getters)]
38
pub struct ConsensusMethodRange {
39
    /// Methods `>=` this
40
    ///
41
    /// Does not need to be `Option` since `>= 0` means there is no lower bound.
42
    #[getter(as_copy)]
43
    closed_start: ConsensusMethod,
44

            
45
    /// Methods `<` this
46
    #[getter(as_copy)]
47
    open_end: Option<ConsensusMethod>,
48
}
49

            
50
impl TrackedConsensusMethod {
51
    /// Create a new `TrackedConsensusMethod`, for a new method-dependent calculation
52
272
    pub fn new(method: SupportedConsensusMethod) -> Self {
53
272
        TrackedConsensusMethod::new_maybe_unsupported(method.into())
54
272
    }
55

            
56
    /// Create a new `TrackedConsensusMethod` with a possibly-unsupported method
57
    ///
58
    /// Private, used only by `TrackedConsensusMethod::new` and by tests.
59
472
    fn new_maybe_unsupported(method: ConsensusMethod) -> Self {
60
472
        TrackedConsensusMethod {
61
472
            method,
62
472
            equivalent: ConsensusMethodRange::new_all().into(),
63
472
        }
64
472
    }
65

            
66
    /// Yield the method range which would give the same answers
67
250
    pub fn finish_get_equivalent(self) -> ConsensusMethodRange {
68
250
        self.equivalent.into_inner()
69
250
    }
70

            
71
    /// Update this range for a boundary test
72
    ///
73
    /// Notes that values `< boundary` and `>= boundary` were maybe treated differently.
74
    ///
75
    /// Called by the `PartialOrd` and `PartialEq` impls generated by `impl_comparisons`
76
3388
    fn record_boundary_below(&self, boundary: ConsensusMethod) {
77
3388
        let mut equiv = self.equivalent.get();
78
3388
        if boundary <= self.method {
79
2014
            // This test was for values below the actual method.
80
2014
            // It narrows the bottom end of the range.
81
2014
            //
82
2014
            // The range being inclusive at the start means that the implied start boundary
83
2014
            // is below the recorded value, so we don't need to adjust `boundary`.
84
2014
            //
85
2014
            // (chain! .max() rather than cmp::max for consistency with the other arm, below)
86
2014
            equiv.closed_start = chain!(Some(equiv.closed_start), Some(boundary))
87
2014
                .max()
88
2014
                .expect("boundaries on input so must be on output");
89
2014
        } else {
90
1374
            // This test was for values above the actual method.
91
1374
            // It narrows the top end of the range.
92
1374
            //
93
1374
            // The range being exclusive at the end means that the implied end boundary
94
1374
            // is below the recorded value, so we don't need to adjust `boundary`.
95
1374
            equiv.open_end = chain!(equiv.open_end, Some(boundary)).min();
96
1374
        }
97
3388
        self.equivalent.set(equiv);
98
3388
    }
99

            
100
    /// Update this range for a boundary test, with boundary *above* the specified value
101
    ///
102
    /// Notes that values `<= boundary` and `> boundary` were maybe treated differently.
103
1694
    fn record_boundary_above(&self, boundary: ConsensusMethod) {
104
1694
        self.record_boundary_below(ConsensusMethod(
105
            //
106
1694
            match boundary.0.checked_add(1) {
107
1694
                Some(y) => y,
108
                None => {
109
                    // The incoming boundary was `MAX`.  We don't need to care about
110
                    // a boundary at the top of the range, since there are no values above it.
111
                    // This is good, because our half-open range cannot represent it.
112
                    //
113
                    // (One might think a similar situation arises at the start, with 0.
114
                    // But our half-open-range *can* represent that, as `Some(0)`,
115
                    // which is semantically equivalent to `None`.
116
                    // So we don't need any special code for that.)
117
                    return;
118
                }
119
            },
120
        ));
121
1694
    }
122
}
123

            
124
impl ConsensusMethodRange {
125
    /// Return a new `ConsensusMethodRange` representing all consensus methods
126
472
    pub fn new_all() -> Self {
127
472
        ConsensusMethodRange {
128
472
            closed_start: ConsensusMethod(0),
129
472
            open_end: None,
130
472
        }
131
472
    }
132

            
133
    /// Returns the (inclusive) start bound, if it's nontrivial
134
    ///
135
    /// Used for providing a faithful implementation of `RangeBounds`,
136
    /// and nicer `Debug` output.
137
438
    fn start_bound_option(&self) -> Option<&ConsensusMethod> {
138
438
        if self.closed_start.0 == 0 {
139
60
            None
140
        } else {
141
378
            Some(&self.closed_start)
142
        }
143
438
    }
144
}
145

            
146
impl RangeBounds<ConsensusMethod> for ConsensusMethodRange {
147
398
    fn start_bound(&self) -> Bound<&ConsensusMethod> {
148
398
        match &self.start_bound_option() {
149
36
            None => Bound::Unbounded,
150
362
            Some(s) => Bound::Included(s),
151
        }
152
398
    }
153
398
    fn end_bound(&self) -> Bound<&ConsensusMethod> {
154
398
        match &self.open_end {
155
118
            None => Bound::Unbounded,
156
280
            Some(s) => Bound::Excluded(s),
157
        }
158
398
    }
159
}
160

            
161
/// Implement comparison traits
162
///
163
/// The input to the macro specifies the traits, methods, and the semantics for each method.
164
///
165
/// Each `$boundary` is `above` or `below` and means that this comparison method
166
/// can give different answers for values below the RHS, or values above it, respectively.
167
macro_rules! impl_comparisons { {
168
    $(
169
        $trait:ident { $(
170
            $fn_name:ident: $($boundary:ident),+ $(,)? -> $return_type:ty;
171
        )* }
172
    )*
173
} => { paste!{
174
    $(
175
        impl $trait<ConsensusMethod> for TrackedConsensusMethod { $(
176
1265
            fn $fn_name(&self, rhs: &ConsensusMethod) -> $return_type {
177
2530
                $( self.[<record_boundary_ $boundary>](*rhs); )+
178
2530
                self.method.$fn_name(rhs)
179
2530
            }
180
1265
        )* }
181
1265
        impl $trait<u32> for TrackedConsensusMethod { $(
182
1265
            fn $fn_name(&self, rhs: &u32) -> $return_type {
183
2530
                TrackedConsensusMethod::$fn_name(self, &ConsensusMethod(*rhs))
184
2530
            }
185
2530
        )* }
186
2530
        // Convenience impl to avoid having to write `*method > 10` etc.
187
2530
        impl $trait<u32> for &'_ TrackedConsensusMethod { $(
188
2530
            fn $fn_name(&self, rhs: &u32) -> $return_type {
189
2530
                TrackedConsensusMethod::$fn_name(self, &ConsensusMethod(*rhs))
190
2530
            }
191
        )* }
192
    )*
193
} } }
194

            
195
impl_comparisons! {
196
    PartialEq {
197
        eq: below, above -> bool;
198
        // We don't reimplement `ne`; the provided impl will call or `eq`
199
    }
200
    PartialOrd {
201
        lt: below -> bool;
202
        le: above -> bool;
203
        gt: above -> bool;
204
        ge: below -> bool;
205
        partial_cmp: below, above -> Option<Ordering>;
206
    }
207
}
208

            
209
// The derived impl is intolerably verbose.
210
impl Debug for ConsensusMethodRange {
211
40
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212
100
        let write_bound = |f: &mut fmt::Formatter, bound| {
213
80
            if let Some(bound) = bound {
214
32
                write!(f, "{bound}")
215
            } else {
216
48
                Ok(())
217
            }
218
80
        };
219
40
        write!(f, "ConsensusMethodRange(")?;
220
40
        write_bound(f, self.start_bound_option())?;
221
40
        write!(f, "..")?;
222
40
        write_bound(f, self.open_end.as_ref())?;
223
40
        write!(f, ")")?;
224
40
        Ok(())
225
40
    }
226
}
227

            
228
#[cfg(test)]
229
pub(crate) mod test {
230
    // @@ begin test lint list maintained by maint/add_warning @@
231
    #![allow(clippy::bool_assert_comparison)]
232
    #![allow(clippy::clone_on_copy)]
233
    #![allow(clippy::dbg_macro)]
234
    #![allow(clippy::mixed_attributes_style)]
235
    #![allow(clippy::print_stderr)]
236
    #![allow(clippy::print_stdout)]
237
    #![allow(clippy::single_char_pattern)]
238
    #![allow(clippy::unwrap_used)]
239
    #![allow(clippy::unchecked_time_subtraction)]
240
    #![allow(clippy::useless_vec)]
241
    #![allow(clippy::needless_pass_by_value)]
242
    #![allow(clippy::string_slice)] // See arti#2571
243
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
244
    use super::*;
245
    use std::collections::HashMap;
246

            
247
    pub(crate) type InterestingOutput = Vec<bool>;
248

            
249
    pub(crate) fn interesting_function(method: &TrackedConsensusMethod) -> InterestingOutput {
250
        vec![
251
            method < 10,
252
            method > 20,
253
            method <= 30,
254
            method >= 40,
255
            method == 50,
256
            method != 60,
257
        ]
258
    }
259

            
260
    #[test]
261
    fn distinctions() {
262
        let mut results = HashMap::<ConsensusMethodRange, InterestingOutput>::new();
263

            
264
        for probe in (1..=100).map(ConsensusMethod) {
265
            eprintln!("probe {probe}");
266
            let tracker = TrackedConsensusMethod::new_maybe_unsupported(probe);
267
            let output = interesting_function(&tracker);
268
            eprintln!("probe {probe}");
269
            let range = tracker.finish_get_equivalent();
270
            assert!(range.contains(&probe), "{probe} not in {range:?}");
271
            let before = results.entry(range).or_insert(output.clone());
272
            assert_eq!(before, &output, "{probe} discrepancy for {range:?}");
273
        }
274
        dbg!(&results);
275

            
276
        let duplicates = results.values().duplicates().collect_vec();
277
        // The ==50 and ==60 tests means 40..50, 51..60, 61.. are all the same
278
        let expected_duplicates = [&results[&ConsensusMethodRange {
279
            closed_start: 40.into(),
280
            open_end: Some(50.into()),
281
        }]];
282
        assert_eq!(duplicates, expected_duplicates);
283
    }
284
}