Skip to main content

csv_adapter_sui/
checkpoint.rs

1//! Sui checkpoint finality verifier
2//!
3//! This module provides checkpoint verification for Sui,
4//! verifying that transactions are in checkpoints certified by 2f+1 validators.
5//!
6//! Sui uses Narwhal consensus, which provides deterministic finality:
7//! once a checkpoint is certified by 2f+1 validators, it cannot be reverted.
8
9use serde::{Deserialize, Serialize};
10
11use crate::config::CheckpointConfig;
12use crate::error::{SuiError, SuiResult};
13use crate::rpc::SuiRpc;
14
15/// Checkpoint information with certification details.
16#[derive(Clone, Debug, Serialize, Deserialize)]
17pub struct CheckpointInfo {
18    /// The checkpoint sequence number
19    pub sequence_number: u64,
20    /// The epoch this checkpoint belongs to
21    pub epoch: u64,
22    /// The digest of the checkpoint
23    pub digest: [u8; 32],
24    /// Total number of transactions in the checkpoint
25    pub total_transactions: u64,
26    /// Whether the checkpoint is certified
27    pub is_certified: bool,
28}
29
30impl CheckpointInfo {
31    /// Returns true if this checkpoint is certified.
32    pub fn is_finalized(&self) -> bool {
33        self.is_certified
34    }
35}
36
37/// Checkpoint finality verifier for Sui
38pub struct CheckpointVerifier {
39    /// Configuration for checkpoint verification
40    config: CheckpointConfig,
41}
42
43impl CheckpointVerifier {
44    /// Create a new checkpoint verifier with default configuration.
45    pub fn new() -> Self {
46        Self::with_config(CheckpointConfig::default())
47    }
48
49    /// Create a new checkpoint verifier with custom configuration.
50    pub fn with_config(config: CheckpointConfig) -> Self {
51        Self { config }
52    }
53
54    /// Get the verifier configuration.
55    pub fn config(&self) -> &CheckpointConfig {
56        &self.config
57    }
58
59    /// Check if a checkpoint is certified.
60    ///
61    /// In Sui, a checkpoint is certified when it receives signatures from
62    /// 2f+1 validators. Once certified, the checkpoint cannot be reverted.
63    ///
64    /// # Arguments
65    /// * `checkpoint_seq` - The checkpoint sequence number to check
66    /// * `rpc` - RPC client for fetching checkpoint data
67    ///
68    /// # Returns
69    /// `Ok(CheckpointInfo)` with certification details, or `Err` on failure.
70    pub fn is_checkpoint_certified(
71        &self,
72        checkpoint_seq: u64,
73        rpc: &dyn SuiRpc,
74    ) -> SuiResult<CheckpointInfo> {
75        // Check timeout
76        let start = std::time::Instant::now();
77
78        let cp = rpc.get_checkpoint(checkpoint_seq).map_err(|e| {
79            if start.elapsed().as_millis() > self.config.timeout_ms as u128 {
80                SuiError::timeout(
81                    &format!("checkpoint_{}", checkpoint_seq),
82                    self.config.timeout_ms,
83                )
84            } else {
85                SuiError::CheckpointFailed(format!("Failed to get checkpoint: {}", e))
86            }
87        })?;
88
89        match cp {
90            Some(cp) => {
91                let is_certified = if self.config.require_certified {
92                    cp.certified
93                } else {
94                    true
95                };
96
97                Ok(CheckpointInfo {
98                    sequence_number: cp.sequence_number,
99                    epoch: cp.epoch,
100                    digest: cp.digest,
101                    total_transactions: cp.network_total_transactions,
102                    is_certified,
103                })
104            }
105            None => Err(SuiError::CheckpointFailed(format!(
106                "Checkpoint {} not found",
107                checkpoint_seq
108            ))),
109        }
110    }
111
112    /// Check if a transaction's checkpoint is finalized.
113    ///
114    /// # Arguments
115    /// * `tx_checkpoint` - The checkpoint sequence number containing the transaction
116    /// * `rpc` - RPC client for fetching checkpoint data
117    pub fn is_tx_finalized(&self, tx_checkpoint: u64, rpc: &dyn SuiRpc) -> SuiResult<bool> {
118        let info = self.is_checkpoint_certified(tx_checkpoint, rpc)?;
119        Ok(info.is_finalized())
120    }
121
122    /// Get the latest certified checkpoint.
123    ///
124    /// # Arguments
125    /// * `rpc` - RPC client for fetching checkpoint data
126    pub fn latest_certified_checkpoint(&self, rpc: &dyn SuiRpc) -> SuiResult<Option<u64>> {
127        let latest = rpc.get_latest_checkpoint_sequence_number().map_err(|e| {
128            SuiError::CheckpointFailed(format!("Failed to get latest checkpoint: {}", e))
129        })?;
130
131        // Walk backwards to find the first certified checkpoint
132        let max_lookback = self.config.max_epoch_lookback;
133        let start = latest.saturating_sub(max_lookback * 1000); // Approximate checkpoints per epoch
134
135        for seq in (start..=latest).rev() {
136            if let Some(cp) = rpc.get_checkpoint(seq).ok().flatten() {
137                if cp.certified {
138                    return Ok(Some(seq));
139                }
140            }
141        }
142        Ok(None)
143    }
144
145    /// Get the current epoch from the network.
146    ///
147    /// # Arguments
148    /// * `rpc` - RPC client for fetching epoch info
149    pub fn current_epoch(&self, rpc: &dyn SuiRpc) -> SuiResult<u64> {
150        let latest = self.latest_certified_checkpoint(rpc)?;
151        match latest {
152            Some(seq) => {
153                let cp = rpc.get_checkpoint(seq).map_err(|e| {
154                    SuiError::CheckpointFailed(format!("Failed to get checkpoint: {}", e))
155                })?;
156                Ok(cp.map(|c| c.epoch).unwrap_or(0))
157            }
158            None => Ok(0),
159        }
160    }
161
162    /// Verify that an epoch boundary has passed.
163    ///
164    /// # Arguments
165    /// * `expected_epoch` - The epoch we expect the network to be in
166    /// * `rpc` - RPC client for fetching current epoch
167    pub fn is_epoch_passed(&self, expected_epoch: u64, rpc: &dyn SuiRpc) -> SuiResult<bool> {
168        let current = self.current_epoch(rpc)?;
169        Ok(current >= expected_epoch)
170    }
171}
172
173impl Default for CheckpointVerifier {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::rpc::{MockSuiRpc, SuiCheckpoint};
183
184    #[test]
185    fn test_certified_checkpoint() {
186        let rpc = MockSuiRpc::new(1000);
187        rpc.add_checkpoint(SuiCheckpoint {
188            sequence_number: 500,
189            digest: [1u8; 32],
190            epoch: 1,
191            network_total_transactions: 50000,
192            certified: true,
193        });
194        rpc.add_checkpoint(SuiCheckpoint {
195            sequence_number: 501,
196            digest: [2u8; 32],
197            epoch: 1,
198            network_total_transactions: 50100,
199            certified: false,
200        });
201
202        let verifier = CheckpointVerifier::new();
203        let result = verifier.is_checkpoint_certified(500, &rpc).unwrap();
204        assert!(result.is_certified);
205        assert_eq!(result.sequence_number, 500);
206        assert_eq!(result.epoch, 1);
207
208        let result = verifier.is_checkpoint_certified(501, &rpc).unwrap();
209        assert!(!result.is_certified);
210
211        assert!(verifier.is_checkpoint_certified(999, &rpc).is_err());
212    }
213
214    #[test]
215    fn test_tx_finalization() {
216        let rpc = MockSuiRpc::new(1000);
217        rpc.add_checkpoint(SuiCheckpoint {
218            sequence_number: 500,
219            digest: [1u8; 32],
220            epoch: 1,
221            network_total_transactions: 50000,
222            certified: true,
223        });
224
225        let verifier = CheckpointVerifier::new();
226        assert!(verifier.is_tx_finalized(500, &rpc).unwrap());
227        assert!(verifier.is_tx_finalized(600, &rpc).is_err());
228    }
229
230    #[test]
231    fn test_latest_certified() {
232        let rpc = MockSuiRpc::new(1000);
233        rpc.add_checkpoint(SuiCheckpoint {
234            sequence_number: 998,
235            digest: [1u8; 32],
236            epoch: 1,
237            network_total_transactions: 99800,
238            certified: true,
239        });
240        rpc.add_checkpoint(SuiCheckpoint {
241            sequence_number: 999,
242            digest: [2u8; 32],
243            epoch: 1,
244            network_total_transactions: 99900,
245            certified: false,
246        });
247        rpc.add_checkpoint(SuiCheckpoint {
248            sequence_number: 1000,
249            digest: [3u8; 32],
250            epoch: 1,
251            network_total_transactions: 100000,
252            certified: false,
253        });
254
255        let verifier = CheckpointVerifier::new();
256        let latest = verifier.latest_certified_checkpoint(&rpc).unwrap();
257        assert_eq!(latest, Some(998));
258    }
259
260    #[test]
261    fn test_checkpoint_config() {
262        let config = CheckpointConfig {
263            require_certified: false,
264            max_epoch_lookback: 3,
265            timeout_ms: 10_000,
266        };
267        let verifier = CheckpointVerifier::with_config(config);
268        assert!(!verifier.config().require_certified);
269        assert_eq!(verifier.config().max_epoch_lookback, 3);
270    }
271
272    #[test]
273    fn test_checkpoint_info() {
274        let info = CheckpointInfo {
275            sequence_number: 100,
276            epoch: 1,
277            digest: [1u8; 32],
278            total_transactions: 10000,
279            is_certified: true,
280        };
281
282        assert!(info.is_finalized());
283        assert_eq!(info.sequence_number, 100);
284    }
285}