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}