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}