1
//! Traits and common types for for calculating consensuses from votes
2

            
3
use super::*;
4

            
5
/// Voter number
6
///
7
/// Valid only within a particular consensus calculation round.
8
/// Corresponds to the index in `ConsensusesContext.votes`.
9
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] //
10
#[derive(derive_more::From, derive_more::Into)]
11
pub(super) struct VoterNum(pub usize);
12

            
13
/// A set of voters
14
pub(super) type VoterSet = HashSet<VoterNum>;
15

            
16
/// Components within a vote (trait alias)
17
///
18
/// Input to [`ConsensusesFromVotes::consensuses`] and [`Aggregate::aggregate`].
19
//
20
// Normally it is a better API to take `IntoIterator` than `Iterator`, since that is
21
// more convenient to use.
22
//
23
// Here, that might be `IntoIterator + Copy`: our iterators typically yield references to
24
// component pieces, generated by field projection on votes.
25
//
26
// But: that merely replaces `.clone()` at call sites of this trait with `.into_iter()`,
27
// and when we make a `ComponentInVotes` it's typically already an `Iterator` anyway,
28
// so there's no convenience benefit.
29
//
30
// Worse, we don't want to insist on `Copy` iterators
31
// since there might be cases where the iterator would be only `Clone`;
32
// and, `IntoIterator + Clone` would mean writing `.clone().into_iter()` or some such.
33
pub(super) trait ComponentInVotes<T>: Iterator<Item = (VoterNum, T)> + Clone {}
34
impl<I, T> ComponentInVotes<T> for I where I: Iterator<Item = (VoterNum, T)> + Clone {}
35

            
36
/// Actual context for computing a field: global context, plus already-computed values
37
///
38
/// Shared references, so `Copy`.
39
#[derive(Debug, Educe, derive_more::Deref)]
40
#[educe(Clone, Copy)]
41
pub(super) struct ConsensusContextRefs<'r, AlreadyComputed> {
42
    /// The `ConsensusContext`, the same for all parts of the computation
43
    ///
44
    /// This is usually the part of the context that's wanted, so
45
    /// for convenience, `ContextAndComputed` derefs to this.
46
    #[deref]
47
    pub context: &'r ConsensusCommonContext<'r>,
48

            
49
    /// Stuff we computed earlier
50
    ///
51
    /// This is needed because some parts of the consensus depend on other parts.
52
    /// For example, the `w` item in a routerstatus depends on network parameters
53
    /// (as found in the preamble of *this* consensus).
54
    ///
55
    /// impls that don't need any precomputed data should be blanket over `AlreadyComputed`.
56
    //
57
    // The alternatives to this would be:
58
    //  a. Build the consensus in mutable variables.  But there are a lot of required
59
    //     fields, so that is quite awkward.
60
    //  b. Have some separate mutable variables in the context, and pass them mutably
61
    //     through all the calculations.
62
    //
63
    // Both a and b would make it easy to write the kind of "didn't assign to
64
    // this variable yet" ordering bugs which are common in languages and
65
    // progrms with mutable globals.
66
    //
67
    //  c. Do some kind of ad-hoc calculation beforehand, and put the answers in
68
    //     `context`.  The ad-hoc calculation couldn't use any of our infrastructure
69
    //     since that requires a `ConsensusContext` which we wouldn't have yet.
70
    //
71
    //  d. Put dummy data in the awkward fields, and fix it up later
72
    //     after the systematic calculation has produced a whole consensus.
73
    //     This makes it easy to accidentally use or emit uninitialised data,
74
    //     and is really a form of (a)/(b).
75
    //
76
    pub computed: &'r AlreadyComputed,
77
}
78

            
79
/// Component of a vote, from which a corresponding consensus component can be calculated
80
///
81
/// Implemented on the *input*, ie the vote or part of a vote.
82
///
83
/// Implement this trait directly when the different flavours need different outputs.
84
///
85
/// Implement [`Aggregate`] instead, if the flavour doesn't matter.
86
/// There is a blanket implementation of `ConsensusesFromVotes` for any [`Aggregate`].
87
pub(super) trait ConsensusesFromVotes<AlreadyComputed> {
88
    /// The plain-flavourconsensus component
89
    type PlainOutput: Sized;
90
    /// The microdescriptor consensus component
91
    type MdOutput: Sized;
92

            
93
    /// Calculate the consensus components corresponding to the `Self` in the votes
94
    ///
95
    /// Takes as input the vote components (one per vote), and
96
    /// returns the consensus components, as a pair, one for each flavour.
97
    ///
98
    /// `inputs` is an iterator of references to the relevant parts of each vote.
99
    fn consensuses<'i>(
100
        context: ConsensusContextRefs<AlreadyComputed>,
101
        inputs: impl ComponentInVotes<&'i Self>,
102
    ) -> Result<(Self::PlainOutput, Self::MdOutput), ConsensusError>
103
    where
104
        Self: 'i;
105
}
106

            
107
/// Component of a vote from which a flavour-independent consensus component can be calculated
108
///
109
/// Implemented on the *input*, ie the vote or part of a vote.
110
///
111
/// Use `[ConsensusesFromVotes`] when flavour is relevant.
112
pub(super) trait Aggregate<AC>: Sized {
113
    /// The output (consensus) component type.  Often `Self`.
114
    type Output: Sized;
115

            
116
    /// Calculate the consensus component corresponding to the `Self` in the votes
117
    ///
118
    /// Takes as input the vote components (one per vote), and
119
    /// returns the corresponding consensus components.
120
    ///
121
    /// `inputs` is an iterator of references to the relevant parts of each vote.
122
    fn aggregate<'i>(
123
        context: ConsensusContextRefs<AC>,
124
        inputs: impl ComponentInVotes<&'i Self>,
125
    ) -> Result<Self::Output, ConsensusError>
