use std::collections::HashMap;
use chia_bls::{PublicKey, SecretKey, Signature};
use dig_protocol::Bytes32;
use dig_slashing::{
AttestationData, AttesterSlashing, BLS_SIGNATURE_SIZE, Checkpoint, IndexedAttestation,
OffenseType, SlashingError, SlashingEvidence, SlashingEvidencePayload, ValidatorEntry,
ValidatorView, verify_evidence,
};
struct TestValidator {
pk: PublicKey,
}
impl ValidatorEntry for TestValidator {
fn public_key(&self) -> &PublicKey {
&self.pk
}
fn puzzle_hash(&self) -> Bytes32 {
Bytes32::new([0u8; 32])
}
fn effective_balance(&self) -> u64 {
32_000_000_000
}
fn is_slashed(&self) -> bool {
false
}
fn activation_epoch(&self) -> u64 {
0
}
fn exit_epoch(&self) -> u64 {
u64::MAX
}
fn is_active_at_epoch(&self, _epoch: u64) -> bool {
true
}
fn slash_absolute(&mut self, _: u64, _: u64) -> u64 {
0
}
fn credit_stake(&mut self, _: u64) -> u64 {
0
}
fn restore_status(&mut self) -> bool {
false
}
fn schedule_exit(&mut self, _: u64) {}
}
struct MapView(HashMap<u32, TestValidator>);
impl ValidatorView for MapView {
fn get(&self, index: u32) -> Option<&dyn ValidatorEntry> {
self.0.get(&index).map(|v| v as &dyn ValidatorEntry)
}
fn get_mut(&mut self, index: u32) -> Option<&mut dyn ValidatorEntry> {
self.0.get_mut(&index).map(|v| v as &mut dyn ValidatorEntry)
}
fn len(&self) -> usize {
self.0.len()
}
}
fn network_id() -> Bytes32 {
Bytes32::new([0xAAu8; 32])
}
fn make_key(seed_byte: u8) -> SecretKey {
SecretKey::from_seed(&[seed_byte; 32])
}
fn make_data(target_epoch: u64, head_vote_byte: u8) -> AttestationData {
AttestationData {
slot: 100,
index: 0,
beacon_block_root: Bytes32::new([head_vote_byte; 32]),
source: Checkpoint {
epoch: 2,
root: Bytes32::new([0x22u8; 32]),
},
target: Checkpoint {
epoch: target_epoch,
root: Bytes32::new([0x33u8; 32]),
},
}
}
fn signed_attestation(indices: Vec<u32>, data: AttestationData) -> (IndexedAttestation, MapView) {
let nid = network_id();
let signing_root = data.signing_root(&nid);
let mut sigs: Vec<Signature> = Vec::new();
let mut map: HashMap<u32, TestValidator> = HashMap::new();
for idx in &indices {
let sk = make_key(*idx as u8);
let pk = sk.public_key();
sigs.push(chia_bls::sign(&sk, signing_root.as_ref()));
map.insert(*idx, TestValidator { pk });
}
let agg = chia_bls::aggregate(&sigs);
let att = IndexedAttestation {
attesting_indices: indices,
data,
signature: agg.to_bytes().to_vec(),
};
(att, MapView(map))
}
fn merge(a: MapView, b: MapView) -> MapView {
let mut merged = a.0;
for (k, v) in b.0 {
merged.insert(k, v);
}
MapView(merged)
}
fn envelope(indices_a: Vec<u32>, indices_b: Vec<u32>) -> (SlashingEvidence, MapView) {
let data_a = make_data(3, 0xA1);
let data_b = make_data(3, 0xB2);
let (att_a, view_a) = signed_attestation(indices_a, data_a);
let (att_b, view_b) = signed_attestation(indices_b, data_b);
let ev = SlashingEvidence {
offense_type: OffenseType::AttesterDoubleVote,
reporter_validator_index: 999,
reporter_puzzle_hash: Bytes32::new([0xCCu8; 32]),
epoch: 3,
payload: SlashingEvidencePayload::Attester(AttesterSlashing {
attestation_a: att_a,
attestation_b: att_b,
}),
};
(ev, merge(view_a, view_b))
}
#[test]
fn test_dsl_016_disjoint_rejected() {
let (ev, view) = envelope(vec![1, 2, 3], vec![4, 5, 6]);
let err =
verify_evidence(&ev, &view, &network_id(), 3).expect_err("disjoint committees must reject");
assert_eq!(err, SlashingError::EmptySlashableIntersection);
}
#[test]
fn test_dsl_016_non_empty_passes() {
let (ev, view) = envelope(vec![1, 2, 3], vec![2, 3, 4]);
let verified =
verify_evidence(&ev, &view, &network_id(), 3).expect("non-empty intersection must pass");
assert_eq!(verified.slashable_validator_indices, vec![2, 3]);
}
#[test]
fn test_dsl_016_runs_before_bls_verify() {
let (mut ev, view) = envelope(vec![1, 2, 3], vec![4, 5, 6]);
if let SlashingEvidencePayload::Attester(p) = &mut ev.payload {
p.attestation_a.signature = vec![0xFFu8; BLS_SIGNATURE_SIZE];
p.attestation_b.signature = vec![0xFFu8; BLS_SIGNATURE_SIZE];
}
let err =
verify_evidence(&ev, &view, &network_id(), 3).expect_err("disjoint must reject before BLS");
assert_eq!(
err,
SlashingError::EmptySlashableIntersection,
"intersection check MUST run before BLS verify",
);
}
#[test]
fn test_dsl_016_single_element_intersection_passes() {
let (ev, view) = envelope(vec![1, 2, 3], vec![3, 4, 5]);
let verified = verify_evidence(&ev, &view, &network_id(), 3).expect("single-overlap must pass");
assert_eq!(verified.slashable_validator_indices, vec![3]);
}
#[test]
fn test_dsl_016_runs_after_predicate_decision() {
let data_a = make_data(3, 0xA1);
let data_b = make_data(4, 0xB2); let (att_a, view_a) = signed_attestation(vec![1, 2, 3], data_a);
let (att_b, view_b) = signed_attestation(vec![10, 11, 12], data_b);
let ev = SlashingEvidence {
offense_type: OffenseType::AttesterDoubleVote,
reporter_validator_index: 999,
reporter_puzzle_hash: Bytes32::new([0xCCu8; 32]),
epoch: 4,
payload: SlashingEvidencePayload::Attester(AttesterSlashing {
attestation_a: att_a,
attestation_b: att_b,
}),
};
let view = merge(view_a, view_b);
let err = verify_evidence(&ev, &view, &network_id(), 4).expect_err("non-slashable must reject");
assert_eq!(
err,
SlashingError::AttesterSlashingNotSlashable,
"predicate failure MUST surface before intersection check",
);
}