Skip to main content

csv_adapter_core/
schema.rs

1//! Schema: contract logic definition and transition rules
2//!
3//! A schema defines the valid state types, transition definitions,
4//! and validation rules for a class of contracts. It is the "class"
5//! in the OOP analogy — contracts are "instances" of schemas.
6
7use alloc::string::String;
8use alloc::vec::Vec;
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12use crate::hash::Hash;
13use crate::state::StateTypeId;
14use crate::transition::Transition;
15
16/// Schema validation errors
17#[derive(Debug)]
18#[allow(missing_docs)]
19pub enum SchemaError {
20    /// Type ID not defined in schema
21    TypeNotFound { type_id: StateTypeId },
22    /// Transition ID not defined in schema
23    TransitionNotFound { transition_id: u16 },
24    /// Transition input types don't match schema definition
25    InputTypeMismatch {
26        transition_id: u16,
27        expected: Vec<StateTypeId>,
28        actual: Vec<StateTypeId>,
29    },
30    /// Transition output types don't match schema definition
31    OutputTypeMismatch {
32        transition_id: u16,
33        expected: Vec<StateTypeId>,
34        actual: Vec<StateTypeId>,
35    },
36}
37
38impl core::fmt::Display for SchemaError {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        match self {
41            SchemaError::TypeNotFound { type_id } => {
42                write!(f, "Type ID {} not found in schema", type_id)
43            }
44            SchemaError::TransitionNotFound { transition_id } => {
45                write!(f, "Transition ID {} not found in schema", transition_id)
46            }
47            SchemaError::InputTypeMismatch {
48                transition_id,
49                expected,
50                actual,
51            } => {
52                write!(
53                    f,
54                    "Transition {} input type mismatch: expected {:?}, got {:?}",
55                    transition_id, expected, actual
56                )
57            }
58            SchemaError::OutputTypeMismatch {
59                transition_id,
60                expected,
61                actual,
62            } => {
63                write!(
64                    f,
65                    "Transition {} output type mismatch: expected {:?}, got {:?}",
66                    transition_id, expected, actual
67                )
68            }
69        }
70    }
71}
72
73/// Transition validation errors
74#[derive(Debug)]
75#[allow(missing_docs)]
76pub enum TransitionValidationError {
77    /// Input state type not found in schema
78    InputNotFound { type_id: StateTypeId },
79    /// Output state type not defined in schema
80    OutputTypeNotDefined { type_id: StateTypeId },
81    /// Validation script execution failed
82    ScriptExecutionFailed(String),
83    /// Signature verification failed
84    SignatureVerificationFailed,
85}
86
87impl core::fmt::Display for TransitionValidationError {
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        match self {
90            TransitionValidationError::InputNotFound { type_id } => {
91                write!(f, "Input state type {} not found", type_id)
92            }
93            TransitionValidationError::OutputTypeNotDefined { type_id } => {
94                write!(f, "Output state type {} not defined in schema", type_id)
95            }
96            TransitionValidationError::ScriptExecutionFailed(msg) => {
97                write!(f, "Validation script failed: {}", msg)
98            }
99            TransitionValidationError::SignatureVerificationFailed => {
100                write!(f, "Signature verification failed")
101            }
102        }
103    }
104}
105
106/// Schema version
107pub const SCHEMA_VERSION: u8 = 1;
108
109/// Data type for state values in the schema
110#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub enum StateDataType {
112    /// Fixed-size binary blob (size in bytes)
113    FixedSize(u32),
114    /// 64-bit unsigned integer
115    Integer64,
116    /// 32-bit unsigned integer
117    Integer32,
118    /// 8-bit unsigned integer
119    Integer8,
120    /// Arbitrary-size binary blob (validated by script)
121    Blob,
122    /// 256-bit hash
123    Hash256,
124}
125
126impl StateDataType {
127    /// Get the fixed size if applicable
128    pub fn fixed_size(&self) -> Option<u32> {
129        match self {
130            StateDataType::FixedSize(s) => Some(*s),
131            StateDataType::Integer64 => Some(8),
132            StateDataType::Integer32 => Some(4),
133            StateDataType::Integer8 => Some(1),
134            StateDataType::Hash256 => Some(32),
135            StateDataType::Blob => None,
136        }
137    }
138}
139
140/// Definition of a global state type in the schema
141///
142/// Global state types represent shared, non-owned state that all
143/// participants can see but no single party owns (e.g., total supply).
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
145pub struct GlobalStateType {
146    /// Type ID (unique within schema)
147    pub type_id: StateTypeId,
148    /// Human-readable name
149    pub name: String,
150    /// Data type for values of this state
151    pub data_type: StateDataType,
152    /// Whether this state supports homomorphic operations (e.g., additive commitments)
153    pub is_homomorphic: bool,
154}
155
156impl GlobalStateType {
157    /// Create a new global state type definition
158    pub fn new(
159        type_id: StateTypeId,
160        name: impl Into<String>,
161        data_type: StateDataType,
162        is_homomorphic: bool,
163    ) -> Self {
164        Self {
165            type_id,
166            name: name.into(),
167            data_type,
168            is_homomorphic,
169        }
170    }
171}
172
173/// Definition of an owned state type in the schema
174///
175/// Owned state types represent state that is controlled by a specific
176/// owner (tied to a seal). These are the primary vehicle for rights.
177#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
178pub struct OwnedStateType {
179    /// Type ID (unique within schema)
180    pub type_id: StateTypeId,
181    /// Human-readable name
182    pub name: String,
183    /// Data type for values of this state
184    pub data_type: StateDataType,
185    /// Whether this state represents a fungible asset (adds to total supply)
186    pub is_fungible: bool,
187}
188
189impl OwnedStateType {
190    /// Create a new owned state type definition
191    pub fn new(
192        type_id: StateTypeId,
193        name: impl Into<String>,
194        data_type: StateDataType,
195        is_fungible: bool,
196    ) -> Self {
197        Self {
198            type_id,
199            name: name.into(),
200            data_type,
201            is_fungible,
202        }
203    }
204}
205
206/// Transition definition in the schema
207///
208/// Defines what inputs a transition consumes, what outputs it produces,
209/// and what validation script must pass.
210#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
211pub struct TransitionDef {
212    /// Transition ID (unique within schema)
213    pub transition_id: u16,
214    /// Human-readable name
215    pub name: String,
216    /// Expected owned input type IDs
217    pub owned_inputs: Vec<StateTypeId>,
218    /// Produced owned output type IDs
219    pub owned_outputs: Vec<StateTypeId>,
220    /// Updated global state type IDs
221    pub global_updates: Vec<StateTypeId>,
222    /// Validation script bytecode (executed by the VM)
223    pub validation_script: Vec<u8>,
224}
225
226impl TransitionDef {
227    /// Create a new transition definition
228    pub fn new(
229        transition_id: u16,
230        name: impl Into<String>,
231        owned_inputs: Vec<StateTypeId>,
232        owned_outputs: Vec<StateTypeId>,
233        global_updates: Vec<StateTypeId>,
234        validation_script: Vec<u8>,
235    ) -> Self {
236        Self {
237            transition_id,
238            name: name.into(),
239            owned_inputs,
240            owned_outputs,
241            global_updates,
242            validation_script,
243        }
244    }
245}
246
247/// Contract schema
248///
249/// A schema defines the rules that govern a class of contracts:
250/// what state types exist, what transitions are valid, and how
251/// to validate them.
252#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
253pub struct Schema {
254    /// Schema version
255    pub version: u8,
256    /// Unique schema identifier
257    pub schema_id: Hash,
258    /// Human-readable name
259    pub name: String,
260    /// Defined global state types
261    pub global_types: Vec<GlobalStateType>,
262    /// Defined owned state types
263    pub owned_types: Vec<OwnedStateType>,
264    /// Defined transition types
265    pub transitions: Vec<TransitionDef>,
266    /// Root validation script (runs on every transition)
267    pub root_script: Vec<u8>,
268}
269
270impl Schema {
271    /// Create a new schema
272    pub fn new(
273        schema_id: Hash,
274        name: impl Into<String>,
275        global_types: Vec<GlobalStateType>,
276        owned_types: Vec<OwnedStateType>,
277        transitions: Vec<TransitionDef>,
278        root_script: Vec<u8>,
279    ) -> Self {
280        Self {
281            version: SCHEMA_VERSION,
282            schema_id,
283            name: name.into(),
284            global_types,
285            owned_types,
286            transitions,
287            root_script,
288        }
289    }
290
291    /// Compute the schema hash
292    pub fn hash(&self) -> Hash {
293        let mut hasher = Sha256::new();
294
295        hasher.update(b"CSV-SCHEMA-v1");
296        hasher.update(&self.version.to_le_bytes());
297        hasher.update(self.name.as_bytes());
298
299        // Global types
300        hasher.update(&(self.global_types.len() as u64).to_le_bytes());
301        for gt in &self.global_types {
302            hasher.update(&gt.type_id.to_le_bytes());
303            hasher.update(gt.name.as_bytes());
304            hasher.update(&(gt.data_type.fixed_size().unwrap_or(0)).to_le_bytes());
305            hasher.update(&[gt.is_homomorphic as u8]);
306        }
307
308        // Owned types
309        hasher.update(&(self.owned_types.len() as u64).to_le_bytes());
310        for ot in &self.owned_types {
311            hasher.update(&ot.type_id.to_le_bytes());
312            hasher.update(ot.name.as_bytes());
313            hasher.update(&(ot.data_type.fixed_size().unwrap_or(0)).to_le_bytes());
314            hasher.update(&[ot.is_fungible as u8]);
315        }
316
317        // Transitions
318        hasher.update(&(self.transitions.len() as u64).to_le_bytes());
319        for t in &self.transitions {
320            hasher.update(&t.transition_id.to_le_bytes());
321            hasher.update(t.name.as_bytes());
322            hasher.update(&(t.owned_inputs.len() as u64).to_le_bytes());
323            for id in &t.owned_inputs {
324                hasher.update(&id.to_le_bytes());
325            }
326            hasher.update(&(t.owned_outputs.len() as u64).to_le_bytes());
327            for id in &t.owned_outputs {
328                hasher.update(&id.to_le_bytes());
329            }
330            hasher.update(&(t.validation_script.len() as u64).to_le_bytes());
331            hasher.update(&t.validation_script);
332        }
333
334        // Root script
335        hasher.update(&(self.root_script.len() as u64).to_le_bytes());
336        hasher.update(&self.root_script);
337
338        let result = hasher.finalize();
339        let mut array = [0u8; 32];
340        array.copy_from_slice(&result);
341        Hash::new(array)
342    }
343
344    /// Get a global state type by ID
345    pub fn global_type(&self, type_id: StateTypeId) -> Option<&GlobalStateType> {
346        self.global_types.iter().find(|t| t.type_id == type_id)
347    }
348
349    /// Get an owned state type by ID
350    pub fn owned_type(&self, type_id: StateTypeId) -> Option<&OwnedStateType> {
351        self.owned_types.iter().find(|t| t.type_id == type_id)
352    }
353
354    /// Get a transition definition by ID
355    pub fn transition_def(&self, transition_id: u16) -> Option<&TransitionDef> {
356        self.transitions
357            .iter()
358            .find(|t| t.transition_id == transition_id)
359    }
360
361    /// Validate that a type ID is defined in the schema
362    pub fn has_type(&self, type_id: StateTypeId) -> bool {
363        self.global_type(type_id).is_some() || self.owned_type(type_id).is_some()
364    }
365
366    /// Validate that a transition ID is defined in the schema
367    pub fn has_transition(&self, transition_id: u16) -> bool {
368        self.transition_def(transition_id).is_some()
369    }
370
371    /// Validate a transition against this schema
372    pub fn validate_transition(&self, transition: &Transition) -> Result<(), SchemaError> {
373        let def = self.transition_def(transition.transition_id).ok_or(
374            SchemaError::TransitionNotFound {
375                transition_id: transition.transition_id,
376            },
377        )?;
378
379        // Check input types match
380        let actual_input_types: Vec<StateTypeId> =
381            transition.owned_inputs.iter().map(|i| i.type_id).collect();
382        if actual_input_types != def.owned_inputs {
383            return Err(SchemaError::InputTypeMismatch {
384                transition_id: transition.transition_id,
385                expected: def.owned_inputs.clone(),
386                actual: actual_input_types,
387            });
388        }
389
390        // Check output types match
391        let actual_output_types: Vec<StateTypeId> =
392            transition.owned_outputs.iter().map(|o| o.type_id).collect();
393        if actual_output_types != def.owned_outputs {
394            return Err(SchemaError::OutputTypeMismatch {
395                transition_id: transition.transition_id,
396                expected: def.owned_outputs.clone(),
397                actual: actual_output_types,
398            });
399        }
400
401        // Check global update types are defined
402        for update in &transition.global_updates {
403            if !self.has_type(update.type_id) {
404                return Err(SchemaError::TypeNotFound {
405                    type_id: update.type_id,
406                });
407            }
408        }
409
410        Ok(())
411    }
412
413    /// Create a minimal fungible token schema
414    pub fn fungible_token(schema_id: Hash, name: impl Into<String>) -> Self {
415        let name = name.into();
416        Self::new(
417            schema_id,
418            name.clone(),
419            vec![GlobalStateType::new(
420                1,
421                "supply",
422                StateDataType::Integer64,
423                true,
424            )],
425            vec![OwnedStateType::new(
426                10,
427                "asset",
428                StateDataType::Integer64,
429                true,
430            )],
431            vec![
432                // Genesis: no inputs, produces asset outputs
433                TransitionDef::new(
434                    0,
435                    "genesis",
436                    vec![],
437                    vec![10],
438                    vec![1],
439                    vec![0x01], // placeholder script
440                ),
441                // Transfer: consumes asset, produces asset outputs
442                TransitionDef::new(
443                    1,
444                    "transfer",
445                    vec![10],
446                    vec![10],
447                    vec![],
448                    vec![0x02], // placeholder script
449                ),
450                // Burn: consumes asset, no outputs
451                TransitionDef::new(
452                    2,
453                    "burn",
454                    vec![10],
455                    vec![],
456                    vec![1],    // updates supply
457                    vec![0x03], // placeholder script
458                ),
459            ],
460            vec![0x00], // minimal root script
461        )
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use crate::seal::SealRef;
469    use crate::state::{StateAssignment, StateRef};
470
471    fn test_schema() -> Schema {
472        Schema::fungible_token(Hash::new([1u8; 32]), "TestToken")
473    }
474
475    #[test]
476    fn test_schema_creation() {
477        let s = test_schema();
478        assert_eq!(s.version, SCHEMA_VERSION);
479        assert_eq!(s.name, "TestToken");
480        assert_eq!(s.global_types.len(), 1);
481        assert_eq!(s.owned_types.len(), 1);
482        assert_eq!(s.transitions.len(), 3);
483    }
484
485    #[test]
486    fn test_schema_hash() {
487        let s = test_schema();
488        let hash = s.hash();
489        assert_eq!(hash.as_bytes().len(), 32);
490    }
491
492    #[test]
493    fn test_schema_hash_deterministic() {
494        let s1 = test_schema();
495        let s2 = test_schema();
496        assert_eq!(s1.hash(), s2.hash());
497    }
498
499    #[test]
500    fn test_schema_hash_differs_by_name() {
501        let mut s = test_schema();
502        let original = s.hash();
503        s.name = "DifferentToken".to_string();
504        assert_ne!(s.hash(), original);
505    }
506
507    #[test]
508    fn test_schema_hash_differs_by_types() {
509        let mut s = test_schema();
510        let original = s.hash();
511        s.global_types.push(GlobalStateType::new(
512            99,
513            "extra",
514            StateDataType::Integer8,
515            false,
516        ));
517        assert_ne!(s.hash(), original);
518    }
519
520    #[test]
521    fn test_global_type_lookup() {
522        let s = test_schema();
523        let gt = s.global_type(1);
524        assert!(gt.is_some());
525        assert_eq!(gt.unwrap().name, "supply");
526        assert!(s.global_type(99).is_none());
527    }
528
529    #[test]
530    fn test_owned_type_lookup() {
531        let s = test_schema();
532        let ot = s.owned_type(10);
533        assert!(ot.is_some());
534        assert_eq!(ot.unwrap().name, "asset");
535        assert!(s.owned_type(99).is_none());
536    }
537
538    #[test]
539    fn test_transition_def_lookup() {
540        let s = test_schema();
541        let td = s.transition_def(1);
542        assert!(td.is_some());
543        assert_eq!(td.unwrap().name, "transfer");
544        assert!(s.transition_def(99).is_none());
545    }
546
547    #[test]
548    fn test_has_type() {
549        let s = test_schema();
550        assert!(s.has_type(1)); // global
551        assert!(s.has_type(10)); // owned
552        assert!(!s.has_type(99));
553    }
554
555    #[test]
556    fn test_has_transition() {
557        let s = test_schema();
558        assert!(s.has_transition(0)); // genesis
559        assert!(s.has_transition(1)); // transfer
560        assert!(s.has_transition(2)); // burn
561        assert!(!s.has_transition(99));
562    }
563
564    #[test]
565    fn test_state_data_type_sizes() {
566        assert_eq!(StateDataType::Integer64.fixed_size(), Some(8));
567        assert_eq!(StateDataType::Integer32.fixed_size(), Some(4));
568        assert_eq!(StateDataType::Integer8.fixed_size(), Some(1));
569        assert_eq!(StateDataType::Hash256.fixed_size(), Some(32));
570        assert_eq!(StateDataType::Blob.fixed_size(), None);
571        assert_eq!(StateDataType::FixedSize(128).fixed_size(), Some(128));
572    }
573
574    #[test]
575    fn test_schema_serialization_roundtrip() {
576        let s = test_schema();
577        let bytes = bincode::serialize(&s).unwrap();
578        let restored: Schema = bincode::deserialize(&bytes).unwrap();
579        assert_eq!(s, restored);
580        assert_eq!(s.hash(), restored.hash());
581    }
582
583    #[test]
584    fn test_fungible_token_schema_structure() {
585        let s = test_schema();
586
587        // Genesis should have no inputs
588        let genesis = s.transition_def(0).unwrap();
589        assert!(genesis.owned_inputs.is_empty());
590        assert_eq!(genesis.owned_outputs, vec![10]);
591
592        // Transfer should consume and produce assets
593        let transfer = s.transition_def(1).unwrap();
594        assert_eq!(transfer.owned_inputs, vec![10]);
595        assert_eq!(transfer.owned_outputs, vec![10]);
596
597        // Burn should consume asset but not produce any
598        let burn = s.transition_def(2).unwrap();
599        assert_eq!(burn.owned_inputs, vec![10]);
600        assert!(burn.owned_outputs.is_empty());
601    }
602
603    #[test]
604    fn test_validate_transition_valid() {
605        let s = test_schema();
606
607        // Valid transfer: input type 10, output type 10
608        let transfer = Transition::new(
609            1,
610            vec![StateRef::new(10, Hash::new([1u8; 32]), 0)],
611            vec![StateAssignment::new(
612                10,
613                SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
614                500u64.to_le_bytes().to_vec(),
615            )],
616            vec![],
617            vec![],
618            vec![0x02],
619            vec![],
620        );
621        assert!(s.validate_transition(&transfer).is_ok());
622    }
623
624    #[test]
625    fn test_validate_transition_unknown_transition_id() {
626        let s = test_schema();
627        let bad = Transition::new(99, vec![], vec![], vec![], vec![], vec![], vec![]);
628        let err = s.validate_transition(&bad).unwrap_err();
629        assert!(matches!(err, SchemaError::TransitionNotFound { .. }));
630    }
631
632    #[test]
633    fn test_validate_transition_input_type_mismatch() {
634        let s = test_schema();
635
636        // Transfer expects input type 10, but we give type 99
637        let bad = Transition::new(
638            1,
639            vec![StateRef::new(99, Hash::new([1u8; 32]), 0)],
640            vec![],
641            vec![],
642            vec![],
643            vec![0x02],
644            vec![],
645        );
646        let err = s.validate_transition(&bad).unwrap_err();
647        assert!(matches!(err, SchemaError::InputTypeMismatch { .. }));
648    }
649
650    #[test]
651    fn test_validate_transition_output_type_mismatch() {
652        let s = test_schema();
653
654        // Transfer expects output type 10, but we give type 99
655        let bad = Transition::new(
656            1,
657            vec![StateRef::new(10, Hash::new([1u8; 32]), 0)],
658            vec![StateAssignment::new(
659                99,
660                SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
661                500u64.to_le_bytes().to_vec(),
662            )],
663            vec![],
664            vec![],
665            vec![0x02],
666            vec![],
667        );
668        let err = s.validate_transition(&bad).unwrap_err();
669        assert!(matches!(err, SchemaError::OutputTypeMismatch { .. }));
670    }
671}