126
    where
127
        Self: 'i;
128
}
129

            
130
impl<AC, V: Aggregate<AC>> ConsensusesFromVotes<AC> for V {
131
    type PlainOutput = V::Output;
132
    type MdOutput = V::Output;
133

            
134
4
    fn consensuses<'i>(
135
4
        context: ConsensusContextRefs<AC>,
136
4
        inputs: impl ComponentInVotes<&'i Self>,
137
4
    ) -> Result<(Self::PlainOutput, Self::MdOutput), ConsensusError>
138
4
    where
139
4
        Self: 'i,
140
    {
141
        Ok((
142
4
            V::aggregate(context, inputs.clone())?,
143
4
            V::aggregate(context, inputs)?,
144
        ))
145
4
    }
146
}
147

            
148
/// Error during calculation of a consensus
149
///
150
/// Normally errors at this stage should be avoided, because that would prevent
151
/// us from participating in the consensus.
152
#[derive(thiserror::Error, Clone, Debug)]
153
#[non_exhaustive]
154
pub enum ConsensusError {
155
    /// Tried to calculate a consensus from no votes!
156
    #[error("tried to calculate a consensus from no votes!")]
157
    NoVotes,
158

            
159
    /// Internal error in calculation algorithm
160
    #[error("bug calculating a consensus")]
161
    Internal(#[from] Bug),
162
}
163

            
164
#[ext(name = TiSliceExt)]
165
pub(super) impl<V> TiSlice<VoterNum, V> {
166
    /// Get the value from one of the votes
167
    ///
168
    /// Convenience method for `.get(vnum)` which avoids recapitulating the error handling.
169
    //
170
    // TODO DIRAUTH after !4463 use this in toplevel.rs in
171
    //  impl<AC> Aggregate<AC> for netstatus::VoteAuthoritySection
172
25
    fn vote(&self, vnum: VoterNum) -> Result<&V, Bug> {
173
25
        self.get(vnum)
174
25
            .ok_or_else(|| internal!("{vnum:?} out of range"))
175
25
    }
176
}
177

            
178
/// "Global" inputs for calculating consensus from votes
179
#[derive(Debug, Clone)]
180
pub(super) struct ConsensusCommonContext<'r> {
181
    /// The consensus method for which to generate a consensus
182
    pub method: SupportedConsensusMethod,
183

            
184
    /// The number of authorities (>= the number of votes)
185
    pub n_authorities: usize,
186

            
187
    /// The input votes (in their entirity)
188
    pub votes: TiVec<VoterNum, &'r netstatus::vote::NetworkStatus>,
189

            
190
    /// The votes' digests (in the same `VoterNum` order)
191
    pub sig_hashes: TiVec<VoterNum, &'r netstatus::DirectorySignaturesHashesAccu>,
192
}
193

            
194
impl<'r> ConsensusCommonContext<'r> {
195
    /// Is `n_some_voters` strictly more than half of all the authorities?
196
117
    pub(super) fn is_more_than_half_all_auths(&self, n_some_voters: usize) -> bool {
197
        // This way of writing it avoids any possibility of over/under-flow
198
117
        n_some_voters > self.n_authorities / 2
199
117
    }
200
}
201

            
202
#[cfg(test)]
203
pub(crate) mod test {
204
    // @@ begin test lint list maintained by maint/add_warning @@
205
    #![allow(clippy::bool_assert_comparison)]
206
    #![allow(clippy::clone_on_copy)]
207
    #![allow(clippy::dbg_macro)]
208
    #![allow(clippy::mixed_attributes_style)]
209
    #![allow(clippy::print_stderr)]
210
    #![allow(clippy::print_stdout)]
211
    #![allow(clippy::single_char_pattern)]
212
    #![allow(clippy::unwrap_used)]
213
    #![allow(clippy::unchecked_time_subtraction)]
214
    #![allow(clippy::useless_vec)]
215
    #![allow(clippy::needless_pass_by_value)]
216
    #![allow(clippy::string_slice)] // See arti#2571
217
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
218
    use super::*;
219

            
220
    impl ConsensusCommonContext<'static> {
221
        pub(crate) fn new_for_test() -> Self {
222
            // TODO obtain (memoised?) from testdata2, using its constructor, when we have one
223
            ConsensusCommonContext {
224
                method: SupportedConsensusMethod::MAX,
225
                n_authorities: 0,
226
                votes: ti_vec![],
227
                sig_hashes: ti_vec![],
228
            }
229
        }
230
    }
231

            
232
    #[test]
233
    fn is_more_than_half_all_auths() {
234
        // This test case is rather like is_for_more_than_half_of_t in functions.rs
235

            
236
        let mut context = ConsensusCommonContext::new_for_test();
237

            
238
        let mut check = |n_authorities, minimum_that_is_more_than_half| {
239
            context.n_authorities = n_authorities;
240
            for t in 0..=(n_authorities + 1) {
241
                assert_eq!(
242
                    context.is_more_than_half_all_auths(t),
243
                    t >= minimum_that_is_more_than_half,
244
                );
245
            }
246
        };
247

            
248
        check(0, 1);
249
        check(1, 1);
250
        check(2, 2);
251
        check(3, 2);
252
        check(4, 3);
253
        check(5, 3);
254
    }
255
}