use crate::{
simplex::{scheme::Scheme, types::Activity},
Reporter,
};
use commonware_cryptography::{certificate, Digest};
use commonware_parallel::Strategy;
use rand_core::CryptoRngCore;
#[derive(Clone)]
pub struct AttributableReporter<
E: Clone + CryptoRngCore + Send + 'static,
S: certificate::Scheme,
D: Digest,
T: Strategy,
R: Reporter<Activity = Activity<S, D>>,
> {
rng: E,
scheme: S,
reporter: R,
strategy: T,
verify: bool,
}
impl<
E: Clone + CryptoRngCore + Send + 'static,
S: certificate::Scheme,
D: Digest,
T: Strategy,
R: Reporter<Activity = Activity<S, D>>,
> AttributableReporter<E, S, D, T, R>
{
pub const fn new(rng: E, scheme: S, reporter: R, strategy: T, verify: bool) -> Self {
Self {
rng,
scheme,
reporter,
strategy,
verify,
}
}
}
impl<
E: Clone + CryptoRngCore + Send + 'static,
S: Scheme<D>,
D: Digest,
T: Strategy,
R: Reporter<Activity = Activity<S, D>>,
> Reporter for AttributableReporter<E, S, D, T, R>
{
type Activity = Activity<S, D>;
async fn report(&mut self, activity: Self::Activity) {
if self.verify
&& !activity.verified()
&& !activity.verify(&mut self.rng, &self.scheme, &self.strategy)
{
return;
}
if !S::is_attributable() {
match activity {
Activity::Notarize(_)
| Activity::Nullify(_)
| Activity::Finalize(_)
| Activity::ConflictingNotarize(_)
| Activity::ConflictingFinalize(_)
| Activity::NullifyFinalize(_) => {
return;
}
Activity::Notarization(_)
| Activity::Certification(_)
| Activity::Nullification(_)
| Activity::Finalization(_) => {
}
}
}
self.reporter.report(activity).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
simplex::{
scheme::{bls12381_threshold::vrf as bls12381_threshold_vrf, ed25519},
types::{Notarization, Notarize, Proposal, Subject},
},
types::{Epoch, Round, View},
};
use commonware_cryptography::{
bls12381::primitives::variant::MinPk,
certificate::{self, mocks::Fixture, Scheme as _},
ed25519::PublicKey as Ed25519PublicKey,
sha256::Digest as Sha256Digest,
Hasher, Sha256,
};
use commonware_parallel::Sequential;
use commonware_utils::{sync::Mutex, test_rng, N3f1};
use futures::executor::block_on;
use std::sync::Arc;
const NAMESPACE: &[u8] = b"test-reporter";
#[derive(Clone)]
struct MockReporter<S: certificate::Scheme, D: Digest> {
activities: Arc<Mutex<Vec<Activity<S, D>>>>,
}
impl<S: certificate::Scheme, D: Digest> MockReporter<S, D> {
fn new() -> Self {
Self {
activities: Arc::new(Mutex::new(Vec::new())),
}
}
fn reported(&self) -> Vec<Activity<S, D>> {
self.activities.lock().clone()
}
fn count(&self) -> usize {
self.activities.lock().len()
}
}
impl<S: certificate::Scheme, D: Digest> Reporter for MockReporter<S, D> {
type Activity = Activity<S, D>;
async fn report(&mut self, activity: Self::Activity) {
self.activities.lock().push(activity);
}
}
fn create_proposal(epoch: u64, view: u64) -> Proposal<Sha256Digest> {
let data = format!("proposal-{epoch}-{view}");
let hash = Sha256::hash(data.as_bytes());
let epoch = Epoch::new(epoch);
let view = View::new(view);
Proposal::new(Round::new(epoch, view), view, hash)
}
#[test]
fn test_invalid_peer_activity_dropped() {
let mut rng = test_rng();
let Fixture { verifier, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
let Fixture {
schemes: wrong_schemes,
..
} = ed25519::fixture(&mut rng, b"wrong-namespace", 4);
assert!(
ed25519::Scheme::is_attributable(),
"Ed25519 must be attributable"
);
let mock = MockReporter::new();
let mut reporter = AttributableReporter::new(rng, verifier, mock.clone(), Sequential, true);
let proposal = create_proposal(0, 1);
let attestation = wrong_schemes[1]
.sign::<Sha256Digest>(Subject::Notarize {
proposal: &proposal,
})
.expect("signing failed");
let notarize = Notarize {
proposal,
attestation,
};
block_on(reporter.report(Activity::Notarize(notarize)));
assert_eq!(mock.count(), 0);
}
#[test]
fn test_skip_verification() {
let mut rng = test_rng();
let Fixture { verifier, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
let Fixture {
schemes: wrong_schemes,
..
} = ed25519::fixture(&mut rng, b"wrong-namespace", 4);
assert!(
ed25519::Scheme::is_attributable(),
"Ed25519 must be attributable"
);
let mock = MockReporter::new();
let mut reporter = AttributableReporter::new(
rng,
verifier,
mock.clone(),
Sequential,
false, );
let proposal = create_proposal(0, 1);
let attestation = wrong_schemes[1]
.sign::<Sha256Digest>(Subject::Notarize {
proposal: &proposal,
})
.expect("signing failed");
let notarize = Notarize {
proposal,
attestation,
};
block_on(reporter.report(Activity::Notarize(notarize)));
assert_eq!(mock.count(), 1);
let reported = mock.reported();
assert!(matches!(reported[0], Activity::Notarize(_)));
}
#[test]
fn test_certificates_always_reported() {
let mut rng = test_rng();
let Fixture {
schemes, verifier, ..
} = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 4);
assert!(
!bls12381_threshold_vrf::Scheme::<Ed25519PublicKey, MinPk>::is_attributable(),
"BLS threshold must be non-attributable"
);
let mock = MockReporter::new();
let mut reporter = AttributableReporter::new(rng, verifier, mock.clone(), Sequential, true);
let proposal = create_proposal(0, 1);
let votes: Vec<_> = schemes
.iter()
.map(|scheme| {
scheme
.sign::<Sha256Digest>(Subject::Notarize {
proposal: &proposal,
})
.expect("signing failed")
})
.collect();
let certificate = schemes[0]
.assemble::<_, N3f1>(votes, &Sequential)
.expect("failed to assemble certificate");
let notarization = Notarization {
proposal,
certificate,
};
block_on(reporter.report(Activity::Notarization(notarization)));
assert_eq!(mock.count(), 1);
let reported = mock.reported();
assert!(matches!(reported[0], Activity::Notarization(_)));
}
#[test]
fn test_non_attributable_filters_peer_activities() {
let mut rng = test_rng();
let Fixture {
schemes, verifier, ..
} = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 4);
assert!(
!bls12381_threshold_vrf::Scheme::<Ed25519PublicKey, MinPk>::is_attributable(),
"BLS threshold must be non-attributable"
);
let mock = MockReporter::new();
let mut reporter = AttributableReporter::new(rng, verifier, mock.clone(), Sequential, true);
let proposal = create_proposal(0, 1);
let attestation = schemes[1]
.sign::<Sha256Digest>(Subject::Notarize {
proposal: &proposal,
})
.expect("signing failed");
let notarize = Notarize {
proposal,
attestation,
};
block_on(reporter.report(Activity::Notarize(notarize)));
assert_eq!(mock.count(), 0);
}
#[test]
fn test_attributable_scheme_reports_peer_activities() {
let mut rng = test_rng();
let Fixture {
schemes, verifier, ..
} = ed25519::fixture(&mut rng, NAMESPACE, 4);
assert!(
ed25519::Scheme::is_attributable(),
"Ed25519 must be attributable"
);
let mock = MockReporter::new();
let mut reporter = AttributableReporter::new(rng, verifier, mock.clone(), Sequential, true);
let proposal = create_proposal(0, 1);
let attestation = schemes[1]
.sign::<Sha256Digest>(Subject::Notarize {
proposal: &proposal,
})
.expect("signing failed");
let notarize = Notarize {
proposal,
attestation,
};
block_on(reporter.report(Activity::Notarize(notarize)));
assert_eq!(mock.count(), 1);
let reported = mock.reported();
assert!(matches!(reported[0], Activity::Notarize(_)));
}
}