dig-slashing
Validator slashing, Ethereum-parity attestation participation accounting, continuous inactivity accounting, and optimistic fraud-proof appeals for the DIG Network L2.
- Crate:
dig-slashing v0.1.0
- Edition: 2024
- Spec: docs/resources/SPEC.md (normative; every public symbol traces to a
DSL-NNN requirement)
- Scope: Validator slashing only. Four offenses (ProposerEquivocation, InvalidBlock, AttesterDoubleVote, AttesterSurroundVote). DFSP / storage-provider slashing is out of scope.
Table of Contents
- Design Overview
- Integration at a Glance
- Top-Level Entry Points
- Embedder-Implemented Traits
- Data Types
- Report Types (Outputs)
- Error Surface
- Wire Format (REMARK)
- Validator-Local Slashing Protection
- Constants
- Determinism & Serde Guarantees
- Full Symbol Index
Design Overview
The crate is state-owning but IO-agnostic. It holds three long-lived trackers (SlashingManager, ParticipationTracker, InactivityScoreTracker) bundled in SlashingSystem. Every interaction with validator stake, bond escrow, reward payouts, collateral, and justification state goes through trait objects (&dyn, &mut dyn) that the embedder implements.
Three processing modes:
| Mode |
Entry point |
Trigger |
| Genesis |
SlashingSystem::genesis |
Chain birth |
| Block admission |
process_block_admissions |
Per block, after executing REMARK conditions |
| Epoch boundary |
run_epoch_boundary |
Per epoch, at block N where N % BLOCKS_PER_EPOCH == 0 |
| Reorg |
rewind_all_on_reorg |
Fork-choice moves tip backward |
| Appeal verdict |
adjudicate_appeal |
After verifier emits AppealVerdict |
Optimistic slashing with 8-epoch appeal window (SLASH_APPEAL_WINDOW_EPOCHS). Evidence admits immediately; finalisation is deferred. Appeals filed during the window may revert the slash.
Integration at a Glance
use dig_slashing::{
GenesisParameters, SlashingSystem,
process_block_admissions, run_epoch_boundary, rewind_all_on_reorg,
adjudicate_appeal,
};
let mut sys = SlashingSystem::genesis(&GenesisParameters {
genesis_epoch: 0,
initial_validator_count: 1_024,
network_id: network_id,
});
let block_report = process_block_admissions(
&remark_payloads,
&mut sys.manager,
&mut my_validator_set, &my_balances, &mut my_bond_escrow, &mut my_reward_payout, &my_proposer_view, sys.network_id(),
);
let epoch_report = run_epoch_boundary(
&mut sys.manager,
&mut sys.participation,
&mut sys.inactivity,
&mut my_validator_set,
&my_balances,
&mut my_bond_escrow,
&mut my_reward_payout,
&my_justification_view, current_epoch_ending,
validator_count,
total_active_balance,
);
let reorg_report = rewind_all_on_reorg(
&mut sys.manager,
&mut sys.participation,
&mut sys.inactivity,
&mut my_slashing_protection,
&mut my_validator_set,
&mut my_collateral, &mut my_bond_escrow,
new_tip_epoch,
new_tip_slot,
validator_count,
)?;
let adj_report = adjudicate_appeal(
verdict,
&mut pending,
&appeal,
&mut my_validator_set,
&my_balances,
Some(&mut my_collateral),
&mut my_bond_escrow,
&mut my_reward_payout,
&mut my_reward_clawback, &mut my_slashed_in_window, proposer_puzzle_hash,
reason_hash,
current_epoch,
)?;
Top-Level Entry Points
All single-call. Inputs are typed state + trait-object handles. Outputs are typed reports with Serialize + Deserialize + PartialEq + Eq.
SlashingSystem::genesis
Bootstrap at chain birth (DSL-128 / DSL-170).
pub fn genesis(params: &GenesisParameters) -> SlashingSystem;
pub fn network_id(&self) -> &Bytes32;
Input: &GenesisParameters { genesis_epoch: u64, initial_validator_count: usize, network_id: Bytes32 }.
Output: SlashingSystem { manager, participation, inactivity, network_id }.
process_block_admissions
Single-call block-level REMARK admission dispatcher (DSL-168).
pub fn process_block_admissions<P: AsRef<[u8]>>(
payloads: &[P], manager: &mut SlashingManager,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
proposer: &dyn ProposerView,
network_id: &Bytes32,
) -> BlockAdmissionReport;
Evidence REMARKs process before appeal REMARKs so a same-block appeal can reference a same-block evidence admission. Per-envelope failures populate rejected vecs; block-cap overflow truncates + counts. Never aborts the block outright.
run_epoch_boundary
Fixed 8-step per-epoch pipeline (DSL-127 / DSL-169). Order is normative: flag deltas → inactivity score update → inactivity penalties → finalise expired slashes → rotate participation → advance manager epoch → resize trackers → prune old state. Rewards are routed through RewardPayout::pay; inactivity penalties debit validator stake via ValidatorEntry::slash_absolute.
pub fn run_epoch_boundary(
manager: &mut SlashingManager,
participation: &mut ParticipationTracker,
inactivity: &mut InactivityScoreTracker,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
justification: &dyn JustificationView,
current_epoch_ending: u64,
validator_count: usize,
total_active_balance: u64,
) -> EpochBoundaryReport;
rewind_all_on_reorg
Global fork-choice reorg (DSL-130). Rewinds manager → participation → inactivity → slashing protection in fixed order. Returns Err(ReorgTooDeep) when current_epoch - new_tip_epoch > CORRELATION_WINDOW_EPOCHS (36).
pub fn rewind_all_on_reorg(
manager: &mut SlashingManager,
participation: &mut ParticipationTracker,
inactivity: &mut InactivityScoreTracker,
protection: &mut SlashingProtection,
validator_set: &mut dyn ValidatorView,
collateral: &mut dyn CollateralSlasher,
bond_escrow: &mut dyn BondEscrow,
new_tip_epoch: u64,
new_tip_slot: u64,
validator_count: usize,
) -> Result<ReorgReport, SlashingError>;
adjudicate_appeal
Appeal adjudication dispatcher (DSL-167). Composes DSL-064..073 slice functions into one end-to-end pass. Sustained branch: revert base slash → revert collateral → restore status → clawback rewards → forfeit reporter bond → absorb shortfall → reporter penalty → status-reverted. Rejected branch: forfeit appellant bond → challenge-open.
pub fn adjudicate_appeal(
verdict: AppealVerdict,
pending: &mut PendingSlash,
appeal: &SlashAppeal,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
collateral: Option<&mut dyn CollateralSlasher>,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
reward_clawback: &mut dyn RewardClawback,
slashed_in_window: &mut BTreeMap<(u64, u32), u64>,
proposer_puzzle_hash: Bytes32,
reason_hash: Bytes32,
current_epoch: u64,
) -> Result<AppealAdjudicationResult, BondError>;
SlashingManager single-envelope methods
impl SlashingManager {
pub fn new(current_epoch: u64) -> Self;
pub fn current_epoch(&self) -> u64;
pub fn set_epoch(&mut self, epoch: u64);
pub fn submit_evidence(
&mut self,
evidence: SlashingEvidence,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
bond_escrow: &mut dyn BondEscrow,
reward_payout: &mut dyn RewardPayout,
proposer: &dyn ProposerView,
network_id: &Bytes32,
) -> Result<SlashingResult, SlashingError>;
pub fn submit_appeal(
&mut self,
appeal: &SlashAppeal,
bond_escrow: &mut dyn BondEscrow,
) -> Result<(), SlashingError>;
pub fn finalise_expired_slashes(
&mut self,
validator_set: &mut dyn ValidatorView,
effective_balances: &dyn EffectiveBalanceView,
bond_escrow: &mut dyn BondEscrow,
total_active_balance: u64,
) -> Vec<FinalisationResult>;
pub fn rewind_on_reorg(
&mut self,
new_tip_epoch: u64,
validator_set: &mut dyn ValidatorView,
collateral: Option<&mut dyn CollateralSlasher>,
bond_escrow: &mut dyn BondEscrow,
) -> Vec<Bytes32>;
pub fn is_processed(&self, hash: &Bytes32) -> bool;
pub fn is_slashed(&self, idx: u32, vs: &dyn ValidatorView) -> bool;
pub fn is_slashed_in_window(&self, epoch: u64, idx: u32) -> bool;
pub fn pending(&self, hash: &Bytes32) -> Option<&PendingSlash>;
pub fn book(&self) -> &PendingSlashBook;
pub fn book_mut(&mut self) -> &mut PendingSlashBook;
pub fn processed_epoch(&self, hash: &Bytes32) -> Option<u64>;
pub fn prune(&mut self, before_epoch: u64) -> usize;
pub fn prune_processed_older_than(&mut self, cutoff_epoch: u64) -> usize;
pub fn mark_processed(&mut self, hash: Bytes32, epoch: u64);
pub fn mark_slashed_in_window(&mut self, epoch: u64, idx: u32, effective_balance: u64);
}
Embedder-Implemented Traits
The embedder supplies concrete types implementing these traits. All are consumed via &dyn / &mut dyn so generics never leak.
ValidatorView + ValidatorEntry
Active validator set + per-validator state accessors.
pub trait ValidatorView {
fn get(&self, index: u32) -> Option<&dyn ValidatorEntry>;
fn get_mut(&mut self, index: u32) -> Option<&mut dyn ValidatorEntry>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool { self.len() == 0 }
}
pub trait ValidatorEntry {
fn public_key(&self) -> &chia_bls::PublicKey;
fn puzzle_hash(&self) -> Bytes32;
fn effective_balance(&self) -> u64;
fn is_slashed(&self) -> bool;
fn activation_epoch(&self) -> u64;
fn exit_epoch(&self) -> u64;
fn is_active_at_epoch(&self, epoch: u64) -> bool;
fn slash_absolute(&mut self, amount_mojos: u64, epoch: u64) -> u64;
fn credit_stake(&mut self, amount_mojos: u64) -> u64;
fn restore_status(&mut self) -> bool;
fn schedule_exit(&mut self, epoch: u64);
}
EffectiveBalanceView
pub trait EffectiveBalanceView {
fn get(&self, index: u32) -> u64;
fn total_active(&self) -> u64;
}
BondEscrow + BondTag + BondError
Reporter + appellant bond accounting.
pub enum BondTag {
Reporter(Bytes32), Appellant(Bytes32), }
pub trait BondEscrow {
fn lock(&mut self, principal_idx: u32, amount: u64, tag: BondTag) -> Result<(), BondError>;
fn release(&mut self, principal_idx: u32, amount: u64, tag: BondTag) -> Result<(), BondError>;
fn forfeit(&mut self, principal_idx: u32, amount: u64, tag: BondTag) -> Result<u64, BondError>;
fn escrowed(&self, principal_idx: u32, tag: BondTag) -> u64;
}
RewardPayout + RewardClawback
pub trait RewardPayout {
fn pay(&mut self, principal_ph: Bytes32, amount_mojos: u64);
}
pub trait RewardClawback {
fn claw_back(&mut self, principal_ph: Bytes32, amount: u64) -> u64; }
CollateralSlasher + CollateralError
Optional (light-client embedders pass None where the signature allows).
pub trait CollateralSlasher {
fn credit(&mut self, validator_idx: u32, amount_mojos: u64);
fn slash(&mut self, _: u32, _: u64, _: u64) -> Result<(u64, u64), CollateralError> {
Err(CollateralError::NoCollateral)
}
}
ProposerView
pub trait ProposerView {
fn proposer_at_slot(&self, slot: u64) -> Option<u32>;
fn current_slot(&self) -> u64;
}
JustificationView
pub trait JustificationView {
fn latest_finalized_epoch(&self) -> u64;
fn current_justified_checkpoint(&self) -> Checkpoint { ... }
fn previous_justified_checkpoint(&self) -> Checkpoint { ... }
fn finalized_checkpoint(&self) -> Checkpoint { ... }
fn canonical_block_root_at_slot(&self, _: u64) -> Option<Bytes32> { None }
fn canonical_target_root_for_epoch(&self, _: u64) -> Option<Bytes32> { None }
}
InvalidBlockOracle + ExecutionOutcome
Deterministic re-execution for invalid-block evidence + appeals.
pub enum ExecutionOutcome {
Valid,
Invalid(InvalidBlockReason),
}
pub trait InvalidBlockOracle {
fn re_execute(&self, header: &SignedBlockHeader, witness: &[u8]) -> ExecutionOutcome;
fn verify_failure(
&self,
_: &SignedBlockHeader,
_: InvalidBlockReason,
_: &[u8],
) -> Result<(), ()> { Ok(()) }
}
PublicKeyLookup
Blanket impl over T: ValidatorView + ?Sized — any ValidatorView is automatically a PublicKeyLookup. Consumers: DSL-006/013 verifiers.
Data Types
Evidence envelopes (admission-side)
pub struct SlashingEvidence {
pub offense_type: OffenseType,
pub reporter_validator_index: u32,
pub reporter_puzzle_hash: Bytes32,
pub epoch: u64,
pub payload: SlashingEvidencePayload,
}
pub enum SlashingEvidencePayload {
Proposer(ProposerSlashing),
Attester(AttesterSlashing),
InvalidBlock(InvalidBlockProof),
}
pub enum OffenseType {
ProposerEquivocation,
InvalidBlock,
AttesterDoubleVote,
AttesterSurroundVote,
}
pub enum InvalidBlockReason {
BadStateRoot, BadTxRoot, BadReceiptRoot, BadGasUsed,
BadBloom, BadDifficulty, BadExtraData, BadTimestamp,
}
impl SlashingEvidence {
pub fn hash(&self) -> Bytes32; pub fn slashable_validators(&self) -> Vec<u32>; }
Per-payload:
pub struct ProposerSlashing {
pub signed_header_a: SignedBlockHeader,
pub signed_header_b: SignedBlockHeader,
}
pub struct AttesterSlashing {
pub attestation_a: IndexedAttestation,
pub attestation_b: IndexedAttestation,
}
pub struct InvalidBlockProof {
pub signed_header: SignedBlockHeader,
#[serde(with = "serde_bytes")] pub failure_witness: Vec<u8>,
pub failure_reason: InvalidBlockReason,
}
pub struct SignedBlockHeader {
pub message: dig_block::L2BlockHeader,
#[serde(with = "serde_bytes")] pub signature: Vec<u8>, }
pub struct IndexedAttestation {
pub attesting_indices: Vec<u32>, pub data: AttestationData,
#[serde(with = "serde_bytes")] pub signature: Vec<u8>,
}
pub struct AttestationData {
pub slot: u64,
pub index: u32,
pub beacon_block_root: Bytes32,
pub source: Checkpoint,
pub target: Checkpoint,
}
pub struct Checkpoint {
pub epoch: u64,
pub root: Bytes32,
}
pub struct VerifiedEvidence {
pub offense_type: OffenseType,
pub slashable_validator_indices: Vec<u32>,
}
Appeal envelopes
pub struct SlashAppeal {
pub evidence_hash: Bytes32,
pub appellant_index: u32,
pub appellant_puzzle_hash: Bytes32,
pub filed_epoch: u64,
pub payload: SlashAppealPayload,
}
pub enum SlashAppealPayload {
Proposer(ProposerSlashingAppeal),
Attester(AttesterSlashingAppeal),
InvalidBlock(InvalidBlockAppeal),
}
pub struct ProposerSlashingAppeal {
pub ground: ProposerAppealGround,
#[serde(with = "serde_bytes")] pub witness: Vec<u8>,
}
pub struct AttesterSlashingAppeal {
pub ground: AttesterAppealGround,
#[serde(with = "serde_bytes")] pub witness: Vec<u8>,
}
pub struct InvalidBlockAppeal {
pub ground: InvalidBlockAppealGround,
#[serde(with = "serde_bytes")] pub witness: Vec<u8>,
}
pub enum ProposerAppealGround {
HeadersIdentical,
ProposerIndexMismatch,
SignatureAInvalid,
SignatureBInvalid,
SlotMismatch,
ValidatorNotActiveAtEpoch,
}
pub enum AttesterAppealGround {
AttestationsIdentical,
NotSlashableByPredicate,
EmptyIntersection,
SignatureAInvalid,
SignatureBInvalid,
InvalidIndexedAttestationStructure,
ValidatorNotInIntersection { validator_index: u32 },
}
pub enum InvalidBlockAppealGround {
BlockActuallyValid,
ProposerSignatureInvalid,
FailureReasonMismatch,
EvidenceEpochMismatch,
}
impl SlashAppeal {
pub fn hash(&self) -> Bytes32; }
pub enum AppealVerdict {
Sustained { reason: AppealSustainReason },
Rejected { reason: AppealRejectReason },
}
impl AppealVerdict {
pub fn to_appeal_outcome(&self) -> AppealOutcome; }
Pending slash + lifecycle
pub struct PendingSlash {
pub evidence_hash: Bytes32,
pub evidence: SlashingEvidence,
pub verified: VerifiedEvidence,
pub status: PendingSlashStatus,
pub submitted_at_epoch: u64,
pub window_expires_at_epoch: u64,
pub base_slash_per_validator: Vec<PerValidatorSlash>,
pub reporter_bond_mojos: u64,
pub appeal_history: Vec<AppealAttempt>,
}
pub enum PendingSlashStatus {
Accepted,
ChallengeOpen { first_appeal_filed_epoch: u64, appeal_count: u8 },
Reverted { winning_appeal_hash: Bytes32, reverted_at_epoch: u64 },
Finalised { finalised_at_epoch: u64 },
}
pub struct AppealAttempt {
pub appeal_hash: Bytes32,
pub appellant_index: u32,
pub filed_epoch: u64,
pub outcome: AppealOutcome,
pub bond_mojos: u64,
}
pub enum AppealOutcome {
Won,
Lost { reason_hash: Bytes32 },
Pending,
}
pub struct PendingSlashBook { }
impl PendingSlashBook {
pub fn new(capacity: usize) -> Self;
pub fn insert(&mut self, record: PendingSlash) -> Result<(), SlashingError>;
pub fn get(&self, hash: &Bytes32) -> Option<&PendingSlash>;
pub fn get_mut(&mut self, hash: &Bytes32) -> Option<&mut PendingSlash>;
pub fn remove(&mut self, hash: &Bytes32) -> Option<PendingSlash>;
pub fn expired_by(&self, current_epoch: u64) -> Vec<Bytes32>;
pub fn submitted_after(&self, new_tip_epoch: u64) -> Vec<Bytes32>;
pub fn len(&self) -> usize;
pub fn capacity(&self) -> usize;
}
Participation / Inactivity
pub struct ParticipationFlags(pub u8);
pub struct ParticipationTracker { }
impl ParticipationTracker {
pub fn new(validator_count: usize, initial_epoch: u64) -> Self;
pub fn current_epoch_number(&self) -> u64;
pub fn current_flags(&self, idx: u32) -> Option<ParticipationFlags>;
pub fn previous_flags(&self, idx: u32) -> Option<ParticipationFlags>;
pub fn validator_count(&self) -> usize;
pub fn rotate_epoch(&mut self, new_epoch: u64, validator_count: usize);
pub fn rewind_on_reorg(&mut self, new_tip_epoch: u64, validator_count: usize) -> u64;
pub fn record_attestation(
&mut self,
data: &AttestationData,
attesting_indices: &[u32],
flags: ParticipationFlags,
) -> Result<(), ParticipationError>;
}
pub struct InactivityScoreTracker { }
impl InactivityScoreTracker {
pub fn new(validator_count: usize) -> Self;
pub fn score(&self, idx: u32) -> Option<u64>;
pub fn set_score(&mut self, idx: u32, score: u64) -> bool;
pub fn validator_count(&self) -> usize;
pub fn update_for_epoch(&mut self, participation: &ParticipationTracker, in_finality_stall: bool);
pub fn epoch_penalties(
&self,
effective_balances: &dyn EffectiveBalanceView,
in_finality_stall: bool,
) -> Vec<(u32, u64)>;
pub fn resize_for(&mut self, validator_count: usize);
pub fn rewind_on_reorg(&mut self, depth: u64) -> u64;
}
pub struct FlagDelta {
pub validator_index: u32,
pub reward: u64,
pub penalty: u64,
}
pub fn compute_flag_deltas(
participation: &ParticipationTracker,
effective_balances: &dyn EffectiveBalanceView,
total_active_balance: u64,
in_finality_stall: bool,
) -> Vec<FlagDelta>;
pub fn base_reward(effective_balance: u64, total_active_balance: u64) -> u64;
pub fn classify_timeliness(inclusion_delay: u64, source_epoch_distance: u64) -> ParticipationFlags;
pub fn proposer_inclusion_reward(attester_base_reward: u64) -> u64;
pub fn in_finality_stall(current_epoch: u64, latest_finalized_epoch: u64) -> bool;
Report Types (Outputs)
All reports derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq); most also Default. Every field serde-roundtrips byte-exact via bincode + serde_json (DSL-163/164/165/167/168).
SlashingResult — output of submit_evidence
pub struct SlashingResult {
pub per_validator: Vec<PerValidatorSlash>,
pub whistleblower_reward: u64,
pub proposer_reward: u64,
pub burn_amount: u64,
pub reporter_bond_escrowed: u64,
pub pending_slash_hash: Bytes32,
}
pub struct PerValidatorSlash {
pub validator_index: u32,
pub base_slash_amount: u64,
pub effective_balance_at_slash: u64,
pub collateral_slashed: u64,
}
FinalisationResult — output of finalise_expired_slashes (one per finalised pending slash)
pub struct FinalisationResult {
pub evidence_hash: Bytes32,
pub per_validator_correlation_penalty: Vec<(u32, u64)>,
pub reporter_bond_returned: u64,
pub exit_lock_until_epoch: u64,
}
EpochBoundaryReport — output of run_epoch_boundary
pub struct EpochBoundaryReport {
pub flag_deltas: Vec<FlagDelta>,
pub inactivity_penalties: Vec<(u32, u64)>,
pub finalisations: Vec<FinalisationResult>,
pub in_finality_stall: bool,
pub pruned_entries: usize,
}
ReorgReport — output of rewind_all_on_reorg
pub struct ReorgReport {
pub rewound_pending_slashes: Vec<Bytes32>,
pub participation_epochs_dropped: u64,
pub inactivity_epochs_dropped: u64,
pub protection_rewound: bool,
}
BlockAdmissionReport — output of process_block_admissions
pub struct BlockAdmissionReport {
pub admitted_evidences: Vec<(Bytes32, SlashingResult)>,
pub rejected_evidences: Vec<(Bytes32, SlashingError)>,
pub admitted_appeals: Vec<Bytes32>,
pub rejected_appeals: Vec<(Bytes32, SlashingError)>,
pub cap_dropped_evidences: usize,
pub cap_dropped_appeals: usize,
}
AppealAdjudicationResult — output of adjudicate_appeal
pub struct AppealAdjudicationResult {
pub appeal_hash: Bytes32,
pub evidence_hash: Bytes32,
pub outcome: AppealOutcome,
pub reverted_stake_mojos: Vec<(u32, u64)>,
pub reverted_collateral_mojos: Vec<(u32, u64)>,
pub clawback_shortfall: u64,
pub reporter_bond_forfeited: u64,
pub appellant_award_mojos: u64,
pub reporter_penalty_mojos: u64,
pub appellant_bond_forfeited: u64,
pub reporter_award_mojos: u64,
pub burn_amount: u64,
}
Adjudicator intermediate structs (exposed for consumers of the slice functions)
pub struct ClawbackResult { pub wb_amount: u64, pub prop_amount: u64, pub wb_clawed: u64, pub prop_clawed: u64, pub shortfall: u64 }
pub struct BondSplitResult { pub forfeited: u64, pub winner_award: u64, pub burn: u64 }
pub struct ShortfallAbsorption { pub clawback_shortfall: u64, pub original_burn: u64, pub final_burn: u64, pub residue: u64 }
pub struct ReporterPenalty { pub reporter_index: u32, pub effective_balance_at_slash: u64, pub penalty_mojos: u64 }
Error Surface
One enum, flat match exhaustive. Variants derive Debug + Clone + PartialEq + Eq + Error + Serialize + Deserialize.
pub enum SlashingError {
OffenseTooOld { offense_epoch: u64, current_epoch: u64 },
ReporterIsAccused(u32),
InvalidProposerSlashing(String),
InvalidAttesterSlashing(String),
InvalidBlockProofRejected(String),
InvalidIndexedAttestation(String),
InvalidBlockPredicateFailed(String),
ValidatorNotRegistered(u32),
BlsVerificationFailed,
AlreadySlashed,
DuplicateEvidence,
DuplicateAppeal,
PendingBookFull,
BondLockFailed,
BlockCapExceeded { actual: usize, limit: usize },
EvidencePayloadTooLarge { actual: usize, limit: usize },
AppealPayloadTooLarge { actual: usize, limit: usize },
UnknownEvidence(String),
AppealWindowExpired { submitted_at: u64, window: u64, current: u64 },
SlashAlreadyReverted,
SlashAlreadyFinalised,
VariantMismatch,
MaxAppealAttemptsExceeded,
AdmissionPuzzleHashMismatch,
ReorgTooDeep { depth: u64, limit: u64 },
}
BondError is separate, propagated from BondEscrow trait operations:
pub enum BondError {
InsufficientBalance { have: u64, need: u64 },
TagNotFound { tag: BondTag },
DoubleLock { tag: BondTag },
}
Wire Format (REMARK)
Evidence + appeals travel on-chain as REMARK conditions. Each payload = magic prefix || serde_json(envelope).
pub const SLASH_EVIDENCE_REMARK_MAGIC_V1: &[u8] = b"DIG_SLASH_EVIDENCE_V1\0";
pub const SLASH_APPEAL_REMARK_MAGIC_V1: &[u8] = b"DIG_SLASH_APPEAL_V1\0";
pub fn encode_slashing_evidence_remark_payload_v1(ev: &SlashingEvidence) -> serde_json::Result<Vec<u8>>;
pub fn encode_slash_appeal_remark_payload_v1(ap: &SlashAppeal) -> serde_json::Result<Vec<u8>>;
pub fn parse_slashing_evidence_from_conditions<P: AsRef<[u8]>>(payloads: &[P]) -> Vec<SlashingEvidence>;
pub fn parse_slash_appeals_from_conditions<P: AsRef<[u8]>>(payloads: &[P]) -> Vec<SlashAppeal>;
pub fn slashing_evidence_remark_puzzle_reveal_v1(ev: &SlashingEvidence) -> Vec<u8>;
pub fn slashing_evidence_remark_puzzle_hash_v1(ev: &SlashingEvidence) -> Bytes32;
pub fn slash_appeal_remark_puzzle_reveal_v1(ap: &SlashAppeal) -> Vec<u8>;
pub fn slash_appeal_remark_puzzle_hash_v1(ap: &SlashAppeal) -> Bytes32;
pub fn enforce_slashing_evidence_remark_admission(...) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_remark_admission(...) -> Result<(), SlashingError>;
pub fn enforce_slashing_evidence_mempool_policy(...) -> Result<(), SlashingError>;
pub fn enforce_slashing_evidence_mempool_dedup_policy(pending: &[SlashingEvidence], incoming: &[SlashingEvidence]) -> Result<(), SlashingError>;
pub fn enforce_slashing_evidence_payload_cap(ev: &SlashingEvidence) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_mempool_policy(...) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_mempool_dedup_policy(pending: &[SlashAppeal], incoming: &[SlashAppeal]) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_payload_cap(ap: &SlashAppeal) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_window_policy(...) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_terminal_status_policy(...) -> Result<(), SlashingError>;
pub fn enforce_slash_appeal_variant_policy(...) -> Result<(), SlashingError>;
pub fn enforce_block_level_slashing_caps(evidences: &[SlashingEvidence]) -> Result<(), SlashingError>;
pub fn enforce_block_level_appeal_caps(appeals: &[SlashAppeal]) -> Result<(), SlashingError>;
Validator-Local Slashing Protection
Prevents a validator's OWN keys from producing slashable evidence (double-propose, double-vote, surround-vote). Persisted independently of consensus state.
pub struct SlashingProtection { }
impl SlashingProtection {
pub fn new() -> Self;
pub fn check_proposal_slot(&self, slot: u64) -> bool;
pub fn record_proposal(&mut self, slot: u64);
pub fn last_proposed_slot(&self) -> u64;
pub fn check_attestation(
&self,
source_epoch: u64,
target_epoch: u64,
block_hash: &Bytes32,
) -> bool;
pub fn record_attestation(&mut self, source_epoch: u64, target_epoch: u64, block_hash: Bytes32);
pub fn last_attested_source_epoch(&self) -> u64;
pub fn last_attested_target_epoch(&self) -> u64;
pub fn last_attested_block_hash(&self) -> Option<&str>;
pub fn rewind_proposal_to_slot(&mut self, new_tip_slot: u64);
pub fn rewind_attestation_to_epoch(&mut self, new_tip_epoch: u64);
pub fn reconcile_with_chain_tip(&mut self, tip_slot: u64, tip_epoch: u64);
pub fn save(&self, path: &Path) -> std::io::Result<()>;
pub fn load(path: &Path) -> std::io::Result<Self>;
}
Constants
Re-exported from dig_slashing::constants:
| Constant |
Value |
Role |
MIN_EFFECTIVE_BALANCE |
32_000_000_000 |
Baseline stake (mojos) |
BPS_DENOMINATOR |
10_000 |
Basis-point scale |
EQUIVOCATION_BASE_BPS |
500 |
Proposer eq. base rate (5%) |
INVALID_BLOCK_BASE_BPS |
300 |
Invalid-block + reporter-penalty rate (3%) |
ATTESTATION_BASE_BPS |
100 |
Attester double/surround rate (1%) |
MAX_PENALTY_BPS |
10_000 |
100% cap |
MIN_SLASHING_PENALTY_QUOTIENT |
32 |
Floor divisor (eff / 32) |
PROPORTIONAL_SLASHING_MULTIPLIER |
3 |
Correlation penalty multiplier |
SLASH_APPEAL_WINDOW_EPOCHS |
8 |
Appeal window |
SLASH_LOCK_EPOCHS |
100 |
Exit lock after finalisation |
MAX_APPEAL_ATTEMPTS_PER_SLASH |
8 |
Per-pending cap |
MAX_PENDING_SLASHES |
4_096 |
Book capacity |
MAX_SLASH_PROPOSALS_PER_BLOCK |
64 |
Per-block evidence cap |
MAX_APPEALS_PER_BLOCK |
64 |
Per-block appeal cap |
MAX_SLASH_PROPOSAL_PAYLOAD_BYTES |
65_536 |
Evidence payload cap |
MAX_APPEAL_PAYLOAD_BYTES |
131_072 |
Appeal payload cap (2× evidence) |
REPORTER_BOND_MOJOS |
MIN_EFFECTIVE_BALANCE / 64 |
Reporter bond |
APPELLANT_BOND_MOJOS |
MIN_EFFECTIVE_BALANCE / 64 |
Appellant bond |
BOND_AWARD_TO_WINNER_BPS |
5_000 |
50% of forfeited bond |
WHISTLEBLOWER_REWARD_QUOTIENT |
512 |
total_eff / 512 |
PROPOSER_REWARD_QUOTIENT |
8 |
wb / 8 |
WEIGHT_DENOMINATOR |
64 |
Participation weight scale |
TIMELY_SOURCE_WEIGHT |
14 |
source-hit share |
TIMELY_TARGET_WEIGHT |
26 |
target-hit share |
TIMELY_HEAD_WEIGHT |
14 |
head-hit share |
PROPOSER_WEIGHT |
8 |
proposer-inclusion share |
TIMELY_SOURCE_FLAG_INDEX |
0 |
bit position |
TIMELY_TARGET_FLAG_INDEX |
1 |
bit position |
TIMELY_HEAD_FLAG_INDEX |
2 |
bit position |
BASE_REWARD_FACTOR |
64 |
reward scaling |
INACTIVITY_PENALTY_QUOTIENT |
16_777_216 |
eff × score / quot |
INACTIVITY_SCORE_BIAS |
4 |
miss-in-stall increment |
INACTIVITY_SCORE_RECOVERY_RATE |
16 |
out-of-stall decrement |
MIN_EPOCHS_TO_INACTIVITY_PENALTY |
4 |
stall threshold |
MIN_ATTESTATION_INCLUSION_DELAY |
1 |
earliest-include delay |
TIMELY_SOURCE_MAX_DELAY_SLOTS |
5 |
source deadline |
TIMELY_TARGET_MAX_DELAY_SLOTS |
32 |
target deadline |
MAX_VALIDATORS_PER_COMMITTEE |
2_048 |
per-committee cap |
BLS_SIGNATURE_SIZE |
96 |
BLS sig wire bytes |
BLS_PUBLIC_KEY_SIZE |
48 |
BLS pubkey wire bytes |
DOMAIN_BEACON_PROPOSER |
b"..." |
Proposer signing-message domain |
DOMAIN_BEACON_ATTESTER |
b"..." |
Attester signing-message domain |
DOMAIN_SLASHING_EVIDENCE |
b"DIG_SLASH_EVIDENCE_V1\0" |
Evidence hash domain |
DOMAIN_SLASH_APPEAL |
b"DIG_SLASH_APPEAL_V1" |
Appeal hash domain |
SLASH_EVIDENCE_REMARK_MAGIC_V1 |
b"DIG_SLASH_EVIDENCE_V1\0" |
REMARK magic prefix |
SLASH_APPEAL_REMARK_MAGIC_V1 |
b"DIG_SLASH_APPEAL_V1\0" |
REMARK magic prefix |
SLASH_LOOKBACK_EPOCHS |
re-exported from dig_epoch |
Max offense age |
Determinism & Serde Guarantees
- Content addressing —
SlashingEvidence::hash() (DSL-002) + SlashAppeal::hash() (DSL-159) are SHA-256(DOMAIN || bincode(envelope)). Deterministic across runs; any one-bit field mutation shifts the digest.
- Byte-exact serde — every
Serialize + Deserialize type round-trips byte-exact via bincode + serde_json. Witness/signature fields use #[serde(with = "serde_bytes")] for binary-tight encoding under bincode.
- Fixed step order —
run_epoch_boundary (8 steps, DSL-127) + rewind_all_on_reorg (4 steps, DSL-130) + adjudicate_appeal sustained branch (8 steps) all execute in normative order pinned by per-DSL tests.
- Saturating arithmetic — correlation penalty (DSL-030/151), inactivity penalty (DSL-092), clawback (DSL-142), slash_absolute (DSL-131) all saturate rather than wrap.
- No custom crypto — BLS via
chia_bls, SHA-256 via chia_sha2, Merkle via chia_sdk_types, canonical bytes via dig_block::block_signing_message + AttestationData::signing_root.
Full Symbol Index
171 DSL-NNN requirements, each with a dedicated tests/dsl_NNN_<name>_test.rs file. See:
Phase breakdown:
| Phase |
DSL range |
Domain |
Status |
| 0 |
001..021 |
Evidence |
✅ |
| 1 |
022..033 |
Lifecycle (admission + finalise) |
✅ |
| 2 |
034..073 |
Appeal (grounds + adjudication) |
✅ |
| 3 |
074..086 |
Participation (Altair) |
✅ |
| 4 |
087..093 |
Inactivity (Bellatrix) |
✅ |
| 5 |
094..101 |
Slashing protection |
✅ |
| 6 |
102..120 |
REMARK wire + policy |
✅ |
| 7 |
121..126 |
Bond accounting |
✅ |
| 8 |
127..130 |
Orchestration |
✅ |
| 9 |
131..145 |
Embedder trait contracts |
✅ |
| 10 |
146..156 |
Gap fills 1 (defensive ops) |
✅ |
| 11 |
157..166 |
Gap fills 2 (serde + BondTag) |
✅ |
| 12 |
167..171 |
Integration closures (dispatchers) |
✅ |
| Total |
171 |
|
171 ✅ |
Upstream Dependencies (must-use)
| Purpose |
Crate |
| BLS signatures |
chia-bls 0.26 |
| SHA-256 |
chia-sha2 0.26 |
| Merkle + run_puzzle (dev) |
chia-sdk-types 0.30 |
| CLVM tree hash |
clvm-utils 0.26 |
| Block types + signing message |
dig-block 0.1 |
| Epoch arithmetic |
dig-epoch 0.1 |
| Network constants |
dig-constants 0.1 |
The crate NEVER reimplements primitives available upstream. Never use custom BLS / SHA / Merkle / epoch math — only the above.