use chia_bls::{PublicKey, Signature};
use dig_protocol::Bytes32;
use crate::constants::{
BLS_SIGNATURE_SIZE, DOMAIN_BEACON_PROPOSER, MAX_SLASH_PROPOSAL_PAYLOAD_BYTES,
};
use crate::error::SlashingError;
use crate::evidence::attester_slashing::AttesterSlashing;
use crate::evidence::envelope::{SlashingEvidence, SlashingEvidencePayload};
use crate::evidence::invalid_block::InvalidBlockProof;
use crate::evidence::offense::OffenseType;
use crate::evidence::proposer_slashing::{ProposerSlashing, SignedBlockHeader};
use crate::traits::{InvalidBlockOracle, PublicKeyLookup, ValidatorView};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct VerifiedEvidence {
pub offense_type: OffenseType,
pub slashable_validator_indices: Vec<u32>,
}
pub fn verify_evidence(
evidence: &SlashingEvidence,
validator_view: &dyn ValidatorView,
network_id: &Bytes32,
current_epoch: u64,
) -> Result<VerifiedEvidence, SlashingError> {
let lookback_sum = evidence
.epoch
.saturating_add(dig_epoch::SLASH_LOOKBACK_EPOCHS);
if lookback_sum < current_epoch {
return Err(SlashingError::OffenseTooOld {
offense_epoch: evidence.epoch,
current_epoch,
});
}
let slashable = evidence.slashable_validators();
if slashable.contains(&evidence.reporter_validator_index) {
return Err(SlashingError::ReporterIsAccused(
evidence.reporter_validator_index,
));
}
let _ = slashable;
match &evidence.payload {
SlashingEvidencePayload::Proposer(p) => {
verify_proposer_slashing(evidence, p, validator_view, network_id)
}
SlashingEvidencePayload::Attester(a) => {
verify_attester_slashing(evidence, a, validator_view, network_id)
}
SlashingEvidencePayload::InvalidBlock(i) => {
verify_invalid_block(evidence, i, validator_view, network_id, None)
}
}
}
pub fn verify_evidence_for_inclusion(
evidence: &SlashingEvidence,
validator_view: &dyn ValidatorView,
network_id: &Bytes32,
current_epoch: u64,
) -> Result<VerifiedEvidence, SlashingError> {
verify_evidence(evidence, validator_view, network_id, current_epoch)
}
pub fn verify_proposer_slashing(
evidence: &SlashingEvidence,
payload: &ProposerSlashing,
validator_view: &dyn ValidatorView,
network_id: &Bytes32,
) -> Result<VerifiedEvidence, SlashingError> {
let header_a = &payload.signed_header_a.message;
let header_b = &payload.signed_header_b.message;
if header_a.height != header_b.height {
return Err(SlashingError::InvalidProposerSlashing(format!(
"slot mismatch: header_a.height={}, header_b.height={}",
header_a.height, header_b.height,
)));
}
if header_a.proposer_index != header_b.proposer_index {
return Err(SlashingError::InvalidProposerSlashing(format!(
"proposer mismatch: header_a.proposer_index={}, header_b.proposer_index={}",
header_a.proposer_index, header_b.proposer_index,
)));
}
let hash_a = header_a.hash();
let hash_b = header_b.hash();
if hash_a == hash_b {
return Err(SlashingError::InvalidProposerSlashing(
"headers are identical (no equivocation)".into(),
));
}
let sig_a = decode_sig(&payload.signed_header_a, "a")?;
let sig_b = decode_sig(&payload.signed_header_b, "b")?;
let proposer_index = header_a.proposer_index;
let entry = validator_view
.get(proposer_index)
.ok_or(SlashingError::ValidatorNotRegistered(proposer_index))?;
if entry.is_slashed() {
return Err(SlashingError::InvalidProposerSlashing(format!(
"proposer {proposer_index} is already slashed",
)));
}
if !entry.is_active_at_epoch(header_a.epoch) {
return Err(SlashingError::InvalidProposerSlashing(format!(
"proposer {proposer_index} not active at epoch {}",
header_a.epoch,
)));
}
let pk = entry.public_key();
let msg_a = block_signing_message(network_id, header_a.epoch, &hash_a, proposer_index);
let msg_b = block_signing_message(network_id, header_b.epoch, &hash_b, proposer_index);
if !chia_bls::verify(&sig_a, pk, &msg_a) {
return Err(SlashingError::InvalidProposerSlashing(
"signature A BLS verify failed".into(),
));
}
if !chia_bls::verify(&sig_b, pk, &msg_b) {
return Err(SlashingError::InvalidProposerSlashing(
"signature B BLS verify failed".into(),
));
}
Ok(VerifiedEvidence {
offense_type: evidence.offense_type,
slashable_validator_indices: vec![proposer_index],
})
}
pub fn verify_attester_slashing(
evidence: &SlashingEvidence,
payload: &AttesterSlashing,
validator_view: &dyn ValidatorView,
network_id: &Bytes32,
) -> Result<VerifiedEvidence, SlashingError> {
payload.attestation_a.validate_structure()?;
payload.attestation_b.validate_structure()?;
if payload.attestation_a == payload.attestation_b {
return Err(SlashingError::InvalidAttesterSlashing(
"attestations are byte-identical (no offense)".into(),
));
}
let a_data = &payload.attestation_a.data;
let b_data = &payload.attestation_b.data;
let is_double_vote = a_data.target.epoch == b_data.target.epoch && a_data != b_data;
let is_surround_vote = (a_data.source.epoch < b_data.source.epoch
&& a_data.target.epoch > b_data.target.epoch)
|| (b_data.source.epoch < a_data.source.epoch && b_data.target.epoch > a_data.target.epoch);
if !(is_double_vote || is_surround_vote) {
return Err(SlashingError::AttesterSlashingNotSlashable);
}
let slashable = payload.slashable_indices();
if slashable.is_empty() {
return Err(SlashingError::EmptySlashableIntersection);
}
let pks = ValidatorViewPubkeys(validator_view);
payload.attestation_a.verify_signature(&pks, network_id)?;
payload.attestation_b.verify_signature(&pks, network_id)?;
Ok(VerifiedEvidence {
offense_type: evidence.offense_type,
slashable_validator_indices: slashable,
})
}
pub fn verify_invalid_block(
evidence: &SlashingEvidence,
payload: &InvalidBlockProof,
validator_view: &dyn ValidatorView,
network_id: &Bytes32,
oracle: Option<&dyn InvalidBlockOracle>,
) -> Result<VerifiedEvidence, SlashingError> {
let header = &payload.signed_header.message;
if header.epoch != evidence.epoch {
return Err(SlashingError::InvalidSlashingEvidence(format!(
"epoch mismatch: header={} envelope={}",
header.epoch, evidence.epoch,
)));
}
let witness_len = payload.failure_witness.len();
if witness_len == 0 {
return Err(SlashingError::InvalidSlashingEvidence(
"failure_witness is empty".into(),
));
}
if witness_len > MAX_SLASH_PROPOSAL_PAYLOAD_BYTES {
return Err(SlashingError::InvalidSlashingEvidence(format!(
"failure_witness length {witness_len} exceeds MAX_SLASH_PROPOSAL_PAYLOAD_BYTES ({MAX_SLASH_PROPOSAL_PAYLOAD_BYTES})",
)));
}
let sig_bytes: &[u8; BLS_SIGNATURE_SIZE] = payload
.signed_header
.signature
.as_slice()
.try_into()
.map_err(|_| {
SlashingError::InvalidSlashingEvidence(format!(
"signature width {} != {BLS_SIGNATURE_SIZE}",
payload.signed_header.signature.len(),
))
})?;
let sig = Signature::from_bytes(sig_bytes).map_err(|_| {
SlashingError::InvalidSlashingEvidence("signature failed to decode as BLS G2".into())
})?;
let proposer_index = header.proposer_index;
let entry = validator_view
.get(proposer_index)
.ok_or(SlashingError::ValidatorNotRegistered(proposer_index))?;
if entry.is_slashed() {
return Err(SlashingError::InvalidSlashingEvidence(format!(
"proposer {proposer_index} is already slashed",
)));
}
if !entry.is_active_at_epoch(header.epoch) {
return Err(SlashingError::InvalidSlashingEvidence(format!(
"proposer {proposer_index} not active at epoch {}",
header.epoch,
)));
}
let msg = block_signing_message(network_id, header.epoch, &header.hash(), proposer_index);
let pk = entry.public_key();
if !chia_bls::verify(&sig, pk, &msg) {
return Err(SlashingError::InvalidSlashingEvidence(
"bad invalid-block signature".into(),
));
}
if let Some(oracle) = oracle {
oracle.verify_failure(header, &payload.failure_witness, payload.failure_reason)?;
}
Ok(VerifiedEvidence {
offense_type: evidence.offense_type,
slashable_validator_indices: vec![proposer_index],
})
}
struct ValidatorViewPubkeys<'a>(&'a dyn ValidatorView);
impl<'a> PublicKeyLookup for ValidatorViewPubkeys<'a> {
fn pubkey_of(&self, index: u32) -> Option<&PublicKey> {
self.0.get(index).map(|e| e.public_key())
}
}
fn decode_sig(signed: &SignedBlockHeader, label: &str) -> Result<Signature, SlashingError> {
let sig_bytes: &[u8; BLS_SIGNATURE_SIZE] =
signed.signature.as_slice().try_into().map_err(|_| {
SlashingError::InvalidProposerSlashing(format!(
"signature {label} has width {}, expected {BLS_SIGNATURE_SIZE}",
signed.signature.len(),
))
})?;
Signature::from_bytes(sig_bytes).map_err(|_| {
SlashingError::InvalidProposerSlashing(format!(
"signature {label} failed to decode as BLS G2 element",
))
})
}
pub fn block_signing_message(
network_id: &Bytes32,
epoch: u64,
header_hash: &Bytes32,
proposer_index: u32,
) -> Vec<u8> {
let mut out = Vec::with_capacity(DOMAIN_BEACON_PROPOSER.len() + 32 + 8 + 32 + 4);
out.extend_from_slice(DOMAIN_BEACON_PROPOSER);
out.extend_from_slice(network_id.as_ref());
out.extend_from_slice(&epoch.to_le_bytes());
out.extend_from_slice(header_hash.as_ref());
out.extend_from_slice(&proposer_index.to_le_bytes());
out
}