dig_slashing/traits.rs
1//! External-state traits.
2//!
3//! Traces to: [SPEC.md §15.2](../docs/resources/SPEC.md), catalogue rows
4//! [DSL-131..145](../docs/requirements/domains/).
5//!
6//! # Role
7//!
8//! The crate does NOT own validator state — it is consumed by external
9//! runtimes (node, validator, fork-choice) that do. This module exposes
10//! the narrow trait surface the crate reads through.
11//!
12//! Each trait is defined in the single DSL-NNN that introduces its first
13//! consumer, with the blanket / concrete impl landing later under the
14//! DSL-131..145 Phase 9 tasks.
15
16use chia_bls::PublicKey;
17use dig_block::L2BlockHeader;
18use dig_protocol::Bytes32;
19
20use crate::error::SlashingError;
21use crate::evidence::invalid_block::InvalidBlockReason;
22
23/// Validator-index → BLS public-key lookup.
24///
25/// Traces to [SPEC §15.2](../../docs/resources/SPEC.md), catalogue row
26/// [DSL-138](../../docs/requirements/domains/).
27///
28/// # Consumers
29///
30/// - `IndexedAttestation::verify_signature` (DSL-006) materializes the
31/// pubkey set for the aggregate BLS verify by looking up every
32/// `attesting_indices[i]`.
33/// - `verify_proposer_slashing` / `verify_invalid_block` (DSL-013 /
34/// DSL-018) fetch the single proposer pubkey per offense.
35///
36/// # Return semantics
37///
38/// `pubkey_of(idx)` returns `None` when `idx` does not correspond to a
39/// registered validator — the caller is responsible for translating
40/// that to a domain-appropriate error. For BLS verify, a missing
41/// pubkey collapses to aggregate-verify failure (DSL-006), which
42/// matches the security model: we do not want to distinguish "unknown
43/// validator" from "bad signature" at this layer because both are
44/// equally invalid evidence.
45pub trait PublicKeyLookup {
46 /// Look up the BLS G1 public key for `index`. Returns `None` if no
47 /// validator is registered at that slot.
48 fn pubkey_of(&self, index: u32) -> Option<&PublicKey>;
49}
50
51/// Blanket: any `ValidatorView` is a `PublicKeyLookup`. DSL-138.
52///
53/// Delegates `pubkey_of(idx)` to `self.get(idx).map(|e|
54/// e.public_key())`. Keeps the BLS-aggregate verify path in
55/// DSL-006 + DSL-013 from having to pass two trait-object
56/// pointers when one suffices.
57impl<T: ValidatorView + ?Sized> PublicKeyLookup for T {
58 fn pubkey_of(&self, index: u32) -> Option<&PublicKey> {
59 self.get(index).map(|entry| entry.public_key())
60 }
61}
62
63/// Reward-payout routing surface.
64///
65/// Traces to [SPEC §12.1](../../docs/resources/SPEC.md), catalogue row
66/// [DSL-141](../../docs/requirements/domains/).
67///
68/// # Consumers
69///
70/// - `SlashingManager::submit_evidence` (DSL-025) routes the
71/// whistleblower + proposer rewards through this trait.
72/// - Appeal adjudication (DSL-067 / DSL-068 / DSL-071) credits the
73/// winning party's reward account.
74///
75/// # Semantics
76///
77/// `pay(ph, amount)` creates-or-credits a pay-to-puzzle-hash account
78/// at the consensus layer. `amount == 0` is legal and MUST still be
79/// recorded — the call pattern is the protocol-observable side
80/// effect (auditors rely on the two-call pattern per admission).
81pub trait RewardPayout {
82 /// Create or credit the reward account for `principal_ph` by
83 /// `amount_mojos`. Idempotent w.r.t. the account's running
84 /// balance — consensus aggregates repeated credits.
85 fn pay(&mut self, principal_ph: Bytes32, amount_mojos: u64);
86}
87
88/// Reward clawback surface — reverses a previous `RewardPayout::pay`.
89///
90/// Traces to [SPEC §12.2](../../docs/resources/SPEC.md), catalogue row
91/// [DSL-142](../../docs/requirements/domains/).
92///
93/// # Consumer
94///
95/// Sustained-appeal adjudication (DSL-067) pulls the paid rewards
96/// back from the reporter + proposer accounts when the base slash is
97/// reverted.
98pub trait RewardClawback {
99 /// Deduct up to `amount` mojos from `principal_ph`'s reward
100 /// account. Returns the mojos ACTUALLY clawed back — may be less
101 /// than `amount` if the principal already withdrew (partial
102 /// clawback is DSL-142's defined semantics).
103 fn claw_back(&mut self, principal_ph: Bytes32, amount: u64) -> u64;
104}
105
106/// Collateral-slash reversal surface.
107///
108/// Traces to [SPEC §15.3](../../docs/resources/SPEC.md), catalogue row
109/// [DSL-065](../../docs/requirements/domains/appeal/specs/DSL-065.md).
110///
111/// # Consumer
112///
113/// - Appeal adjudication (DSL-065) calls
114/// `credit(validator_index, amount_mojos)` per reverted validator
115/// when a `CollateralSlasher` is supplied. Semantically a revert of
116/// the consensus-layer collateral debit that ran alongside the
117/// `ValidatorEntry::slash_absolute` stake debit at admission.
118///
119/// # Optional wiring
120///
121/// Light-client deployments may not track collateral at all. The
122/// adjudicator accepts `Option<&mut dyn CollateralSlasher>` and
123/// no-ops when `None` — collateral revert is a full-node concern.
124///
125/// # Idempotence
126///
127/// The trait does not specify idempotence. Callers MUST call
128/// `credit` exactly once per reverted validator — the adjudicator
129/// does so by construction (one pass over `base_slash_per_validator`).
130pub trait CollateralSlasher {
131 /// Credit `amount_mojos` of collateral back to `validator_index`.
132 /// Consensus-layer impl restores whatever collateral-position
133 /// bookkeeping it chose to debit at admission.
134 fn credit(&mut self, validator_index: u32, amount_mojos: u64);
135
136 /// Debit `amount_mojos` of collateral from `validator_index`
137 /// at `epoch`. Implements the slash leg of DSL-139; companion
138 /// to [`credit`](Self::credit).
139 ///
140 /// Default impl returns `Err(CollateralError::NoCollateral)`:
141 /// current production wiring only uses `credit` (via
142 /// DSL-129 reorg rewind + DSL-065 sustained-appeal revert);
143 /// collateral debits land later under a consensus-layer
144 /// slasher. Providing a default keeps every existing
145 /// `impl CollateralSlasher` (test spies + future production
146 /// impls) working without a breaking signature change.
147 ///
148 /// # Returns
149 ///
150 /// - `Ok((slashed, remaining))` — actual debit and post-
151 /// debit collateral balance.
152 /// - `Err(CollateralError::NoCollateral)` — validator has
153 /// no collateral position. Soft failure; DSL-022
154 /// submit_evidence ignores this and still slashes stake.
155 fn slash(
156 &mut self,
157 _validator_index: u32,
158 _amount_mojos: u64,
159 _epoch: u64,
160 ) -> Result<(u64, u64), CollateralError> {
161 Err(CollateralError::NoCollateral)
162 }
163}
164
165/// Failure modes for [`CollateralSlasher::slash`].
166///
167/// Traces to SPEC §15.2. Soft-failure contract: a
168/// `NoCollateral` result must NOT abort slashing — stake-side
169/// debit proceeds regardless (DSL-022).
170#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
171pub enum CollateralError {
172 /// Validator has no collateral position to debit. Soft
173 /// failure; callers treat as no-op and continue.
174 #[error("validator has no collateral to slash")]
175 NoCollateral,
176 /// Collateral slashing is disabled at the consensus layer
177 /// (network has not enabled collateral positions). Also a
178 /// soft failure at DSL-022; hard failure nowhere currently.
179 #[error("collateral slashing disabled")]
180 Disabled,
181}
182
183/// Block-proposer lookup surface.
184///
185/// Traces to [SPEC §15.3](../../docs/resources/SPEC.md), catalogue row
186/// [DSL-144](../../docs/requirements/domains/).
187///
188/// # Consumer
189///
190/// `SlashingManager::submit_evidence` (DSL-025) queries
191/// `proposer_at_slot(current_slot())` to identify the proposer whose
192/// block includes the evidence, then routes the proposer-inclusion
193/// reward to that validator's puzzle hash.
194pub trait ProposerView {
195 /// Validator index of the proposer at `slot`. `None` when the
196 /// slot is outside the known range.
197 fn proposer_at_slot(&self, slot: u64) -> Option<u32>;
198 /// Current chain-tip slot — drives the "who proposed the block
199 /// that includes this evidence" lookup.
200 fn current_slot(&self) -> u64;
201}
202
203/// Per-validator effective-balance read surface.
204///
205/// Traces to [SPEC §15.2](../../docs/resources/SPEC.md), catalogue row
206/// [DSL-137](../../docs/requirements/domains/).
207///
208/// # Consumers
209///
210/// - `SlashingManager::submit_evidence` (DSL-022) reads `get(idx)` per
211/// slashable validator to compute `base_slash = max(eff_bal * bps /
212/// 10_000, eff_bal / 32)`.
213/// - Reward math (DSL-081..085) reads `get` + `total_active` to derive
214/// per-epoch base rewards.
215///
216/// Separate from `ValidatorView` because some impls (light clients)
217/// maintain effective balances in a dedicated index without the full
218/// per-validator entry state.
219pub trait EffectiveBalanceView {
220 /// Effective balance of the validator at `index`, in mojos. Returns
221 /// `0` when the index is unknown — consistent with the DSL-022
222 /// edge case `eff_bal = 0 → base_slash = 0`.
223 fn get(&self, index: u32) -> u64;
224 /// Sum of effective balances of all active validators, in mojos.
225 /// Used by reward-per-validator derivations.
226 fn total_active(&self) -> u64;
227}
228
229/// Validator-set read+write surface consumed by the verifiers and
230/// slashing manager.
231///
232/// Traces to [SPEC §15.1](../../docs/resources/SPEC.md), catalogue row
233/// [DSL-136](../../docs/requirements/domains/).
234///
235/// # Scope
236///
237/// `ValidatorView` is the narrow surface `dig-slashing` needs to read
238/// (and mutate, on slash/appeal) per-validator state owned by the
239/// consensus layer. The full trait surface is defined here so every
240/// function signature in this crate can accept `&dyn ValidatorView` /
241/// `&mut dyn ValidatorView`; concrete impls land in `dig-consensus`
242/// and in test fixtures.
243///
244/// # Consumer list
245///
246/// - `verify_evidence` (DSL-011..020) — read-only for precondition
247/// checks (registered, active, not-already-slashed).
248/// - `SlashingManager::submit_evidence` (DSL-022) — mutating for
249/// per-validator debit via `ValidatorEntry::slash_absolute`.
250/// - `AppealAdjudicator` (DSL-064..067) — mutating for credit /
251/// restore_status on sustained appeals.
252pub trait ValidatorView {
253 /// Immutable lookup. `None` when `index` is not registered.
254 fn get(&self, index: u32) -> Option<&dyn ValidatorEntry>;
255 /// Mutable lookup (for slash / credit / restore on adjudication).
256 fn get_mut(&mut self, index: u32) -> Option<&mut dyn ValidatorEntry>;
257 /// Number of registered validators.
258 fn len(&self) -> usize;
259 /// Convenience predicate matching `Vec::is_empty` contract.
260 fn is_empty(&self) -> bool {
261 self.len() == 0
262 }
263}
264
265/// Per-validator state accessor.
266///
267/// Traces to [SPEC §15.1](../../docs/resources/SPEC.md), catalogue rows
268/// [DSL-131..135](../../docs/requirements/domains/).
269///
270/// # Invariants (enforced by DSL-131..135 when those impls land)
271///
272/// - `slash_absolute` saturates at effective-balance floor — cannot
273/// drive balance negative.
274/// - `credit_stake` returns the amount actually credited after any
275/// ceiling clamp.
276/// - `restore_status` is idempotent — returns `true` only when status
277/// actually changed.
278/// - `is_active_at_epoch(epoch)` is inclusive on activation, exclusive
279/// on exit (DSL-134 boundary behaviour).
280pub trait ValidatorEntry {
281 /// Validator's BLS G1 public key. Used by all signature verifiers.
282 fn public_key(&self) -> &PublicKey;
283 /// Payout puzzle hash for participation rewards / whistleblower
284 /// rewards (DSL-025, DSL-141).
285 fn puzzle_hash(&self) -> Bytes32;
286 /// Current effective balance in mojos. Drives base-penalty math
287 /// (DSL-022) and reward/penalty deltas (DSL-081..085).
288 fn effective_balance(&self) -> u64;
289 /// `true` if this validator has an outstanding slash (pending or
290 /// finalised). Gates duplicate slashing (DSL-026).
291 fn is_slashed(&self) -> bool;
292 /// Epoch the validator became active.
293 fn activation_epoch(&self) -> u64;
294 /// Epoch the validator scheduled exit at (or u64::MAX if none).
295 fn exit_epoch(&self) -> u64;
296 /// Activation-inclusive, exit-exclusive membership check at `epoch`.
297 fn is_active_at_epoch(&self, epoch: u64) -> bool;
298 /// Debit `amount_mojos` from the effective balance (saturating).
299 /// Returns the amount actually debited. DSL-131.
300 fn slash_absolute(&mut self, amount_mojos: u64, epoch: u64) -> u64;
301 /// Undo a prior `slash_absolute` (sustained appeal / reorg).
302 /// Returns the amount actually credited. DSL-132.
303 fn credit_stake(&mut self, amount_mojos: u64) -> u64;
304 /// Clear `Slashed` flag; restore active state. Idempotent. DSL-133.
305 /// Returns `true` iff state actually changed.
306 fn restore_status(&mut self) -> bool;
307 /// Schedule the post-finalisation exit lock. DSL-135.
308 fn schedule_exit(&mut self, exit_lock_until_epoch: u64);
309}
310
311/// Block re-execution result used by `InvalidBlockOracle`.
312///
313/// Traces to [SPEC §15.3](../../docs/resources/SPEC.md), catalogue row
314/// [DSL-145](../../docs/requirements/domains/).
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub enum ExecutionOutcome {
317 /// Block re-executed successfully — it is NOT invalid.
318 Valid,
319 /// Block is invalid; variant carries the specific failure reason
320 /// so the adjudicator can cross-check against
321 /// `InvalidBlockProof::failure_reason` (DSL-051
322 /// `FailureReasonMismatch` appeal ground).
323 Invalid(InvalidBlockReason),
324}
325
326/// Full-node block re-execution hook.
327///
328/// Traces to [SPEC §15.3](../../docs/resources/SPEC.md), catalogue rows
329/// [DSL-020](../../docs/requirements/domains/evidence/specs/DSL-020.md)
330/// + [DSL-049](../../docs/requirements/domains/) + [DSL-145].
331///
332/// # Role
333///
334/// - `verify_invalid_block` (DSL-020) calls `verify_failure` when the
335/// caller supplied an oracle; absence means bootstrap mode (the
336/// evidence is admitted and defers to the challenge window).
337/// - `InvalidBlockAppeal::BlockActuallyValid` (DSL-049) calls
338/// `re_execute` to adjudicate whether the accused block really is
339/// invalid.
340///
341/// # Default `verify_failure`
342///
343/// The default body is `Ok(())` — bootstrap mode where every
344/// well-signed evidence envelope is admitted. Real full-node impls
345/// override to re-execute the block and cross-check the claimed
346/// failure reason.
347///
348/// # Determinism
349///
350/// `re_execute` MUST be deterministic — same inputs → same outcome
351/// (DSL-145). Non-determinism here would let the same block flip
352/// between "valid" and "invalid" across honest nodes, breaking
353/// evidence consensus.
354pub trait InvalidBlockOracle {
355 /// Verify the caller's claim that `header` is invalid for the
356 /// stated `reason`, using `witness` bytes (trie proofs, state
357 /// diff, etc.).
358 ///
359 /// Default: accept — bootstrap path. Full nodes override.
360 fn verify_failure(
361 &self,
362 _header: &L2BlockHeader,
363 _witness: &[u8],
364 _reason: InvalidBlockReason,
365 ) -> Result<(), SlashingError> {
366 Ok(())
367 }
368 /// Re-execute the block deterministically. Returns whether it is
369 /// Valid or Invalid (with the specific reason when invalid).
370 fn re_execute(
371 &self,
372 header: &L2BlockHeader,
373 witness: &[u8],
374 ) -> Result<ExecutionOutcome, SlashingError>;
375}