use sui_crypto::bls12381::ValidatorCommitteeSignatureVerifier;
use sui_sdk_types::CheckpointSummary;
use sui_sdk_types::ValidatorAggregatedSignature;
use sui_sdk_types::ValidatorCommittee;
use crate::Client;
use crate::field::FieldMask;
use crate::field::FieldMaskUtil;
use crate::proto::sui::rpc::v2::GetCheckpointRequest;
use crate::proto::sui::rpc::v2::GetEpochRequest;
use super::EpochCache;
use super::RatchetConfig;
use super::error::LightClientError;
use super::retry;
pub async fn ratchet_to_checkpoint(
client: &mut Client,
cache: &mut EpochCache,
target_seq: u64,
) -> Result<(), LightClientError> {
ratchet_to_checkpoint_with_config(client, None, cache, target_seq, &RatchetConfig::default())
.await
}
pub async fn ratchet_to_checkpoint_with_config(
fullnode: &mut Client,
archive: Option<&mut Client>,
cache: &mut EpochCache,
target_seq: u64,
config: &RatchetConfig,
) -> Result<(), LightClientError> {
let mut archive = archive;
let to_advance = discover_epochs_to_advance(
fullnode,
archive.as_deref_mut(),
cache.current_epoch(),
target_seq,
config,
)
.await?;
if to_advance.is_empty() {
return Ok(());
}
let fetched = fetch_end_of_epoch_summaries(fullnode, archive, &to_advance, config).await?;
for (end_seq, summary, signature) in fetched {
apply_verified_end_of_epoch(cache, end_seq, &summary, &signature)?;
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
struct EpochToAdvance {
epoch: u64,
end_of_epoch_seq: u64,
}
async fn discover_epochs_to_advance(
fullnode: &mut Client,
mut archive: Option<&mut Client>,
start_epoch: u64,
target_seq: u64,
config: &RatchetConfig,
) -> Result<Vec<EpochToAdvance>, LightClientError> {
let mut to_advance = Vec::new();
let mut epoch_number = start_epoch;
loop {
if to_advance.len() as u64 >= config.max_ratchet_gap {
return Err(LightClientError::RatchetGapTooLarge {
current: start_epoch,
target: epoch_number,
max: config.max_ratchet_gap,
});
}
let epoch = fetch_one_epoch(fullnode, archive.as_deref_mut(), epoch_number, config).await?;
let Some(last_of_epoch) = epoch.last_checkpoint else {
break;
};
if last_of_epoch >= target_seq {
break;
}
to_advance.push(EpochToAdvance {
epoch: epoch_number,
end_of_epoch_seq: last_of_epoch,
});
epoch_number += 1;
}
Ok(to_advance)
}
async fn fetch_one_epoch(
fullnode: &mut Client,
archive: Option<&mut Client>,
epoch_number: u64,
config: &RatchetConfig,
) -> Result<crate::proto::sui::rpc::v2::Epoch, LightClientError> {
if let Some(archive) = archive {
let request = GetEpochRequest::new(epoch_number)
.with_read_mask(FieldMask::from_paths(["last_checkpoint"]));
if let Ok(resp) = archive.ledger_client().get_epoch(request).await
&& let Some(epoch) = resp.into_inner().epoch
&& epoch.last_checkpoint.is_some()
{
return Ok(epoch);
}
}
let mut attempt: u32 = 0;
loop {
let request = GetEpochRequest::new(epoch_number)
.with_read_mask(FieldMask::from_paths(["last_checkpoint"]));
match fullnode.ledger_client().get_epoch(request).await {
Ok(resp) => {
return resp.into_inner().epoch.ok_or_else(|| {
LightClientError::Proto(crate::proto::TryFromProtoError::missing("epoch"))
});
}
Err(status) if status.code() == tonic::Code::NotFound => {
return Err(LightClientError::EpochNotFound {
epoch: epoch_number,
});
}
Err(status) => {
retry::step(config, LightClientError::Rpc(status), &mut attempt).await?;
}
}
}
}
async fn fetch_end_of_epoch_summaries(
fullnode: &mut Client,
archive: Option<&mut Client>,
to_advance: &[EpochToAdvance],
config: &RatchetConfig,
) -> Result<Vec<(u64, CheckpointSummary, ValidatorAggregatedSignature)>, LightClientError> {
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
let fullnode_clients: Vec<_> = (0..to_advance.len())
.map(|_| fullnode.ledger_client())
.collect();
let archive_clients: Vec<_> = match archive {
Some(archive) => (0..to_advance.len())
.map(|_| Some(archive.ledger_client()))
.collect(),
None => (0..to_advance.len()).map(|_| None).collect(),
};
let concurrency = config.concurrency.max(1);
let futures = to_advance
.iter()
.copied()
.zip(fullnode_clients.into_iter().zip(archive_clients))
.map(|(item, (mut fullnode_ledger, archive_ledger))| async move {
let mut response = None;
if let Some(mut archive_ledger) = archive_ledger {
let request = GetCheckpointRequest::by_sequence_number(item.end_of_epoch_seq)
.with_read_mask(FieldMask::from_paths(["summary.bcs", "signature"]));
if let Ok(resp) = archive_ledger.get_checkpoint(request).await {
response = Some(resp.into_inner());
}
}
let response = if let Some(resp) = response {
resp
} else {
let mut attempt: u32 = 0;
loop {
let request = GetCheckpointRequest::by_sequence_number(item.end_of_epoch_seq)
.with_read_mask(FieldMask::from_paths(["summary.bcs", "signature"]));
match fullnode_ledger.get_checkpoint(request).await {
Ok(resp) => break resp.into_inner(),
Err(status) if status.code() == tonic::Code::NotFound => {
return Err(LightClientError::EpochNotFound { epoch: item.epoch });
}
Err(status) => {
retry::step(config, LightClientError::Rpc(status), &mut attempt)
.await?;
}
}
}
};
let checkpoint = response.checkpoint.ok_or_else(|| {
LightClientError::Proto(crate::proto::TryFromProtoError::missing("checkpoint"))
})?;
let summary_bcs = checkpoint
.summary
.as_ref()
.and_then(|s| s.bcs.as_ref())
.ok_or_else(|| {
LightClientError::Proto(crate::proto::TryFromProtoError::missing("summary.bcs"))
})?;
let signature_proto = checkpoint.signature.as_ref().ok_or_else(|| {
LightClientError::Proto(crate::proto::TryFromProtoError::missing("signature"))
})?;
let summary: CheckpointSummary = summary_bcs.deserialize()?;
let signature: ValidatorAggregatedSignature = signature_proto.try_into()?;
Ok::<_, LightClientError>((item.epoch, item.end_of_epoch_seq, summary, signature))
});
let mut fetched: Vec<_> = futures::stream::iter(futures)
.buffer_unordered(concurrency)
.try_collect()
.await?;
fetched.sort_by_key(|(epoch, _, _, _)| *epoch);
Ok(fetched
.into_iter()
.map(|(_, end_seq, summary, signature)| (end_seq, summary, signature))
.collect())
}
pub(crate) fn apply_verified_end_of_epoch(
cache: &mut EpochCache,
end_of_epoch_seq: u64,
summary: &CheckpointSummary,
signature: &ValidatorAggregatedSignature,
) -> Result<(), LightClientError> {
let verifier = ValidatorCommitteeSignatureVerifier::new(cache.current_committee().clone())?;
verifier.verify_checkpoint_summary(summary, signature)?;
let next_committee = extract_next_epoch_committee(cache, summary, end_of_epoch_seq)?;
cache.apply_ratchet_update(next_committee)?;
Ok(())
}
fn extract_next_epoch_committee(
cache: &EpochCache,
summary: &CheckpointSummary,
checkpoint: u64,
) -> Result<ValidatorCommittee, LightClientError> {
let end_of_epoch_data = summary
.end_of_epoch_data
.as_ref()
.ok_or(LightClientError::MissingEndOfEpochData { checkpoint })?;
Ok(ValidatorCommittee {
epoch: cache.current_epoch() + 1,
members: end_of_epoch_data.next_epoch_committee.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use sui_sdk_types::CheckpointCommitment;
use sui_sdk_types::Digest;
use sui_sdk_types::EndOfEpochData;
use sui_sdk_types::GasCostSummary;
use sui_sdk_types::ValidatorCommitteeMember;
fn committee(epoch: u64) -> ValidatorCommittee {
ValidatorCommittee {
epoch,
members: Vec::new(),
}
}
fn make_end_of_epoch_summary(
epoch: u64,
members: Vec<ValidatorCommitteeMember>,
) -> CheckpointSummary {
CheckpointSummary {
epoch,
sequence_number: 99,
network_total_transactions: 0,
content_digest: Digest::ZERO,
previous_digest: None,
epoch_rolling_gas_cost_summary: GasCostSummary::default(),
timestamp_ms: 0,
checkpoint_commitments: Vec::<CheckpointCommitment>::new(),
end_of_epoch_data: Some(EndOfEpochData {
next_epoch_committee: members,
next_epoch_protocol_version: 1,
epoch_commitments: Vec::new(),
}),
version_specific_data: Vec::new(),
}
}
#[test]
fn extract_next_epoch_committee_uses_summary_members_and_advances_epoch() {
let cache = EpochCache::new(committee(7));
let members: Vec<ValidatorCommitteeMember> = Vec::new();
let summary = make_end_of_epoch_summary(7, members);
let next = extract_next_epoch_committee(&cache, &summary, 99).unwrap();
assert_eq!(next.epoch, 8);
assert!(next.members.is_empty());
}
#[test]
fn extract_next_epoch_committee_requires_end_of_epoch_data() {
let cache = EpochCache::new(committee(0));
let summary = CheckpointSummary {
epoch: 0,
sequence_number: 42,
network_total_transactions: 0,
content_digest: Digest::ZERO,
previous_digest: None,
epoch_rolling_gas_cost_summary: GasCostSummary::default(),
timestamp_ms: 0,
checkpoint_commitments: Vec::new(),
end_of_epoch_data: None,
version_specific_data: Vec::new(),
};
let err = extract_next_epoch_committee(&cache, &summary, 42).unwrap_err();
assert!(
matches!(
err,
LightClientError::MissingEndOfEpochData { checkpoint: 42 }
),
"got {err:?}"
);
}
}