Lines
98.89 %
Functions
38.33 %
Branches
100 %
//! "Tracked" consensus method
use super::*;
// Let's not have this in the whole crate, so not in internal_prelude.rs.
use std::ops::Bound;
/// Consensus method value but which also tracks comparisons that are made
///
/// Passed to microdescriptor calculation algorithm.
/// This allows us to avoid recalculating identical microdescriptors
/// for successive consensus methods when we can see that the algorithm didn't care.
/// This type deliberately does not give access to the underlying consensus method.
/// Instead, make comparisons with integers (`u32`), or with [`ConsensusMethod`].
//
// At the time of writing (July 2026) there are four methods still supported by most dirauths:
// 32-35 inclusive. which imply only 2 rather than 4 different microdescriptor calculations.
#[derive(Debug)] // Should not be Clone: the original wouldn't track tests on the clone
pub struct TrackedConsensusMethod {
/// The actual method
/// We provide no access to this other than via `PartialOrd` and `PartialEq`.
/// That prevents untracked inspection of the value,
/// which would allow us to miss ways the algorithm *does* depend on the method.
method: ConsensusMethod,
/// Range of other methods that are (so far) equivalent to this one.
equivalent: Cell<ConsensusMethodRange>,
}
/// Range of consensus methods
/// Normally, use this with [`.contains`](RangeBounds::contains).
// Astonishingly, std doesn't have a sensible type for this!
#[derive(Clone, Copy, Eq, PartialEq, Hash, amplify::Getters)]
pub struct ConsensusMethodRange {
/// Methods `>=` this
/// Does not need to be `Option` since `>= 0` means there is no lower bound.
#[getter(as_copy)]
closed_start: ConsensusMethod,
/// Methods `<` this
open_end: Option<ConsensusMethod>,
impl TrackedConsensusMethod {
/// Create a new `TrackedConsensusMethod`, for a new method-dependent calculation
pub fn new(method: SupportedConsensusMethod) -> Self {
TrackedConsensusMethod::new_maybe_unsupported(method.into())
/// Create a new `TrackedConsensusMethod` with a possibly-unsupported method
/// Private, used only by `TrackedConsensusMethod::new` and by tests.
fn new_maybe_unsupported(method: ConsensusMethod) -> Self {
TrackedConsensusMethod {
method,
equivalent: ConsensusMethodRange::new_all().into(),
/// Yield the method range which would give the same answers
pub fn finish_get_equivalent(self) -> ConsensusMethodRange {
self.equivalent.into_inner()
/// Update this range for a boundary test
/// Notes that values `< boundary` and `>= boundary` were maybe treated differently.
/// Called by the `PartialOrd` and `PartialEq` impls generated by `impl_comparisons`
fn record_boundary_below(&self, boundary: ConsensusMethod) {
let mut equiv = self.equivalent.get();
if boundary <= self.method {
// This test was for values below the actual method.
// It narrows the bottom end of the range.
// The range being inclusive at the start means that the implied start boundary
// is below the recorded value, so we don't need to adjust `boundary`.
// (chain! .max() rather than cmp::max for consistency with the other arm, below)
equiv.closed_start = chain!(Some(equiv.closed_start), Some(boundary))
.max()
.expect("boundaries on input so must be on output");
} else {
// This test was for values above the actual method.
// It narrows the top end of the range.
// The range being exclusive at the end means that the implied end boundary
equiv.open_end = chain!(equiv.open_end, Some(boundary)).min();
self.equivalent.set(equiv);
/// Update this range for a boundary test, with boundary *above* the specified value
/// Notes that values `<= boundary` and `> boundary` were maybe treated differently.
fn record_boundary_above(&self, boundary: ConsensusMethod) {
self.record_boundary_below(ConsensusMethod(
match boundary.0.checked_add(1) {
Some(y) => y,
None => {
// The incoming boundary was `MAX`. We don't need to care about
// a boundary at the top of the range, since there are no values above it.
// This is good, because our half-open range cannot represent it.
// (One might think a similar situation arises at the start, with 0.
// But our half-open-range *can* represent that, as `Some(0)`,
// which is semantically equivalent to `None`.
// So we don't need any special code for that.)
return;
},
));
impl ConsensusMethodRange {
/// Return a new `ConsensusMethodRange` representing all consensus methods
pub fn new_all() -> Self {
ConsensusMethodRange {
closed_start: ConsensusMethod(0),
open_end: None,
/// Returns the (inclusive) start bound, if it's nontrivial
/// Used for providing a faithful implementation of `RangeBounds`,
/// and nicer `Debug` output.
fn start_bound_option(&self) -> Option<&ConsensusMethod> {
if self.closed_start.0 == 0 {
None
Some(&self.closed_start)
impl RangeBounds<ConsensusMethod> for ConsensusMethodRange {
fn start_bound(&self) -> Bound<&ConsensusMethod> {
match &self.start_bound_option() {
None => Bound::Unbounded,
Some(s) => Bound::Included(s),
fn end_bound(&self) -> Bound<&ConsensusMethod> {
match &self.open_end {
Some(s) => Bound::Excluded(s),
/// Implement comparison traits
/// The input to the macro specifies the traits, methods, and the semantics for each method.
/// Each `$boundary` is `above` or `below` and means that this comparison method
/// can give different answers for values below the RHS, or values above it, respectively.
macro_rules! impl_comparisons { {
$(
$trait:ident { $(
$fn_name:ident: $($boundary:ident),+ $(,)? -> $return_type:ty;
)* }
)*
} => { paste!{
impl $trait<ConsensusMethod> for TrackedConsensusMethod { $(
fn $fn_name(&self, rhs: &ConsensusMethod) -> $return_type {
$( self.[<record_boundary_ $boundary>](*rhs); )+
self.method.$fn_name(rhs)
impl $trait<u32> for TrackedConsensusMethod { $(
fn $fn_name(&self, rhs: &u32) -> $return_type {
TrackedConsensusMethod::$fn_name(self, &ConsensusMethod(*rhs))
// Convenience impl to avoid having to write `*method > 10` etc.
impl $trait<u32> for &'_ TrackedConsensusMethod { $(
} } }
impl_comparisons! {
PartialEq {
eq: below, above -> bool;
// We don't reimplement `ne`; the provided impl will call or `eq`
PartialOrd {
lt: below -> bool;
le: above -> bool;
gt: above -> bool;
ge: below -> bool;
partial_cmp: below, above -> Option<Ordering>;
// The derived impl is intolerably verbose.
impl Debug for ConsensusMethodRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let write_bound = |f: &mut fmt::Formatter, bound| {
if let Some(bound) = bound {
write!(f, "{bound}")
Ok(())
};
write!(f, "ConsensusMethodRange(")?;
write_bound(f, self.start_bound_option())?;
write!(f, "..")?;
write_bound(f, self.open_end.as_ref())?;
write!(f, ")")?;
#[cfg(test)]
pub(crate) mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::string_slice)] // See arti#2571
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
use std::collections::HashMap;
pub(crate) type InterestingOutput = Vec<bool>;
pub(crate) fn interesting_function(method: &TrackedConsensusMethod) -> InterestingOutput {
vec![
method < 10,
method > 20,
method <= 30,
method >= 40,
method == 50,
method != 60,
]
#[test]
fn distinctions() {
let mut results = HashMap::<ConsensusMethodRange, InterestingOutput>::new();
for probe in (1..=100).map(ConsensusMethod) {
eprintln!("probe {probe}");
let tracker = TrackedConsensusMethod::new_maybe_unsupported(probe);
let output = interesting_function(&tracker);
let range = tracker.finish_get_equivalent();
assert!(range.contains(&probe), "{probe} not in {range:?}");
let before = results.entry(range).or_insert(output.clone());
assert_eq!(before, &output, "{probe} discrepancy for {range:?}");
dbg!(&results);
let duplicates = results.values().duplicates().collect_vec();
// The ==50 and ==60 tests means 40..50, 51..60, 61.. are all the same
let expected_duplicates = [&results[&ConsensusMethodRange {
closed_start: 40.into(),
open_end: Some(50.into()),
}]];
assert_eq!(duplicates, expected_duplicates);