Skip to main content

dig_slashing/evidence/
envelope.rs

1//! `SlashingEvidence` — outer wrapper that carries offense classification,
2//! reporter identity, epoch, and the per-offense payload.
3//!
4//! Traces to: [SPEC.md §3.5](../../docs/resources/SPEC.md), catalogue rows
5//! [DSL-002](../../docs/requirements/domains/evidence/specs/DSL-002.md) +
6//! [DSL-010](../../docs/requirements/domains/evidence/specs/DSL-010.md) +
7//! [DSL-157](../../docs/requirements/domains/evidence/specs/DSL-157.md).
8//!
9//! # Role
10//!
11//! Everything the lifecycle needs to ingest an offense report flows
12//! through this envelope:
13//!
14//!   - `offense_type` — tags the slash for base-penalty lookup
15//!     (DSL-001) and REMARK magic dispatch (DSL-102).
16//!   - `reporter_validator_index` + `reporter_puzzle_hash` — reward
17//!     routing (DSL-025) and self-accuse short-circuit (DSL-012).
18//!   - `epoch` — drives `OffenseTooOld` check (DSL-011) and bond-escrow
19//!     lifetime.
20//!   - `payload` — the variant-specific fraud proof bytes that verifiers
21//!     re-execute (DSL-013 / DSL-014..017 / DSL-018..020).
22//!
23//! # Content-addressed identity
24//!
25//! [`SlashingEvidence::hash`] (DSL-002) is the primary key for two
26//! runtime structures:
27//!
28//!   - `SlashingManager::processed` — dedup map keyed by envelope hash
29//!     (DSL-026 AlreadySlashed short-circuit).
30//!   - `BondEscrow::Reporter(hash)` — bond tag binding the reporter's
31//!     escrowed bond to the exact envelope they submitted (DSL-023).
32//!
33//! Both structures require bit-exact determinism AND total field coverage:
34//! mutating any byte of the envelope MUST shift the hash, else a reporter
35//! could submit a mutated envelope under a colliding key and double-spend
36//! either the dedup slot or the bond.
37//!
38//! The digest is `SHA-256(DOMAIN_SLASHING_EVIDENCE || bincode(self))`
39//! using `chia_sha2::Sha256` (same hasher as attester signing roots, so
40//! one crypto stack for the whole crate).
41//!
42//! # Per-validator fan-out
43//!
44//! [`SlashingEvidence::slashable_validators`] (DSL-010) returns the list
45//! of validator indices the evidence accuses. Proposer / InvalidBlock
46//! always return `[proposer_index]` (cardinality 1); Attester returns
47//! the sorted `slashable_indices()` intersection (cardinality 0..=N,
48//! DSL-007).
49
50use chia_sha2::Sha256;
51use dig_protocol::Bytes32;
52use serde::{Deserialize, Serialize};
53
54use crate::constants::DOMAIN_SLASHING_EVIDENCE;
55use crate::evidence::attester_slashing::AttesterSlashing;
56use crate::evidence::invalid_block::InvalidBlockProof;
57use crate::evidence::offense::OffenseType;
58use crate::evidence::proposer_slashing::ProposerSlashing;
59
60/// Per-offense fraud-proof payload.
61///
62/// One variant per `OffenseType`, but note that `AttesterDoubleVote` and
63/// `AttesterSurroundVote` share the `Attester` variant: the two predicates
64/// are distinguished by `verify_attester_slashing` (DSL-014 / DSL-015),
65/// not by a payload tag.
66///
67/// Per [SPEC §3.5](../../docs/resources/SPEC.md).
68///
69/// # Enum size
70///
71/// The variants are deliberately asymmetric — `ProposerSlashing` carries
72/// two full `L2BlockHeader`s (~1.5 KB), `Attester` carries two indexed
73/// attestation index-lists (up to 2_048 × 4 bytes each), `InvalidBlock`
74/// carries one header plus witness bytes. We accept the variance because
75/// this enum is itself heap-resident inside `SlashingEvidence` and is
76/// never used in tight loops — boxing would just add indirection to every
77/// `hash()`/`bincode` path without changing the on-wire bytes or the
78/// size of the enclosing `SlashingEvidence`. The wire format is what
79/// matters, not the in-memory layout of a type that only ever lives one
80/// per reporter-submission.
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[allow(clippy::large_enum_variant)]
83pub enum SlashingEvidencePayload {
84    /// Equivocation: two distinct signed headers at the same slot from
85    /// the same proposer (DSL-013).
86    Proposer(ProposerSlashing),
87    /// Double-vote or surround-vote: two IndexedAttestations whose
88    /// slashable-indices intersection is non-empty and whose
89    /// `AttestationData` pair satisfies either predicate
90    /// (DSL-014 / DSL-015).
91    Attester(AttesterSlashing),
92    /// Canonical-validation failure: proposer signed a block that fails
93    /// re-execution (DSL-018 / DSL-019 / DSL-020).
94    InvalidBlock(InvalidBlockProof),
95}
96
97/// Slashing-evidence envelope.
98///
99/// Per [SPEC §3.5](../../docs/resources/SPEC.md). Fields are frozen wire
100/// protocol; their order matters for the deterministic bincode serialization
101/// consumed by [`SlashingEvidence::hash`] — do NOT reorder without bumping
102/// the protocol version.
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
104pub struct SlashingEvidence {
105    /// Classification of the offense. Drives base-penalty lookup
106    /// (DSL-001) and REMARK dispatch (DSL-102).
107    pub offense_type: OffenseType,
108    /// Validator index of the reporter. Receives the whistleblower
109    /// reward (DSL-025). MUST NOT appear in `slashable_validators`
110    /// (DSL-012 self-accuse check).
111    pub reporter_validator_index: u32,
112    /// Puzzle hash the whistleblower reward is paid to (DSL-025).
113    /// Bound into the hash so a malicious reporter cannot swap payout
114    /// addresses after the evidence is admitted.
115    pub reporter_puzzle_hash: Bytes32,
116    /// Epoch at which the offense occurred. Used by DSL-011
117    /// (`OffenseTooOld` lookback check).
118    pub epoch: u64,
119    /// The per-offense payload carrying the fraud-proof bytes.
120    pub payload: SlashingEvidencePayload,
121}
122
123impl SlashingEvidence {
124    /// Content-addressed identity of the envelope.
125    ///
126    /// Implements [DSL-002](../../docs/requirements/domains/evidence/specs/DSL-002.md).
127    /// Traces to SPEC §3.5.
128    ///
129    /// # Construction
130    ///
131    /// ```text
132    /// hash = SHA-256(
133    ///   DOMAIN_SLASHING_EVIDENCE          (24 bytes, "DIG_SLASHING_EVIDENCE_V1")
134    ///   || bincode::serialize(self)       (variable, full envelope encoding)
135    /// )
136    /// ```
137    ///
138    /// Hasher: `chia_sha2::Sha256` (matches attester signing root, DSL-004).
139    ///
140    /// # Invariants (all enforced by the DSL-002 test suite)
141    ///
142    /// - **Deterministic:** identical envelopes always produce identical
143    ///   digests, across runs and processes.
144    /// - **Domain-bound:** `DOMAIN_SLASHING_EVIDENCE` is mixed in as the
145    ///   first input, so the digest cannot collide with any other
146    ///   protocol hash.
147    /// - **Field-covering:** every byte of every field (including the
148    ///   payload enum discriminant + inner variant bytes) participates;
149    ///   mutating any one shifts the digest.
150    ///
151    /// # Why bincode
152    ///
153    /// bincode produces a compact, deterministic encoding with explicit
154    /// length prefixes on variable-length fields. `serde_json` would be
155    /// non-canonical (field-order flexibility, whitespace). Serialization
156    /// failure is treated as unreachable — `SlashingEvidence` contains no
157    /// types bincode cannot encode, so `.expect` is the honest signal on
158    /// a programmer bug rather than a runtime `Result` every caller must
159    /// thread.
160    pub fn hash(&self) -> Bytes32 {
161        let mut h = Sha256::new();
162        h.update(DOMAIN_SLASHING_EVIDENCE);
163        let encoded = bincode::serialize(self).expect("SlashingEvidence bincode must not fail");
164        h.update(&encoded);
165        let out: [u8; 32] = h.finalize();
166        Bytes32::new(out)
167    }
168
169    /// List of validator indices this envelope accuses.
170    ///
171    /// Implements [DSL-010](../../docs/requirements/domains/evidence/specs/DSL-010.md).
172    /// Traces to SPEC §3.5.
173    ///
174    /// # Returns
175    ///
176    /// - `Proposer` / `InvalidBlock` → exactly one index (the
177    ///   `proposer_index` from the signed header).
178    /// - `Attester` → sorted, deduplicated intersection of the two
179    ///   indexed-attestation index lists (DSL-007). Cardinality 0..=N.
180    ///
181    /// Consumed by `verify_evidence` (DSL-012 reporter-self-accuse) and
182    /// `SlashingManager::submit_evidence` per-validator loop (DSL-022).
183    pub fn slashable_validators(&self) -> Vec<u32> {
184        match &self.payload {
185            SlashingEvidencePayload::Proposer(p) => {
186                vec![p.signed_header_a.message.proposer_index]
187            }
188            SlashingEvidencePayload::Attester(a) => a.slashable_indices(),
189            SlashingEvidencePayload::InvalidBlock(i) => {
190                vec![i.signed_header.message.proposer_index]
191            }
192        }
193    }
194}