use serde::{Deserialize, Serialize};
use crate::config::CheckpointConfig;
use crate::error::{SuiError, SuiResult};
use crate::rpc::SuiRpc;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CheckpointInfo {
pub sequence_number: u64,
pub epoch: u64,
pub digest: [u8; 32],
pub total_transactions: u64,
pub is_certified: bool,
}
impl CheckpointInfo {
pub fn is_finalized(&self) -> bool {
self.is_certified
}
}
pub struct CheckpointVerifier {
config: CheckpointConfig,
}
impl CheckpointVerifier {
pub fn new() -> Self {
Self::with_config(CheckpointConfig::default())
}
pub fn with_config(config: CheckpointConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &CheckpointConfig {
&self.config
}
pub fn is_checkpoint_certified(
&self,
checkpoint_seq: u64,
rpc: &dyn SuiRpc,
) -> SuiResult<CheckpointInfo> {
let start = std::time::Instant::now();
let cp = rpc.get_checkpoint(checkpoint_seq).map_err(|e| {
if start.elapsed().as_millis() > self.config.timeout_ms as u128 {
SuiError::timeout(
&format!("checkpoint_{}", checkpoint_seq),
self.config.timeout_ms,
)
} else {
SuiError::CheckpointFailed(format!("Failed to get checkpoint: {}", e))
}
})?;
match cp {
Some(cp) => {
let is_certified = if self.config.require_certified {
cp.certified
} else {
true
};
Ok(CheckpointInfo {
sequence_number: cp.sequence_number,
epoch: cp.epoch,
digest: cp.digest,
total_transactions: cp.network_total_transactions,
is_certified,
})
}
None => Err(SuiError::CheckpointFailed(format!(
"Checkpoint {} not found",
checkpoint_seq
))),
}
}
pub fn is_tx_finalized(&self, tx_checkpoint: u64, rpc: &dyn SuiRpc) -> SuiResult<bool> {
let info = self.is_checkpoint_certified(tx_checkpoint, rpc)?;
Ok(info.is_finalized())
}
pub fn latest_certified_checkpoint(&self, rpc: &dyn SuiRpc) -> SuiResult<Option<u64>> {
let latest = rpc.get_latest_checkpoint_sequence_number().map_err(|e| {
SuiError::CheckpointFailed(format!("Failed to get latest checkpoint: {}", e))
})?;
let max_lookback = self.config.max_epoch_lookback;
let start = latest.saturating_sub(max_lookback * 1000);
for seq in (start..=latest).rev() {
if let Some(cp) = rpc.get_checkpoint(seq).ok().flatten() {
if cp.certified {
return Ok(Some(seq));
}
}
}
Ok(None)
}
pub fn current_epoch(&self, rpc: &dyn SuiRpc) -> SuiResult<u64> {
let latest = self.latest_certified_checkpoint(rpc)?;
match latest {
Some(seq) => {
let cp = rpc.get_checkpoint(seq).map_err(|e| {
SuiError::CheckpointFailed(format!("Failed to get checkpoint: {}", e))
})?;
Ok(cp.map(|c| c.epoch).unwrap_or(0))
}
None => Ok(0),
}
}
pub fn is_epoch_passed(&self, expected_epoch: u64, rpc: &dyn SuiRpc) -> SuiResult<bool> {
let current = self.current_epoch(rpc)?;
Ok(current >= expected_epoch)
}
}
impl Default for CheckpointVerifier {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::{MockSuiRpc, SuiCheckpoint};
#[test]
fn test_certified_checkpoint() {
let rpc = MockSuiRpc::new(1000);
rpc.add_checkpoint(SuiCheckpoint {
sequence_number: 500,
digest: [1u8; 32],
epoch: 1,
network_total_transactions: 50000,
certified: true,
});
rpc.add_checkpoint(SuiCheckpoint {
sequence_number: 501,
digest: [2u8; 32],
epoch: 1,
network_total_transactions: 50100,
certified: false,
});
let verifier = CheckpointVerifier::new();
let result = verifier.is_checkpoint_certified(500, &rpc).unwrap();
assert!(result.is_certified);
assert_eq!(result.sequence_number, 500);
assert_eq!(result.epoch, 1);
let result = verifier.is_checkpoint_certified(501, &rpc).unwrap();
assert!(!result.is_certified);
assert!(verifier.is_checkpoint_certified(999, &rpc).is_err());
}
#[test]
fn test_tx_finalization() {
let rpc = MockSuiRpc::new(1000);
rpc.add_checkpoint(SuiCheckpoint {
sequence_number: 500,
digest: [1u8; 32],
epoch: 1,
network_total_transactions: 50000,
certified: true,
});
let verifier = CheckpointVerifier::new();
assert!(verifier.is_tx_finalized(500, &rpc).unwrap());
assert!(verifier.is_tx_finalized(600, &rpc).is_err());
}
#[test]
fn test_latest_certified() {
let rpc = MockSuiRpc::new(1000);
rpc.add_checkpoint(SuiCheckpoint {
sequence_number: 998,
digest: [1u8; 32],
epoch: 1,
network_total_transactions: 99800,
certified: true,
});
rpc.add_checkpoint(SuiCheckpoint {
sequence_number: 999,
digest: [2u8; 32],
epoch: 1,
network_total_transactions: 99900,
certified: false,
});
rpc.add_checkpoint(SuiCheckpoint {
sequence_number: 1000,
digest: [3u8; 32],
epoch: 1,
network_total_transactions: 100000,
certified: false,
});
let verifier = CheckpointVerifier::new();
let latest = verifier.latest_certified_checkpoint(&rpc).unwrap();
assert_eq!(latest, Some(998));
}
#[test]
fn test_checkpoint_config() {
let config = CheckpointConfig {
require_certified: false,
max_epoch_lookback: 3,
timeout_ms: 10_000,
};
let verifier = CheckpointVerifier::with_config(config);
assert!(!verifier.config().require_certified);
assert_eq!(verifier.config().max_epoch_lookback, 3);
}
#[test]
fn test_checkpoint_info() {
let info = CheckpointInfo {
sequence_number: 100,
epoch: 1,
digest: [1u8; 32],
total_transactions: 10000,
is_certified: true,
};
assert!(info.is_finalized());
assert_eq!(info.sequence_number, 100);
}
}