Skip to main content

dig_block/types/
checkpoint.rs

1//! Checkpoint domain types: [`Checkpoint`] (CKP-001), [`CheckpointSubmission`] (CKP-002).
2//!
3//! ## Requirements trace
4//!
5//! - **[CKP-001](docs/requirements/domains/checkpoint/specs/CKP-001.md)** — [`Checkpoint`]: nine public fields + [`Checkpoint::new`] default instance.
6//! - **[CKP-002](docs/requirements/domains/checkpoint/specs/CKP-002.md)** — [`CheckpointSubmission`]: checkpoint + [`crate::SignerBitmap`] + aggregate BLS + score + submitter + L1 tracking options.
7//! - **[NORMATIVE § CKP-001 / CKP-002](docs/requirements/domains/checkpoint/NORMATIVE.md)** — checkpoint + submission field layouts and constructors.
8//! - **[SPEC §2.6](docs/resources/SPEC.md)** — checkpoint as epoch summary anchored toward L1.
9//! - **[CKP-004](docs/requirements/domains/checkpoint/specs/CKP-004.md)** — [`Checkpoint::compute_score`]: `stake_percentage * block_count` (epoch competition score).
10//! - **[CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)** — [`CheckpointSubmission`]: [`CheckpointSubmission::hash`], [`CheckpointSubmission::epoch`], threshold helpers, L1 [`CheckpointSubmission::record_submission`].
11//! - **[CKP-006](docs/requirements/domains/checkpoint/specs/CKP-006.md)** — [`crate::CheckpointBuilder`] accumulates block / withdrawal hashes and produces `block_root` / `withdrawals_root` using the same internal Merkle helper as BLK-004 (`merkle_tree_root` in `merkle_util.rs`).
12//! - **[HSH-002](docs/requirements/domains/hashing/specs/HSH-002.md)** / **[SPEC §3.2](docs/resources/SPEC.md)** — [`Checkpoint::hash`]: SHA-256 over 160-byte fixed-order preimage ([`chia_sha2::Sha256`]).
13//! - **[SER-001](docs/requirements/domains/serialization/specs/SER-001.md)** — bincode via [`Serialize`] / [`Deserialize`] on wire-bearing structs.
14//! - **[SER-002](docs/requirements/domains/serialization/specs/SER-002.md)** — [`Checkpoint::to_bytes`] / [`Checkpoint::from_bytes`] and [`CheckpointSubmission::to_bytes`] / [`CheckpointSubmission::from_bytes`]
15//!   with [`CheckpointError::InvalidData`](crate::CheckpointError::InvalidData) on decode failures.
16//!
17//! ## Rationale
18//!
19//! - **Public fields:** Same ergonomics as [`crate::types::receipt::Receipt`] — consensus / builder layers assign values; this crate stays a typed bag of record ([CKP-001](docs/requirements/domains/checkpoint/specs/CKP-001.md) acceptance: read/write access).
20//! - **Default roots:** [`Bytes32::default`] is the all-zero hash, matching “empty Merkle” conventions used elsewhere ([`crate::constants::EMPTY_ROOT`] is the documented empty-tree sentinel; callers may normalize roots when building real checkpoints — CKP-006).
21//! - **`CheckpointSubmission` + L1 options:** `submission_height` / `submission_coin` start [`None`] at construction;
22//!   [`CheckpointSubmission::record_submission`](CheckpointSubmission::record_submission) persists L1 proof ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md), [CKP-002](docs/requirements/domains/checkpoint/specs/CKP-002.md) notes).
23
24use chia_sha2::Sha256;
25use serde::{Deserialize, Serialize};
26
27use super::signer_bitmap::SignerBitmap;
28use crate::error::CheckpointError;
29use crate::primitives::{Bytes32, PublicKey, Signature};
30
31/// Epoch summary checkpoint: aggregate stats and Merkle roots for one L1-anchored epoch ([SPEC §2.6](docs/resources/SPEC.md), [CKP-001](docs/requirements/domains/checkpoint/specs/CKP-001.md)).
32///
33/// ## Field semantics (informal)
34///
35/// - **`epoch`:** Monotonic epoch id this summary closes.
36/// - **`state_root`:** Post-epoch L2 state commitment.
37/// - **`block_root`:** Merkle root over block hashes in the epoch ([CKP-006](docs/requirements/domains/checkpoint/specs/CKP-006.md) will define construction).
38/// - **`block_count` / `tx_count` / `total_fees`:** Scalar aggregates for light verification and scoring; `block_count` is the block factor in [`compute_score`](Checkpoint::compute_score) ([CKP-004](docs/requirements/domains/checkpoint/specs/CKP-004.md)).
39/// - **`prev_checkpoint`:** Hash / identity of the prior checkpoint header for chained verification ([CKP-001](docs/requirements/domains/checkpoint/specs/CKP-001.md) implementation notes).
40/// - **`withdrawals_root` / `withdrawal_count`:** Merkle root and count over withdrawal records in the epoch.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct Checkpoint {
43    /// Epoch number this checkpoint summarizes.
44    pub epoch: u64,
45    /// L2 state root after applying all blocks in the epoch.
46    pub state_root: Bytes32,
47    /// Merkle root over block hashes included in this epoch.
48    pub block_root: Bytes32,
49    /// Number of L2 blocks in the epoch.
50    pub block_count: u32,
51    /// Total transactions across blocks in the epoch.
52    pub tx_count: u64,
53    /// Sum of fees collected in the epoch (same currency unit as receipt fees — see RCP-*).
54    pub total_fees: u64,
55    /// Link to the previous checkpoint (continuity for fraud/validity proofs).
56    pub prev_checkpoint: Bytes32,
57    /// Merkle root over withdrawal hashes in the epoch.
58    pub withdrawals_root: Bytes32,
59    /// Number of withdrawals in the epoch.
60    pub withdrawal_count: u32,
61}
62
63impl Checkpoint {
64    /// Default empty checkpoint: epoch `0`, zero counts, all roots [`Bytes32::default`] ([CKP-001](docs/requirements/domains/checkpoint/specs/CKP-001.md) constructor spec).
65    ///
66    /// **Usage:** [`crate::builder::checkpoint_builder::CheckpointBuilder`] and tests start from this value then overwrite fields; production checkpoints should set non-zero roots before signing (CKP-005 / HSH-002).
67    #[must_use]
68    pub fn new() -> Self {
69        Self {
70            epoch: 0,
71            state_root: Bytes32::default(),
72            block_root: Bytes32::default(),
73            block_count: 0,
74            tx_count: 0,
75            total_fees: 0,
76            prev_checkpoint: Bytes32::default(),
77            withdrawals_root: Bytes32::default(),
78            withdrawal_count: 0,
79        }
80    }
81
82    /// Competition score: `stake_percentage * block_count` ([CKP-004](docs/requirements/domains/checkpoint/specs/CKP-004.md), [NORMATIVE § CKP-004](docs/requirements/domains/checkpoint/NORMATIVE.md)).
83    ///
84    /// ## Parameters
85    ///
86    /// - **`stake_percentage`:** Integer stake share for the submitter, typically `0..=100` ([CKP-004](docs/requirements/domains/checkpoint/specs/CKP-004.md) implementation notes). The type is `u64` so callers can scale fixed-point stake if needed without API churn.
87    ///
88    /// ## Returns
89    ///
90    /// Product in `u64`. Favors checkpoints with more blocks and higher backing stake. **Overflow:** Uses ordinary
91    /// `u64` multiplication (wraps on overflow; debug builds may panic on overflow — keep inputs within protocol bounds,
92    /// e.g. epoch length ×100).
93    #[must_use]
94    pub fn compute_score(&self, stake_percentage: u64) -> u64 {
95        stake_percentage * u64::from(self.block_count)
96    }
97
98    /// Byte length of the SHA-256 preimage for [`Self::hash`] ([SPEC §3.2](docs/resources/SPEC.md): 8+32+32+4+8+8+32+32+4).
99    pub const HASH_PREIMAGE_LEN: usize = 160;
100
101    /// Fixed-order **160-byte** preimage for [HSH-002](docs/requirements/domains/hashing/specs/HSH-002.md) /
102    /// [SPEC §3.2](docs/resources/SPEC.md) (same bytes as fed to [`Self::hash`]).
103    ///
104    /// **Order:** `epoch`, `state_root`, `block_root`, `block_count`, `tx_count`, `total_fees`, `prev_checkpoint`,
105    /// `withdrawals_root`, `withdrawal_count`.
106    ///
107    /// **Note:** [HSH-002 spec](docs/requirements/domains/hashing/specs/HSH-002.md) pseudocode lists some counts as `u64`;
108    /// the wire table in SPEC §3.2 and this struct use `u32` LE for `block_count` and `withdrawal_count` (4 bytes each).
109    #[must_use]
110    pub fn hash_preimage_bytes(&self) -> [u8; Self::HASH_PREIMAGE_LEN] {
111        fn put(buf: &mut [u8; Checkpoint::HASH_PREIMAGE_LEN], i: &mut usize, bytes: &[u8]) {
112            buf[*i..*i + bytes.len()].copy_from_slice(bytes);
113            *i += bytes.len();
114        }
115        let mut buf = [0u8; Self::HASH_PREIMAGE_LEN];
116        let mut i = 0usize;
117        put(&mut buf, &mut i, &self.epoch.to_le_bytes());
118        put(&mut buf, &mut i, self.state_root.as_ref());
119        put(&mut buf, &mut i, self.block_root.as_ref());
120        put(&mut buf, &mut i, &self.block_count.to_le_bytes());
121        put(&mut buf, &mut i, &self.tx_count.to_le_bytes());
122        put(&mut buf, &mut i, &self.total_fees.to_le_bytes());
123        put(&mut buf, &mut i, self.prev_checkpoint.as_ref());
124        put(&mut buf, &mut i, self.withdrawals_root.as_ref());
125        put(&mut buf, &mut i, &self.withdrawal_count.to_le_bytes());
126        debug_assert_eq!(i, Self::HASH_PREIMAGE_LEN);
127        buf
128    }
129
130    /// Canonical checkpoint identity: SHA-256 over [`Self::hash_preimage_bytes`] ([SPEC §3.2](docs/resources/SPEC.md)).
131    ///
132    /// **Encoding:** `epoch`, `tx_count`, `total_fees` as `u64` LE; `block_count`, `withdrawal_count` as `u32` LE
133    /// (4 bytes each); four [`Bytes32`] roots as raw 32-byte slices ([HSH-002](docs/requirements/domains/hashing/specs/HSH-002.md),
134    /// [NORMATIVE § HSH-002](docs/requirements/domains/hashing/NORMATIVE.md)).
135    ///
136    /// **Primitive:** [`Sha256`] from `chia-sha2` only (project crypto rules). [`CheckpointSubmission::hash`](CheckpointSubmission::hash) delegates here ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)).
137    #[must_use]
138    pub fn hash(&self) -> Bytes32 {
139        let mut hasher = Sha256::new();
140        hasher.update(self.hash_preimage_bytes());
141        Bytes32::new(hasher.finalize())
142    }
143
144    /// Serialize checkpoint summary to **bincode** bytes ([SER-002](docs/requirements/domains/serialization/specs/SER-002.md), SPEC §8.2).
145    #[must_use]
146    pub fn to_bytes(&self) -> Vec<u8> {
147        bincode::serialize(self).expect("Checkpoint serialization should never fail")
148    }
149
150    /// Deserialize a checkpoint from **bincode** bytes ([SER-002](docs/requirements/domains/serialization/specs/SER-002.md)).
151    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CheckpointError> {
152        bincode::deserialize(bytes).map_err(|e| CheckpointError::InvalidData(e.to_string()))
153    }
154}
155
156impl Default for Checkpoint {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162/// Signed checkpoint submission: epoch summary plus validator attestation material ([SPEC §2.7](docs/resources/SPEC.md), [CKP-002](docs/requirements/domains/checkpoint/specs/CKP-002.md)).
163///
164/// ## Field semantics
165///
166/// - **`checkpoint`:** The [`Checkpoint`] being proposed for L1 anchoring (CKP-001).
167/// - **`signer_bitmap` / `aggregate_signature` / `aggregate_pubkey`:** Who attested and the aggregated BLS proof
168///   over the checkpoint preimage (exact signing protocol is outside this crate; types match ATT-001 / ATT-004 patterns).
169/// - **`score`:** Competition score, often populated from [`Checkpoint::compute_score`](Checkpoint::compute_score) ([CKP-004](docs/requirements/domains/checkpoint/specs/CKP-004.md)).
170/// - **`submitter`:** Validator **index** in the epoch set who published this submission ([CKP-002](docs/requirements/domains/checkpoint/specs/CKP-002.md) implementation notes).
171/// - **`submission_height` / `submission_coin`:** L1 observation metadata; [`None`] until [`record_submission`](CheckpointSubmission::record_submission).
172///
173/// **Serialization:** [`Serialize`] / [`Deserialize`] for bincode ([SER-001](docs/requirements/domains/serialization/specs/SER-001.md)).
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct CheckpointSubmission {
176    /// Epoch summary being submitted.
177    pub checkpoint: Checkpoint,
178    /// Which validators signed this submission ([`SignerBitmap`], ATT-004).
179    pub signer_bitmap: SignerBitmap,
180    /// Aggregated BLS signature over the checkpoint commitment.
181    pub aggregate_signature: Signature,
182    /// Aggregated BLS public key corresponding to `aggregate_signature`.
183    pub aggregate_pubkey: PublicKey,
184    /// Off-chain / protocol score used to compare competing submissions ([CKP-004](docs/requirements/domains/checkpoint/specs/CKP-004.md)).
185    pub score: u64,
186    /// Index of the validator who broadcast this submission.
187    pub submitter: u32,
188    /// L1 block height where the submission transaction was observed, once recorded ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)).
189    pub submission_height: Option<u32>,
190    /// L1 coin ID for the submission transaction, once recorded ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)).
191    pub submission_coin: Option<Bytes32>,
192}
193
194impl CheckpointSubmission {
195    /// Build a submission with attestation material but **no** L1 inclusion data yet ([CKP-002](docs/requirements/domains/checkpoint/specs/CKP-002.md)).
196    ///
197    /// **`submission_height` / `submission_coin`:** Initialized to [`None`]; use [`Self::record_submission`] after L1
198    /// confirmation ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)).
199    #[must_use]
200    pub fn new(
201        checkpoint: Checkpoint,
202        signer_bitmap: SignerBitmap,
203        aggregate_signature: Signature,
204        aggregate_pubkey: PublicKey,
205        score: u64,
206        submitter: u32,
207    ) -> Self {
208        Self {
209            checkpoint,
210            signer_bitmap,
211            aggregate_signature,
212            aggregate_pubkey,
213            score,
214            submitter,
215            submission_height: None,
216            submission_coin: None,
217        }
218    }
219
220    /// Delegates to [`Checkpoint::hash`] — canonical epoch-summary identity ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md), HSH-002).
221    #[must_use]
222    pub fn hash(&self) -> Bytes32 {
223        self.checkpoint.hash()
224    }
225
226    /// Epoch number from the wrapped [`Checkpoint`] ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)).
227    #[must_use]
228    pub fn epoch(&self) -> u64 {
229        self.checkpoint.epoch
230    }
231
232    /// Validator participation as an integer percent `0..=100` — delegates to [`SignerBitmap::signing_percentage`] ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md), ATT-004).
233    #[must_use]
234    pub fn signing_percentage(&self) -> u64 {
235        self.signer_bitmap.signing_percentage()
236    }
237
238    /// `true` iff [`Self::signing_percentage`] `>= threshold_pct` ([`SignerBitmap::has_threshold`](crate::SignerBitmap::has_threshold), CKP-005).
239    #[must_use]
240    pub fn meets_threshold(&self, threshold_pct: u64) -> bool {
241        self.signer_bitmap.has_threshold(threshold_pct)
242    }
243
244    /// Record L1 inclusion: block height and submission coin id ([CKP-005](docs/requirements/domains/checkpoint/specs/CKP-005.md)).
245    ///
246    /// **Normative:** Both fields become [`Some`]; [`Self::is_submitted`] becomes `true` because `submission_height` is set.
247    pub fn record_submission(&mut self, height: u32, coin_id: Bytes32) {
248        self.submission_height = Some(height);
249        self.submission_coin = Some(coin_id);
250    }
251
252    /// `true` once `submission_height` is [`Some`] ([NORMATIVE § CKP-005](docs/requirements/domains/checkpoint/NORMATIVE.md)).
253    #[must_use]
254    pub fn is_submitted(&self) -> bool {
255        self.submission_height.is_some()
256    }
257
258    /// Serialize submission (checkpoint + attestation material) to **bincode** bytes ([SER-002](docs/requirements/domains/serialization/specs/SER-002.md)).
259    #[must_use]
260    pub fn to_bytes(&self) -> Vec<u8> {
261        bincode::serialize(self).expect("CheckpointSubmission serialization should never fail")
262    }
263
264    /// Deserialize a submission from **bincode** bytes ([SER-002](docs/requirements/domains/serialization/specs/SER-002.md)).
265    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CheckpointError> {
266        bincode::deserialize(bytes).map_err(|e| CheckpointError::InvalidData(e.to_string()))
267    }
268}