commonware_consensus/simplex/signing_scheme/
reporter.rs1use crate::{
22 simplex::{signing_scheme::Scheme, types::Activity},
23 Reporter,
24};
25use commonware_cryptography::Digest;
26use rand::{CryptoRng, Rng};
27
28#[derive(Clone)]
34pub struct AttributableReporter<
35 E: Clone + Rng + CryptoRng + Send + 'static,
36 S: 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: Scheme,
55 D: Digest,
56 R: Reporter<Activity = Activity<S, D>>,
57 > AttributableReporter<E, S, D, R>
58{
59 pub 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,
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::Nullification(_)
104 | Activity::Finalization(_) => {
105 }
107 }
108 }
109
110 self.reporter.report(activity).await;
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::{
118 simplex::{
119 mocks::fixtures::{bls12381_threshold, ed25519, Fixture},
120 signing_scheme::Scheme,
121 types::{Notarization, Notarize, Proposal, VoteContext},
122 },
123 types::Round,
124 };
125 use commonware_cryptography::{
126 bls12381::primitives::variant::MinPk, sha256::Digest as Sha256Digest, Hasher, Sha256,
127 };
128 use futures::executor::block_on;
129 use rand::{rngs::StdRng, SeedableRng};
130 use std::sync::{Arc, Mutex};
131
132 const NAMESPACE: &[u8] = b"test-reporter";
133
134 #[derive(Clone)]
135 struct MockReporter<S: Scheme, D: Digest> {
136 activities: Arc<Mutex<Vec<Activity<S, D>>>>,
137 }
138
139 impl<S: Scheme, D: Digest> MockReporter<S, D> {
140 fn new() -> Self {
141 Self {
142 activities: Arc::new(Mutex::new(Vec::new())),
143 }
144 }
145
146 fn reported(&self) -> Vec<Activity<S, D>> {
147 self.activities.lock().unwrap().clone()
148 }
149
150 fn count(&self) -> usize {
151 self.activities.lock().unwrap().len()
152 }
153 }
154
155 impl<S: Scheme, D: Digest> Reporter for MockReporter<S, D> {
156 type Activity = Activity<S, D>;
157
158 async fn report(&mut self, activity: Self::Activity) {
159 self.activities.lock().unwrap().push(activity);
160 }
161 }
162
163 fn create_proposal(epoch: u64, view: u64) -> Proposal<Sha256Digest> {
164 let data = format!("proposal-{epoch}-{view}");
165 let hash = Sha256::hash(data.as_bytes());
166 Proposal::new(Round::new(epoch, view), view, hash)
167 }
168
169 #[test]
170 fn test_invalid_peer_activity_dropped() {
171 let mut rng = StdRng::seed_from_u64(42);
173 let Fixture {
174 schemes, verifier, ..
175 } = ed25519(&mut rng, 4);
176
177 assert!(verifier.is_attributable(), "Ed25519 must be attributable");
178
179 let mock = MockReporter::new();
180 let mut reporter =
181 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
182
183 let proposal = create_proposal(0, 1);
185 let vote = schemes[1]
186 .sign_vote::<Sha256Digest>(
187 &[], VoteContext::Notarize {
189 proposal: &proposal,
190 },
191 )
192 .expect("signing failed");
193 let notarize = Notarize { proposal, vote };
194
195 block_on(reporter.report(Activity::Notarize(notarize)));
197
198 assert_eq!(mock.count(), 0);
200 }
201
202 #[test]
203 fn test_skip_verification() {
204 let mut rng = StdRng::seed_from_u64(42);
206 let Fixture {
207 schemes, verifier, ..
208 } = ed25519(&mut rng, 4);
209
210 assert!(verifier.is_attributable(), "Ed25519 must be attributable");
211
212 let mock = MockReporter::new();
213 let mut reporter = AttributableReporter::new(
214 rng,
215 verifier,
216 NAMESPACE.to_vec(),
217 mock.clone(),
218 false, );
220
221 let proposal = create_proposal(0, 1);
223 let vote = schemes[1]
224 .sign_vote::<Sha256Digest>(
225 &[], VoteContext::Notarize {
227 proposal: &proposal,
228 },
229 )
230 .expect("signing failed");
231 let notarize = Notarize { proposal, vote };
232
233 block_on(reporter.report(Activity::Notarize(notarize)));
235
236 assert_eq!(mock.count(), 1);
238 let reported = mock.reported();
239 assert!(matches!(reported[0], Activity::Notarize(_)));
240 }
241
242 #[test]
243 fn test_certificates_always_reported() {
244 let mut rng = StdRng::seed_from_u64(42);
246 let Fixture {
247 schemes, verifier, ..
248 } = bls12381_threshold::<MinPk, _>(&mut rng, 4);
249
250 assert!(
251 !verifier.is_attributable(),
252 "BLS threshold must be non-attributable"
253 );
254
255 let mock = MockReporter::new();
256 let mut reporter =
257 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
258
259 let proposal = create_proposal(0, 1);
261 let votes: Vec<_> = schemes
262 .iter()
263 .map(|scheme| {
264 scheme
265 .sign_vote::<Sha256Digest>(
266 NAMESPACE,
267 VoteContext::Notarize {
268 proposal: &proposal,
269 },
270 )
271 .expect("signing failed")
272 })
273 .collect();
274
275 let certificate = schemes[0]
276 .assemble_certificate(votes)
277 .expect("failed to assemble certificate");
278
279 let notarization = Notarization {
280 proposal,
281 certificate,
282 };
283
284 block_on(reporter.report(Activity::Notarization(notarization)));
286
287 assert_eq!(mock.count(), 1);
289 let reported = mock.reported();
290 assert!(matches!(reported[0], Activity::Notarization(_)));
291 }
292
293 #[test]
294 fn test_non_attributable_filters_peer_activities() {
295 let mut rng = StdRng::seed_from_u64(42);
297 let Fixture {
298 schemes, verifier, ..
299 } = bls12381_threshold::<MinPk, _>(&mut rng, 4);
300
301 assert!(
302 !verifier.is_attributable(),
303 "BLS threshold must be non-attributable"
304 );
305
306 let mock = MockReporter::new();
307 let mut reporter =
308 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
309
310 let proposal = create_proposal(0, 1);
312 let vote = schemes[1]
313 .sign_vote::<Sha256Digest>(
314 NAMESPACE,
315 VoteContext::Notarize {
316 proposal: &proposal,
317 },
318 )
319 .expect("signing failed");
320
321 let notarize = Notarize { proposal, vote };
322
323 block_on(reporter.report(Activity::Notarize(notarize)));
325
326 assert_eq!(mock.count(), 0);
328 }
329
330 #[test]
331 fn test_attributable_scheme_reports_peer_activities() {
332 let mut rng = StdRng::seed_from_u64(42);
334 let Fixture {
335 schemes, verifier, ..
336 } = ed25519(&mut rng, 4);
337
338 assert!(verifier.is_attributable(), "Ed25519 must be attributable");
339
340 let mock = MockReporter::new();
341 let mut reporter =
342 AttributableReporter::new(rng, verifier, NAMESPACE.to_vec(), mock.clone(), true);
343
344 let proposal = create_proposal(0, 1);
346 let vote = schemes[1]
347 .sign_vote::<Sha256Digest>(
348 NAMESPACE,
349 VoteContext::Notarize {
350 proposal: &proposal,
351 },
352 )
353 .expect("signing failed");
354
355 let notarize = Notarize { proposal, vote };
356
357 block_on(reporter.report(Activity::Notarize(notarize)));
359
360 assert_eq!(mock.count(), 1);
362 let reported = mock.reported();
363 assert!(matches!(reported[0], Activity::Notarize(_)));
364 }
365}