1
//! Calculate the body of a routerstatus we have decided to include
2

            
3
use super::*;
4
use rs_common::*;
5

            
6
impl<'i> ResolvedRouterStatusInputs<'i> {
7
    /// Calculate the plain and md routerstatuses
8
    ///
9
    /// Similar to `ConsensusesFromVotes::consensuses`,
10
    /// but takes one `ResolvedRouterStatusInputs`, which includes not only
11
    /// the routerstatuses from each vote, but also the id-tuple and status-tuple
12
    /// from the resolution part of the algorithm.
13
    ///
14
    /// Can return `None` to mean that this router should not be listed after all
15
    /// (eg, because its listing would lack the Running flag),
16
    /// or `Some((md, None))` to list in the plain consensus, but not in the md consensus.
17
    pub(super) fn consensuses(
18
        &self,
19
        context: ConsensusContextRefs<PlainPreamble>,
20
    ) -> Result<
21
        Option<(
22
            netstatus::plain::RouterStatus,
23
            Option<netstatus::md::RouterStatus>,
24
        )>,
25
        ConsensusError,
26
    > {
27
        let inputs = self
28
            .per_voter
29
            .iter_enumerated()
30
            .filter_map(|(vnum, rs_v)| Some((vnum, rs_v.as_ref()?)));
31

            
32
        /// Copy a field from `self.status_tuple` to the output.
33
        ///
34
        /// `from_status_tuple! { OUT . FIELD; SUFFIX }` is equivalent to
35
        /// `calc! { OUT.FIELD = self.status_tuple.FIELD SUFFIX }``
36
        ///
37
        /// The `; SUFFIX` may be omitted.
38
        macro_rules! from_status_tuple {
39
            { $out:ident . $field:ident $(; $($suffix:tt)* )? } => {
40
                calc! { $out.$field = self.status_tuple.$field $( $($suffix)+ )? }
41
            };
42
        }
43

            
44
        let (plain_r, md_r) = {
45
            calc! { both.identity    = self.id.rsa }
46
            calc! { both.publication = netstatus::IgnoredPublicationTimeSp }
47
            from_status_tuple! { both.ip }
48
            from_status_tuple! { both.nickname; .clone() }
49
            from_status_tuple! { plain.doc_digest; .into() }
50
            from_status_tuple! { both.or_port }
51
            from_status_tuple! { both.dir_port }
52

            
53
            // TODO DIRAUTH replace dummy value; actually calculate which md digest to include
54
            calc! { md.doc_digest = Default::default() }
55

            
56
            construct_both! {
57
                netstatus::plain::RouterStatusIntroItem, netstatus::md::RouterStatusIntroItem {
58
                    both. nickname, identity, doc_digest, publication, ip;
59
                } {
60
                    both. or_port, dir_port;
61
                }
62
            }
63
        };
64

            
65
        calc! { both.ed25519_id = NotPresent }
66
        calc! { plain.m = NotPresent }
67
        calc! { plain, md .weight }
68

            
69
        calc! { out.flags }
70
        // "routers that do not have the Running flag are not listed at all."
71
        if !out_flags.contains(RelayFlag::Running) {
72
            return Ok(None);
73
        }
74
        calc! { both.flags = out_flags.clone() }
75

            
76
        // TODO DIRAUTH replace routerstatus dummy values
77
        calc! { both.protos = Default::default() }
78
        calc! { md.m = [0; 32].into() }
79

            
80
        let (plain, md) = construct_both! {
81
            netstatus::plain::RouterStatus, netstatus::md::RouterStatus {
82
                both. r, m, flags, protos, weight, ed25519_id;
83
            } {
84
                // TODO DIRAUTH routerstatus fields missing
85
            }
86
        };
87
        Ok(Some((plain, Some(md))))
88
    }
89
}
90

            
91
impl Aggregate<PlainPreamble> for VoteRelayWeightsItem {
92
    type Output = RelayWeightsItem;
93

            
94
    #[allow(clippy::needless_late_init)] // re median_inputs and unmeasured; clearer this way
95
    fn aggregate<'i>(
96
        context: ConsensusContextRefs<PlainPreamble>,
97
        inputs: impl ComponentInVotes<&'i VoteRelayWeightsItem>,
98
    ) -> Result<RelayWeightsItem, ConsensusError>
99
    where
100
        Self: 'i,
