Skip to main content

dig_slashing/evidence/
attestation_data.rs

1//! `AttestationData` — the Ethereum-parity attester vote payload.
2//!
3//! Traces to: [SPEC.md §3.3](../../docs/resources/SPEC.md), catalogue row
4//! [DSL-004](../../docs/requirements/domains/evidence/specs/DSL-004.md).
5//!
6//! # Role
7//!
8//! `AttestationData` is the signable payload every attester BLS-signs. It
9//! carries:
10//!
11//! - `slot` + `index` — the committee coordinates.
12//! - `beacon_block_root` — the head vote.
13//! - `source` + `target` — the FFG vote pair ([`Checkpoint`]).
14//!
15//! `signing_root(&network_id)` hashes the payload under `DOMAIN_BEACON_ATTESTER`
16//! with the network id mixed in; the result is the BLS signing message
17//! consumed by `IndexedAttestation::verify_signature` (DSL-006) and by
18//! `classify_timeliness` (DSL-075..077) participation tracking.
19//!
20//! # Determinism + replay resistance
21//!
22//! - Identical inputs always produce identical output (verified by
23//!   `test_dsl_004_signing_root_deterministic`).
24//! - The domain tag stops a signature produced here from verifying against
25//!   a proposer signing message (which uses a different tag, DSL-050).
26//! - The network_id mix stops cross-network replay (testnet → mainnet).
27//! - Every field (including both `Checkpoint`s in full) participates in
28//!   the hash, so mutation anywhere shifts the output.
29
30use chia_sha2::Sha256;
31use dig_protocol::Bytes32;
32use serde::{Deserialize, Serialize};
33
34use crate::constants::DOMAIN_BEACON_ATTESTER;
35use crate::evidence::checkpoint::Checkpoint;
36
37/// Attester vote payload.
38///
39/// Per [SPEC §3.3](../../docs/resources/SPEC.md). Field layout is frozen
40/// as wire protocol — see [`AttestationData::signing_root`] for the exact
41/// byte order used by BLS signing.
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43pub struct AttestationData {
44    /// L2 slot the attestation targets.
45    pub slot: u64,
46    /// Committee index within the slot.
47    pub index: u64,
48    /// Head vote: canonical beacon block root at `slot`.
49    pub beacon_block_root: Bytes32,
50    /// FFG source checkpoint.
51    pub source: Checkpoint,
52    /// FFG target checkpoint.
53    pub target: Checkpoint,
54}
55
56impl AttestationData {
57    /// Compute the BLS signing root for this attestation.
58    ///
59    /// Implements [DSL-004](../../docs/requirements/domains/evidence/specs/DSL-004.md).
60    /// Traces to SPEC §3.3.
61    ///
62    /// # Wire layout
63    ///
64    /// The hasher is fed the following bytes, in order:
65    ///
66    /// ```text
67    /// DOMAIN_BEACON_ATTESTER    (22 bytes, "DIG_BEACON_ATTESTER_V1")
68    /// network_id                (32 bytes)
69    /// slot                      (8 bytes, little-endian u64)
70    /// index                     (8 bytes, little-endian u64)
71    /// beacon_block_root         (32 bytes)
72    /// source.epoch              (8 bytes, little-endian u64)
73    /// source.root               (32 bytes)
74    /// target.epoch              (8 bytes, little-endian u64)
75    /// target.root               (32 bytes)
76    /// ```
77    ///
78    /// Total input: 182 bytes. Output: 32-byte SHA-256 digest.
79    ///
80    /// # Invariants
81    ///
82    /// - **Deterministic:** identical `(self, network_id)` inputs always
83    ///   produce bit-identical output.
84    /// - **Domain-bound:** prefixed with `DOMAIN_BEACON_ATTESTER`; the tag
85    ///   is NOT a separate field of the struct.
86    /// - **Network-bound:** `network_id` mixed in after the tag; a
87    ///   signing root produced for testnet does NOT verify under mainnet.
88    /// - **Field-covering:** every field (including both `Checkpoint`
89    ///   components) contributes to the digest; mutating any one shifts
90    ///   the output.
91    ///
92    /// All four invariants are enforced by
93    /// `tests/dsl_004_attestation_data_signing_root_test.rs`.
94    ///
95    /// # Endianness
96    ///
97    /// All integer fields (`slot`, `index`, `source.epoch`, `target.epoch`)
98    /// use little-endian encoding via `u64::to_le_bytes`. Little-endian is
99    /// the wire-level standard for DIG / Chia; the test suite guards
100    /// against accidental big-endian drift.
101    ///
102    /// # No custom hashing
103    ///
104    /// Uses `chia_sha2::Sha256` directly — do NOT introduce a generic
105    /// `sha2` crate dep for this codebase (SPEC §5 hard rule, dt-hard-rules
106    /// Rule 4).
107    pub fn signing_root(&self, network_id: &Bytes32) -> Bytes32 {
108        let mut h = Sha256::new();
109        h.update(DOMAIN_BEACON_ATTESTER);
110        h.update(network_id.as_ref());
111        h.update(self.slot.to_le_bytes());
112        h.update(self.index.to_le_bytes());
113        h.update(self.beacon_block_root.as_ref());
114        h.update(self.source.epoch.to_le_bytes());
115        h.update(self.source.root.as_ref());
116        h.update(self.target.epoch.to_le_bytes());
117        h.update(self.target.root.as_ref());
118        let out: [u8; 32] = h.finalize();
119        Bytes32::new(out)
120    }
121
122    /// Whether this attestation forms a slashable pair with `other`.
123    ///
124    /// The single source of truth for the attester-slashing predicate
125    /// (DSL-014 double-vote, DSL-015 surround-vote). Both the evidence
126    /// verifier (`verify_attester_slashing`) and the appeal ground
127    /// (`verify_attester_appeal_not_slashable_by_predicate`) decide
128    /// slashability from HERE. Keeping one definition is load-bearing for
129    /// soundness: the appeal is the exact logical inverse of the evidence
130    /// check, so a second copy could drift into an unfair slash (evidence
131    /// slashes, appeal cannot revert) or an un-slashable one.
132    ///
133    /// Returns `true` iff EITHER predicate holds:
134    /// - **Double vote (DSL-014):** same `target.epoch`, but the two
135    ///   attestations are not byte-identical.
136    /// - **Surround vote (DSL-015):** one `(source.epoch, target.epoch)`
137    ///   window strictly surrounds the other, checked in both directions.
138    ///
139    /// Symmetric in its two operands:
140    /// `a.is_slashable_against(b) == b.is_slashable_against(a)`.
141    #[must_use]
142    pub(crate) fn is_slashable_against(&self, other: &AttestationData) -> bool {
143        let double_vote = self.target.epoch == other.target.epoch && self != other;
144        let surround_vote = (self.source.epoch < other.source.epoch
145            && self.target.epoch > other.target.epoch)
146            || (other.source.epoch < self.source.epoch && other.target.epoch > self.target.epoch);
147        double_vote || surround_vote
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::evidence::checkpoint::Checkpoint;
155
156    /// Build an `AttestationData` from `(source_epoch, target_epoch)`; the
157    /// checkpoint roots vary with the epoch so distinct epochs give
158    /// distinct (non-byte-identical) attestations.
159    fn att(source_epoch: u64, target_epoch: u64) -> AttestationData {
160        AttestationData {
161            slot: target_epoch,
162            index: 0,
163            beacon_block_root: Bytes32::new([0u8; 32]),
164            source: Checkpoint {
165                epoch: source_epoch,
166                root: Bytes32::new([source_epoch as u8; 32]),
167            },
168            target: Checkpoint {
169                epoch: target_epoch,
170                root: Bytes32::new([target_epoch as u8; 32]),
171            },
172        }
173    }
174
175    #[test]
176    fn double_vote_same_target_different_data_is_slashable() {
177        let a = att(1, 5);
178        let mut b = att(1, 5);
179        b.beacon_block_root = Bytes32::new([9u8; 32]);
180        assert!(a.is_slashable_against(&b));
181        assert!(b.is_slashable_against(&a));
182    }
183
184    #[test]
185    fn byte_identical_attestations_are_not_slashable() {
186        let a = att(1, 5);
187        let b = att(1, 5);
188        assert!(!a.is_slashable_against(&b));
189    }
190
191    #[test]
192    fn surround_vote_is_slashable_both_directions() {
193        // a = [1, 6] strictly surrounds b = [2, 5].
194        let a = att(1, 6);
195        let b = att(2, 5);
196        assert!(a.is_slashable_against(&b));
197        assert!(b.is_slashable_against(&a));
198    }
199
200    #[test]
201    fn distinct_non_surrounding_non_double_vote_is_not_slashable() {
202        // Disjoint windows, different target epochs, neither surrounds.
203        let a = att(1, 2);
204        let b = att(3, 4);
205        assert!(!a.is_slashable_against(&b));
206        assert!(!b.is_slashable_against(&a));
207    }
208
209    #[test]
210    fn predicate_is_symmetric_across_a_matrix() {
211        for sa in 0..4u64 {
212            for ta in sa..sa + 4 {
213                for sb in 0..4u64 {
214                    for tb in sb..sb + 4 {
215                        let a = att(sa, ta);
216                        let b = att(sb, tb);
217                        assert_eq!(
218                            a.is_slashable_against(&b),
219                            b.is_slashable_against(&a),
220                            "asymmetry at a=({sa},{ta}) b=({sb},{tb})"
221                        );
222                    }
223                }
224            }
225        }
226    }
227}