dig_epoch/types/checkpoint_competition.rs
1//! # `types::checkpoint_competition` — `CheckpointCompetition` struct and `CompetitionStatus` enum
2//!
3//! **Introduced by:** `STR-002` — Module hierarchy (SPEC §13).
4//!
5//! **Owner:** `CKP-001` — struct/enum surface. Lifecycle methods (start, submit,
6//! finalize, lifecycle transitions) are added by `CKP-002`..`CKP-005`.
7//!
8//! **Spec reference:** [`SPEC.md` §3.9, §3.10](../../../docs/resources/SPEC.md).
9//!
10//! Per start.md Hard Requirement 1, this module MUST NOT redefine block types.
11//! `Checkpoint` and `CheckpointSubmission` come from [`dig_block`].
12
13/// Sentinel marker proving the module exists and is reachable at
14/// `dig_epoch::types::checkpoint_competition::STR_002_MODULE_PRESENT`.
15#[doc(hidden)]
16pub const STR_002_MODULE_PRESENT: () = ();
17
18use chia_protocol::Bytes32;
19use dig_block::CheckpointSubmission;
20use serde::{Deserialize, Serialize};
21
22use crate::error::{CheckpointCompetitionError, EpochError};
23
24// -----------------------------------------------------------------------------
25// CKP-001 — CompetitionStatus
26// -----------------------------------------------------------------------------
27
28/// State machine for a checkpoint competition.
29///
30/// Spec ref: SPEC §3.10 / CKP-001.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub enum CompetitionStatus {
33 /// Competition created but not yet accepting submissions.
34 Pending,
35 /// Actively accepting checkpoint submissions.
36 Collecting,
37 /// A winning submission has been identified by score.
38 WinnerSelected {
39 /// Hash of the winning checkpoint.
40 winner_hash: Bytes32,
41 /// Score of the winning submission.
42 winner_score: u64,
43 },
44 /// Winner confirmed and anchored to an L1 height.
45 Finalized {
46 /// Hash of the winning checkpoint.
47 winner_hash: Bytes32,
48 /// L1 height at which the winner was anchored.
49 l1_height: u32,
50 },
51 /// Competition ended due to timeout or error.
52 Failed,
53}
54
55// -----------------------------------------------------------------------------
56// CKP-001 — CheckpointCompetition
57// -----------------------------------------------------------------------------
58
59/// Per-epoch checkpoint competition, collecting submissions and selecting a winner.
60///
61/// Spec ref: SPEC §3.9 / CKP-001.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CheckpointCompetition {
64 /// Epoch this competition belongs to.
65 pub epoch: u64,
66 /// All checkpoint submissions received.
67 pub submissions: Vec<CheckpointSubmission>,
68 /// Current competition state.
69 pub status: CompetitionStatus,
70 /// Index into `submissions` of the current leader, if any.
71 pub current_winner: Option<usize>,
72}
73
74impl CheckpointCompetition {
75 /// Creates a new competition for `epoch` in `Pending` state with no submissions.
76 pub fn new(epoch: u64) -> Self {
77 Self {
78 epoch,
79 submissions: Vec::new(),
80 status: CompetitionStatus::Pending,
81 current_winner: None,
82 }
83 }
84
85 /// True when the competition has reached the `Finalized` variant.
86 pub fn is_finalized(&self) -> bool {
87 matches!(self.status, CompetitionStatus::Finalized { .. })
88 }
89
90 // -------------------------------------------------------------------------
91 // CKP-002 — start
92 // -------------------------------------------------------------------------
93
94 /// Transitions `Pending` → `Collecting`.
95 ///
96 /// Returns `Err(NotStarted)` if already past `Pending`.
97 /// (Reusing `NotStarted` as the generic "wrong-lifecycle-state" error;
98 /// specific variants may be refined in later tightening.)
99 pub fn start(&mut self) -> Result<(), CheckpointCompetitionError> {
100 if self.status != CompetitionStatus::Pending {
101 return Err(CheckpointCompetitionError::AlreadyFinalized);
102 }
103 self.status = CompetitionStatus::Collecting;
104 Ok(())
105 }
106
107 // -------------------------------------------------------------------------
108 // CKP-003 — submit
109 // -------------------------------------------------------------------------
110
111 /// Records a checkpoint submission and, if its score beats the current leader,
112 /// transitions status to `WinnerSelected` (or updates the existing one).
113 ///
114 /// Returns `true` when the submission became the new leader.
115 ///
116 /// Errors:
117 /// - `NotStarted` — status is `Pending`
118 /// - `AlreadyFinalized` — status is `Finalized` / `Failed`
119 /// - `EpochMismatch` — `submission.epoch` != `self.epoch`
120 /// - `ScoreNotHigher` — score does not exceed current leader
121 pub fn submit(
122 &mut self,
123 submission: CheckpointSubmission,
124 ) -> Result<bool, CheckpointCompetitionError> {
125 match self.status {
126 CompetitionStatus::Pending => {
127 return Err(CheckpointCompetitionError::NotStarted);
128 }
129 CompetitionStatus::Finalized { .. } | CompetitionStatus::Failed => {
130 return Err(CheckpointCompetitionError::AlreadyFinalized);
131 }
132 CompetitionStatus::Collecting | CompetitionStatus::WinnerSelected { .. } => {}
133 }
134 if submission.checkpoint.epoch != self.epoch {
135 return Err(CheckpointCompetitionError::EpochMismatch {
136 expected: self.epoch,
137 got: submission.checkpoint.epoch,
138 });
139 }
140 let new_score = submission.score;
141 let current_score = match &self.status {
142 CompetitionStatus::WinnerSelected { winner_score, .. } => *winner_score,
143 _ => 0,
144 };
145 // Always record the submission for auditability.
146 self.submissions.push(submission);
147 let idx = self.submissions.len() - 1;
148 let is_new_leader = match &self.status {
149 CompetitionStatus::WinnerSelected { .. } => new_score > current_score,
150 _ => new_score > 0,
151 };
152 if is_new_leader {
153 let winner_hash = self.submissions[idx].checkpoint.hash();
154 self.status = CompetitionStatus::WinnerSelected {
155 winner_hash,
156 winner_score: new_score,
157 };
158 self.current_winner = Some(idx);
159 Ok(true)
160 } else {
161 Err(CheckpointCompetitionError::ScoreNotHigher {
162 current: current_score,
163 submitted: new_score,
164 })
165 }
166 }
167
168 // -------------------------------------------------------------------------
169 // CKP-004 — finalize
170 // -------------------------------------------------------------------------
171
172 /// Transitions `WinnerSelected` → `Finalized { winner_hash, l1_height }`.
173 ///
174 /// Returns the winning checkpoint hash.
175 pub fn finalize(&mut self, l1_height: u32) -> Result<Bytes32, CheckpointCompetitionError> {
176 let winner_hash = match self.status {
177 CompetitionStatus::WinnerSelected { winner_hash, .. } => winner_hash,
178 CompetitionStatus::Finalized { .. } => {
179 return Err(CheckpointCompetitionError::AlreadyFinalized);
180 }
181 _ => return Err(CheckpointCompetitionError::NotStarted),
182 };
183 self.status = CompetitionStatus::Finalized {
184 winner_hash,
185 l1_height,
186 };
187 Ok(winner_hash)
188 }
189
190 /// Transitions to `Failed` (terminal). Legal from `Collecting` or
191 /// `WinnerSelected`.
192 pub fn fail(&mut self) -> Result<(), CheckpointCompetitionError> {
193 match self.status {
194 CompetitionStatus::Collecting | CompetitionStatus::WinnerSelected { .. } => {
195 self.status = CompetitionStatus::Failed;
196 Ok(())
197 }
198 CompetitionStatus::Finalized { .. } | CompetitionStatus::Failed => {
199 Err(CheckpointCompetitionError::AlreadyFinalized)
200 }
201 CompetitionStatus::Pending => Err(CheckpointCompetitionError::NotStarted),
202 }
203 }
204
205 /// Returns the winning checkpoint submission, if one has been selected.
206 pub fn winner(&self) -> Option<&CheckpointSubmission> {
207 self.current_winner.and_then(|i| self.submissions.get(i))
208 }
209
210 /// Serializes with bincode. Infallible for well-formed structs.
211 pub fn to_bytes(&self) -> Vec<u8> {
212 bincode::serialize(self).expect("CheckpointCompetition serialization should never fail")
213 }
214
215 /// Deserializes from bincode bytes, returning `EpochError::InvalidData` on failure.
216 pub fn from_bytes(bytes: &[u8]) -> Result<Self, EpochError> {
217 bincode::deserialize(bytes).map_err(|e| EpochError::InvalidData(e.to_string()))
218 }
219}