101
    {
102
        // https://spec.torproject.org/dir-spec/computing-consensus.html#router-status-entries
103
        // under "`w` item".
104

            
105
        const MEASURED: &str = "Measured";
106
        const BANDWIDTH: &str = "Bandwidth";
107
        const UNMEASURED: &str = "Unmeasured";
108
        const MAX_UNMEASURED_BW_PARAM: &str = "maxunmeasuredbw";
109
        const MEASURED_THRESHOLD: usize = 3;
110

            
111
        let get_inputs = |k| {
112
            inputs
113
                .clone()
114
                .filter_map(move |(_vnum, rwi)| rwi.w.as_ref()?.get(k).copied())
115
        };
116
        let calc_median = |k| functions::low_median_raw(get_inputs(k));
117

            
118
        let out; // the calculated output value, for Bandwdith
119
        let unmeasured; // the (keyword, value) for Unmeasured, or None
120
        if get_inputs(MEASURED).count() >= MEASURED_THRESHOLD {
121
            out = calc_median(MEASURED).expect(".count() was >= MEASURED_THRESHOLD so >0");
122
            const_assert!(MEASURED_THRESHOLD > 0);
123
            unmeasured = None;
124
        } else if let Some(median) = calc_median(BANDWIDTH) {
125
            let max_unmeasured = context
126
                .computed
127
                .params
128
                .get(MAX_UNMEASURED_BW_PARAM)
129
                .copied()
130
                // w items are unsigned 32-bit, but it's capped using netparams which are
131
                // are *signed* 32-bit.  If the netparam is negative, ignore it
132
                // (rather than treating it as zero).
133
                .and_then(|mu: i32| u32::try_from(mu).ok());
134

            
135
            out = chain!([median], max_unmeasured,)
136
                .min()
137
                .expect("[median] is nonempthy");
138
            unmeasured = Some((UNMEASURED, 1));
139
        } else {
140
            // There were no w lines, or not enough had Measured, or none had Bandwidth
141
            return Ok(RelayWeightsItem::default());
142
        }
143

            
144
        let out = chain!([(BANDWIDTH, out)], unmeasured)
145
            .collect::<NetParams<_>>()
146
            .try_into()
147
            .map_err(into_internal!("generated bad w item"))?;
148

            
149
        Ok(out)
150
    }
151
}
152

            
153
impl Aggregate<PlainPreamble> for DocRelayFlags {
154
    type Output = DocRelayFlags;
155

            
156
1
    fn aggregate<'i>(
157
1
        context: ConsensusContextRefs<PlainPreamble>,
158
1
        inputs: impl ComponentInVotes<&'i Self>,
159
1
    ) -> Result<Self::Output, ConsensusError>
160
1
    where
161
1
        Self: 'i,
162
    {
163
1
        let mut out: DocRelayFlags = context
164
1
            .computed
165
1
            .known_flags
166
            // "A routerstatus has a flag set if that is included by more than half of the
167
            // authorities who care about that flag."
168
1
            .iter_incl_unknown()?
169
5
            .try_filter(|flag| {
170
                // Check the votes' opinions about `flag`
171
5
                let mut tally = BoolIndexedArray::<usize>::default();
172
25
                for (vnum, vote_rs_flags) in inputs.clone() {
173
25
                    let vote_known_flags = &context.votes.vote(vnum)?.preamble.known_flags;
174
25
                    if !vote_known_flags.contains_incl_unknown(flag)? {
175
                        // This authority didn't advertise this flag as one it knows about.
176
                        // It's possible that it is included in that authority's vote for this
177
                        // relay anyway; if so we disregard it.
178
12
                        continue;
179
13
                    }
180
13
                    let is_in_favour: bool = vote_rs_flags.contains_incl_unknown(flag)?;
181
13
                    let update = &mut tally[is_in_favour];
182
                    // can't overflow, but let's use saturating add anyway
183
13
                    *update = update.saturating_add(1);
184
                }
185
5
                let y = tally.more_yes_than_no();
186
5
                Ok::<_, Bug>(y)
187
5
            })
188
1
            .try_collect()?;
189

            
190
        // https://spec.torproject.org/dir-spec/computing-consensus.html#routerstatuses
191
        #[allow(clippy::single_element_loop)] // more uniform this way
192
        {
193
            // TODO DIRAUTH this needs to be tested somehow
194
            use RelayFlag as F;
195
1
            let out = &mut out.known;
196
1
            if out.contains(F::MiddleOnly) {
197
                for clear in [F::Exit, F::Guard, F::V2Dir, F::HSDir] {
198
                    out.remove(clear);
199
                }
200
                for set in [F::BadExit] {
201
                    if context.computed.known_flags.contains(set) {
202
                        out.insert(set);
203
                    }
204
                }
205
1
            }
206
        }
207

            
208
1
        Ok(out)
209
1
    }
