diff --git a/simulator/src/main.rs b/simulator/src/main.rs index 54ac7fd1..12e36d7c 100644 --- a/simulator/src/main.rs +++ b/simulator/src/main.rs @@ -72,6 +72,7 @@ fn send_error(msg: String) { stack_trace: Some(trace), wasm_offset: None, linear_memory_dump: None, + snapshots: None, }; if let Ok(json) = serde_json::to_string(&res) { println!("{}", json); @@ -272,7 +273,10 @@ fn check_signature_verification_mocks( } } -fn categorize_events(events: &soroban_env_host::events::Events) -> Vec { +fn categorize_events( + events: &soroban_env_host::events::Events, + wasm_module: Option<&vm::WasmModule>, +) -> Vec { events .0 .iter() @@ -298,6 +302,10 @@ fn categorize_events(events: &soroban_env_host::events::Events) -> Vec Vec { if let Err(e) = vm::enforce_soroban_compatibility(&wasm_bytes) { return send_error(format!("Strict VM enforcement failed: {}", e)); } - let mapper = SourceMapper::new_with_options(wasm_bytes, request.no_cache); + let mapper = SourceMapper::new_with_options(wasm_bytes.clone(), request.no_cache); + let module = vm::WasmModule::new(wasm_bytes); if mapper.has_debug_symbols() { eprintln!("Debug symbols found in WASM"); - Some(mapper) } else { eprintln!("No debug symbols found in WASM"); - None } + (Some(mapper), Some(module)) } Err(e) => { eprintln!("Failed to decode WASM base64: {e}"); - None + (None, None) } } } else { - None + (None, None) }; // Initialize Host @@ -667,8 +678,9 @@ fn main() { contract_id, topics, data, - in_successful_contract_call: !event.failed_call, wasm_instruction, + wasm_location: None, + in_successful_contract_call: !event.failed_call, } }) .collect(); @@ -681,10 +693,24 @@ fn main() { }; categorized_events = match host.get_events() { - Ok(evs) => categorize_events(&evs), + Ok(evs) => categorize_events(&evs, wasm_module.as_ref()), Err(_) => vec![], }; + let snapshots: Vec = categorized_events + .iter() + .filter(|e| e.event.wasm_location.is_some()) + .enumerate() + .map(|(i, e)| StateSnapshot { + step: i, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64, + location: e.event.wasm_location.clone(), + }) + .collect(); + let mut final_logs = vec![ format!("Host Initialized with Budget: {:?}", budget), format!("Loaded {} Ledger Entries", loaded_entries_count), @@ -731,10 +757,15 @@ fn main() { flamegraph: flamegraph_svg, optimization_report, budget_usage: Some(budget_usage), - source_location: None, stack_trace: None, wasm_offset: None, linear_memory_dump: None, + snapshots: if snapshots.is_empty() { + None + } else { + Some(snapshots.clone()) + }, + source_location: None, }; if let Ok(json) = serde_json::to_string(&response) { @@ -768,6 +799,11 @@ fn main() { .as_ref() .and_then(|m| m.map_wasm_offset_to_source(0)), linear_memory_dump: None, + snapshots: if snapshots.is_empty() { + None + } else { + Some(snapshots) + }, }; if let Ok(json) = serde_json::to_string(&response) { @@ -809,7 +845,7 @@ fn main() { ]; let _categorized_events = match host.get_events() { - Ok(evs) => categorize_events(&evs), + Ok(evs) => categorize_events(&evs, wasm_module.as_ref()), Err(_) => vec![], }; @@ -900,6 +936,7 @@ fn main() { stack_trace: Some(wasm_trace), wasm_offset, linear_memory_dump: None, + snapshots: None, }; if let Ok(json) = serde_json::to_string(&response) { println!("{}", json); @@ -945,6 +982,7 @@ fn main() { stack_trace: Some(wasm_trace), wasm_offset: None, linear_memory_dump: None, + snapshots: None, }; if let Ok(json) = serde_json::to_string(&response) { println!("{}", json); @@ -1217,7 +1255,7 @@ mod tests { // failed_call = true → in_successful_contract_call must be false let evs_failed = Events(vec![make_event(true)]); - let categorized = categorize_events(&evs_failed); + let categorized = categorize_events(&evs_failed, None); assert_eq!(categorized.len(), 1); assert!( !categorized[0].event.in_successful_contract_call, @@ -1226,7 +1264,7 @@ mod tests { // failed_call = false → in_successful_contract_call must be true let evs_ok = Events(vec![make_event(false)]); - let categorized = categorize_events(&evs_ok); + let categorized = categorize_events(&evs_ok, None); assert_eq!(categorized.len(), 1); assert!( categorized[0].event.in_successful_contract_call, @@ -1263,7 +1301,7 @@ mod tests { make_typed_event(ContractEventType::Diagnostic), ]); - let cats = categorize_events(&evs); + let cats = categorize_events(&evs, None); assert_eq!(cats[0].category, "Contract"); assert_eq!(cats[1].category, "System"); assert_eq!(cats[2].category, "Diagnostic"); diff --git a/simulator/src/types.rs b/simulator/src/types.rs index b72ec697..acc45948 100644 --- a/simulator/src/types.rs +++ b/simulator/src/types.rs @@ -77,6 +77,21 @@ pub struct SimulationResponse { pub wasm_offset: Option, #[serde(skip_serializing_if = "Option::is_none")] pub linear_memory_dump: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshots: Option>, +} + +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct WasmLocation { + pub function: String, + pub offset: u64, +} + +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct StateSnapshot { + pub step: usize, + pub timestamp: i64, + pub location: Option, } #[derive(Debug, Serialize)] @@ -88,6 +103,8 @@ pub struct DiagnosticEvent { pub in_successful_contract_call: bool, #[serde(skip_serializing_if = "Option::is_none")] pub wasm_instruction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub wasm_location: Option, } #[derive(Debug, Serialize)] diff --git a/simulator/src/vm.rs b/simulator/src/vm.rs index ccbd6a5a..21069bc2 100644 --- a/simulator/src/vm.rs +++ b/simulator/src/vm.rs @@ -1,7 +1,8 @@ // Copyright 2026 Erst Users // SPDX-License-Identifier: Apache-2.0 -use wasmparser::{Operator, Parser, Payload}; +use crate::types::{WasmLocation}; +use wasmparser::{NameSectionReader, Operator, Parser, Payload, BinaryReader, TypeRef, Import}; pub fn enforce_soroban_compatibility(wasm: &[u8]) -> Result<(), String> { for payload in Parser::new(0).parse_all(wasm) { @@ -29,14 +30,86 @@ pub fn enforce_soroban_compatibility(wasm: &[u8]) -> Result<(), String> { } fn is_float_op<'a>(op: &Operator<'a>) -> bool { - // Many of the `Operator` variants are prefixed with `F32` or `F64` when - // they perform floating-point operations. To avoid having to keep an - // exhaustive list in sync with whatever version of `wasmparser` is pulled - // in, simply look at the debug representation and check for the prefix. - // - // This is slightly less strict than matching individual variants, but it's - // good enough for our compatibility check: any float-related opcode will - // trigger the `starts_with` condition. let name = format!("{:?}", op); name.contains("F32") || name.contains("F64") } + +/// Helper to resolve function names and offsets from WASM bytes using wasmparser. +pub struct WasmModule { + wasm: Vec, + function_names: std::collections::HashMap, +} + +impl WasmModule { + pub fn new(wasm: Vec) -> Self { + let mut function_names = std::collections::HashMap::new(); + + // Parse name section to get function names + let parser = Parser::new(0); + for payload in parser.parse_all(&wasm) { + if let Ok(Payload::CustomSection(section)) = payload { + if section.name() == "name" { + let reader = NameSectionReader::new(BinaryReader::new(section.data(), section.range().start)); + for name in reader { + if let Ok(wasmparser::Name::Function(func_map)) = name { + for naming in func_map { + if let Ok(naming) = naming { + function_names.insert(naming.index, naming.name.to_string()); + } + } + } + } + } + } + } + + Self { wasm, function_names } + } + + pub fn get_function_name(&self, func_index: u32) -> String { + self.function_names + .get(&func_index) + .cloned() + .unwrap_or_else(|| format!("func[{}]", func_index)) + } + + /// Finds the function index containing the given offset. + pub fn find_function_at_offset(&self, offset: u64) -> Option { + let mut current_func_index = 0; + let parser = Parser::new(0); + for payload in parser.parse_all(&self.wasm) { + match payload { + Ok(Payload::ImportSection(reader)) => { + for import in reader { + if let Ok(imp) = import { + // Use Debug representation to identify function imports if field names vary + let debug = format!("{:?}", imp); + if debug.contains("Func") { + current_func_index += 1; + } + } + } + } + Ok(Payload::FunctionSection(_reader)) => { + // This section just gives us the count/sigs, actual code is in CodeSection + } + Ok(Payload::CodeSectionEntry(body)) => { + let range = body.range(); + if offset >= range.start as u64 && offset < range.end as u64 { + return Some(current_func_index); + } + current_func_index += 1; + } + _ => {} + } + } + None + } + + pub fn resolve_location(&self, offset: u64) -> Option { + self.find_function_at_offset(offset).map(|idx| WasmLocation { + function: self.get_function_name(idx), + offset, + }) + } +}