Skip to main content

csv_adapter_core/
commitment_chain.rs

1//! Commitment Chain Verification
2//!
3//! Walks a chain of commitments from present back to genesis,
4//! verifying that each commitment's `previous_commitment` field
5//! matches the hash of the prior commitment.
6//!
7//! ## Overview
8//!
9//! Each commitment in a contract's history references the previous
10//! commitment via its `previous_commitment` field. This forms a
11//! hash chain:
12//!
13//! ```text
14//! Genesis Commitment (previous_commitment = 0)
15//!   ↓ hash
16//! Commitment 1 (previous_commitment = hash(genesis))
17//!   ↓ hash
18//! Commitment 2 (previous_commitment = hash(commitment_1))
19//!   ↓ hash
20//! Latest Commitment
21//! ```
22//!
23//! The commitment chain walker verifies:
24//! 1. Each commitment's `previous_commitment` matches the hash of the prior commitment
25//! 2. The chain traces back to a genesis commitment (previous_commitment = zero hash)
26//! 3. No commitments are missing in the sequence
27//! 4. All commitments belong to the same contract (contract_id consistency)
28
29use alloc::vec::Vec;
30
31use crate::commitment::Commitment;
32use crate::hash::Hash;
33
34/// Result of commitment chain verification.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ChainVerificationResult {
37    /// The ordered chain of commitments (genesis → latest)
38    pub chain: Vec<Commitment>,
39    /// The genesis commitment (first in the chain)
40    pub genesis: Commitment,
41    /// The latest commitment (last in the chain)
42    pub latest: Commitment,
43    /// Total number of commitments in the chain
44    pub length: usize,
45    /// The contract ID that all commitments belong to
46    pub contract_id: Hash,
47}
48
49/// Errors that can occur during commitment chain verification.
50#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
51#[allow(missing_docs)]
52pub enum ChainError {
53    #[error("Empty commitment chain")]
54    EmptyChain,
55    #[error("Chain does not start at genesis (first commitment has non-zero previous_commitment)")]
56    NotGenesis,
57    #[error(
58        "Commitment chain broken at index {index}: expected previous {expected}, got {actual}"
59    )]
60    BrokenChain {
61        index: usize,
62        expected: Hash,
63        actual: Hash,
64    },
65    #[error("Commitment at index {index} has inconsistent contract_id: expected {expected}, got {actual}")]
66    ContractIdMismatch {
67        index: usize,
68        expected: Hash,
69        actual: Hash,
70    },
71    #[error("Duplicate commitment found at index {index}")]
72    DuplicateCommitment { index: usize },
73    #[error("Cycle detected in commitment chain at index {index}")]
74    CycleDetected { index: usize },
75}
76
77/// Verifies a commitment chain from a collection of commitments.
78///
79/// This function takes a set of commitments and attempts to reconstruct
80/// and verify the commitment chain by following the `previous_commitment`
81/// references.
82///
83/// # Arguments
84/// * `commitments` - A collection of commitments to verify
85/// * `latest_commitment_hash` - The hash of the latest commitment (starting point)
86///
87/// # Returns
88/// A `ChainVerificationResult` if the chain is valid, or a `ChainError` if invalid.
89pub fn verify_commitment_chain(
90    commitments: &[Commitment],
91    latest_commitment_hash: Hash,
92) -> Result<ChainVerificationResult, ChainError> {
93    if commitments.is_empty() {
94        return Err(ChainError::EmptyChain);
95    }
96
97    // Build a map from commitment hash to commitment
98    let mut commitment_map: alloc::collections::BTreeMap<Hash, &Commitment> =
99        alloc::collections::BTreeMap::new();
100
101    for commitment in commitments {
102        let hash = commitment.hash();
103        commitment_map.insert(hash, commitment);
104    }
105
106    // Start from the latest commitment and walk backwards
107    let mut chain: Vec<Commitment> = Vec::new();
108    let mut seen: alloc::collections::BTreeSet<Hash> = alloc::collections::BTreeSet::new();
109    let mut current_hash = latest_commitment_hash;
110
111    loop {
112        // Check for cycles
113        if seen.contains(&current_hash) {
114            return Err(ChainError::CycleDetected { index: chain.len() });
115        }
116        seen.insert(current_hash);
117
118        // Find the commitment with this hash
119        let commitment =
120            commitment_map
121                .get(&current_hash)
122                .ok_or_else(|| ChainError::BrokenChain {
123                    index: chain.len(),
124                    expected: current_hash,
125                    actual: Hash::new([0u8; 32]),
126                })?;
127
128        // Check for duplicates
129        if chain.iter().any(|c: &Commitment| c.hash() == current_hash) {
130            return Err(ChainError::DuplicateCommitment { index: chain.len() });
131        }
132
133        chain.push((**commitment).clone());
134
135        // Check if this is the genesis commitment
136        let previous = commitment.previous_commitment;
137        if previous == Hash::new([0u8; 32]) {
138            // This is genesis - we've completed the chain
139            break;
140        }
141
142        // Move to the previous commitment
143        current_hash = previous;
144    }
145
146    // Verify the first commitment is actually genesis
147    let genesis = chain.last().ok_or(ChainError::EmptyChain)?;
148
149    if genesis.previous_commitment != Hash::new([0u8; 32]) {
150        return Err(ChainError::NotGenesis);
151    }
152
153    // Verify all commitments have the same contract_id
154    let contract_id = genesis.contract_id;
155    for (i, commitment) in chain.iter().enumerate() {
156        if commitment.contract_id != contract_id {
157            return Err(ChainError::ContractIdMismatch {
158                index: i,
159                expected: contract_id,
160                actual: commitment.contract_id,
161            });
162        }
163    }
164
165    // Reverse the chain so it's in chronological order (genesis → latest)
166    chain.reverse();
167
168    let genesis_commitment = chain.first().unwrap().clone();
169    let latest_commitment = chain.last().unwrap().clone();
170    let chain_length = chain.len();
171
172    Ok(ChainVerificationResult {
173        chain,
174        genesis: genesis_commitment,
175        latest: latest_commitment,
176        length: chain_length,
177        contract_id,
178    })
179}
180
181/// Verifies a pre-ordered commitment chain.
182///
183/// This is a simpler version that assumes the commitments are already
184/// in chronological order (genesis first, latest last).
185///
186/// # Arguments
187/// * `ordered_commitments` - Commitments in chronological order
188///
189/// # Returns
190/// A `ChainVerificationResult` if valid, or a `ChainError` if invalid.
191pub fn verify_ordered_commitment_chain(
192    ordered_commitments: &[Commitment],
193) -> Result<ChainVerificationResult, ChainError> {
194    if ordered_commitments.is_empty() {
195        return Err(ChainError::EmptyChain);
196    }
197
198    // Verify the chain links
199    for i in 1..ordered_commitments.len() {
200        let current = &ordered_commitments[i];
201        let previous = &ordered_commitments[i - 1];
202
203        // Current's previous_commitment should match previous's hash
204        let expected_previous = previous.hash();
205        if current.previous_commitment != expected_previous {
206            return Err(ChainError::BrokenChain {
207                index: i,
208                expected: expected_previous,
209                actual: current.previous_commitment,
210            });
211        }
212
213        // Verify contract_id consistency
214        if current.contract_id != previous.contract_id {
215            return Err(ChainError::ContractIdMismatch {
216                index: i,
217                expected: previous.contract_id,
218                actual: current.contract_id,
219            });
220        }
221    }
222
223    // Verify first is genesis
224    if ordered_commitments.first().unwrap().previous_commitment != Hash::new([0u8; 32]) {
225        return Err(ChainError::NotGenesis);
226    }
227
228    // Check for duplicates
229    let mut seen = alloc::collections::BTreeSet::new();
230    for (i, commitment) in ordered_commitments.iter().enumerate() {
231        let hash = commitment.hash();
232        if !seen.insert(hash) {
233            return Err(ChainError::DuplicateCommitment { index: i });
234        }
235    }
236
237    let genesis = ordered_commitments.first().unwrap().clone();
238    let latest = ordered_commitments.last().unwrap().clone();
239    let contract_id = genesis.contract_id;
240
241    Ok(ChainVerificationResult {
242        chain: ordered_commitments.to_vec(),
243        genesis,
244        latest,
245        length: ordered_commitments.len(),
246        contract_id,
247    })
248}
249
250/// Computes the expected hash of the previous commitment given a commitment.
251///
252/// This is a helper to verify that a commitment correctly references
253/// its predecessor.
254pub fn verify_commitment_link(
255    previous_commitment: &Commitment,
256    current_commitment: &Commitment,
257) -> Result<(), ChainError> {
258    let expected = previous_commitment.hash();
259    let actual = current_commitment.previous_commitment;
260
261    if expected != actual {
262        return Err(ChainError::BrokenChain {
263            index: 0,
264            expected,
265            actual,
266        });
267    }
268
269    // Verify contract_id consistency
270    if previous_commitment.contract_id != current_commitment.contract_id {
271        return Err(ChainError::ContractIdMismatch {
272            index: 0,
273            expected: previous_commitment.contract_id,
274            actual: current_commitment.contract_id,
275        });
276    }
277
278    Ok(())
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::commitment::Commitment;
285    use crate::seal::SealRef;
286
287    fn make_genesis_commitment(contract_id: Hash) -> Commitment {
288        let domain = [0u8; 32];
289        let seal = SealRef::new(vec![0x01], None).unwrap();
290        Commitment::simple(
291            contract_id,
292            Hash::new([0u8; 32]), // Genesis has zero previous_commitment
293            Hash::new([0u8; 32]),
294            &seal,
295            domain,
296        )
297    }
298
299    fn make_commitment(contract_id: Hash, previous_commitment: Hash, seal_id: u8) -> Commitment {
300        let domain = [0u8; 32];
301        let seal = SealRef::new(vec![seal_id], None).unwrap();
302        Commitment::simple(
303            contract_id,
304            previous_commitment,
305            Hash::new([0u8; 32]),
306            &seal,
307            domain,
308        )
309    }
310
311    #[test]
312    fn test_verify_ordered_chain_valid() {
313        let contract_id = Hash::new([0xAB; 32]);
314        let genesis = make_genesis_commitment(contract_id);
315        let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
316        let c2 = make_commitment(contract_id, c1.hash(), 0x03);
317
318        let result = verify_ordered_commitment_chain(&[genesis.clone(), c1.clone(), c2.clone()]);
319        assert!(result.is_ok());
320
321        let result = result.unwrap();
322        assert_eq!(result.length, 3);
323        assert_eq!(result.genesis.hash(), genesis.hash());
324        assert_eq!(result.latest.hash(), c2.hash());
325        assert_eq!(result.contract_id, contract_id);
326    }
327
328    #[test]
329    fn test_verify_ordered_chain_broken_link() {
330        let contract_id = Hash::new([0xAB; 32]);
331        let genesis = make_genesis_commitment(contract_id);
332        let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
333        // c2 references a wrong previous commitment
334        let c2 = make_commitment(contract_id, Hash::new([0xFF; 32]), 0x03);
335
336        let result = verify_ordered_commitment_chain(&[genesis, c1, c2]);
337        assert!(result.is_err());
338        assert!(matches!(
339            result.unwrap_err(),
340            ChainError::BrokenChain { .. }
341        ));
342    }
343
344    #[test]
345    fn test_verify_ordered_chain_not_genesis() {
346        let contract_id = Hash::new([0xAB; 32]);
347        // First commitment is not genesis (has non-zero previous)
348        let c1 = make_commitment(contract_id, Hash::new([0x01; 32]), 0x01);
349        let c2 = make_commitment(contract_id, c1.hash(), 0x02);
350
351        let result = verify_ordered_commitment_chain(&[c1, c2]);
352        assert!(result.is_err());
353        assert!(matches!(result.unwrap_err(), ChainError::NotGenesis));
354    }
355
356    #[test]
357    fn test_verify_ordered_chain_contract_id_mismatch() {
358        let contract_id_1 = Hash::new([0xAB; 32]);
359        let contract_id_2 = Hash::new([0xCD; 32]);
360
361        let genesis = make_genesis_commitment(contract_id_1);
362        // c1 has different contract_id
363        let c1 = make_commitment(contract_id_2, genesis.hash(), 0x02);
364
365        let result = verify_ordered_commitment_chain(&[genesis, c1]);
366        assert!(result.is_err());
367        assert!(matches!(
368            result.unwrap_err(),
369            ChainError::ContractIdMismatch { .. }
370        ));
371    }
372
373    #[test]
374    fn test_verify_ordered_chain_empty() {
375        let result = verify_ordered_commitment_chain(&[]);
376        assert!(result.is_err());
377        assert!(matches!(result.unwrap_err(), ChainError::EmptyChain));
378    }
379
380    #[test]
381    fn test_verify_ordered_chain_single_genesis() {
382        let contract_id = Hash::new([0xAB; 32]);
383        let genesis = make_genesis_commitment(contract_id);
384
385        let result = verify_ordered_commitment_chain(&[genesis.clone()]);
386        assert!(result.is_ok());
387
388        let result = result.unwrap();
389        assert_eq!(result.length, 1);
390        assert_eq!(result.genesis.hash(), genesis.hash());
391        assert_eq!(result.latest.hash(), genesis.hash());
392    }
393
394    #[test]
395    fn test_verify_ordered_chain_duplicates() {
396        let contract_id = Hash::new([0xAB; 32]);
397        let genesis = make_genesis_commitment(contract_id);
398        let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
399
400        // Duplicate c1 in the chain
401        let result = verify_ordered_commitment_chain(&[genesis.clone(), c1.clone(), c1.clone()]);
402        // The chain will fail at index 2 because c1's hash doesn't match c1's previous_commitment
403        // Actually c1.clone() twice means the third element has c1's previous_commitment pointing to genesis
404        // but the second element IS c1, so this creates a broken chain, not a duplicate
405        // To properly test duplicates, we need a valid chain with the same commitment appearing twice
406        assert!(result.is_err()); // Should fail for either BrokenChain or DuplicateCommitment
407    }
408
409    #[test]
410    fn test_verify_commitment_link_valid() {
411        let contract_id = Hash::new([0xAB; 32]);
412        let genesis = make_genesis_commitment(contract_id);
413        let c1 = make_commitment(contract_id, genesis.hash(), 0x02);
414
415        assert!(verify_commitment_link(&genesis, &c1).is_ok());
416    }
417
418    #[test]
419    fn test_verify_commitment_link_broken() {
420        let contract_id = Hash::new([0xAB; 32]);
421        let genesis = make_genesis_commitment(contract_id);
422        let c1 = make_commitment(contract_id, Hash::new([0xFF; 32]), 0x02);
423
424        assert!(verify_commitment_link(&genesis, &c1).is_err());
425    }
426
427    #[test]
428    fn test_long_chain_verification() {
429        let contract_id = Hash::new([0xAB; 32]);
430        let mut commitments = Vec::new();
431
432        // Create a chain of 50 commitments
433        let genesis = make_genesis_commitment(contract_id);
434        commitments.push(genesis.clone());
435
436        let mut previous = genesis;
437        for i in 1..50 {
438            let c = make_commitment(contract_id, previous.hash(), (i + 1) as u8);
439            commitments.push(c.clone());
440            previous = c;
441        }
442
443        let result = verify_ordered_commitment_chain(&commitments);
444        assert!(result.is_ok());
445        assert_eq!(result.unwrap().length, 50);
446    }
447}