1use serde::{Deserialize, Serialize};
10
11use crate::config::CheckpointConfig;
12use crate::error::{AptosError, AptosResult};
13use crate::rpc::AptosRpc;
14
15#[derive(Clone, Debug, Serialize, Deserialize)]
17pub struct CheckpointInfo {
18 pub version: u64,
20 pub epoch: u64,
22 pub round: u64,
24 pub signatures_count: u64,
26 pub is_certified: bool,
28}
29
30impl CheckpointInfo {
31 pub fn has_quorum(&self, required_signatures: u64) -> bool {
33 self.signatures_count >= required_signatures
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_version_finalized(
72 &self,
73 version: u64,
74 rpc: &dyn AptosRpc,
75 required_signatures: u64,
76 ) -> AptosResult<CheckpointInfo> {
77 let start = std::time::Instant::now();
79
80 let block = rpc.get_block_by_version(version).map_err(|e| {
81 if start.elapsed().as_millis() > self.config.timeout_ms as u128 {
82 AptosError::timeout(&format!("version_{}", version), self.config.timeout_ms)
83 } else {
84 AptosError::CheckpointFailed(format!("Failed to get block: {}", e))
85 }
86 })?;
87
88 match block {
89 Some(block) => {
90 let is_certified = if self.config.require_certified {
93 required_signatures > 0 && block.round > 0
96 } else {
97 true
99 };
100
101 Ok(CheckpointInfo {
102 version,
103 epoch: block.epoch,
104 round: block.round,
105 signatures_count: required_signatures,
106 is_certified,
107 })
108 }
109 None => Err(AptosError::CheckpointFailed(format!(
110 "Block containing version {} not found",
111 version
112 ))),
113 }
114 }
115
116 pub fn is_resource_present(
125 &self,
126 address: [u8; 32],
127 resource_type: &str,
128 rpc: &dyn AptosRpc,
129 ) -> AptosResult<bool> {
130 let resource = rpc.get_resource(address, resource_type, None)?;
131 Ok(resource.is_some())
132 }
133
134 pub fn verify_event_in_transaction(
141 &self,
142 tx_version: u64,
143 expected_event_data: &[u8],
144 rpc: &dyn AptosRpc,
145 ) -> AptosResult<bool> {
146 let tx = rpc.get_transaction_by_version(tx_version)?;
147 match tx {
148 Some(tx) => {
149 if !tx.success {
150 return Ok(false);
151 }
152 Ok(tx.events.iter().any(|e| e.data == expected_event_data))
153 }
154 None => Err(AptosError::EventProofFailed(format!(
155 "Transaction at version {} not found",
156 tx_version
157 ))),
158 }
159 }
160
161 pub fn current_epoch(&self, rpc: &dyn AptosRpc) -> AptosResult<u64> {
166 let ledger = rpc.get_ledger_info()?;
167 Ok(ledger.epoch)
168 }
169
170 pub fn is_epoch_passed(&self, expected_epoch: u64, rpc: &dyn AptosRpc) -> AptosResult<bool> {
178 let current = self.current_epoch(rpc)?;
179 Ok(current >= expected_epoch)
180 }
181}
182
183impl Default for CheckpointVerifier {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::rpc::{AptosBlockInfo, AptosEvent, AptosResource, AptosTransaction, MockAptosRpc};
193
194 #[test]
195 fn test_version_finalization() {
196 let rpc = MockAptosRpc::new(5000);
197 rpc.set_block(
198 1500,
199 AptosBlockInfo {
200 version: 1500,
201 block_hash: [1u8; 32],
202 epoch: 1,
203 round: 42,
204 timestamp_usecs: 1234567890,
205 },
206 );
207
208 let verifier = CheckpointVerifier::new();
209 let result = verifier.is_version_finalized(1500, &rpc, 3).unwrap();
210 assert!(result.is_certified);
211 assert_eq!(result.version, 1500);
212 assert_eq!(result.epoch, 1);
213 assert_eq!(result.round, 42);
214 }
215
216 #[test]
217 fn test_version_not_found() {
218 let rpc = MockAptosRpc::new(5000);
219
220 let verifier = CheckpointVerifier::new();
221 let result = verifier.is_version_finalized(9999, &rpc, 3);
222 assert!(result.is_err());
223 }
224
225 #[test]
226 fn test_resource_presence() {
227 let rpc = MockAptosRpc::new(5000);
228 rpc.set_resource(
229 [1u8; 32],
230 "CSV::Seal",
231 AptosResource {
232 data: vec![1, 2, 3],
233 },
234 );
235
236 let verifier = CheckpointVerifier::new();
237 assert!(verifier
238 .is_resource_present([1u8; 32], "CSV::Seal", &rpc)
239 .unwrap());
240 assert!(!verifier
241 .is_resource_present([99u8; 32], "CSV::Seal", &rpc)
242 .unwrap());
243 }
244
245 #[test]
246 fn test_failed_transaction_event() {
247 let rpc = MockAptosRpc::new(5000);
248 rpc.add_transaction(
249 1500,
250 AptosTransaction {
251 version: 1500,
252 hash: [3u8; 32],
253 state_change_hash: [0u8; 32],
254 event_root_hash: [0u8; 32],
255 state_checkpoint_hash: None,
256 epoch: 1,
257 round: 0,
258 events: vec![AptosEvent {
259 event_sequence_number: 0,
260 key: "CSV::Seal".to_string(),
261 data: vec![0xAB, 0xCD],
262 transaction_version: 1500,
263 }],
264 payload: vec![],
265 success: false,
266 vm_status: "Execution failed".to_string(),
267 gas_used: 0,
268 cumulative_gas_used: 0,
269 },
270 );
271
272 let verifier = CheckpointVerifier::new();
273 assert!(!verifier
274 .verify_event_in_transaction(1500, &[0xAB, 0xCD], &rpc)
275 .unwrap());
276 assert!(!verifier
277 .verify_event_in_transaction(1500, &[0xFF], &rpc)
278 .unwrap());
279 assert!(verifier
280 .verify_event_in_transaction(9999, &[0xAB], &rpc)
281 .is_err());
282 }
283
284 #[test]
285 fn test_event_in_transaction() {
286 let rpc = MockAptosRpc::new(5000);
287 rpc.add_transaction(
288 1500,
289 AptosTransaction {
290 version: 1500,
291 hash: [3u8; 32],
292 state_change_hash: [0u8; 32],
293 event_root_hash: [0u8; 32],
294 state_checkpoint_hash: None,
295 epoch: 1,
296 round: 0,
297 events: vec![AptosEvent {
298 event_sequence_number: 0,
299 key: "CSV::Seal".to_string(),
300 data: vec![0xAB, 0xCD],
301 transaction_version: 1500,
302 }],
303 payload: vec![],
304 success: true,
305 vm_status: "Executed".to_string(),
306 gas_used: 0,
307 cumulative_gas_used: 0,
308 },
309 );
310
311 let verifier = CheckpointVerifier::new();
312 assert!(verifier
313 .verify_event_in_transaction(1500, &[0xAB, 0xCD], &rpc)
314 .unwrap());
315 }
316
317 #[test]
318 fn test_checkpoint_config() {
319 let config = CheckpointConfig {
320 require_certified: false,
321 max_epoch_lookback: 3,
322 timeout_ms: 10_000,
323 };
324 let verifier = CheckpointVerifier::with_config(config);
325 assert!(!verifier.config().require_certified);
326 assert_eq!(verifier.config().max_epoch_lookback, 3);
327 }
328
329 #[test]
330 fn test_checkpoint_info_quorum() {
331 let info = CheckpointInfo {
332 version: 100,
333 epoch: 1,
334 round: 42,
335 signatures_count: 67,
336 is_certified: true,
337 };
338
339 assert!(info.has_quorum(67));
340 assert!(info.has_quorum(50));
341 assert!(!info.has_quorum(100));
342 }
343}