Skip to main content

csv_adapter_aptos/
checkpoint.rs

1//! Aptos checkpoint finality verifier
2//!
3//! This module provides checkpoint verification for Aptos,
4//! verifying that transactions are in blocks certified by 2f+1 validators.
5//!
6//! Aptos uses HotStuff consensus, which provides deterministic finality:
7//! once a block is certified by 2f+1 validators, it cannot be reverted.
8
9use serde::{Deserialize, Serialize};
10
11use crate::config::CheckpointConfig;
12use crate::error::{AptosError, AptosResult};
13use crate::rpc::AptosRpc;
14
15/// Checkpoint (block) information with certification details.
16#[derive(Clone, Debug, Serialize, Deserialize)]
17pub struct CheckpointInfo {
18    /// The checkpoint version (same as transaction version range)
19    pub version: u64,
20    /// The epoch this checkpoint belongs to
21    pub epoch: u64,
22    /// The round number within the epoch
23    pub round: u64,
24    /// Number of validator signatures (should be >= 2f+1)
25    pub signatures_count: u64,
26    /// Whether the checkpoint is certified
27    pub is_certified: bool,
28}
29
30impl CheckpointInfo {
31    /// Returns true if this checkpoint has sufficient validator signatures.
32    pub fn has_quorum(&self, required_signatures: u64) -> bool {
33        self.signatures_count >= required_signatures
34    }
35}
36
37/// Checkpoint finality verifier for Aptos
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 transaction version is in a certified block.
60    ///
61    /// In Aptos, a block is certified when it receives signatures from
62    /// 2f+1 validators. Once certified, the block cannot be reverted.
63    ///
64    /// # Arguments
65    /// * `version` - The transaction version to check
66    /// * `rpc` - RPC client for fetching block data
67    /// * `required_signatures` - Required number of validator signatures (2f+1)
68    ///
69    /// # Returns
70    /// `Ok(CheckpointInfo)` with certification details, or `Err` on failure.
71    pub fn is_version_finalized(
72        &self,
73        version: u64,
74        rpc: &dyn AptosRpc,
75        required_signatures: u64,
76    ) -> AptosResult<CheckpointInfo> {
77        // Check timeout
78        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                // In production: verify block header signatures
91                // The block should have 2f+1 validator signatures
92                let is_certified = if self.config.require_certified {
93                    // Check if the round indicates certification
94                    // Aptos blocks are certified when they have 2f+1 signatures
95                    required_signatures > 0 && block.round > 0
96                } else {
97                    // Just check block exists
98                    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    /// Check if a resource still exists (for seal verification).
117    ///
118    /// This verifies that a seal resource has not been consumed yet.
119    ///
120    /// # Arguments
121    /// * `address` - The account address
122    /// * `resource_type` - The resource type tag
123    /// * `rpc` - RPC client for fetching resource data
124    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    /// Verify an event was emitted in a specific transaction.
135    ///
136    /// # Arguments
137    /// * `tx_version` - The transaction version to check
138    /// * `expected_event_data` - The expected event data bytes
139    /// * `rpc` - RPC client for fetching transaction data
140    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    /// Get the current epoch from the network.
162    ///
163    /// # Arguments
164    /// * `rpc` - RPC client for fetching epoch info
165    pub fn current_epoch(&self, rpc: &dyn AptosRpc) -> AptosResult<u64> {
166        let ledger = rpc.get_ledger_info()?;
167        Ok(ledger.epoch)
168    }
169
170    /// Verify that an epoch boundary has passed.
171    ///
172    /// This is useful for ensuring the network has progressed beyond a certain point.
173    ///
174    /// # Arguments
175    /// * `expected_epoch` - The epoch we expect the network to be in
176    /// * `rpc` - RPC client for fetching current epoch
177    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}