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