commonware_consensus/simplex/scheme/
reporter.rs1use crate::{
22 simplex::{scheme::Scheme, types::Activity},
23 Reporter,
24};
25use commonware_cryptography::{certificate, Digest};
26use rand::{CryptoRng, Rng};
27
28#[derive(Clone)]
34pub struct AttributableReporter<
35 E: Clone + Rng + CryptoRng + Send + 'static,
36 S: certificate::Scheme,
37 D: Digest,
38 R: Reporter<Activity = Activity<S, D>>,
39> {
40 rng: E,
42 scheme: S,
44 namespace: Vec<u8>,
46 reporter: R,
48 verify: bool,
50}
51
52impl<
53 E: Clone + Rng + CryptoRng + Send + 'static,
54 S: certificate::Scheme,
55 D: Digest,
56 R: Reporter<Activity = Activity<S, D>>,
57 > AttributableReporter<E, S, D, R>
58{
59 pub const fn new(rng: E, scheme: S, namespace: Vec<u8>, reporter: R, verify: bool) -> Self {
61 Self {
62 rng,
63 scheme,
64 namespace,
65 reporter,
66 verify,
67 }
68 }
69}
70
71impl<
72 E: Clone + Rng + CryptoRng + Send + 'static,
73 S: Scheme<D>,
74 D: Digest,
75 R: Reporter<Activity = Activity<S, D>>,
76 > Reporter for AttributableReporter<E, S, D, R>
77{
78 type Activity = Activity<S, D>;
79
80 async fn report(&mut self, activity: Self::Activity) {
81 if self.verify
83 && !activity.verified()
84 && !activity.verify(&mut self.rng, &self.scheme, &self.namespace)
85 {
86 return;
88 }
89
90 if !self.scheme.is_attributable() {
92 match activity {
93 Activity::Notarize(_)
94 | Activity::Nullify(_)
95 | Activity::Finalize(_)
96 | Activity::ConflictingNotarize(_)
97 | Activity::ConflictingFinalize(_)
98 | Activity::NullifyFinalize(_) => {
99 return;
101 }
102 Activity::Notarization(_)
103 | Activity::Certification(_)
104 | Activity::Nullification(_)
105 | Activity::Finalization(_) => {
106 }
108 }
109 }
110
111 self.reporter.report(activity).await;
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::{
119 simplex::{
120 scheme::{bls12381_threshold, ed25519},
121 types::{Notarization, Notarize, Proposal, Subject},
122 },
123 types::{Epoch, Round, View},
124 };
125 use commonware_cryptography::{
126 bls12381::primitives::variant::MinPk,
127 certificate::{self, mocks::Fixture, Scheme as _},
128 sha256::Digest as Sha256Digest,
129 Hasher, Sha256,
130 };
131 use futures::executor::block_on;
132 use rand::{rngs::StdRng, SeedableRng};
133 use std::sync::{Arc, Mutex};
134
135 const NAMESPACE: &[u8] = b"test-reporter";
136
137 #[derive(Clone)]
138 struct MockReporter<S: certificate::Scheme, D: Digest> {
139 activities: Arc<Mutex<Vec<Activity<S, D>>>>,
140 }
141
142 impl<S: certificate::Scheme, D: Digest> MockReporter<S, D> {
143 fn new() -> Self {
144 Self {
145 activities: Arc::new(Mutex::new(Vec::new())),
146 }
147 }
148
149 fn reported(&self) -> Vec<Activity<S, D>> {
150 self.activities.lock().unwrap().clone()
151 }
152
153 fn count(&self) -> usize {
154 self.activities.lock().unwrap().len()
155 }
156 }
157
158 impl<S: certificate::Scheme, D: Digest> Reporter for MockReporter<S, D> {
159 type Activity = Activity<S, D>;
160
161 async fn report(&mut self, activity: Self::Activity) {
162 self.activities.lock().unwrap().push(activity);
163 }
164 }
165
166 fn create_proposal(epoch: u64, view: u64) -> Proposal<Sha256Digest> {
167 let data = format!("proposal-{epoch}-{view}");
168 let hash = Sha256::hash(data.as_bytes());
169 let epoch = Epoch::new(epoch);
170 let view = View::new(view);
171 Proposal::new(Round::new(epoch, view), view, hash)
172 }
173
174 #[test]
175 fn test_invalid_peer_activity_dropped() {
176 let mut rng = StdRng::seed_from_u64(42);
178 let Fixture {
179 schemes, verifier, ..
180 } = ed25519::fixture(&mut rng, 4);
181
182 assert!(verifier.is_attributable(), "Ed25519 must be attributable");
183
184 let mock = MockReporter::new();
185 let mut reporter =
186 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
187
188 let proposal = create_proposal(0, 1);
190 let attestation = schemes[1]
191 .sign::<Sha256Digest>(
192 &[], Subject::Notarize {
194 proposal: &proposal,
195 },
196 )
197 .expect("signing failed");
198 let notarize = Notarize {
199 proposal,
200 attestation,
201 };
202
203 block_on(reporter.report(Activity::Notarize(notarize)));
205
206 assert_eq!(mock.count(), 0);
208 }
209
210 #[test]
211 fn test_skip_verification() {
212 let mut rng = StdRng::seed_from_u64(42);
214 let Fixture {
215 schemes, verifier, ..
216 } = ed25519::fixture(&mut rng, 4);
217
218 assert!(verifier.is_attributable(), "Ed25519 must be attributable");
219
220 let mock = MockReporter::new();
221 let mut reporter = AttributableReporter::new(
222 rng,
223 verifier,
224 NAMESPACE.to_vec(),
225 mock.clone(),
226 false, );
228
229 let proposal = create_proposal(0, 1);
231 let attestation = schemes[1]
232 .sign::<Sha256Digest>(
233 &[], Subject::Notarize {
235 proposal: &proposal,
236 },
237 )
238 .expect("signing failed");
239 let notarize = Notarize {
240 proposal,
241 attestation,
242 };
243
244 block_on(reporter.report(Activity::Notarize(notarize)));
246
247 assert_eq!(mock.count(), 1);
249 let reported = mock.reported();
250 assert!(matches!(reported[0], Activity::Notarize(_)));
251 }
252
253 #[test]
254 fn test_certificates_always_reported() {
255 let mut rng = StdRng::seed_from_u64(42);
257 let Fixture {
258 schemes, verifier, ..
259 } = bls12381_threshold::fixture::<MinPk, _>(&mut rng, 4);
260
261 assert!(
262 !verifier.is_attributable(),
263 "BLS threshold must be non-attributable"
264 );
265
266 let mock = MockReporter::new();
267 let mut reporter =
268 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
269
270 let proposal = create_proposal(0, 1);
272 let votes: Vec<_> = schemes
273 .iter()
274 .map(|scheme| {
275 scheme
276 .sign::<Sha256Digest>(
277 NAMESPACE,
278 Subject::Notarize {
279 proposal: &proposal,
280 },
281 )
282 .expect("signing failed")
283 })
284 .collect();
285
286 let certificate = schemes[0]
287 .assemble(votes)
288 .expect("failed to assemble certificate");
289
290 let notarization = Notarization {
291 proposal,
292 certificate,
293 };
294
295 block_on(reporter.report(Activity::Notarization(notarization)));
297
298 assert_eq!(mock.count(), 1);
300 let reported = mock.reported();
301 assert!(matches!(reported[0], Activity::Notarization(_)));
302 }
303
304 #[test]
305 fn test_non_attributable_filters_peer_activities() {
306 let mut rng = StdRng::seed_from_u64(42);
308 let Fixture {
309 schemes, verifier, ..
310 } = bls12381_threshold::fixture::<MinPk, _>(&mut rng, 4);
311
312 assert!(
313 !verifier.is_attributable(),
314 "BLS threshold must be non-attributable"
315 );
316
317 let mock = MockReporter::new();
318 let mut reporter =
319 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
320
321 let proposal = create_proposal(0, 1);
323 let attestation = schemes[1]
324 .sign::<Sha256Digest>(
325 NAMESPACE,
326 Subject::Notarize {
327 proposal: &proposal,
328 },
329 )
330 .expect("signing failed");
331
332 let notarize = Notarize {
333 proposal,
334 attestation,
335 };
336
337 block_on(reporter.report(Activity::Notarize(notarize)));
339
340 assert_eq!(mock.count(), 0);
342 }
343
344 #[test]
345 fn test_attributable_scheme_reports_peer_activities() {
346 let mut rng = StdRng::seed_from_u64(42);
348 let Fixture {
349 schemes, verifier, ..
350 } = ed25519::fixture(&mut rng, 4);
351
352 assert!(verifier.is_attributable(), "Ed25519 must be attributable");
353
354 let mock = MockReporter::new();
355 let mut reporter =
356 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
357
358 let proposal = create_proposal(0, 1);
360 let attestation = schemes[1]
361 .sign::<Sha256Digest>(
362 NAMESPACE,
363 Subject::Notarize {
364 proposal: &proposal,
365 },
366 )
367 .expect("signing failed");
368
369 let notarize = Notarize {
370 proposal,
371 attestation,
372 };
373
374 block_on(reporter.report(Activity::Notarize(notarize)));
376
377 assert_eq!(mock.count(), 1);
379 let reported = mock.reported();
380 assert!(matches!(reported[0], Activity::Notarize(_)));
381 }
382}