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}