Skip to main content

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}