Skip to main content

csv_adapter_sui/
proofs.rs

1//! Proof verification for the Sui adapter
2//!
3//! This module provides proof verification for Sui's object model,
4//! including object existence proofs, transaction proofs, and event verification.
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{SuiError, SuiResult};
9use crate::rpc::{SuiObject, SuiRpc};
10
11/// State proof for object existence/ownership verification.
12#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct StateProof {
14    /// The object ID being proven
15    pub object_id: [u8; 32],
16    /// Object version
17    pub version: u64,
18    /// Merkle proof of object existence in state
19    pub merkle_proof: Vec<u8>,
20    /// State root hash at the time of proof
21    pub state_root: [u8; 32],
22}
23
24impl StateProof {
25    /// Create a new state proof.
26    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    /// Compute the leaf hash for this state proof.
41    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/// Transaction proof for verifying a transaction was included in a checkpoint.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct TransactionProof {
53    /// Transaction digest
54    pub tx_digest: [u8; 32],
55    /// Checkpoint sequence number
56    pub checkpoint: u64,
57    /// Effects signature proving inclusion
58    pub effects_signature: Vec<u8>,
59}
60
61impl TransactionProof {
62    /// Create a new transaction proof.
63    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/// Event proof for verifying commitment events in transactions.
73#[derive(Clone, Debug, Serialize, Deserialize)]
74pub struct EventProof {
75    /// Transaction digest containing the event
76    pub tx_digest: [u8; 32],
77    /// Event index within the transaction
78    pub event_index: u64,
79    /// Expected event data hash
80    pub expected_hash: [u8; 32],
81}
82
83impl EventProof {
84    /// Create a new event proof.
85    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    /// Compute the hash of event data.
94    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
102/// Verifier for state proofs (object existence/ownership).
103pub struct StateProofVerifier;
104
105impl StateProofVerifier {
106    /// Verify that an object exists on-chain.
107    ///
108    /// # Arguments
109    /// * `object_id` - The object ID to check
110    /// * `rpc` - RPC client for fetching object data
111    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    /// Verify that an object has been consumed (deleted).
122    ///
123    /// # Arguments
124    /// * `object_id` - The object ID to check
125    /// * `rpc` - RPC client for fetching object data
126    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    /// Verify that a transaction consumed a specific object.
134    ///
135    /// # Arguments
136    /// * `tx_digest` - The transaction digest
137    /// * `object_id` - The object ID that should have been consumed
138    /// * `rpc` - RPC client for fetching transaction data
139    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                // Check if the object was deleted or mutated in the transaction effects
151                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
165/// Verifier for event proofs.
166pub struct EventProofVerifier;
167
168impl EventProofVerifier {
169    /// Verify that an event was emitted in a transaction.
170    ///
171    /// This verifies the event by:
172    /// 1. Fetching the transaction to confirm it succeeded
173    /// 2. Fetching the events for the transaction
174    /// 3. Computing hash of expected event data
175    /// 4. Comparing against emitted event data hashes
176    ///
177    /// # Arguments
178    /// * `tx_digest` - The transaction digest
179    /// * `expected_event_data` - The expected event data bytes
180    /// * `rpc` - RPC client for fetching transaction data
181    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                // Check if transaction was successful
193                if tx_block.effects.status != crate::rpc::SuiExecutionStatus::Success {
194                    return Ok(false);
195                }
196
197                // Fetch events for this transaction
198                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                // Compute expected event hash
207                let expected_hash = EventProof::compute_event_hash(expected_event_data);
208
209                // Check if any emitted event matches our expected hash
210                for event in &events {
211                    // Hash the event data bytes
212                    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
228/// Convert hex string to bytes (local helper for proof verification)
229fn 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
234/// Builder for commitment events emitted when seals are consumed.
235pub struct CommitmentEventBuilder {
236    /// Package ID of the CSV seal module
237    package_id: [u8; 32],
238    /// Event type tag
239    event_type: String,
240}
241
242impl CommitmentEventBuilder {
243    /// Create a new event builder.
244    ///
245    /// # Arguments
246    /// * `package_id` - The package ID where CSVSeal is deployed
247    /// * `event_type` - The event type (e.g., "csv_seal::AnchorEvent")
248    pub fn new(package_id: [u8; 32], event_type: String) -> Self {
249        Self {
250            package_id,
251            event_type,
252        }
253    }
254
255    /// Build the expected event data for a commitment.
256    ///
257    /// # Arguments
258    /// * `commitment_hash` - The 32-byte commitment hash
259    /// * `seal_object_id` - The object ID of the consumed seal
260    pub fn build(&self, commitment_hash: [u8; 32], seal_object_id: [u8; 32]) -> Vec<u8> {
261        // Event format: package_id (32) + commitment (32) + seal_object_id (32)
262        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    /// Parse event data back into commitment and seal components.
270    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        // Object not in mock means it's "consumed"
318        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        // Hash should be deterministic
380        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        // Failed transaction should not verify events
400        assert!(!EventProofVerifier::verify_event_in_tx([1u8; 32], &[], &rpc).unwrap());
401    }
402}