Skip to main content

csv_adapter_core/
vm.rs

1//! Deterministic VM trait for CSV contract execution
2//!
3//! This trait defines the interface that any deterministic VM must implement
4//! to serve as the execution engine for CSV contract transitions.
5//!
6//! The core invariant: the same bytecode + inputs must always produce
7//! the same outputs, regardless of the executing environment.
8
9use alloc::collections::BTreeMap;
10use alloc::string::String;
11use alloc::vec::Vec;
12use serde::{Deserialize, Serialize};
13
14use crate::seal::SealRef;
15use crate::state::{GlobalState, Metadata, OwnedState, StateAssignment, StateRef, StateTypeId};
16
17/// Errors that can occur during VM execution
18#[derive(Debug)]
19#[allow(missing_docs)]
20pub enum VMError {
21    /// Bytecode is malformed or invalid
22    InvalidBytecode(String),
23    /// Execution ran out of steps (loop detection / gas limit)
24    ExecutionLimitExceeded { max_steps: u64, actual_steps: u64 },
25    /// A state input referenced in the transition was not found
26    StateNotFound { state_ref: StateRef },
27    /// The VM produced inconsistent output (e.g., negative supply)
28    InconsistentOutput(String),
29    /// Signature verification failed
30    InvalidSignature(String),
31    /// Seal was already consumed (replay detected)
32    SealReplay { seal: SealRef },
33    /// Schema validation failed
34    SchemaViolation(String),
35    /// Generic execution error
36    ExecutionError(String),
37}
38
39impl core::fmt::Display for VMError {
40    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        match self {
42            VMError::InvalidBytecode(msg) => write!(f, "Invalid bytecode: {}", msg),
43            VMError::ExecutionLimitExceeded {
44                max_steps,
45                actual_steps,
46            } => {
47                write!(
48                    f,
49                    "Execution limit exceeded: {} steps (max {})",
50                    actual_steps, max_steps
51                )
52            }
53            VMError::StateNotFound { state_ref } => {
54                write!(f, "State not found: {:?}", state_ref)
55            }
56            VMError::InconsistentOutput(msg) => write!(f, "Inconsistent output: {}", msg),
57            VMError::InvalidSignature(msg) => write!(f, "Invalid signature: {}", msg),
58            VMError::SealReplay { seal } => write!(f, "Seal replay detected: {:?}", seal),
59            VMError::SchemaViolation(msg) => write!(f, "Schema violation: {}", msg),
60            VMError::ExecutionError(msg) => write!(f, "Execution error: {}", msg),
61        }
62    }
63}
64
65/// Input state for VM execution
66///
67/// Contains all state that must be consumed as input to a transition,
68/// including owned states, global state, metadata, and seal data.
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70pub struct VMInputs {
71    /// Owned states being consumed (resolved from StateRef)
72    pub owned_inputs: Vec<OwnedState>,
73    /// Current global state values
74    pub global_state: Vec<GlobalState>,
75    /// Transition metadata
76    pub metadata: Vec<Metadata>,
77    /// Seal data being consumed (authorizes this transition)
78    pub seal_data: Vec<u8>,
79}
80
81impl VMInputs {
82    /// Create new VM inputs
83    pub fn new(
84        owned_inputs: Vec<OwnedState>,
85        global_state: Vec<GlobalState>,
86        metadata: Vec<Metadata>,
87        seal_data: Vec<u8>,
88    ) -> Self {
89        Self {
90            owned_inputs,
91            global_state,
92            metadata,
93            seal_data,
94        }
95    }
96
97    /// Look up a global state by type ID
98    pub fn global_state_of(&self, type_id: StateTypeId) -> Vec<&GlobalState> {
99        self.global_state
100            .iter()
101            .filter(|s| s.type_id == type_id)
102            .collect()
103    }
104
105    /// Look up owned states by type ID
106    pub fn owned_state_of(&self, type_id: StateTypeId) -> Vec<&OwnedState> {
107        self.owned_inputs
108            .iter()
109            .filter(|s| s.type_id == type_id)
110            .collect()
111    }
112}
113
114/// Output state from VM execution
115#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
116pub struct VMOutputs {
117    /// New owned state assignments (who gets what)
118    pub owned_outputs: Vec<StateAssignment>,
119    /// Updated global state values
120    pub global_updates: Vec<GlobalState>,
121    /// Updated metadata
122    pub metadata_updates: Vec<Metadata>,
123    /// The next seal to be consumed (derived from the transition)
124    pub next_seal: Option<SealRef>,
125}
126
127impl VMOutputs {
128    /// Create new VM outputs
129    pub fn new(
130        owned_outputs: Vec<StateAssignment>,
131        global_updates: Vec<GlobalState>,
132        metadata_updates: Vec<Metadata>,
133        next_seal: Option<SealRef>,
134    ) -> Self {
135        Self {
136            owned_outputs,
137            global_updates,
138            metadata_updates,
139            next_seal,
140        }
141    }
142
143    /// Get total value by type ID (for fungible asset validation)
144    pub fn total_by_type(&self) -> BTreeMap<StateTypeId, u64> {
145        let mut totals = BTreeMap::new();
146
147        for assignment in &self.owned_outputs {
148            let value = decode_integer(&assignment.data).unwrap_or(0);
149            *totals.entry(assignment.type_id).or_insert(0) += value;
150        }
151
152        for update in &self.global_updates {
153            let value = decode_integer(&update.data).unwrap_or(0);
154            *totals.entry(update.type_id).or_insert(0) += value;
155        }
156
157        totals
158    }
159}
160
161/// The DeterministicVM trait defines the interface for CSV contract execution.
162///
163/// Any VM implementing this trait must guarantee:
164/// - The same bytecode + inputs always produce the same outputs
165/// - No access to external state (time, network, random, etc.)
166/// - Bounded execution (no infinite loops)
167pub trait DeterministicVM {
168    /// Execute a transition's bytecode with the given inputs.
169    ///
170    /// # Arguments
171    /// * `bytecode` - The validation script (e.g., AluVM bytecode)
172    /// * `inputs` - The input state being consumed
173    /// * `signatures` - Authorizing signatures
174    ///
175    /// # Returns
176    /// The output state produced by execution.
177    fn execute(
178        &self,
179        bytecode: &[u8],
180        inputs: VMInputs,
181        signatures: &[Vec<u8>],
182    ) -> Result<VMOutputs, VMError>;
183
184    /// Validate that outputs are consistent with the schema.
185    ///
186    /// This is called after execution to ensure the VM
187    /// hasn't produced invalid state (e.g., negative supply,
188    /// undefined type IDs).
189    fn validate_outputs(&self, inputs: &VMInputs, outputs: &VMOutputs) -> Result<(), VMError>;
190}
191
192/// PassthroughVM: a stub implementation that passes inputs through as outputs.
193///
194/// This is used for testing the proof pipeline without a full VM.
195/// It validates that total input value >= total output value for
196/// each type ID (conservation of supply).
197pub struct PassthroughVM {
198    /// Maximum execution steps before loop detection triggers
199    pub max_steps: u64,
200}
201
202impl PassthroughVM {
203    /// Create a new PassthroughVM with the given step limit
204    pub fn new(max_steps: u64) -> Self {
205        Self { max_steps }
206    }
207}
208
209impl Default for PassthroughVM {
210    fn default() -> Self {
211        Self::new(1000)
212    }
213}
214
215impl DeterministicVM for PassthroughVM {
216    fn execute(
217        &self,
218        _bytecode: &[u8],
219        inputs: VMInputs,
220        _signatures: &[Vec<u8>],
221    ) -> Result<VMOutputs, VMError> {
222        // Simulate execution: pass inputs through as outputs
223        // In a real VM, this would execute the bytecode
224
225        // Check step limit (simulated)
226        let steps = inputs.owned_inputs.len() as u64 + 1;
227        if steps > self.max_steps {
228            return Err(VMError::ExecutionLimitExceeded {
229                max_steps: self.max_steps,
230                actual_steps: steps,
231            });
232        }
233
234        // Owned outputs mirror the input owned states
235        let owned_outputs: Vec<StateAssignment> = inputs
236            .owned_inputs
237            .iter()
238            .map(|state| {
239                StateAssignment::new(
240                    state.type_id,
241                    state.seal.clone(), // Same seal (pass-through)
242                    state.data.clone(),
243                )
244            })
245            .collect();
246
247        Ok(VMOutputs::new(
248            owned_outputs,
249            Vec::new(), // No global updates
250            inputs.metadata.clone(),
251            None, // No next seal
252        ))
253    }
254
255    fn validate_outputs(&self, inputs: &VMInputs, outputs: &VMOutputs) -> Result<(), VMError> {
256        // Conservation of supply: output total <= input total for each type
257        let mut input_totals: BTreeMap<StateTypeId, u64> = BTreeMap::new();
258        for state in &inputs.owned_inputs {
259            let value = decode_integer(&state.data).unwrap_or(0);
260            *input_totals.entry(state.type_id).or_insert(0) += value;
261        }
262        // Add input global state
263        for state in &inputs.global_state {
264            let value = decode_integer(&state.data).unwrap_or(0);
265            *input_totals.entry(state.type_id).or_insert(0) += value;
266        }
267
268        let output_totals = outputs.total_by_type();
269
270        for (type_id, output_total) in &output_totals {
271            let input_total = input_totals.get(type_id).copied().unwrap_or(0);
272            if *output_total > input_total {
273                return Err(VMError::InconsistentOutput(format!(
274                    "Output total {} exceeds input total {} for type {}",
275                    output_total, input_total, type_id
276                )));
277            }
278        }
279
280        Ok(())
281    }
282}
283
284/// Decode a state data value as a u64 (little-endian)
285fn decode_integer(data: &[u8]) -> Option<u64> {
286    if data.len() < 8 {
287        return None;
288    }
289    let mut bytes = [0u8; 8];
290    bytes.copy_from_slice(&data[..8]);
291    Some(u64::from_le_bytes(bytes))
292}
293
294/// Execute a transition through the VM and validate
295///
296/// This is the primary entry point for consignment validation.
297pub fn execute_transition(
298    vm: &impl DeterministicVM,
299    bytecode: &[u8],
300    inputs: VMInputs,
301    signatures: &[Vec<u8>],
302) -> Result<VMOutputs, VMError> {
303    let outputs = vm.execute(bytecode, inputs.clone(), signatures)?;
304    vm.validate_outputs(&inputs, &outputs)?;
305    Ok(outputs)
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::Hash;
312
313    fn test_inputs() -> VMInputs {
314        VMInputs::new(
315            vec![OwnedState::from_hash(
316                10,
317                SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
318                Hash::new([1u8; 32]),
319            )],
320            vec![GlobalState::from_hash(1, Hash::new([100u8; 32]))],
321            vec![Metadata::from_string("memo", "test")],
322            vec![0x01, 0x02, 0x03],
323        )
324    }
325
326    // ─────────────────────────────────────────────
327    // VMInputs tests
328    // ─────────────────────────────────────────────
329
330    #[test]
331    fn test_vm_inputs_creation() {
332        let inputs = test_inputs();
333        assert_eq!(inputs.owned_inputs.len(), 1);
334        assert_eq!(inputs.global_state.len(), 1);
335    }
336
337    #[test]
338    fn test_vm_inputs_global_state_lookup() {
339        let inputs = test_inputs();
340        let states = inputs.global_state_of(1);
341        assert_eq!(states.len(), 1);
342        assert!(inputs.global_state_of(99).is_empty());
343    }
344
345    #[test]
346    fn test_vm_inputs_owned_state_lookup() {
347        let inputs = test_inputs();
348        let states = inputs.owned_state_of(10);
349        assert_eq!(states.len(), 1);
350        assert!(inputs.owned_state_of(99).is_empty());
351    }
352
353    // ─────────────────────────────────────────────
354    // VMOutputs tests
355    // ─────────────────────────────────────────────
356
357    #[test]
358    fn test_vm_outputs_creation() {
359        let outputs = VMOutputs::new(
360            vec![StateAssignment::new(
361                10,
362                SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
363                1000u64.to_le_bytes().to_vec(),
364            )],
365            vec![GlobalState::from_hash(1, Hash::new([100u8; 32]))],
366            vec![],
367            None,
368        );
369        assert_eq!(outputs.owned_outputs.len(), 1);
370    }
371
372    #[test]
373    fn test_vm_outputs_total_by_type() {
374        let outputs = VMOutputs::new(
375            vec![
376                StateAssignment::new(
377                    10,
378                    SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
379                    600u64.to_le_bytes().to_vec(),
380                ),
381                StateAssignment::new(
382                    10,
383                    SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
384                    400u64.to_le_bytes().to_vec(),
385                ),
386            ],
387            vec![],
388            vec![],
389            None,
390        );
391        let totals = outputs.total_by_type();
392        assert_eq!(totals.get(&10), Some(&1000));
393    }
394
395    // ─────────────────────────────────────────────
396    // PassthroughVM tests
397    // ─────────────────────────────────────────────
398
399    #[test]
400    fn test_passthrough_vm_basic() {
401        let vm = PassthroughVM::default();
402        let inputs = test_inputs();
403        let outputs = vm.execute(&[0x01], inputs.clone(), &[]).unwrap();
404        assert_eq!(outputs.owned_outputs.len(), inputs.owned_inputs.len());
405    }
406
407    #[test]
408    fn test_passthrough_vm_validation_conservation() {
409        let vm = PassthroughVM::default();
410        let inputs = test_inputs();
411        let outputs = vm.execute(&[0x01], inputs.clone(), &[]).unwrap();
412        vm.validate_outputs(&inputs, &outputs).unwrap();
413    }
414
415    #[test]
416    fn test_passthrough_vm_execution_limit() {
417        let vm = PassthroughVM::new(0); // Zero steps allowed
418        let inputs = test_inputs();
419        let result = vm.execute(&[0x01], inputs, &[]);
420        assert!(result.is_err());
421        assert!(matches!(
422            result.unwrap_err(),
423            VMError::ExecutionLimitExceeded { .. }
424        ));
425    }
426
427    #[test]
428    fn test_execute_transition_valid() {
429        let vm = PassthroughVM::default();
430        let inputs = test_inputs();
431        let outputs = execute_transition(&vm, &[0x01], inputs.clone(), &[]).unwrap();
432        assert_eq!(outputs.owned_outputs.len(), 1);
433    }
434
435    // ─────────────────────────────────────────────
436    // decode_integer tests
437    // ─────────────────────────────────────────────
438
439    #[test]
440    fn test_decode_integer_valid() {
441        let bytes = 1000u64.to_le_bytes();
442        assert_eq!(decode_integer(&bytes), Some(1000));
443    }
444
445    #[test]
446    fn test_decode_integer_too_short() {
447        assert_eq!(decode_integer(&[1, 2, 3]), None);
448    }
449
450    #[test]
451    fn test_decode_integer_extra_bytes() {
452        let mut bytes = 42u64.to_le_bytes().to_vec();
453        bytes.push(0xFF);
454        assert_eq!(decode_integer(&bytes), Some(42));
455    }
456
457    // ─────────────────────────────────────────────
458    // VMError display tests
459    // ─────────────────────────────────────────────
460
461    #[test]
462    fn test_vm_error_display() {
463        let err = VMError::InvalidBytecode("bad opcode".to_string());
464        assert!(err.to_string().contains("Invalid bytecode"));
465
466        let err = VMError::ExecutionLimitExceeded {
467            max_steps: 100,
468            actual_steps: 200,
469        };
470        assert!(err.to_string().contains("200"));
471
472        let err = VMError::SealReplay {
473            seal: SealRef::new(vec![1], Some(1)).unwrap(),
474        };
475        assert!(err.to_string().contains("replay"));
476    }
477
478    // ─────────────────────────────────────────────
479    // Integration: full transition through VM
480    // ─────────────────────────────────────────────
481
482    #[test]
483    fn test_full_transition_with_multiple_inputs() {
484        let vm = PassthroughVM::new(100);
485
486        let inputs = VMInputs::new(
487            vec![
488                OwnedState::from_hash(
489                    10,
490                    SealRef::new(vec![0xAA; 16], Some(1)).unwrap(),
491                    Hash::new([1u8; 32]),
492                ),
493                OwnedState::from_hash(
494                    10,
495                    SealRef::new(vec![0xBB; 16], Some(2)).unwrap(),
496                    Hash::new([2u8; 32]),
497                ),
498            ],
499            vec![GlobalState::from_hash(1, Hash::new([100u8; 32]))],
500            vec![Metadata::from_string("memo", "multi-input transition")],
501            vec![0xDE, 0xAD],
502        );
503
504        let outputs = execute_transition(&vm, &[0x01, 0x02], inputs, &[]).unwrap();
505        assert_eq!(outputs.owned_outputs.len(), 2);
506    }
507}