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