1use alloc::vec::Vec;
30
31use crate::commitment::Commitment;
32use crate::hash::Hash;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ChainVerificationResult {
37 pub chain: Vec<Commitment>,
39 pub genesis: Commitment,
41 pub latest: Commitment,
43 pub length: usize,
45 pub contract_id: Hash,
47}
48
49#[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
77pub 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 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 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 if seen.contains(¤t_hash) {
114 return Err(ChainError::CycleDetected { index: chain.len() });
115 }
116 seen.insert(current_hash);
117
118 let commitment =
120 commitment_map
121 .get(¤t_hash)
122 .ok_or_else(|| ChainError::BrokenChain {
123 index: chain.len(),
124 expected: current_hash,
125 actual: Hash::new([0u8; 32]),
126 })?;
127
128 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 let previous = commitment.previous_commitment;
137 if previous == Hash::new([0u8; 32]) {
138 break;
140 }
141
142 current_hash = previous;
144 }
145
146 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 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 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
181pub 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 for i in 1..ordered_commitments.len() {
200 let current = &ordered_commitments[i];
201 let previous = &ordered_commitments[i - 1];
202
203 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 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 if ordered_commitments.first().unwrap().previous_commitment != Hash::new([0u8; 32]) {
225 return Err(ChainError::NotGenesis);
226 }
227
228 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
250pub 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 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]), 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 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 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 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 let result = verify_ordered_commitment_chain(&[genesis.clone(), c1.clone(), c1.clone()]);
402 assert!(result.is_err()); }
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 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}