Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions simulator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -272,7 +273,10 @@ fn check_signature_verification_mocks(
}
}

fn categorize_events(events: &soroban_env_host::events::Events) -> Vec<CategorizedEvent> {
fn categorize_events(
events: &soroban_env_host::events::Events,
wasm_module: Option<&vm::WasmModule>,
) -> Vec<CategorizedEvent> {
events
.0
.iter()
Expand All @@ -298,6 +302,10 @@ fn categorize_events(events: &soroban_env_host::events::Events) -> Vec<Categoriz
};

let wasm_instruction = extract_wasm_instruction(&topics, &data);
let wasm_location = wasm_instruction.as_ref().and_then(|_instr| {
let offset = extract_wasm_offset(&data);
offset.and_then(|off| wasm_module.and_then(|m| m.resolve_location(off)))
});
CategorizedEvent {
category,
event: DiagnosticEvent {
Expand All @@ -314,6 +322,7 @@ fn categorize_events(events: &soroban_env_host::events::Events) -> Vec<Categoriz
topics,
data,
wasm_instruction,
wasm_location,
in_successful_contract_call: !e.failed_call,
},
}
Expand Down Expand Up @@ -357,6 +366,7 @@ fn main() {
stack_trace: None,
wasm_offset: None,
linear_memory_dump: None,
snapshots: None,
};
if let Ok(json) = serde_json::to_string(&res) {
println!("{}", json);
Expand Down Expand Up @@ -388,6 +398,7 @@ fn main() {
stack_trace: None,
wasm_offset: None,
linear_memory_dump: None,
snapshots: None,
};
println!(
"{}",
Expand Down Expand Up @@ -447,29 +458,29 @@ fn main() {
}
};

// Initialize source mapper if WASM is provided
let source_mapper = if let Some(wasm_base64) = &request.contract_wasm {
// Initialize source mapper and WASM module inspector if WASM is provided
let (source_mapper, wasm_module) = if let Some(wasm_base64) = &request.contract_wasm {
match base64::engine::general_purpose::STANDARD.decode(wasm_base64) {
Ok(wasm_bytes) => {
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
Expand Down Expand Up @@ -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();
Expand All @@ -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<StateSnapshot> = 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),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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![],
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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");
Expand Down
17 changes: 17 additions & 0 deletions simulator/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ pub struct SimulationResponse {
pub wasm_offset: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub linear_memory_dump: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub snapshots: Option<Vec<StateSnapshot>>,
}

#[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<WasmLocation>,
}

#[derive(Debug, Serialize)]
Expand All @@ -88,6 +103,8 @@ pub struct DiagnosticEvent {
pub in_successful_contract_call: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub wasm_instruction: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wasm_location: Option<WasmLocation>,
}

#[derive(Debug, Serialize)]
Expand Down
91 changes: 82 additions & 9 deletions simulator/src/vm.rs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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<u8>,
function_names: std::collections::HashMap<u32, String>,
}

impl WasmModule {
pub fn new(wasm: Vec<u8>) -> 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<u32> {
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<WasmLocation> {
self.find_function_at_offset(offset).map(|idx| WasmLocation {
function: self.get_function_name(idx),
offset,
})
}
}
Loading