1use 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
17pub const CONSIGNMENT_VERSION: u8 = 1;
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Anchor {
26 pub anchor_ref: AnchorRef,
28 pub commitment: Hash,
30 pub inclusion_proof: Vec<u8>,
32 pub finality_proof: Vec<u8>,
34}
35
36impl Anchor {
37 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57pub struct SealAssignment {
58 pub seal_ref: crate::seal::SealRef,
60 pub assignment: StateAssignment,
62 pub metadata: Vec<Metadata>,
64}
65
66impl SealAssignment {
67 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Consignment {
88 pub version: u8,
90 pub genesis: Genesis,
92 pub transitions: Vec<Transition>,
94 pub seal_assignments: Vec<SealAssignment>,
96 pub anchors: Vec<Anchor>,
98 pub schema_id: Hash,
100}
101
102impl Consignment {
103 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 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 hasher.update(self.genesis.hash().as_bytes());
133
134 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 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 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 pub fn contract_id(&self) -> Hash {
163 self.genesis.contract_id
164 }
165
166 pub fn transition_count(&self) -> usize {
168 self.transitions.len()
169 }
170
171 pub fn assignment_count(&self) -> usize {
173 self.seal_assignments.len()
174 }
175
176 pub fn anchor_count(&self) -> usize {
178 self.anchors.len()
179 }
180
181 pub fn latest_state_for_seal(&self, seal: &crate::seal::SealRef) -> Option<&StateAssignment> {
186 self.seal_assignments
188 .iter()
189 .rev()
190 .find(|a| &a.seal_ref == seal)
191 .map(|a| &a.assignment)
192 }
193
194 pub fn current_seals(&self) -> alloc::collections::BTreeSet<Vec<u8>> {
199 let mut active: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
201
202 for owned in &self.genesis.owned_state {
204 active.insert(owned.seal.to_vec());
205 }
206
207 for transition in &self.transitions {
209 for output in &transition.owned_outputs {
210 active.insert(output.seal.to_vec());
211 }
212 }
213
214 active
218 }
219
220 pub fn validate_structure(&self) -> Result<(), ConsignmentError> {
222 if self.version != CONSIGNMENT_VERSION {
224 return Err(ConsignmentError::VersionMismatch {
225 expected: CONSIGNMENT_VERSION,
226 actual: self.version,
227 });
228 }
229
230 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 if self.genesis.contract_id != self.contract_id() {
240 return Err(ConsignmentError::ContractIdMismatch);
241 }
242
243 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 pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
256 bincode::serialize(self)
257 }
258
259 pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
261 const MAX_SIZE: usize = 50 * 1024 * 1024; 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 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 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#[derive(Debug)]
294#[allow(missing_docs)]
295pub enum ConsignmentError {
296 VersionMismatch { expected: u8, actual: u8 },
298 SchemaMismatch {
300 genesis_schema: Hash,
301 consignment_schema: Hash,
302 },
303 ContractIdMismatch,
305 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()), ],
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, 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], vec![0xEE; 32], );
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 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]); 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 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}