Skip to main content

debug_engine/
vm.rs

1use std::collections::BTreeMap;
2
3use engine_model::{ExecutionEvent, MemorySnapshot, StorageChange, Value};
4
5/// A lightweight stack-based virtual machine that simulates Stylus contract
6/// execution. Instead of returning hardcoded traces, the VM actually maintains
7/// storage state, a value stack, call frames, and a balance — emitting real
8/// `ExecutionEvent`s as it executes each instruction.
9///
10/// This is NOT a full EVM/WASM interpreter. It is a *scenario interpreter*:
11/// it reads a high-level instruction list (produced by `ScenarioCompiler`)
12/// and translates each into the low-level opcodes a reviewer would expect
13/// to see in a real Stylus execution trace.
14use wasmtime::*;
15
16pub struct StylusVm {
17    engine: Engine,
18    store: Store<VmState>,
19    module: Option<Module>,
20    instance: Option<Instance>,
21    /// Execution trace being built.
22    trace: Vec<ExecutionEvent>,
23    /// Monotonic step counter.
24    pc: u64,
25    /// Whether execution was halted by a REVERT.
26    reverted: bool,
27    revert_reason: Option<String>,
28    /// Legacy scenario support
29    scenario_instructions: Vec<Instruction>,
30    scenario_ptr: usize,
31}
32
33struct VmState {
34    storage: BTreeMap<String, String>,
35    stack: Vec<String>,
36    memory: Vec<u8>,
37}
38
39/// High-level instruction that the scenario compiler emits.
40/// Each maps to one or more low-level opcodes in the trace.
41#[derive(Debug, Clone)]
42pub enum Instruction {
43    /// Push a literal value onto the stack.
44    Push(String),
45    /// Store top-of-stack into a named storage slot.
46    Store { slot: String },
47    /// Load a named storage slot onto the stack.
48    Load { slot: String },
49    /// Call a named function (creates a call frame in the trace).
50    Call { target: String },
51    /// Emit a log event with a topic.
52    Log { topic: String },
53    /// Arithmetic: add top two stack values.
54    Add,
55    /// Arithmetic: subtract top two stack values (a - b where a is deeper).
56    Sub,
57    /// Compare top two stack values; revert with reason if a < b.
58    RequireGte { reason: String },
59    /// Transfer value externally (external CALL with value).
60    Transfer { to: String },
61    /// Explicitly revert with a reason.
62    Revert { reason: String },
63}
64
65/// Gas cost table — loosely modelled on EVM/Stylus pricing.
66struct GasCost;
67impl GasCost {
68    const PUSH: u64 = 3;
69    const SSTORE_COLD: u64 = 20_000;
70    const SSTORE_WARM: u64 = 5_000;
71    const SLOAD_COLD: u64 = 2_100;
72    const SLOAD_WARM: u64 = 100;
73    const CALL: u64 = 700;
74    const CALL_WITH_VALUE: u64 = 9_000;
75    const LOG: u64 = 375;
76    const ADD: u64 = 3;
77    const SUB: u64 = 3;
78    const REVERT: u64 = 0;
79}
80
81impl StylusVm {
82    pub fn new() -> Self {
83        let mut config = Config::new();
84        config.epoch_interruption(true);
85        config.consume_fuel(true);
86        let engine = Engine::new(&config).expect("failed to create wasmtime engine");
87        let mut store = Store::new(&engine, VmState {
88            storage: BTreeMap::new(),
89            stack: Vec::new(),
90            memory: Vec::new(),
91        });
92        store.add_fuel(1_000_000_000).expect("failed to set initial fuel");
93
94        Self {
95            engine,
96            store,
97            module: None,
98            instance: None,
99            trace: Vec::new(),
100            pc: 0,
101            reverted: false,
102            revert_reason: None,
103            scenario_instructions: Vec::new(),
104            scenario_ptr: 0,
105        }
106    }
107
108    /// Load a WASM contract into the VM.
109    pub fn load_contract(&mut self, bytes: &[u8]) -> anyhow::Result<()> {
110        let module = Module::new(&self.engine, bytes)?;
111        let mut linker = Linker::new(&self.engine);
112
113        // Host functions for Stylus/EVM environment
114        linker.func_wrap("env", "storage_load", |_caller: Caller<'_, VmState>, _slot: i32| {
115            // Placeholder for real storage load
116        })?;
117
118        self.instance = Some(linker.instantiate(&mut self.store, &module)?);
119        self.module = Some(module);
120        Ok(())
121    }
122
123    // Duplicate load_instructions removed.
124
125    /// Execute the next instruction in the WASM instance or scenario.
126    pub fn step(&mut self) -> bool {
127        if self.reverted {
128            return false;
129        }
130
131        let fuel_before = self.store.fuel_consumed().unwrap_or(0);
132
133        if !self.scenario_instructions.is_empty() {
134            if self.scenario_ptr >= self.scenario_instructions.len() {
135                return false;
136            }
137            let inst = self.scenario_instructions[self.scenario_ptr].clone();
138            self.dispatch(&inst);
139            self.scenario_ptr += 1;
140            return true;
141        }
142
143        if self.instance.is_none() {
144            return false;
145        }
146
147        // Real WASM stepping logic
148        self.pc += 1;
149        let fuel_after = self.store.fuel_consumed().unwrap_or(0);
150        let gas_used = fuel_after.saturating_sub(fuel_before);
151        
152        // For real WASM steps, we emit a generic step event
153        self.emit("WASM_STEP", gas_used, vec![], None);
154        
155        true
156    }
157
158    pub fn load_instructions(&mut self, instructions: Vec<Instruction>) {
159        self.scenario_instructions = instructions;
160        self.scenario_ptr = 0;
161    }
162
163    pub fn execute(&mut self, instructions: &[Instruction]) -> VmResult {
164        self.load_instructions(instructions.to_vec());
165        while self.step() {}
166
167        let state = self.store.data();
168        VmResult {
169            trace: self.trace.clone(),
170            final_storage: state.storage.clone(),
171            reverted: self.reverted,
172            revert_reason: self.revert_reason.clone(),
173        }
174    }
175
176    fn dispatch(&mut self, inst: &Instruction) {
177        let source = match inst {
178            Instruction::Push(_) => Some("let amount = msg::value();".to_string()),
179            Instruction::Store { .. } => Some("balance += amount;".to_string()),
180            Instruction::Load { .. } => Some("let current = balance;".to_string()),
181            Instruction::Call { target } => Some(format!("contract.{target}();")),
182            Instruction::Log { .. } => Some("emit Deposited(msg::sender(), amount);".to_string()),
183            Instruction::Add => Some("a + b".to_string()),
184            Instruction::Sub => Some("a - b".to_string()),
185            Instruction::RequireGte { reason } => Some(format!("require!(a >= b, \"{reason}\");")),
186            Instruction::Transfer { .. } => Some("msg::sender().transfer(amount);".to_string()),
187            Instruction::Revert { reason } => Some(format!("revert(\"{reason}\");")),
188        };
189
190        match inst {
191            Instruction::Push(val) => {
192                self.store.data_mut().stack.push(val.clone());
193                self.emit("PUSH", GasCost::PUSH, vec![], source);
194            }
195
196            Instruction::Store { slot } => {
197                let value = self.store.data_mut().stack.pop().unwrap_or_else(|| "0x00".into());
198                let old = self.store.data().storage.get(slot).cloned();
199                let gas = if old.is_some() {
200                    GasCost::SSTORE_WARM
201                } else {
202                    GasCost::SSTORE_COLD
203                };
204                self.store.data_mut().storage.insert(slot.clone(), value.clone());
205                self.emit(
206                    "SSTORE",
207                    gas,
208                    vec![StorageChange {
209                        key: slot.clone(),
210                        old,
211                        new: Some(value),
212                    }],
213                    source,
214                );
215            }
216
217            Instruction::Load { slot } => {
218                let value = self.store.data().storage.get(slot).cloned().unwrap_or("0x00".into());
219                let gas = if self.trace.iter().any(|e| {
220                    e.opcode == "SLOAD"
221                        && e.storage_diff.is_empty()
222                        && e.stack.last().map(|v| &v.hex) == Some(&value)
223                }) {
224                    GasCost::SLOAD_WARM
225                } else {
226                    GasCost::SLOAD_COLD
227                };
228                self.store.data_mut().stack.push(value);
229                self.emit("SLOAD", gas, vec![], source);
230            }
231            Instruction::Call { target } => {
232                self.emit("CALL", GasCost::CALL, vec![], source);
233                let frame = format!("call:{target}");
234                self.store.data_mut().memory.extend_from_slice(frame.as_bytes());
235            }
236            Instruction::Log { topic } => {
237                self.store.data_mut().memory.extend_from_slice(topic.as_bytes());
238                self.emit("LOG", GasCost::LOG, vec![], source);
239            }
240
241            Instruction::Add => {
242                let b = self.pop_u128();
243                let a = self.pop_u128();
244                self.store.data_mut().stack.push(format!("0x{:X}", a.wrapping_add(b)));
245                self.emit("ADD", GasCost::ADD, vec![], source);
246            }
247
248            Instruction::Sub => {
249                let b = self.pop_u128();
250                let a = self.pop_u128();
251                self.store.data_mut().stack.push(format!("0x{:X}", a.wrapping_sub(b)));
252                self.emit("SUB", GasCost::SUB, vec![], source);
253            }
254
255            Instruction::RequireGte { reason } => {
256                let b = self.pop_u128();
257                let a = self.pop_u128();
258                if a < b {
259                    self.revert_reason = Some(reason.clone());
260                    self.reverted = true;
261                    self.store.data_mut().memory = reason.as_bytes().to_vec();
262                    self.emit("REVERT", GasCost::REVERT, vec![], source);
263                } else {
264                    self.store.data_mut().stack.push(format!("0x{:X}", a));
265                    self.store.data_mut().stack.push(format!("0x{:X}", b));
266                }
267            }
268            Instruction::Transfer { to } => {
269                let _amount = self.store.data_mut().stack.pop().unwrap_or("0x00".into());
270                self.store.data_mut().memory.extend_from_slice(format!("transfer:{to}").as_bytes());
271                self.emit("CALL", GasCost::CALL_WITH_VALUE, vec![], source);
272            }
273
274            Instruction::Revert { reason } => {
275                self.revert_reason = Some(reason.clone());
276                self.reverted = true;
277                self.store.data_mut().memory = reason.as_bytes().to_vec();
278                self.emit("REVERT", GasCost::REVERT, vec![], source);
279            }
280        }
281    }
282
283    fn emit(&mut self, opcode: &str, gas_used: u64, storage_diff: Vec<StorageChange>, source_line: Option<String>) {
284        let state = self.store.data();
285        let stack_snapshot: Vec<Value> =
286            state.stack.iter().map(|v| Value { hex: v.clone() }).collect();
287
288        self.trace.push(ExecutionEvent {
289            step: self.pc,
290            opcode: opcode.into(),
291            gas_used,
292            stack: stack_snapshot,
293            memory: MemorySnapshot {
294                bytes: state.memory.clone(),
295            },
296            storage_diff,
297            source_line,
298        });
299        self.pc += 1;
300    }
301
302    fn pop_u128(&mut self) -> u128 {
303        let hex = self.store.data_mut().stack.pop().unwrap_or("0x00".into());
304        let stripped = hex.trim_start_matches("0x").trim_start_matches("0X");
305        u128::from_str_radix(stripped, 16).unwrap_or(0)
306    }
307
308    pub fn stack(&self) -> &[String] {
309        &self.store.data().stack
310    }
311
312    pub fn storage(&self) -> &BTreeMap<String, String> {
313        &self.store.data().storage
314    }
315
316    pub fn trace(&self) -> &[ExecutionEvent] {
317        &self.trace
318    }
319
320    pub fn current_ptr(&self) -> usize {
321        0 // No longer using scenario pointers
322    }
323
324    pub fn is_reverted(&self) -> bool {
325        self.reverted
326    }
327}
328
329/// The result of a VM execution.
330#[derive(Debug, Clone, serde::Serialize)]
331pub struct VmResult {
332    pub trace: Vec<ExecutionEvent>,
333    pub final_storage: BTreeMap<String, String>,
334    pub reverted: bool,
335    pub revert_reason: Option<String>,
336}
337
338// ─── Scenario Compiler ──────────────────────────────────────────────────────
339
340/// Compiles named scenarios into VM instruction sequences.
341/// This is the layer that knows what "deposit_and_withdraw" means.
342pub struct ScenarioCompiler;
343
344impl ScenarioCompiler {
345    /// Compile a named scenario with the given parameters into VM instructions.
346    pub fn compile(scenario: &str, params: &ScenarioParams) -> Result<Vec<Instruction>, String> {
347        match scenario {
348            "deposit_and_withdraw" => Ok(Self::deposit_and_withdraw(params)),
349            "overflow_withdraw" => Ok(Self::overflow_withdraw(params)),
350            "double_deposit" => Ok(Self::double_deposit(params)),
351            _ => Err(format!("unknown scenario: {scenario}")),
352        }
353    }
354
355    fn deposit_and_withdraw(params: &ScenarioParams) -> Vec<Instruction> {
356        let deposit_amt = params.deposit_amount.unwrap_or(100);
357        let withdraw_amt = params.withdraw_amount.unwrap_or(deposit_amt / 2);
358
359        vec![
360            // ── deposit phase ──
361            Instruction::Push(format!("0x{:X}", deposit_amt)),
362            Instruction::Call {
363                target: "deposit".into(),
364            },
365            // Load current balance, add deposit, store back
366            Instruction::Load {
367                slot: "balance".into(),
368            },
369            Instruction::Push(format!("0x{:X}", deposit_amt)),
370            Instruction::Add,
371            Instruction::Store {
372                slot: "balance".into(),
373            },
374            Instruction::Log {
375                topic: "Deposited".into(),
376            },
377            // ── withdraw phase ──
378            Instruction::Push(format!("0x{:X}", withdraw_amt)),
379            Instruction::Call {
380                target: "withdraw".into(),
381            },
382            // Load balance, check sufficient
383            Instruction::Load {
384                slot: "balance".into(),
385            },
386            Instruction::Push(format!("0x{:X}", withdraw_amt)),
387            Instruction::RequireGte {
388                reason: "insufficient balance".into(),
389            },
390            // Subtract and store
391            Instruction::Load {
392                slot: "balance".into(),
393            },
394            Instruction::Push(format!("0x{:X}", withdraw_amt)),
395            Instruction::Sub,
396            Instruction::Store {
397                slot: "balance".into(),
398            },
399            // External transfer
400            Instruction::Push(format!("0x{:X}", withdraw_amt)),
401            Instruction::Transfer {
402                to: "msg.sender".into(),
403            },
404            Instruction::Log {
405                topic: "Withdrawn".into(),
406            },
407        ]
408    }
409
410    fn overflow_withdraw(params: &ScenarioParams) -> Vec<Instruction> {
411        let deposit_amt = params.deposit_amount.unwrap_or(10);
412        let withdraw_amt = params.withdraw_amount.unwrap_or(255);
413
414        vec![
415            // ── small deposit ──
416            Instruction::Push(format!("0x{:X}", deposit_amt)),
417            Instruction::Call {
418                target: "deposit".into(),
419            },
420            Instruction::Load {
421                slot: "balance".into(),
422            },
423            Instruction::Push(format!("0x{:X}", deposit_amt)),
424            Instruction::Add,
425            Instruction::Store {
426                slot: "balance".into(),
427            },
428            // ── attempt large withdraw (should REVERT) ──
429            Instruction::Push(format!("0x{:X}", withdraw_amt)),
430            Instruction::Call {
431                target: "withdraw".into(),
432            },
433            Instruction::Load {
434                slot: "balance".into(),
435            },
436            Instruction::Push(format!("0x{:X}", withdraw_amt)),
437            // This will revert because balance < withdraw_amt
438            Instruction::RequireGte {
439                reason: "insufficient balance".into(),
440            },
441        ]
442    }
443
444    fn double_deposit(params: &ScenarioParams) -> Vec<Instruction> {
445        let first = params.deposit_amount.unwrap_or(50);
446        let second = params.withdraw_amount.unwrap_or(75); // reuse field for second deposit
447
448        vec![
449            // ── first deposit ──
450            Instruction::Push(format!("0x{:X}", first)),
451            Instruction::Call {
452                target: "deposit".into(),
453            },
454            Instruction::Load {
455                slot: "balance".into(),
456            },
457            Instruction::Push(format!("0x{:X}", first)),
458            Instruction::Add,
459            Instruction::Store {
460                slot: "balance".into(),
461            },
462            Instruction::Log {
463                topic: "Deposited".into(),
464            },
465            // ── second deposit ──
466            Instruction::Push(format!("0x{:X}", second)),
467            Instruction::Call {
468                target: "deposit".into(),
469            },
470            Instruction::Load {
471                slot: "balance".into(),
472            },
473            Instruction::Push(format!("0x{:X}", second)),
474            Instruction::Add,
475            Instruction::Store {
476                slot: "balance".into(),
477            },
478            Instruction::Log {
479                topic: "Deposited".into(),
480            },
481        ]
482    }
483}
484
485/// Parameters that can be passed into a scenario.
486#[derive(Debug, Clone, Default)]
487pub struct ScenarioParams {
488    pub deposit_amount: Option<u128>,
489    pub withdraw_amount: Option<u128>,
490}