use serde::{Deserialize, Serialize};
use crate::error::{SuiError, SuiResult};
use crate::rpc::{SuiObject, SuiRpc};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StateProof {
pub object_id: [u8; 32],
pub version: u64,
pub merkle_proof: Vec<u8>,
pub state_root: [u8; 32],
}
impl StateProof {
pub fn new(
object_id: [u8; 32],
version: u64,
merkle_proof: Vec<u8>,
state_root: [u8; 32],
) -> Self {
Self {
object_id,
version,
merkle_proof,
state_root,
}
}
pub fn leaf_hash(&self) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(self.object_id);
hasher.update(self.version.to_le_bytes());
hasher.finalize().into()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TransactionProof {
pub tx_digest: [u8; 32],
pub checkpoint: u64,
pub effects_signature: Vec<u8>,
}
impl TransactionProof {
pub fn new(tx_digest: [u8; 32], checkpoint: u64, effects_signature: Vec<u8>) -> Self {
Self {
tx_digest,
checkpoint,
effects_signature,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EventProof {
pub tx_digest: [u8; 32],
pub event_index: u64,
pub expected_hash: [u8; 32],
}
impl EventProof {
pub fn new(tx_digest: [u8; 32], event_index: u64, expected_hash: [u8; 32]) -> Self {
Self {
tx_digest,
event_index,
expected_hash,
}
}
pub fn compute_event_hash(data: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().into()
}
}
pub struct StateProofVerifier;
impl StateProofVerifier {
pub fn verify_object_exists(
object_id: [u8; 32],
rpc: &dyn SuiRpc,
) -> SuiResult<Option<SuiObject>> {
let obj = rpc
.get_object(object_id)
.map_err(|e| SuiError::StateProofFailed(format!("Failed to fetch object: {}", e)))?;
Ok(obj)
}
pub fn verify_object_consumed(object_id: [u8; 32], rpc: &dyn SuiRpc) -> SuiResult<bool> {
let obj = rpc
.get_object(object_id)
.map_err(|e| SuiError::StateProofFailed(format!("Failed to fetch object: {}", e)))?;
Ok(obj.is_none())
}
pub fn verify_object_consumed_in_tx(
tx_digest: [u8; 32],
object_id: [u8; 32],
rpc: &dyn SuiRpc,
) -> SuiResult<bool> {
let tx = rpc.get_transaction_block(tx_digest).map_err(|e| {
SuiError::StateProofFailed(format!("Failed to fetch transaction: {}", e))
})?;
match tx {
Some(tx_block) => {
let consumed = tx_block.effects.modified_objects.iter().any(|change| {
change.object_id == object_id
&& (change.change_type == "deleted" || change.change_type == "mutated")
});
Ok(consumed)
}
None => Err(SuiError::StateProofFailed(format!(
"Transaction {:?} not found",
tx_digest
))),
}
}
}
pub struct EventProofVerifier;
impl EventProofVerifier {
pub fn verify_event_in_tx(
tx_digest: [u8; 32],
expected_event_data: &[u8],
rpc: &dyn SuiRpc,
) -> SuiResult<bool> {
let tx = rpc.get_transaction_block(tx_digest).map_err(|e| {
SuiError::EventProofFailed(format!("Failed to fetch transaction: {}", e))
})?;
match tx {
Some(tx_block) => {
if tx_block.effects.status != crate::rpc::SuiExecutionStatus::Success {
return Ok(false);
}
let events = rpc.get_transaction_events(tx_digest).map_err(|e| {
SuiError::EventProofFailed(format!("Failed to fetch events: {}", e))
})?;
if events.is_empty() {
return Ok(false);
}
let expected_hash = EventProof::compute_event_hash(expected_event_data);
for event in &events {
let event_hash = EventProof::compute_event_hash(&event.data);
if event_hash == expected_hash {
return Ok(true);
}
}
Ok(false)
}
None => Err(SuiError::EventProofFailed(format!(
"Transaction {:?} not found",
tx_digest
))),
}
}
}
fn hex_to_bytes_for_proof(hex: &str) -> Result<Vec<u8>, String> {
let hex_str = hex.strip_prefix("0x").unwrap_or(hex);
hex::decode(hex_str).map_err(|e| format!("Invalid hex: {}", e))
}
pub struct CommitmentEventBuilder {
package_id: [u8; 32],
event_type: String,
}
impl CommitmentEventBuilder {
pub fn new(package_id: [u8; 32], event_type: String) -> Self {
Self {
package_id,
event_type,
}
}
pub fn build(&self, commitment_hash: [u8; 32], seal_object_id: [u8; 32]) -> Vec<u8> {
let mut data = Vec::with_capacity(96);
data.extend_from_slice(&self.package_id);
data.extend_from_slice(&commitment_hash);
data.extend_from_slice(&seal_object_id);
data
}
pub fn parse(&self, event_data: &[u8]) -> Result<([u8; 32], [u8; 32]), SuiError> {
if event_data.len() < 96 {
return Err(SuiError::EventProofFailed(format!(
"Event data too short: expected 96 bytes, got {}",
event_data.len()
)));
}
let mut commitment = [0u8; 32];
let mut seal_id = [0u8; 32];
commitment.copy_from_slice(&event_data[32..64]);
seal_id.copy_from_slice(&event_data[64..96]);
Ok((commitment, seal_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::{
MockSuiRpc, SuiExecutionStatus, SuiObject, SuiObjectChange, SuiTransactionBlock,
SuiTransactionEffects,
};
#[test]
fn test_verify_object_exists() {
let rpc = MockSuiRpc::new(1000);
rpc.add_object(SuiObject {
object_id: [1u8; 32],
version: 1,
owner: vec![2, 3],
object_type: "CSV::Seal".to_string(),
has_public_transfer: false,
});
let result = StateProofVerifier::verify_object_exists([1u8; 32], &rpc).unwrap();
assert!(result.is_some());
assert!(StateProofVerifier::verify_object_exists([99u8; 32], &rpc)
.unwrap()
.is_none());
}
#[test]
fn test_verify_object_consumed() {
let rpc = MockSuiRpc::new(1000);
assert!(StateProofVerifier::verify_object_consumed([99u8; 32], &rpc).unwrap());
}
#[test]
fn test_verify_object_consumed_in_tx() {
let rpc = MockSuiRpc::new(1000);
rpc.add_transaction(SuiTransactionBlock {
digest: [1u8; 32],
checkpoint: Some(100),
effects: SuiTransactionEffects {
status: SuiExecutionStatus::Success,
gas_used: 1000,
modified_objects: vec![SuiObjectChange {
object_id: [2u8; 32],
change_type: "deleted".to_string(),
}],
},
});
assert!(
StateProofVerifier::verify_object_consumed_in_tx([1u8; 32], [2u8; 32], &rpc).unwrap()
);
assert!(
!StateProofVerifier::verify_object_consumed_in_tx([1u8; 32], [99u8; 32], &rpc).unwrap()
);
}
#[test]
fn test_event_proof_hash() {
let data = vec![0xAB, 0xCD, 0xEF];
let hash1 = EventProof::compute_event_hash(&data);
let hash2 = EventProof::compute_event_hash(&data);
assert_eq!(hash1, hash2);
let different_data = vec![0xFF];
let hash3 = EventProof::compute_event_hash(&different_data);
assert_ne!(hash1, hash3);
}
#[test]
fn test_commitment_event_builder() {
let builder = CommitmentEventBuilder::new([1u8; 32], "csv_seal::AnchorEvent".to_string());
let event_data = builder.build([2u8; 32], [3u8; 32]);
assert_eq!(event_data.len(), 96);
let (commitment, seal_id) = builder.parse(&event_data).unwrap();
assert_eq!(commitment, [2u8; 32]);
assert_eq!(seal_id, [3u8; 32]);
}
#[test]
fn test_commitment_event_builder_parse_error() {
let builder = CommitmentEventBuilder::new([1u8; 32], "csv_seal::AnchorEvent".to_string());
let short_data = vec![0u8; 50];
assert!(builder.parse(&short_data).is_err());
}
#[test]
fn test_state_proof_leaf_hash() {
let proof = StateProof::new([1u8; 32], 1, vec![], [0u8; 32]);
let hash = proof.leaf_hash();
let hash2 = proof.leaf_hash();
assert_eq!(hash, hash2);
}
#[test]
fn test_verify_event_failed_tx() {
let rpc = MockSuiRpc::new(1000);
rpc.add_transaction(SuiTransactionBlock {
digest: [1u8; 32],
checkpoint: Some(100),
effects: SuiTransactionEffects {
status: SuiExecutionStatus::Failure {
error: "out of gas".to_string(),
},
gas_used: 1000,
modified_objects: vec![],
},
});
assert!(!EventProofVerifier::verify_event_in_tx([1u8; 32], &[], &rpc).unwrap());
}
}