use sui_crypto::bls12381::ValidatorCommitteeSignatureVerifier;
use sui_sdk_types::Address;
use sui_sdk_types::Object;
use sui_sdk_types::ObjectReference;
use sui_sdk_types::SignedCheckpointSummary;
use sui_sdk_types::ValidatorCommittee;
use sui_sdk_types::proof::OcsInclusionProof;
use sui_sdk_types::proof::OcsNonInclusionProof;
use crate::Client;
use crate::field::FieldMask;
use crate::field::FieldMaskUtil;
use crate::proto::TryFromProtoError;
use crate::proto::sui::rpc::v2::GetCheckpointRequest;
use crate::proto::sui::rpc::v2alpha::GetCheckpointObjectProofRequest;
use crate::proto::sui::rpc::v2alpha::get_checkpoint_object_proof_response;
use super::EpochCache;
use super::RatchetConfig;
use super::error::LightClientError;
use super::error::ObjectDataMismatch;
use super::ratchet::ratchet_to_checkpoint_with_config;
#[derive(Debug)]
#[non_exhaustive]
pub enum CheckpointObjectProof {
Inclusion {
object_ref: ObjectReference,
object: Option<Box<Object>>,
},
NonInclusion,
}
pub struct LightClient {
rpc: Client,
archive: Option<Client>,
cache: EpochCache,
ratchet_config: RatchetConfig,
}
impl LightClient {
pub fn new(rpc: Client, starting_committee: ValidatorCommittee) -> Self {
Self {
rpc,
archive: None,
cache: EpochCache::new(starting_committee),
ratchet_config: RatchetConfig::default(),
}
}
pub fn with_ratchet_config(mut self, config: RatchetConfig) -> Self {
self.ratchet_config = config;
self
}
pub fn with_archive(mut self, archive: Client) -> Self {
self.archive = Some(archive);
self
}
pub fn epoch_cache(&self) -> &EpochCache {
&self.cache
}
pub fn rpc(&mut self) -> &mut Client {
&mut self.rpc
}
pub async fn latest_checkpoint_seq(&mut self) -> Result<u64, LightClientError> {
let request = GetCheckpointRequest::latest()
.with_read_mask(FieldMask::from_paths(["sequence_number"]));
let response = self
.rpc
.ledger_client()
.get_checkpoint(request)
.await?
.into_inner();
response
.checkpoint
.and_then(|c| c.sequence_number)
.ok_or_else(|| TryFromProtoError::missing("checkpoint.sequence_number").into())
}
pub async fn prove_object_at_checkpoint(
&mut self,
object_id: &Address,
checkpoint_seq: u64,
) -> Result<CheckpointObjectProof, LightClientError> {
let request = GetCheckpointObjectProofRequest::default()
.with_object_id(object_id.to_string())
.with_checkpoint(checkpoint_seq);
let response = self
.rpc
.proof_client()
.get_checkpoint_object_proof(request)
.await?
.into_inner();
let summary_bytes = response
.checkpoint_summary
.ok_or_else(|| TryFromProtoError::missing("checkpoint_summary"))?;
let proof = response
.proof
.ok_or_else(|| TryFromProtoError::missing("proof"))?;
let signed_summary: SignedCheckpointSummary = bcs::from_bytes(&summary_bytes)?;
let summary_seq = signed_summary.checkpoint.sequence_number;
if summary_seq != checkpoint_seq {
return Err(LightClientError::CheckpointMismatch {
requested: checkpoint_seq,
returned: summary_seq,
});
}
ratchet_to_checkpoint_with_config(
&mut self.rpc,
self.archive.as_mut(),
&mut self.cache,
summary_seq,
&self.ratchet_config,
)
.await?;
let summary_epoch = signed_summary.checkpoint.epoch;
let committee = self.cache.committee_for_epoch(summary_epoch).ok_or(
LightClientError::NoCommitteeForEpoch {
epoch: summary_epoch,
},
)?;
let verifier = ValidatorCommitteeSignatureVerifier::new((*committee).clone())?;
verifier
.verify_checkpoint_summary(&signed_summary.checkpoint, &signed_summary.signature)?;
match proof {
get_checkpoint_object_proof_response::Proof::Inclusion(inclusion_proto) => {
verify_inclusion(&signed_summary, object_id, inclusion_proto)
}
get_checkpoint_object_proof_response::Proof::NonInclusion(non_inclusion_proto) => {
verify_non_inclusion(&signed_summary, object_id, non_inclusion_proto)
}
}
}
}
fn verify_inclusion(
signed_summary: &SignedCheckpointSummary,
object_id: &Address,
inclusion_proto: crate::proto::sui::rpc::v2alpha::OcsInclusionProof,
) -> Result<CheckpointObjectProof, LightClientError> {
let object_ref_proto = inclusion_proto
.object_ref
.as_ref()
.ok_or_else(|| TryFromProtoError::missing("proof.inclusion.object_ref"))?;
let object_ref: ObjectReference = object_ref_proto.try_into()?;
if object_ref.object_id() != object_id {
return Err(LightClientError::ObjectIdMismatch {
requested: *object_id,
returned: *object_ref.object_id(),
});
}
let inclusion_proof: OcsInclusionProof = (&inclusion_proto).try_into()?;
inclusion_proof.verify(&signed_summary.checkpoint, &object_ref)?;
let object = inclusion_proto
.object_data
.as_ref()
.map(|bytes| -> Result<Box<Object>, LightClientError> {
let object: Object = bcs::from_bytes(bytes)?;
let returned_ref =
ObjectReference::new(object.object_id(), object.version(), object.digest());
if returned_ref != object_ref {
return Err(LightClientError::ObjectDataMismatch(Box::new(
ObjectDataMismatch {
expected: object_ref.clone(),
returned: returned_ref,
},
)));
}
Ok(Box::new(object))
})
.transpose()?;
Ok(CheckpointObjectProof::Inclusion { object_ref, object })
}
fn verify_non_inclusion(
signed_summary: &SignedCheckpointSummary,
object_id: &Address,
non_inclusion_proto: crate::proto::sui::rpc::v2alpha::OcsNonInclusionProof,
) -> Result<CheckpointObjectProof, LightClientError> {
let non_inclusion_proof: OcsNonInclusionProof = (&non_inclusion_proto).try_into()?;
non_inclusion_proof.verify(&signed_summary.checkpoint, object_id)?;
Ok(CheckpointObjectProof::NonInclusion)
}