Skip to main content

commonware_consensus/simplex/scheme/
reporter.rs

1//! Wrapper for scheme-dependent activity filtering and verification.
2//!
3//! # Overview
4//!
5//! The [`AttributableReporter`] provides a composable wrapper around consensus reporters
6//! that automatically filters and verifies activities based on scheme attributability.
7//! This ensures that:
8//!
9//! 1. **Peer activities are cryptographically verified** before being reported
10//! 2. **Non-attributable schemes** suppress per-validator activities from peers to prevent
11//!    signature forgery attacks
12//! 3. **Certificates** are always reported as they contain valid quorum proofs
13//!
14//! # Security Rationale
15//!
16//! With [`super::bls12381_threshold`], any `t` valid partial signatures
17//! can be used to forge a partial signature for any participant. If per-validator activities
18//! were exposed for such schemes, adversaries could fabricate evidence of either liveness or of committing a fault.
19//! This wrapper prevents that attack by suppressing peer activities for non-attributable schemes.
20
21use 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
32/// Reporter wrapper that filters and verifies activities based on scheme attributability.
33///
34/// This wrapper provides scheme-aware activity filtering with automatic verification of peer
35/// activities. It prevents signature forgery attacks on non-attributable schemes while ensuring
36/// all activities are cryptographically valid before reporting.
37pub 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 for certificate verification
45    rng: Arc<Mutex<E>>,
46    /// Signing scheme for verification
47    scheme: S,
48    /// Inner reporter that receives filtered activities
49    reporter: R,
50    /// Strategy for parallel operations.
51    strategy: T,
52    /// Whether to always verify peer activities
53    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    /// Creates a new `AttributableReporter` that wraps an inner reporter.
84    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        // Verify peer activities if verification is enabled
107        if self.verify
108            && !activity.verified()
109            && !activity.verify(&mut *self.rng.lock(), &self.scheme, &self.strategy)
110        {
111            // Ignore unverified peer activity.
112            return Feedback::Ok;
113        }
114
115        // Filter based on scheme attributability
116        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                    // Ignore per-validator peer activity for non-attributable schemes.
125                    return Feedback::Ok;
126                }
127                Activity::Notarization(_)
128                | Activity::Certification(_)
129                | Activity::Nullification(_)
130                | Activity::Finalization(_) => {
131                    // Always report certificates
132                }
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        // Invalid peer activities should be ignored when verification is enabled.
204        let mut rng = test_rng();
205        let Fixture { verifier, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
206
207        // Create a scheme with wrong namespace to generate invalid signatures
208        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        // Create an invalid activity (signed with wrong namespace scheme)
222        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        // Report it.
234        assert_eq!(reporter.report(Activity::Notarize(notarize)), Feedback::Ok);
235
236        // Should be ignored.
237        assert_eq!(mock.count(), 0);
238    }
239
240    #[test]
241    fn test_skip_verification() {
242        // When verification is disabled, invalid activities pass through
243        let mut rng = test_rng();
244        let Fixture { verifier, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
245
246        // Create a scheme with wrong namespace to generate invalid signatures
247        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, // Disable verification
264        );
265
266        // Create an invalid activity (signed with wrong namespace scheme)
267        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        // Report it
279        assert_eq!(reporter.report(Activity::Notarize(notarize)), Feedback::Ok);
280
281        // Should be reported even though it's invalid
282        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        // Certificates should always be reported, even for non-attributable schemes
290        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        // Create a certificate from multiple validators
304        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        // Report it
326        assert_eq!(
327            reporter.report(Activity::Notarization(notarization)),
328            Feedback::Ok
329        );
330
331        // Should be reported even though scheme is non-attributable (certificates are quorum proofs)
332        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        // Non-attributable schemes (like BLS threshold) must filter peer per-validator activities
340        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        // Create peer activity (from validator 1)
354        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        // Report peer per-validator activity
367        assert_eq!(reporter.report(Activity::Notarize(notarize)), Feedback::Ok);
368
369        // Must be filtered
370        assert_eq!(mock.count(), 0);
371    }
372
373    #[test]
374    fn test_attributable_scheme_reports_peer_activities() {
375        // Ed25519 (attributable) should report peer per-validator activities
376        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        // Create a peer activity (from validator 1)
390        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        // Report the peer per-validator activity
403        assert_eq!(reporter.report(Activity::Notarize(notarize)), Feedback::Ok);
404
405        // Should be reported since scheme is attributable
406        assert_eq!(mock.count(), 1);
407        let reported = mock.reported();
408        assert!(matches!(reported[0], Activity::Notarize(_)));
409    }
410}