Skip to main content

csv_adapter_core/
transition.rs

1//! Transition: typed state changes in a CSV contract
2//!
3//! Transitions define how state changes: they consume owned state inputs,
4//! produce owned state outputs, update global state, and attach metadata.
5//! Each transition is validated by the VM and anchored to a seal.
6
7use alloc::vec::Vec;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use crate::dag::DAGNode;
12use crate::hash::Hash;
13use crate::seal::SealRef;
14use crate::state::{GlobalState, Metadata, StateAssignment, StateRef};
15
16/// A contract transition
17///
18/// A transition consumes existing state, executes validation logic (bytecode),
19/// and produces new state. It is authorized by consuming a single-use seal.
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub struct Transition {
22    /// Unique transition ID (defined by the schema)
23    pub transition_id: u16,
24    /// Owned state inputs being consumed
25    pub owned_inputs: Vec<StateRef>,
26    /// Owned state outputs being created
27    pub owned_outputs: Vec<StateAssignment>,
28    /// Global state updates
29    pub global_updates: Vec<GlobalState>,
30    /// Transition metadata
31    pub metadata: Vec<Metadata>,
32    /// Validation bytecode (e.g., AluVM)
33    pub validation_script: Vec<u8>,
34    /// Authorizing signatures
35    pub signatures: Vec<Vec<u8>>,
36}
37
38impl Transition {
39    /// Create a new transition
40    pub fn new(
41        transition_id: u16,
42        owned_inputs: Vec<StateRef>,
43        owned_outputs: Vec<StateAssignment>,
44        global_updates: Vec<GlobalState>,
45        metadata: Vec<Metadata>,
46        validation_script: Vec<u8>,
47        signatures: Vec<Vec<u8>>,
48    ) -> Self {
49        Self {
50            transition_id,
51            owned_inputs,
52            owned_outputs,
53            global_updates,
54            metadata,
55            validation_script,
56            signatures,
57        }
58    }
59
60    /// Compute the transition hash
61    pub fn hash(&self) -> Hash {
62        let mut hasher = Sha256::new();
63
64        hasher.update(b"CSV-TRANSITION-v1");
65        hasher.update(&self.transition_id.to_le_bytes());
66
67        // Owned inputs
68        hasher.update(&(self.owned_inputs.len() as u64).to_le_bytes());
69        for input in &self.owned_inputs {
70            hasher.update(&input.type_id.to_le_bytes());
71            hasher.update(input.commitment.as_bytes());
72            hasher.update(&input.output_index.to_le_bytes());
73        }
74
75        // Owned outputs
76        hasher.update(&(self.owned_outputs.len() as u64).to_le_bytes());
77        for output in &self.owned_outputs {
78            hasher.update(&output.type_id.to_le_bytes());
79            hasher.update(&output.seal.to_vec());
80            hasher.update(&output.data);
81        }
82
83        // Global updates
84        hasher.update(&(self.global_updates.len() as u64).to_le_bytes());
85        for update in &self.global_updates {
86            hasher.update(&update.type_id.to_le_bytes());
87            hasher.update(&update.data);
88        }
89
90        // Metadata
91        hasher.update(&(self.metadata.len() as u64).to_le_bytes());
92        for meta in &self.metadata {
93            hasher.update(meta.key.as_bytes());
94            hasher.update(&meta.value);
95        }
96
97        // Validation script
98        hasher.update(&(self.validation_script.len() as u64).to_le_bytes());
99        hasher.update(&self.validation_script);
100
101        // Signatures
102        hasher.update(&(self.signatures.len() as u64).to_le_bytes());
103        for sig in &self.signatures {
104            hasher.update(sig);
105        }
106
107        let result = hasher.finalize();
108        let mut array = [0u8; 32];
109        array.copy_from_slice(&result);
110        Hash::new(array)
111    }
112
113    /// Get all seals consumed by this transition (from owned inputs)
114    pub fn consumed_seals(&self) -> Vec<SealRef> {
115        // StateRef doesn't contain SealRef directly — seals are resolved
116        // from the parent transition that created each output.
117        // This method is a placeholder; actual resolution requires
118        // walking the transition chain.
119        Vec::new()
120    }
121
122    /// Get all seals that receive new state from this transition
123    pub fn assigned_seals(&self) -> Vec<SealRef> {
124        self.owned_outputs.iter().map(|o| o.seal.clone()).collect()
125    }
126
127    /// Check if this transition has no inputs (genesis-like transition)
128    pub fn is_genesis_like(&self) -> bool {
129        self.owned_inputs.is_empty()
130    }
131
132    /// Check if this transition destroys all state (no outputs)
133    pub fn is_destructive(&self) -> bool {
134        self.owned_outputs.is_empty() && self.global_updates.is_empty()
135    }
136
137    /// Convert to a DAG node for backwards compatibility
138    pub fn to_dag_node(&self) -> DAGNode {
139        DAGNode::new(
140            self.hash(),
141            self.validation_script.clone(),
142            self.signatures.clone(),
143            self.metadata
144                .iter()
145                .flat_map(|m| m.value.clone())
146                .collect::<Vec<_>>()
147                .chunks(32)
148                .map(|c| c.to_vec())
149                .collect(),
150            Vec::new(), // Parents must be set externally
151        )
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn test_transition() -> Transition {
160        Transition::new(
161            1, // transition_id: e.g., "transfer"
162            vec![
163                StateRef::new(10, Hash::new([1u8; 32]), 0), // consume 1000 tokens from genesis output 0
164            ],
165            vec![
166                StateAssignment::new(
167                    10,
168                    SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
169                    600u64.to_le_bytes().to_vec(),
170                ), // 600 to seal A
171                StateAssignment::new(
172                    10,
173                    SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
174                    400u64.to_le_bytes().to_vec(),
175                ), // 400 to seal B (change)
176            ],
177            vec![
178                GlobalState::new(1, vec![100, 200]), // update total supply indicator
179            ],
180            vec![Metadata::from_string("memo", "payment for services")],
181            vec![0x01, 0x02, 0x03], // validation bytecode
182            vec![vec![0xAB; 64]],   // signature
183        )
184    }
185
186    #[test]
187    fn test_transition_creation() {
188        let t = test_transition();
189        assert_eq!(t.transition_id, 1);
190        assert_eq!(t.owned_inputs.len(), 1);
191        assert_eq!(t.owned_outputs.len(), 2);
192        assert_eq!(t.global_updates.len(), 1);
193        assert_eq!(t.metadata.len(), 1);
194    }
195
196    #[test]
197    fn test_transition_hash() {
198        let t = test_transition();
199        let hash = t.hash();
200        assert_eq!(hash.as_bytes().len(), 32);
201    }
202
203    #[test]
204    fn test_transition_hash_deterministic() {
205        let t1 = test_transition();
206        let t2 = test_transition();
207        assert_eq!(t1.hash(), t2.hash());
208    }
209
210    #[test]
211    fn test_transition_hash_differs_by_inputs() {
212        let mut t1 = test_transition();
213        let t2 = test_transition();
214        t1.owned_inputs
215            .push(StateRef::new(10, Hash::new([99u8; 32]), 1));
216        assert_ne!(t1.hash(), t2.hash());
217    }
218
219    #[test]
220    fn test_transition_hash_differs_by_outputs() {
221        let mut t1 = test_transition();
222        let t2 = test_transition();
223        t1.owned_outputs.push(StateAssignment::new(
224            10,
225            SealRef::new(vec![0xCC; 16], Some(3)).unwrap(),
226            vec![100],
227        ));
228        assert_ne!(t1.hash(), t2.hash());
229    }
230
231    #[test]
232    fn test_transition_hash_differs_by_script() {
233        let mut t1 = test_transition();
234        let t2 = test_transition();
235        t1.validation_script = vec![0xFF, 0xFF, 0xFF];
236        assert_ne!(t1.hash(), t2.hash());
237    }
238
239    #[test]
240    fn test_transition_hash_differs_by_signatures() {
241        let mut t1 = test_transition();
242        let t2 = test_transition();
243        t1.signatures.push(vec![0xCD; 64]);
244        assert_ne!(t1.hash(), t2.hash());
245    }
246
247    #[test]
248    fn test_transition_hash_bytecode_order() {
249        let t1 = Transition::new(
250            1,
251            vec![],
252            vec![],
253            vec![],
254            vec![],
255            vec![0x01, 0x02, 0x03],
256            vec![],
257        );
258        let t2 = Transition::new(
259            1,
260            vec![],
261            vec![],
262            vec![],
263            vec![],
264            vec![0x03, 0x02, 0x01],
265            vec![],
266        );
267        assert_ne!(t1.hash(), t2.hash());
268    }
269
270    #[test]
271    fn test_assigned_seals() {
272        let t = test_transition();
273        let seals = t.assigned_seals();
274        assert_eq!(seals.len(), 2);
275        assert_eq!(seals[0].nonce, Some(1));
276        assert_eq!(seals[1].nonce, Some(2));
277    }
278
279    #[test]
280    fn test_is_genesis_like() {
281        let genesis_like = Transition::new(
282            0,
283            vec![], // no inputs
284            vec![StateAssignment::new(
285                10,
286                SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
287                1000u64.to_le_bytes().to_vec(),
288            )],
289            vec![],
290            vec![],
291            vec![0x01],
292            vec![],
293        );
294        assert!(genesis_like.is_genesis_like());
295
296        let normal = test_transition();
297        assert!(!normal.is_genesis_like());
298    }
299
300    #[test]
301    fn test_is_destructive() {
302        let destructive = Transition::new(
303            2, // e.g., "burn"
304            vec![StateRef::new(10, Hash::new([1u8; 32]), 0)],
305            vec![], // no outputs
306            vec![], // no global updates
307            vec![],
308            vec![0x01],
309            vec![],
310        );
311        assert!(destructive.is_destructive());
312
313        let normal = test_transition();
314        assert!(!normal.is_destructive());
315    }
316
317    #[test]
318    fn test_empty_transition() {
319        let t = Transition::new(0, vec![], vec![], vec![], vec![], vec![], vec![]);
320        assert!(t.is_genesis_like());
321        assert!(t.is_destructive());
322        assert_eq!(t.hash().as_bytes().len(), 32);
323    }
324
325    #[test]
326    fn test_transition_serialization_roundtrip() {
327        let t = test_transition();
328        let bytes = bincode::serialize(&t).unwrap();
329        let restored: Transition = bincode::deserialize(&bytes).unwrap();
330        assert_eq!(t, restored);
331        assert_eq!(t.hash(), restored.hash());
332    }
333
334    #[test]
335    fn test_to_dag_node() {
336        let t = test_transition();
337        let node = t.to_dag_node();
338        assert_eq!(node.node_id, t.hash());
339        assert_eq!(node.bytecode, t.validation_script);
340        assert_eq!(node.signatures, t.signatures);
341    }
342}