Skip to main content

dig_slashing/evidence/
offense.rs

1//! `OffenseType` — the four discrete slashable consensus offenses.
2//!
3//! Traces to: [SPEC.md §3.2](../../docs/resources/SPEC.md), catalogue row
4//! [DSL-001](../../docs/requirements/domains/evidence/specs/DSL-001.md).
5//!
6//! Scope reminder: validator slashing only. DFSP / storage-provider slashing
7//! is out of scope for this crate.
8//!
9//! # Design
10//!
11//! Four variants, three BPS floors (both attester variants share
12//! `ATTESTATION_BASE_BPS`). The variant-to-BPS mapping is protocol law — it
13//! lives in `base_penalty_bps()` and nowhere else in the codebase. Downstream
14//! callers (the base-slash formula in `SlashingManager::submit_evidence`,
15//! DSL-022; the reporter-penalty path in `AppealAdjudicator`, DSL-069) query
16//! this method rather than hard-coding the BPS values.
17//!
18//! Serde + `Copy` + `Eq` + `Hash` derives keep the enum cheap to pass by
19//! value through every downstream type (`SlashingEvidence`, `VerifiedEvidence`,
20//! `AppealAdjudicationResult`).
21
22use serde::{Deserialize, Serialize};
23
24use crate::constants::{ATTESTATION_BASE_BPS, EQUIVOCATION_BASE_BPS, INVALID_BLOCK_BASE_BPS};
25
26/// The four slashable consensus offenses.
27///
28/// Per [SPEC §3.2](../../docs/resources/SPEC.md), a validator can be slashed
29/// for exactly one of these reasons on the DIG L2 blockchain. Inactivity
30/// leak is NOT a slashable event — it is continuous accounting
31/// (see `InactivityScoreTracker`, SPEC §9).
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
33pub enum OffenseType {
34    /// Validator signed two different blocks at the same slot.
35    ///
36    /// Evidence: two `SignedBlockHeader`s with matching `slot` and
37    /// `proposer_index` but different message hashes, both with valid BLS
38    /// signatures under the validator's pubkey. Verified by
39    /// `verify_proposer_slashing` (DSL-013).
40    ProposerEquivocation,
41
42    /// Validator proposed a block that fails canonical validation.
43    ///
44    /// Evidence: one `SignedBlockHeader` plus a failure witness that the
45    /// consensus layer (via `InvalidBlockOracle`, DSL-020) can reproduce.
46    /// Verified by `verify_invalid_block` (DSL-018..020).
47    InvalidBlock,
48
49    /// Validator cast two attestations with the same target epoch but
50    /// different data — "double vote" in Ethereum terminology.
51    ///
52    /// Evidence: two `IndexedAttestation`s with `a.data.target.epoch ==
53    /// b.data.target.epoch && a.data != b.data`, both aggregate-signed by
54    /// an overlapping committee. Verified by `verify_attester_slashing`
55    /// double-vote predicate (DSL-014).
56    AttesterDoubleVote,
57
58    /// Validator's attestations form a surround vote — one's FFG span
59    /// strictly contains the other's.
60    ///
61    /// Evidence: two `IndexedAttestation`s where
62    /// `a.source.epoch < b.source.epoch && a.target.epoch > b.target.epoch`
63    /// (or the mirror). Verified by `verify_attester_slashing` surround-vote
64    /// predicate (DSL-015).
65    AttesterSurroundVote,
66}
67
68impl OffenseType {
69    /// Base penalty in basis points (10_000 = 100%) for this offense.
70    ///
71    /// Implements [DSL-001](../../docs/requirements/domains/evidence/specs/DSL-001.md).
72    /// Traces to SPEC §2.1 (BPS constants) and SPEC §3.2 (mapping table).
73    ///
74    /// # Returns
75    ///
76    /// | Variant | Return value | Source constant |
77    /// |---------|-------------|-----------------|
78    /// | `ProposerEquivocation` | 500 | `EQUIVOCATION_BASE_BPS` |
79    /// | `InvalidBlock` | 300 | `INVALID_BLOCK_BASE_BPS` |
80    /// | `AttesterDoubleVote` | 100 | `ATTESTATION_BASE_BPS` |
81    /// | `AttesterSurroundVote` | 100 | `ATTESTATION_BASE_BPS` |
82    ///
83    /// # Invariants
84    ///
85    /// - Return value `< MAX_PENALTY_BPS` (1_000) for every variant.
86    /// - Return value `> 0` for every variant.
87    ///
88    /// Both invariants are enforced by `tests/dsl_001_offense_type_bps_mapping_test.rs`.
89    ///
90    /// # Downstream consumers
91    ///
92    /// - Base-slash formula in `SlashingManager::submit_evidence`
93    ///   (DSL-022): `base_slash = max(eff_bal * base_penalty_bps() / 10_000,
94    ///   eff_bal / MIN_SLASHING_PENALTY_QUOTIENT)`.
95    /// - Reporter-penalty path in `AppealAdjudicator` (DSL-069): uses
96    ///   `InvalidBlock` BPS as the false-evidence cost.
97    ///
98    /// # Why a method, not a `const`
99    ///
100    /// A method keeps the variant-to-BPS mapping a single source of truth
101    /// that `match` exhaustiveness can defend. If a new `OffenseType` variant
102    /// is ever added, the compiler refuses to build until `base_penalty_bps()`
103    /// is updated — which is exactly the review point the protocol wants.
104    pub const fn base_penalty_bps(&self) -> u16 {
105        match self {
106            Self::ProposerEquivocation => EQUIVOCATION_BASE_BPS,
107            Self::InvalidBlock => INVALID_BLOCK_BASE_BPS,
108            Self::AttesterDoubleVote | Self::AttesterSurroundVote => ATTESTATION_BASE_BPS,
109        }
110    }
111}