use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::genesis::Genesis;
use crate::hash::Hash;
use crate::seal::AnchorRef;
use crate::state::{Metadata, StateAssignment};
use crate::transition::Transition;
pub const CONSIGNMENT_VERSION: u8 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Anchor {
pub anchor_ref: AnchorRef,
pub commitment: Hash,
pub inclusion_proof: Vec<u8>,
pub finality_proof: Vec<u8>,
}
impl Anchor {
pub fn new(
anchor_ref: AnchorRef,
commitment: Hash,
inclusion_proof: Vec<u8>,
finality_proof: Vec<u8>,
) -> Self {
Self {
anchor_ref,
commitment,
inclusion_proof,
finality_proof,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SealAssignment {
pub seal_ref: crate::seal::SealRef,
pub assignment: StateAssignment,
pub metadata: Vec<Metadata>,
}
impl SealAssignment {
pub fn new(
seal_ref: crate::seal::SealRef,
assignment: StateAssignment,
metadata: Vec<Metadata>,
) -> Self {
Self {
seal_ref,
assignment,
metadata,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Consignment {
pub version: u8,
pub genesis: Genesis,
pub transitions: Vec<Transition>,
pub seal_assignments: Vec<SealAssignment>,
pub anchors: Vec<Anchor>,
pub schema_id: Hash,
}
impl Consignment {
pub fn new(
genesis: Genesis,
transitions: Vec<Transition>,
seal_assignments: Vec<SealAssignment>,
anchors: Vec<Anchor>,
schema_id: Hash,
) -> Self {
Self {
version: CONSIGNMENT_VERSION,
genesis,
transitions,
seal_assignments,
anchors,
schema_id,
}
}
pub fn state_root(&self) -> Hash {
let mut hasher = Sha256::new();
hasher.update(b"CSV-CONSIGNMENT-v1");
hasher.update(&self.version.to_le_bytes());
hasher.update(self.genesis.hash().as_bytes());
hasher.update(&(self.transitions.len() as u64).to_le_bytes());
for transition in &self.transitions {
hasher.update(transition.hash().as_bytes());
}
hasher.update(&(self.seal_assignments.len() as u64).to_le_bytes());
for assignment in &self.seal_assignments {
hasher.update(&assignment.seal_ref.to_vec());
hasher.update(&assignment.assignment.seal.to_vec());
hasher.update(&assignment.assignment.data);
}
hasher.update(&(self.anchors.len() as u64).to_le_bytes());
for anchor in &self.anchors {
hasher.update(anchor.commitment.as_bytes());
hasher.update(&anchor.anchor_ref.to_vec());
}
let result = hasher.finalize();
let mut array = [0u8; 32];
array.copy_from_slice(&result);
Hash::new(array)
}
pub fn contract_id(&self) -> Hash {
self.genesis.contract_id
}
pub fn transition_count(&self) -> usize {
self.transitions.len()
}
pub fn assignment_count(&self) -> usize {
self.seal_assignments.len()
}
pub fn anchor_count(&self) -> usize {
self.anchors.len()
}
pub fn latest_state_for_seal(&self, seal: &crate::seal::SealRef) -> Option<&StateAssignment> {
self.seal_assignments
.iter()
.rev()
.find(|a| &a.seal_ref == seal)
.map(|a| &a.assignment)
}
pub fn current_seals(&self) -> alloc::collections::BTreeSet<Vec<u8>> {
let mut active: alloc::collections::BTreeSet<Vec<u8>> = alloc::collections::BTreeSet::new();
for owned in &self.genesis.owned_state {
active.insert(owned.seal.to_vec());
}
for transition in &self.transitions {
for output in &transition.owned_outputs {
active.insert(output.seal.to_vec());
}
}
active
}
pub fn validate_structure(&self) -> Result<(), ConsignmentError> {
if self.version != CONSIGNMENT_VERSION {
return Err(ConsignmentError::VersionMismatch {
expected: CONSIGNMENT_VERSION,
actual: self.version,
});
}
if self.genesis.schema_id != self.schema_id {
return Err(ConsignmentError::SchemaMismatch {
genesis_schema: self.genesis.schema_id,
consignment_schema: self.schema_id,
});
}
if self.genesis.contract_id != self.contract_id() {
return Err(ConsignmentError::ContractIdMismatch);
}
if self.transitions.len() != self.anchors.len() {
return Err(ConsignmentError::AnchorCountMismatch {
transitions: self.transitions.len(),
anchors: self.anchors.len(),
});
}
Ok(())
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
bincode::serialize(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
const MAX_SIZE: usize = 50 * 1024 * 1024; if bytes.len() > MAX_SIZE {
return Err(bincode::ErrorKind::Custom(format!(
"Consignment too large: {} bytes (max {})",
bytes.len(),
MAX_SIZE
))
.into());
}
let consignment: Consignment = bincode::deserialize(bytes)?;
if consignment.version != CONSIGNMENT_VERSION {
return Err(bincode::ErrorKind::Custom(format!(
"Unsupported consignment version: {}",
consignment.version
))
.into());
}
Ok(consignment)
}
pub fn from_genesis(genesis: Genesis) -> Self {
let schema_id = genesis.schema_id;
Self::new(genesis, vec![], vec![], vec![], schema_id)
}
}
#[derive(Debug)]
#[allow(missing_docs)]
pub enum ConsignmentError {
VersionMismatch { expected: u8, actual: u8 },
SchemaMismatch {
genesis_schema: Hash,
consignment_schema: Hash,
},
ContractIdMismatch,
AnchorCountMismatch { transitions: usize, anchors: usize },
}
impl core::fmt::Display for ConsignmentError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ConsignmentError::VersionMismatch { expected, actual } => {
write!(
f,
"Consignment version mismatch: expected {}, got {}",
expected, actual
)
}
ConsignmentError::SchemaMismatch {
genesis_schema,
consignment_schema,
} => {
write!(
f,
"Schema mismatch: genesis has {}, consignment has {}",
genesis_schema, consignment_schema
)
}
ConsignmentError::ContractIdMismatch => {
write!(f, "Contract ID inconsistency")
}
ConsignmentError::AnchorCountMismatch {
transitions,
anchors,
} => {
write!(
f,
"Anchor count mismatch: {} transitions but {} anchors",
transitions, anchors
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genesis::Genesis;
use crate::seal::SealRef;
use crate::state::{GlobalState, Metadata, OwnedState};
use crate::state::{StateAssignment, StateRef};
fn test_consignment() -> Consignment {
let genesis = Genesis::new(
Hash::new([1u8; 32]),
Hash::new([2u8; 32]),
vec![
GlobalState::new(1, 1000u64.to_le_bytes().to_vec()), ],
vec![OwnedState::new(
10,
SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
1000u64.to_le_bytes().to_vec(),
)],
vec![Metadata::from_string("issuer", "test")],
);
let transition = Transition::new(
1, vec![StateRef::new(10, Hash::new([1u8; 32]), 0)],
vec![
StateAssignment::new(
10,
SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
600u64.to_le_bytes().to_vec(),
),
StateAssignment::new(
10,
SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
400u64.to_le_bytes().to_vec(),
),
],
vec![],
vec![],
vec![0x01, 0x02],
vec![vec![0xAB; 64]],
);
let seal_assignment = SealAssignment::new(
SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
StateAssignment::new(
10,
SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
600u64.to_le_bytes().to_vec(),
),
vec![],
);
let anchor = Anchor::new(
AnchorRef::new(vec![0xCC; 32], 100, vec![]).unwrap(),
transition.hash(),
vec![0xDD; 64], vec![0xEE; 32], );
Consignment::new(
genesis,
vec![transition],
vec![seal_assignment],
vec![anchor],
Hash::new([2u8; 32]),
)
}
#[test]
fn test_consignment_creation() {
let c = test_consignment();
assert_eq!(c.version, CONSIGNMENT_VERSION);
assert_eq!(c.transition_count(), 1);
assert_eq!(c.assignment_count(), 1);
assert_eq!(c.anchor_count(), 1);
}
#[test]
fn test_consignment_state_root() {
let c = test_consignment();
let root = c.state_root();
assert_eq!(root.as_bytes().len(), 32);
}
#[test]
fn test_consignment_state_root_deterministic() {
let c1 = test_consignment();
let c2 = test_consignment();
assert_eq!(c1.state_root(), c2.state_root());
}
#[test]
fn test_consignment_state_root_differs_by_transition() {
let mut c1 = test_consignment();
let c2 = test_consignment();
c1.transitions[0].validation_script = vec![0xFF];
assert_ne!(c1.state_root(), c2.state_root());
}
#[test]
fn test_contract_id() {
let c = test_consignment();
assert_eq!(c.contract_id(), Hash::new([1u8; 32]));
}
#[test]
fn test_latest_state_for_seal() {
let c = test_consignment();
let seal = SealRef::new(vec![0xBB; 16], Some(2)).unwrap();
let state = c.latest_state_for_seal(&seal);
assert!(state.is_some());
assert_eq!(state.unwrap().data, 600u64.to_le_bytes().to_vec());
}
#[test]
fn test_latest_state_for_seal_not_found() {
let c = test_consignment();
let seal = SealRef::new(vec![0xFF; 16], Some(99)).unwrap();
let state = c.latest_state_for_seal(&seal);
assert!(state.is_none());
}
#[test]
fn test_validate_structure_valid() {
let c = test_consignment();
assert!(c.validate_structure().is_ok());
}
#[test]
fn test_validate_structure_wrong_version() {
let mut c = test_consignment();
c.version = 99;
assert!(c.validate_structure().is_err());
}
#[test]
fn test_validate_structure_schema_mismatch() {
let mut c = test_consignment();
c.schema_id = Hash::new([99u8; 32]); assert!(c.validate_structure().is_err());
}
#[test]
fn test_validate_structure_anchor_count_mismatch() {
let mut c = test_consignment();
c.anchors.push(Anchor::new(
AnchorRef::new(vec![0xFF; 32], 200, vec![]).unwrap(),
Hash::zero(),
vec![],
vec![],
));
assert!(c.validate_structure().is_err());
}
#[test]
fn test_from_genesis() {
let genesis = Genesis::new(
Hash::new([1u8; 32]),
Hash::new([2u8; 32]),
vec![],
vec![],
vec![],
);
let c = Consignment::from_genesis(genesis.clone());
assert_eq!(c.version, CONSIGNMENT_VERSION);
assert_eq!(c.transition_count(), 0);
assert_eq!(c.assignment_count(), 0);
assert_eq!(c.anchor_count(), 0);
assert_eq!(c.contract_id(), Hash::new([1u8; 32]));
assert!(c.validate_structure().is_ok());
}
#[test]
fn test_consignment_serialization_roundtrip() {
let c = test_consignment();
let bytes = c.to_bytes().unwrap();
let restored = Consignment::from_bytes(&bytes).unwrap();
assert_eq!(c, restored);
assert_eq!(c.state_root(), restored.state_root());
}
#[test]
fn test_consignment_wrong_version_rejected() {
let mut c = test_consignment();
c.version = 99;
let bytes = bincode::serialize(&c).unwrap();
let result = Consignment::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn test_current_seals() {
let c = test_consignment();
let seals = c.current_seals();
assert!(!seals.is_empty());
}
#[test]
fn test_empty_consignment_structure() {
let genesis = Genesis::new(
Hash::new([1u8; 32]),
Hash::new([2u8; 32]),
vec![],
vec![],
vec![],
);
let c = Consignment::from_genesis(genesis);
assert!(c.validate_structure().is_ok());
assert_eq!(c.state_root().as_bytes().len(), 32);
}
}