csv_adapter_sui/
checkpoint.rs1use serde::{Deserialize, Serialize};
10
11use crate::config::CheckpointConfig;
12use crate::error::{SuiError, SuiResult};
13use crate::rpc::SuiRpc;
14
15#[derive(Clone, Debug, Serialize, Deserialize)]
17pub struct CheckpointInfo {
18 pub sequence_number: u64,
20 pub epoch: u64,
22 pub digest: [u8; 32],
24 pub total_transactions: u64,
26 pub is_certified: bool,
28}
29
30impl CheckpointInfo {
31 pub fn is_finalized(&self) -> bool {
33 self.is_certified
34 }
35}
36
37pub struct CheckpointVerifier {
39 config: CheckpointConfig,
41}
42
43impl CheckpointVerifier {
44 pub fn new() -> Self {
46 Self::with_config(CheckpointConfig::default())
47 }
48
49 pub fn with_config(config: CheckpointConfig) -> Self {
51 Self { config }
52 }
53
54 pub fn config(&self) -> &CheckpointConfig {
56 &self.config
57 }
58
59 pub fn is_checkpoint_certified(
71 &self,
72 checkpoint_seq: u64,
73 rpc: &dyn SuiRpc,
74 ) -> SuiResult<CheckpointInfo> {
75 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 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 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 let max_lookback = self.config.max_epoch_lookback;
133 let start = latest.saturating_sub(max_lookback * 1000); 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 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 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}