210
}
211

            
212
#[cfg(test)]
213
mod test {
214
    // @@ begin test lint list maintained by maint/add_warning @@
215
    #![allow(clippy::bool_assert_comparison)]
216
    #![allow(clippy::clone_on_copy)]
217
    #![allow(clippy::dbg_macro)]
218
    #![allow(clippy::mixed_attributes_style)]
219
    #![allow(clippy::print_stderr)]
220
    #![allow(clippy::print_stdout)]
221
    #![allow(clippy::single_char_pattern)]
222
    #![allow(clippy::unwrap_used)]
223
    #![allow(clippy::unchecked_time_subtraction)]
224
    #![allow(clippy::useless_vec)]
225
    #![allow(clippy::needless_pass_by_value)]
226
    #![allow(clippy::string_slice)] // See arti#2571
227
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
228
    use super::*;
229

            
230
    #[test]
231
    fn relay_flags() -> Result<(), ConsensusError> {
232
        use tor_netdoc::types::relay_flags::DocRelayFlag;
233

            
234
        let n_voters = 5;
235

            
236
        type One = (&'static str, &'static [usize], &'static [usize], Option<()>);
237
        #[rustfmt::skip]
238
        let cases: &[One] = &[
239
            // flag name      known_flags   rs.f        expected
240
            ("unknown",       &[],          &[],        None),
241
            ("one-against",   &[0],         &[],        None),
242
            ("one-in-favour", &[0],         &[0],       Some(())),
243
            ("equal",         &[0,1,2,3],   &[0,1],     None),
244
            ("greater",       &[0,1,2,3],   &[0,1,2],   Some(())),
245
            ("contradict",    &[0,1,2],     &[0,3],     None),
246
        ];
247

            
248
        // We use `Unknown` always here.  That's OK because we're not trying to test
249
        // DocRelayFlag etc. (which is pretty straightforward anyway).
250
        let f_name = |one: &One| DocRelayFlag::Unknown(one.0.into());
251
        let f_known = |one: &One| one.1;
252
        let f_vote = |one: &One| one.2;
253
        let f_outcome = |one: &One| one.3;
254

            
255
        let mut cc = ConsensusCommonContext::new_for_test();
256
        cc.n_authorities = n_voters;
257
        let votes = (0..n_voters)
258
            .into_iter()
259
            .map(|v| {
260
                let mut vote = tor_netdoc::testdata_live::netstatus_vote().clone();
261
                vote.routers.truncate(1);
262

            
263
                let mk_flags = |extractor: fn(&One) -> &'static [usize]| {
264
                    cases
265
                        .iter()
266
                        .filter(|one| extractor(one).contains(&v))
267
                        .map(f_name)
268
                        .collect()
269
                };
270

            
271
                vote.preamble.known_flags = mk_flags(f_known);
272
                vote.routers[0].flags = mk_flags(f_vote);
273

            
274
                println!("vote");
275
                println!("    known   {:?}", vote.preamble.known_flags);
276
                println!("    router  {:?}", vote.routers[0].flags);
277

            
278
                vote
279
            })
280
            .collect_vec();
281
        cc.votes = votes.iter().collect();
282

            
283
        // TODO it seems likely that much of this testdata- and type-wrestling
284
        // will want to be reused/simplified as we add more test cases.
285

            
286
        let (plain_preamble, _) = netstatus::vote::Preamble::consensuses(
287
            ConsensusContextRefs {
288
                context: &cc,
289
                computed: &(),
290
            },
291
            cc.votes
292
                .iter_enumerated()
293
                .map(|(vnum, vote)| (vnum, &vote.preamble)),
294
        )?;
295

            
296
        let output = DocRelayFlags::aggregate(
297
            ConsensusContextRefs {
298
                context: &cc,
299
                computed: &plain_preamble,
300
            },
301
            cc.votes
302
                .iter_enumerated()
303
                .map(|(vnum, vote)| (vnum, &vote.routers[0].flags)),
304
        )?;
305

            
306
        println!("output");
307
        println!("    known   {:?}", plain_preamble.known_flags);
308
        println!("    router  {:?}", output);
309

            
310
        for one in cases {
311
            assert_eq!(
312
                output.contains_incl_unknown(&f_name(one))?.then_some(()),
313
                f_outcome(one),
314
                "{:?}",
315
                one,
316
            );
317
        }
318

            
319
        Ok(())
320
    }
321
}