1use serde::{Deserialize, Serialize};
7
8use crate::error::{SuiError, SuiResult};
9use crate::rpc::{SuiObject, SuiRpc};
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct StateProof {
14 pub object_id: [u8; 32],
16 pub version: u64,
18 pub merkle_proof: Vec<u8>,
20 pub state_root: [u8; 32],
22}
23
24impl StateProof {
25 pub fn new(
27 object_id: [u8; 32],
28 version: u64,
29 merkle_proof: Vec<u8>,
30 state_root: [u8; 32],
31 ) -> Self {
32 Self {
33 object_id,
34 version,
35 merkle_proof,
36 state_root,
37 }
38 }
39
40 pub fn leaf_hash(&self) -> [u8; 32] {
42 use sha2::{Digest, Sha256};
43 let mut hasher = Sha256::new();
44 hasher.update(self.object_id);
45 hasher.update(self.version.to_le_bytes());
46 hasher.finalize().into()
47 }
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct TransactionProof {
53 pub tx_digest: [u8; 32],
55 pub checkpoint: u64,
57 pub effects_signature: Vec<u8>,
59}
60
61impl TransactionProof {
62 pub fn new(tx_digest: [u8; 32], checkpoint: u64, effects_signature: Vec<u8>) -> Self {
64 Self {
65 tx_digest,
66 checkpoint,
67 effects_signature,
68 }
69 }
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
74pub struct EventProof {
75 pub tx_digest: [u8; 32],
77 pub event_index: u64,
79 pub expected_hash: [u8; 32],
81}
82
83impl EventProof {
84 pub fn new(tx_digest: [u8; 32], event_index: u64, expected_hash: [u8; 32]) -> Self {
86 Self {
87 tx_digest,
88 event_index,
89 expected_hash,
90 }
91 }
92
93 pub fn compute_event_hash(data: &[u8]) -> [u8; 32] {
95 use sha2::{Digest, Sha256};
96 let mut hasher = Sha256::new();
97 hasher.update(data);
98 hasher.finalize().into()
99 }
100}
101
102pub struct StateProofVerifier;
104
105impl StateProofVerifier {
106 pub fn verify_object_exists(
112 object_id: [u8; 32],
113 rpc: &dyn SuiRpc,
114 ) -> SuiResult<Option<SuiObject>> {
115 let obj = rpc
116 .get_object(object_id)
117 .map_err(|e| SuiError::StateProofFailed(format!("Failed to fetch object: {}", e)))?;
118 Ok(obj)
119 }
120
121 pub fn verify_object_consumed(object_id: [u8; 32], rpc: &dyn SuiRpc) -> SuiResult<bool> {
127 let obj = rpc
128 .get_object(object_id)
129 .map_err(|e| SuiError::StateProofFailed(format!("Failed to fetch object: {}", e)))?;
130 Ok(obj.is_none())
131 }
132
133 pub fn verify_object_consumed_in_tx(
140 tx_digest: [u8; 32],
141 object_id: [u8; 32],
142 rpc: &dyn SuiRpc,
143 ) -> SuiResult<bool> {
144 let tx = rpc.get_transaction_block(tx_digest).map_err(|e| {
145 SuiError::StateProofFailed(format!("Failed to fetch transaction: {}", e))
146 })?;
147
148 match tx {
149 Some(tx_block) => {
150 let consumed = tx_block.effects.modified_objects.iter().any(|change| {
152 change.object_id == object_id
153 && (change.change_type == "deleted" || change.change_type == "mutated")
154 });
155 Ok(consumed)
156 }
157 None => Err(SuiError::StateProofFailed(format!(
158 "Transaction {:?} not found",
159 tx_digest
160 ))),
161 }
162 }
163}
164
165pub struct EventProofVerifier;
167
168impl EventProofVerifier {
169 pub fn verify_event_in_tx(
182 tx_digest: [u8; 32],
183 expected_event_data: &[u8],
184 rpc: &dyn SuiRpc,
185 ) -> SuiResult<bool> {
186 let tx = rpc.get_transaction_block(tx_digest).map_err(|e| {
187 SuiError::EventProofFailed(format!("Failed to fetch transaction: {}", e))
188 })?;
189
190 match tx {
191 Some(tx_block) => {
192 if tx_block.effects.status != crate::rpc::SuiExecutionStatus::Success {
194 return Ok(false);
195 }
196
197 let events = rpc.get_transaction_events(tx_digest).map_err(|e| {
199 SuiError::EventProofFailed(format!("Failed to fetch events: {}", e))
200 })?;
201
202 if events.is_empty() {
203 return Ok(false);
204 }
205
206 let expected_hash = EventProof::compute_event_hash(expected_event_data);
208
209 for event in &events {
211 let event_hash = EventProof::compute_event_hash(&event.data);
213 if event_hash == expected_hash {
214 return Ok(true);
215 }
216 }
217
218 Ok(false)
219 }
220 None => Err(SuiError::EventProofFailed(format!(
221 "Transaction {:?} not found",
222 tx_digest
223 ))),
224 }
225 }
226}
227
228fn hex_to_bytes_for_proof(hex: &str) -> Result<Vec<u8>, String> {
230 let hex_str = hex.strip_prefix("0x").unwrap_or(hex);
231 hex::decode(hex_str).map_err(|e| format!("Invalid hex: {}", e))
232}
233
234pub struct CommitmentEventBuilder {
236 package_id: [u8; 32],
238 event_type: String,
240}
241
242impl CommitmentEventBuilder {
243 pub fn new(package_id: [u8; 32], event_type: String) -> Self {
249 Self {
250 package_id,
251 event_type,
252 }
253 }
254
255 pub fn build(&self, commitment_hash: [u8; 32], seal_object_id: [u8; 32]) -> Vec<u8> {
261 let mut data = Vec::with_capacity(96);
263 data.extend_from_slice(&self.package_id);
264 data.extend_from_slice(&commitment_hash);
265 data.extend_from_slice(&seal_object_id);
266 data
267 }
268
269 pub fn parse(&self, event_data: &[u8]) -> Result<([u8; 32], [u8; 32]), SuiError> {
271 if event_data.len() < 96 {
272 return Err(SuiError::EventProofFailed(format!(
273 "Event data too short: expected 96 bytes, got {}",
274 event_data.len()
275 )));
276 }
277
278 let mut commitment = [0u8; 32];
279 let mut seal_id = [0u8; 32];
280
281 commitment.copy_from_slice(&event_data[32..64]);
282 seal_id.copy_from_slice(&event_data[64..96]);
283
284 Ok((commitment, seal_id))
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::rpc::{
292 MockSuiRpc, SuiExecutionStatus, SuiObject, SuiObjectChange, SuiTransactionBlock,
293 SuiTransactionEffects,
294 };
295
296 #[test]
297 fn test_verify_object_exists() {
298 let rpc = MockSuiRpc::new(1000);
299 rpc.add_object(SuiObject {
300 object_id: [1u8; 32],
301 version: 1,
302 owner: vec![2, 3],
303 object_type: "CSV::Seal".to_string(),
304 has_public_transfer: false,
305 });
306
307 let result = StateProofVerifier::verify_object_exists([1u8; 32], &rpc).unwrap();
308 assert!(result.is_some());
309 assert!(StateProofVerifier::verify_object_exists([99u8; 32], &rpc)
310 .unwrap()
311 .is_none());
312 }
313
314 #[test]
315 fn test_verify_object_consumed() {
316 let rpc = MockSuiRpc::new(1000);
317 assert!(StateProofVerifier::verify_object_consumed([99u8; 32], &rpc).unwrap());
319 }
320
321 #[test]
322 fn test_verify_object_consumed_in_tx() {
323 let rpc = MockSuiRpc::new(1000);
324 rpc.add_transaction(SuiTransactionBlock {
325 digest: [1u8; 32],
326 checkpoint: Some(100),
327 effects: SuiTransactionEffects {
328 status: SuiExecutionStatus::Success,
329 gas_used: 1000,
330 modified_objects: vec![SuiObjectChange {
331 object_id: [2u8; 32],
332 change_type: "deleted".to_string(),
333 }],
334 },
335 });
336
337 assert!(
338 StateProofVerifier::verify_object_consumed_in_tx([1u8; 32], [2u8; 32], &rpc).unwrap()
339 );
340 assert!(
341 !StateProofVerifier::verify_object_consumed_in_tx([1u8; 32], [99u8; 32], &rpc).unwrap()
342 );
343 }
344
345 #[test]
346 fn test_event_proof_hash() {
347 let data = vec![0xAB, 0xCD, 0xEF];
348 let hash1 = EventProof::compute_event_hash(&data);
349 let hash2 = EventProof::compute_event_hash(&data);
350 assert_eq!(hash1, hash2);
351
352 let different_data = vec![0xFF];
353 let hash3 = EventProof::compute_event_hash(&different_data);
354 assert_ne!(hash1, hash3);
355 }
356
357 #[test]
358 fn test_commitment_event_builder() {
359 let builder = CommitmentEventBuilder::new([1u8; 32], "csv_seal::AnchorEvent".to_string());
360 let event_data = builder.build([2u8; 32], [3u8; 32]);
361 assert_eq!(event_data.len(), 96);
362
363 let (commitment, seal_id) = builder.parse(&event_data).unwrap();
364 assert_eq!(commitment, [2u8; 32]);
365 assert_eq!(seal_id, [3u8; 32]);
366 }
367
368 #[test]
369 fn test_commitment_event_builder_parse_error() {
370 let builder = CommitmentEventBuilder::new([1u8; 32], "csv_seal::AnchorEvent".to_string());
371 let short_data = vec![0u8; 50];
372 assert!(builder.parse(&short_data).is_err());
373 }
374
375 #[test]
376 fn test_state_proof_leaf_hash() {
377 let proof = StateProof::new([1u8; 32], 1, vec![], [0u8; 32]);
378 let hash = proof.leaf_hash();
379 let hash2 = proof.leaf_hash();
381 assert_eq!(hash, hash2);
382 }
383
384 #[test]
385 fn test_verify_event_failed_tx() {
386 let rpc = MockSuiRpc::new(1000);
387 rpc.add_transaction(SuiTransactionBlock {
388 digest: [1u8; 32],
389 checkpoint: Some(100),
390 effects: SuiTransactionEffects {
391 status: SuiExecutionStatus::Failure {
392 error: "out of gas".to_string(),
393 },
394 gas_used: 1000,
395 modified_objects: vec![],
396 },
397 });
398
399 assert!(!EventProofVerifier::verify_event_in_tx([1u8; 32], &[], &rpc).unwrap());
401 }
402}