Skip to main content

csv_adapter_core/
consignment.rs

1//! Consignment: the wire format for CSV contract state transfer
2//!
3//! A consignment contains the complete provable history of a contract:
4//! genesis, all transitions, seal assignments, and anchor proofs.
5//! It is the unit of state transfer between peers.
6
7use alloc::vec::Vec;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use crate::genesis::Genesis;
12use crate::hash::Hash;
13use crate::seal::AnchorRef;
14use crate::state::{Metadata, StateAssignment};
15use crate::transition::Transition;
16
17/// Consignment version for forward compatibility
18pub const CONSIGNMENT_VERSION: u8 = 1;
19
20/// Anchor proof: links a commitment to an on-chain reference
21///
22/// An anchor provides cryptographic proof that a commitment was
23/// published to a blockchain at a specific location.
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Anchor {
26    /// Anchor reference (on-chain location of the commitment)
27    pub anchor_ref: AnchorRef,
28    /// Commitment hash that was anchored
29    pub commitment: Hash,
30    /// Inclusion proof bytes (chain-specific)
31    pub inclusion_proof: Vec<u8>,
32    /// Finality proof bytes (chain-specific)
33    pub finality_proof: Vec<u8>,
34}
35
36impl Anchor {
37    /// Create a new anchor from its components
38    pub fn new(
39        anchor_ref: AnchorRef,
40        commitment: Hash,
41        inclusion_proof: Vec<u8>,
42        finality_proof: Vec<u8>,
43    ) -> Self {
44        Self {
45            anchor_ref,
46            commitment,
47            inclusion_proof,
48            finality_proof,
49        }
50    }
51}
52
53/// Seal assignment record in a consignment
54///
55/// Records which state was assigned to which seal during a transition.
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57pub struct SealAssignment {
58    /// Seal being assigned to
59    pub seal_ref: crate::seal::SealRef,
60    /// State being assigned to this seal
61    pub assignment: StateAssignment,
62    /// Metadata for this assignment
63    pub metadata: Vec<Metadata>,
64}
65
66impl SealAssignment {
67    /// Create a new seal assignment
68    pub fn new(
69        seal_ref: crate::seal::SealRef,
70        assignment: StateAssignment,
71        metadata: Vec<Metadata>,
72    ) -> Self {
73        Self {
74            seal_ref,
75            assignment,
76            metadata,
77        }
78    }
79}
80
81/// Complete contract consignment
82///
83/// This is the wire format for transferring CSV contract state between peers.
84/// A valid consignment contains a complete, verifiable chain from genesis
85/// to the current state.
86#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Consignment {
88    /// Consignment version
89    pub version: u8,
90    /// Contract genesis
91    pub genesis: Genesis,
92    /// State transitions in topological order
93    pub transitions: Vec<Transition>,
94    /// Seal assignments (indexed by transition output)
95    pub seal_assignments: Vec<SealAssignment>,
96    /// Anchor proofs (on-chain commitment locations)
97    pub anchors: Vec<Anchor>,
98    /// Schema ID (for validation against contract rules)
99    pub schema_id: Hash,
100}
101
102impl Consignment {
103    /// Create a new consignment
104    pub fn new(
105        genesis: Genesis,
106        transitions: Vec<Transition>,
107        seal_assignments: Vec<SealAssignment>,
108        anchors: Vec<Anchor>,
109        schema_id: Hash,
110    ) -> Self {
111        Self {
112            version: CONSIGNMENT_VERSION,
113            genesis,
114            transitions,
115            seal_assignments,
116            anchors,
117            schema_id,
118        }
119    }
120
121    /// Compute the consignment state root hash
122    ///
123    /// This hash represents the current state of the contract after all
124    /// transitions have been applied.
125    pub fn state_root(&self) -> Hash {
126        let mut hasher = Sha256::new();
127
128        hasher.update(b"CSV-CONSIGNMENT-v1");
129        hasher.update(&self.version.to_le_bytes());
130
131        // Genesis hash
132        hasher.update(self.genesis.hash().as_bytes());
133
134        // Transition hashes in order
135        hasher.update(&(self.transitions.len() as u64).to_le_bytes());
136        for transition in &self.transitions {
137            hasher.update(transition.hash().as_bytes());
138        }
139
140        // Seal assignments
141        hasher.update(&(self.seal_assignments.len() as u64).to_le_bytes());
142        for assignment in &self.seal_assignments {
143            hasher.update(&assignment.seal_ref.to_vec());
144            hasher.update(&assignment.assignment.seal.to_vec());
145            hasher.update(&assignment.assignment.data);
146        }
147
148        // Anchors
149        hasher.update(&(self.anchors.len() as u64).to_le_bytes());
150        for anchor in &self.anchors {
151            hasher.update(anchor.commitment.as_bytes());
152            hasher.update(&anchor.anchor_ref.to_vec());
153        }
154
155        let result = hasher.finalize();
156        let mut array = [0u8; 32];
157        array.copy_from_slice(&result);
158        Hash::new(array)
159    }
160
161    /// Get the contract ID (from genesis)
162    pub fn contract_id(&self) -> Hash {
163        self.genesis.contract_id
164    }
165
166    /// Get the number of transitions
167    pub fn transition_count(&self) -> usize {
168        self.transitions.len()
169    }
170
171    /// Get the number of seal assignments
172    pub fn assignment_count(&self) -> usize {
173        self.seal_assignments.len()
174    }
175
176    /// Get the number of anchors
177    pub fn anchor_count(&self) -> usize {
178        self.anchors.len()
179    }
180
181    /// Get the latest state for a given seal
182    ///
183    /// Walks transitions in order to find the most recent assignment
184    /// to the given seal.
185    pub fn latest_state_for_seal(&self, seal: &crate::seal::SealRef) -> Option<&StateAssignment> {
186        // Walk assignments in reverse to find the latest for this seal
187        self.seal_assignments
188            .iter()
189            .rev()
190            .find(|a| &a.seal_ref == seal)
191            .map(|a| &a.assignment)
192    }
193
194    /// Get all current seal owners
195    ///
196    /// Returns the set of seals that have received state but haven't
197    /// been consumed by any transition.
198    pub fn current_seals(&self) -> alloc::collections::BTreeSet<Vec<u8>> {
199        // Start with genesis owned state
200        let mut active: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
201
202        // Genesis outputs
203        for owned in &self.genesis.owned_state {
204            active.insert(owned.seal.to_vec());
205        }
206
207        // Transition outputs
208        for transition in &self.transitions {
209            for output in &transition.owned_outputs {
210                active.insert(output.seal.to_vec());
211            }
212        }
213
214        // Note: determining which seals are actually consumed requires
215        // resolving StateRef -> SealRef mapping through the transition chain.
216        // This is a simplified view; full resolution needs VM execution.
217        active
218    }
219
220    /// Validate basic consignment structure
221    pub fn validate_structure(&self) -> Result<(), ConsignmentError> {
222        // Version check
223        if self.version != CONSIGNMENT_VERSION {
224            return Err(ConsignmentError::VersionMismatch {
225                expected: CONSIGNMENT_VERSION,
226                actual: self.version,
227            });
228        }
229
230        // Schema ID consistency
231        if self.genesis.schema_id != self.schema_id {
232            return Err(ConsignmentError::SchemaMismatch {
233                genesis_schema: self.genesis.schema_id,
234                consignment_schema: self.schema_id,
235            });
236        }
237
238        // Contract ID consistency
239        if self.genesis.contract_id != self.contract_id() {
240            return Err(ConsignmentError::ContractIdMismatch);
241        }
242
243        // Transition count vs anchor count (each transition should have an anchor)
244        if self.transitions.len() != self.anchors.len() {
245            return Err(ConsignmentError::AnchorCountMismatch {
246                transitions: self.transitions.len(),
247                anchors: self.anchors.len(),
248            });
249        }
250
251        Ok(())
252    }
253
254    /// Serialize consignment to bytes
255    pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
256        bincode::serialize(self)
257    }
258
259    /// Deserialize consignment from bytes with size limit (50MB max)
260    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
261        const MAX_SIZE: usize = 50 * 1024 * 1024; // 50MB
262        if bytes.len() > MAX_SIZE {
263            return Err(bincode::ErrorKind::Custom(format!(
264                "Consignment too large: {} bytes (max {})",
265                bytes.len(),
266                MAX_SIZE
267            ))
268            .into());
269        }
270
271        let consignment: Consignment = bincode::deserialize(bytes)?;
272
273        // Verify version before accepting
274        if consignment.version != CONSIGNMENT_VERSION {
275            return Err(bincode::ErrorKind::Custom(format!(
276                "Unsupported consignment version: {}",
277                consignment.version
278            ))
279            .into());
280        }
281
282        Ok(consignment)
283    }
284
285    /// Create a consignment with only genesis (no transitions yet)
286    pub fn from_genesis(genesis: Genesis) -> Self {
287        let schema_id = genesis.schema_id;
288        Self::new(genesis, vec![], vec![], vec![], schema_id)
289    }
290}
291
292/// Consignment validation errors
293#[derive(Debug)]
294#[allow(missing_docs)]
295pub enum ConsignmentError {
296    /// Version mismatch between expected and actual consignment
297    VersionMismatch { expected: u8, actual: u8 },
298    /// Schema ID doesn't match between genesis and consignment
299    SchemaMismatch {
300        genesis_schema: Hash,
301        consignment_schema: Hash,
302    },
303    /// Contract ID inconsistency between components
304    ContractIdMismatch,
305    /// Transition count doesn't match anchor count
306    AnchorCountMismatch { transitions: usize, anchors: usize },
307}
308
309impl core::fmt::Display for ConsignmentError {
310    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
311        match self {
312            ConsignmentError::VersionMismatch { expected, actual } => {
313                write!(
314                    f,
315                    "Consignment version mismatch: expected {}, got {}",
316                    expected, actual
317                )
318            }
319            ConsignmentError::SchemaMismatch {
320                genesis_schema,
321                consignment_schema,
322            } => {
323                write!(
324                    f,
325                    "Schema mismatch: genesis has {}, consignment has {}",
326                    genesis_schema, consignment_schema
327                )
328            }
329            ConsignmentError::ContractIdMismatch => {
330                write!(f, "Contract ID inconsistency")
331            }
332            ConsignmentError::AnchorCountMismatch {
333                transitions,
334                anchors,
335            } => {
336                write!(
337                    f,
338                    "Anchor count mismatch: {} transitions but {} anchors",
339                    transitions, anchors
340                )
341            }
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::genesis::Genesis;
350    use crate::seal::SealRef;
351    use crate::state::{GlobalState, Metadata, OwnedState};
352    use crate::state::{StateAssignment, StateRef};
353
354    fn test_consignment() -> Consignment {
355        let genesis = Genesis::new(
356            Hash::new([1u8; 32]),
357            Hash::new([2u8; 32]),
358            vec![
359                GlobalState::new(1, 1000u64.to_le_bytes().to_vec()), // total supply
360            ],
361            vec![OwnedState::new(
362                10,
363                SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
364                1000u64.to_le_bytes().to_vec(),
365            )],
366            vec![Metadata::from_string("issuer", "test")],
367        );
368
369        let transition = Transition::new(
370            1, // transfer
371            vec![StateRef::new(10, Hash::new([1u8; 32]), 0)],
372            vec![
373                StateAssignment::new(
374                    10,
375                    SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
376                    600u64.to_le_bytes().to_vec(),
377                ),
378                StateAssignment::new(
379                    10,
380                    SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
381                    400u64.to_le_bytes().to_vec(),
382                ),
383            ],
384            vec![],
385            vec![],
386            vec![0x01, 0x02],
387            vec![vec![0xAB; 64]],
388        );
389
390        let seal_assignment = SealAssignment::new(
391            SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
392            StateAssignment::new(
393                10,
394                SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
395                600u64.to_le_bytes().to_vec(),
396            ),
397            vec![],
398        );
399
400        let anchor = Anchor::new(
401            AnchorRef::new(vec![0xCC; 32], 100, vec![]).unwrap(),
402            transition.hash(),
403            vec![0xDD; 64], // inclusion proof
404            vec![0xEE; 32], // finality proof
405        );
406
407        Consignment::new(
408            genesis,
409            vec![transition],
410            vec![seal_assignment],
411            vec![anchor],
412            Hash::new([2u8; 32]),
413        )
414    }
415
416    #[test]
417    fn test_consignment_creation() {
418        let c = test_consignment();
419        assert_eq!(c.version, CONSIGNMENT_VERSION);
420        assert_eq!(c.transition_count(), 1);
421        assert_eq!(c.assignment_count(), 1);
422        assert_eq!(c.anchor_count(), 1);
423    }
424
425    #[test]
426    fn test_consignment_state_root() {
427        let c = test_consignment();
428        let root = c.state_root();
429        assert_eq!(root.as_bytes().len(), 32);
430    }
431
432    #[test]
433    fn test_consignment_state_root_deterministic() {
434        let c1 = test_consignment();
435        let c2 = test_consignment();
436        assert_eq!(c1.state_root(), c2.state_root());
437    }
438
439    #[test]
440    fn test_consignment_state_root_differs_by_transition() {
441        let mut c1 = test_consignment();
442        let c2 = test_consignment();
443        // Modify transition bytecode
444        c1.transitions[0].validation_script = vec![0xFF];
445        assert_ne!(c1.state_root(), c2.state_root());
446    }
447
448    #[test]
449    fn test_contract_id() {
450        let c = test_consignment();
451        assert_eq!(c.contract_id(), Hash::new([1u8; 32]));
452    }
453
454    #[test]
455    fn test_latest_state_for_seal() {
456        let c = test_consignment();
457        let seal = SealRef::new(vec![0xBB; 16], Some(2)).unwrap();
458        let state = c.latest_state_for_seal(&seal);
459        assert!(state.is_some());
460        assert_eq!(state.unwrap().data, 600u64.to_le_bytes().to_vec());
461    }
462
463    #[test]
464    fn test_latest_state_for_seal_not_found() {
465        let c = test_consignment();
466        let seal = SealRef::new(vec![0xFF; 16], Some(99)).unwrap();
467        let state = c.latest_state_for_seal(&seal);
468        assert!(state.is_none());
469    }
470
471    #[test]
472    fn test_validate_structure_valid() {
473        let c = test_consignment();
474        assert!(c.validate_structure().is_ok());
475    }
476
477    #[test]
478    fn test_validate_structure_wrong_version() {
479        let mut c = test_consignment();
480        c.version = 99;
481        assert!(c.validate_structure().is_err());
482    }
483
484    #[test]
485    fn test_validate_structure_schema_mismatch() {
486        let mut c = test_consignment();
487        c.schema_id = Hash::new([99u8; 32]); // Different from genesis schema_id
488        assert!(c.validate_structure().is_err());
489    }
490
491    #[test]
492    fn test_validate_structure_anchor_count_mismatch() {
493        let mut c = test_consignment();
494        c.anchors.push(Anchor::new(
495            AnchorRef::new(vec![0xFF; 32], 200, vec![]).unwrap(),
496            Hash::zero(),
497            vec![],
498            vec![],
499        ));
500        assert!(c.validate_structure().is_err());
501    }
502
503    #[test]
504    fn test_from_genesis() {
505        let genesis = Genesis::new(
506            Hash::new([1u8; 32]),
507            Hash::new([2u8; 32]),
508            vec![],
509            vec![],
510            vec![],
511        );
512        let c = Consignment::from_genesis(genesis.clone());
513        assert_eq!(c.version, CONSIGNMENT_VERSION);
514        assert_eq!(c.transition_count(), 0);
515        assert_eq!(c.assignment_count(), 0);
516        assert_eq!(c.anchor_count(), 0);
517        assert_eq!(c.contract_id(), Hash::new([1u8; 32]));
518        assert!(c.validate_structure().is_ok());
519    }
520
521    #[test]
522    fn test_consignment_serialization_roundtrip() {
523        let c = test_consignment();
524        let bytes = c.to_bytes().unwrap();
525        let restored = Consignment::from_bytes(&bytes).unwrap();
526        assert_eq!(c, restored);
527        assert_eq!(c.state_root(), restored.state_root());
528    }
529
530    #[test]
531    fn test_consignment_wrong_version_rejected() {
532        let mut c = test_consignment();
533        c.version = 99;
534        let bytes = bincode::serialize(&c).unwrap();
535        let result = Consignment::from_bytes(&bytes);
536        assert!(result.is_err());
537    }
538
539    #[test]
540    fn test_current_seals() {
541        let c = test_consignment();
542        let seals = c.current_seals();
543        // Should include seals from genesis and transition outputs
544        assert!(!seals.is_empty());
545    }
546
547    #[test]
548    fn test_empty_consignment_structure() {
549        let genesis = Genesis::new(
550            Hash::new([1u8; 32]),
551            Hash::new([2u8; 32]),
552            vec![],
553            vec![],
554            vec![],
555        );
556        let c = Consignment::from_genesis(genesis);
557        assert!(c.validate_structure().is_ok());
558        assert_eq!(c.state_root().as_bytes().len(), 32);
559    }
560}