From b3257864fe224195d0927128b3527579e429fbd4 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 7 Mar 2024 13:14:48 -0800 Subject: [PATCH] Add an eager per-host ModuleCache, gate work on feature="next" --- soroban-env-host/Cargo.toml | 2 +- .../benches/common/cost_types/invoke.rs | 2 +- .../benches/common/cost_types/vm_ops.rs | 357 ++++++++---- .../common/cost_types/wasm_insn_exec.rs | 20 +- .../benches/common/experimental/vm_ops.rs | 4 +- soroban-env-host/benches/common/mod.rs | 32 +- soroban-env-host/src/budget.rs | 210 +++++-- soroban-env-host/src/budget/wasmi_helper.rs | 9 +- .../src/cost_runner/cost_types/vm_ops.rs | 173 ++++-- soroban-env-host/src/host.rs | 19 +- soroban-env-host/src/host/data_helper.rs | 29 +- soroban-env-host/src/host/frame.rs | 10 +- soroban-env-host/src/host/lifecycle.rs | 51 +- soroban-env-host/src/test/basic.rs | 2 +- soroban-env-host/src/test/e2e_tests.rs | 25 +- soroban-env-host/src/vm.rs | 546 +++-------------- soroban-env-host/src/vm/module_cache.rs | 97 +++ soroban-env-host/src/vm/parsed_module.rs | 550 ++++++++++++++++++ 18 files changed, 1415 insertions(+), 723 deletions(-) create mode 100644 soroban-env-host/src/vm/module_cache.rs create mode 100644 soroban-env-host/src/vm/parsed_module.rs diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml index 52e9c108f..67132ecda 100644 --- a/soroban-env-host/Cargo.toml +++ b/soroban-env-host/Cargo.toml @@ -8,7 +8,7 @@ license = "Apache-2.0" version.workspace = true readme = "../README.md" edition = "2021" -rust-version.workspace = true +rust-version = "1.74" build = "build.rs" exclude = ["observations/"] diff --git a/soroban-env-host/benches/common/cost_types/invoke.rs b/soroban-env-host/benches/common/cost_types/invoke.rs index 51f0b96ae..d26a92b48 100644 --- a/soroban-env-host/benches/common/cost_types/invoke.rs +++ b/soroban-env-host/benches/common/cost_types/invoke.rs @@ -28,7 +28,7 @@ impl HostCostMeasurement for InvokeVmFunctionMeasure { fn new_random_case(host: &Host, _rng: &mut StdRng, _input: u64) -> (Rc, Vec) { let id: Hash = [0; 32].into(); let code = wasm_module_with_empty_invoke(); - let vm = Vm::new(&host, id, &code, None).unwrap(); + let vm = Vm::new(&host, id, &code).unwrap(); let args = vec![Value::I64(0); MAX_VM_ARGS]; (vm, args) } diff --git a/soroban-env-host/benches/common/cost_types/vm_ops.rs b/soroban-env-host/benches/common/cost_types/vm_ops.rs index e31c0bfe7..45d9869a6 100644 --- a/soroban-env-host/benches/common/cost_types/vm_ops.rs +++ b/soroban-env-host/benches/common/cost_types/vm_ops.rs @@ -1,92 +1,110 @@ -#[allow(unused)] -use super::wasm_insn_exec::{ - wasm_module_with_n_data_segments, wasm_module_with_n_elem_segments, wasm_module_with_n_exports, - wasm_module_with_n_globals, wasm_module_with_n_imports, wasm_module_with_n_insns, - wasm_module_with_n_internal_funcs, wasm_module_with_n_table_entries, wasm_module_with_n_types, -}; +use super::wasm_insn_exec::wasm_module_with_n_internal_funcs; use crate::common::{util, HostCostMeasurement}; use rand::{rngs::StdRng, Rng}; use soroban_env_host::{ - cost_runner::{ - VmInstantiationDataSegmentsRun, VmInstantiationElemSegmentsRun, VmInstantiationExportsRun, - VmInstantiationFunctionsRun, VmInstantiationGlobalsRun, VmInstantiationImportsRun, - VmInstantiationInstructionsRun, VmInstantiationRun, VmInstantiationSample, - VmInstantiationTableEntriesRun, VmInstantiationTypesRun, - }, - xdr, Host, Vm, + cost_runner::{VmInstantiationRun, VmInstantiationSample}, + vm::{ParsedModule, VersionedContractCodeCostInputs}, + xdr, Host, }; +use std::rc::Rc; +// Protocol 20 coarse cost model. pub(crate) struct VmInstantiationMeasure; -pub(crate) struct VmInstantiationInstructionsMeasure; -pub(crate) struct VmInstantiationFunctionsMeasure; -pub(crate) struct VmInstantiationGlobalsMeasure; -pub(crate) struct VmInstantiationTableEntriesMeasure; -pub(crate) struct VmInstantiationTypesMeasure; -pub(crate) struct VmInstantiationDataSegmentsMeasure; -pub(crate) struct VmInstantiationElemSegmentsMeasure; -pub(crate) struct VmInstantiationImportsMeasure; -pub(crate) struct VmInstantiationExportsMeasure; -// This measures the cost of instantiating a host::Vm on a variety of possible -// wasm modules, of different sizes. The input value should be the size of the -// module, though for now we're just selecting modules from the fixed example -// repertoire. Costs should be linear. +// This measures the cost of parsing wasm and/or instantiating a host::Vm on a +// variety of possible wasm modules, of different sizes. macro_rules! impl_measurement_for_instantiation_cost_type { - ($RUNNER:ty, $MEASURE:ty, $BUILD:ident, $HAS_INPUTS:expr, $MAGNITUDE:expr) => { + ($RUNNER:ty, $MEASURE:ty, $BUILD:ident, $USE_REFINED_INPUTS:expr, $MAGNITUDE:expr) => { impl HostCostMeasurement for $MEASURE { type Runner = $RUNNER; fn new_best_case(_host: &Host, _rng: &mut StdRng) -> VmInstantiationSample { let id: xdr::Hash = [0; 32].into(); let wasm: Vec = soroban_test_wasms::ADD_I32.into(); + let cost_inputs = VersionedContractCodeCostInputs::V0 { + wasm_bytes: wasm.len(), + }; + let module = Rc::new( + ParsedModule::new_with_isolated_engine(_host, &wasm, cost_inputs.clone()) + .unwrap(), + ); VmInstantiationSample { id: Some(id), wasm, - cost_inputs: None, + module, } } - fn new_worst_case(host: &Host, _rng: &mut StdRng, input: u64) -> VmInstantiationSample { + fn new_worst_case( + _host: &Host, + _rng: &mut StdRng, + input: u64, + ) -> VmInstantiationSample { let id: xdr::Hash = [0; 32].into(); let n = (Self::INPUT_BASE_SIZE + input * $MAGNITUDE) as usize; let wasm = $BUILD(n); - let cost_inputs = if $HAS_INPUTS { - let vm = Vm::new(host, id.clone(), &wasm[..], None).unwrap(); - Some(vm.get_contract_code_cost_inputs()) - } else { - None + #[allow(unused_mut)] + let mut cost_inputs = VersionedContractCodeCostInputs::V0 { + wasm_bytes: wasm.len(), }; + #[cfg(feature = "next")] + if $USE_REFINED_INPUTS { + cost_inputs = VersionedContractCodeCostInputs::V1( + soroban_env_host::vm::ParsedModule::extract_refined_contract_cost_inputs( + _host, + &wasm[..], + ) + .unwrap(), + ) + } + let module = Rc::new( + ParsedModule::new_with_isolated_engine(_host, &wasm, cost_inputs.clone()) + .unwrap(), + ); VmInstantiationSample { id: Some(id), wasm, - cost_inputs, + module, } } fn new_random_case( - host: &Host, + _host: &Host, rng: &mut StdRng, _input: u64, ) -> VmInstantiationSample { let id: xdr::Hash = [0; 32].into(); let idx = rng.gen_range(0..=10) % util::TEST_WASMS.len(); let wasm: Vec = util::TEST_WASMS[idx].into(); - let cost_inputs = if $HAS_INPUTS { - let vm = Vm::new(host, id.clone(), &wasm[..], None).unwrap(); - Some(vm.get_contract_code_cost_inputs()) - } else { - None + #[allow(unused_mut)] + let mut cost_inputs = VersionedContractCodeCostInputs::V0 { + wasm_bytes: wasm.len(), }; + #[cfg(feature = "next")] + if $USE_REFINED_INPUTS { + cost_inputs = VersionedContractCodeCostInputs::V1( + soroban_env_host::vm::ParsedModule::extract_refined_contract_cost_inputs( + _host, + &wasm[..], + ) + .unwrap(), + ); + } + let module = Rc::new( + ParsedModule::new_with_isolated_engine(_host, &wasm, cost_inputs.clone()) + .unwrap(), + ); VmInstantiationSample { id: Some(id), wasm, - cost_inputs, + module, } } } }; } +// Protocol 20 coarse cost model impl_measurement_for_instantiation_cost_type!( VmInstantiationRun, VmInstantiationMeasure, @@ -95,66 +113,195 @@ impl_measurement_for_instantiation_cost_type!( 30 ); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationInstructionsRun, - VmInstantiationInstructionsMeasure, - wasm_module_with_n_insns, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationFunctionsRun, - VmInstantiationFunctionsMeasure, - wasm_module_with_n_internal_funcs, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationGlobalsRun, - VmInstantiationGlobalsMeasure, - wasm_module_with_n_globals, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationTableEntriesRun, - VmInstantiationTableEntriesMeasure, - wasm_module_with_n_table_entries, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationTypesRun, - VmInstantiationTypesMeasure, - wasm_module_with_n_types, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationDataSegmentsRun, - VmInstantiationDataSegmentsMeasure, - wasm_module_with_n_data_segments, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationElemSegmentsRun, - VmInstantiationElemSegmentsMeasure, - wasm_module_with_n_elem_segments, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationImportsRun, - VmInstantiationImportsMeasure, - wasm_module_with_n_imports, - true, - 30 -); -impl_measurement_for_instantiation_cost_type!( - VmInstantiationExportsRun, - VmInstantiationExportsMeasure, - wasm_module_with_n_exports, - true, - 30 -); +// Protocol 21 refined cost model. +#[cfg(feature = "next")] +pub(crate) use v21::*; +#[cfg(feature = "next")] +mod v21 { + use super::super::wasm_insn_exec::{ + wasm_module_with_n_data_segments, wasm_module_with_n_elem_segments, + wasm_module_with_n_exports, wasm_module_with_n_globals, wasm_module_with_n_imports, + wasm_module_with_n_insns, wasm_module_with_n_internal_funcs, + wasm_module_with_n_memory_pages, wasm_module_with_n_table_entries, + wasm_module_with_n_types, + }; + use super::*; + use soroban_env_host::{ + cost_runner::{ + InstantiateWasmDataSegmentsRun, InstantiateWasmElemSegmentsRun, + InstantiateWasmExportsRun, InstantiateWasmFunctionsRun, InstantiateWasmGlobalsRun, + InstantiateWasmImportsRun, InstantiateWasmInstructionsRun, + InstantiateWasmMemoryPagesRun, InstantiateWasmTableEntriesRun, InstantiateWasmTypesRun, + ParseWasmDataSegmentsRun, ParseWasmElemSegmentsRun, ParseWasmExportsRun, + ParseWasmFunctionsRun, ParseWasmGlobalsRun, ParseWasmImportsRun, + ParseWasmInstructionsRun, ParseWasmMemoryPagesRun, ParseWasmTableEntriesRun, + ParseWasmTypesRun, VmInstantiationSample, + }, + xdr, Host, + }; + + pub(crate) struct ParseWasmInstructionsMeasure; + pub(crate) struct ParseWasmFunctionsMeasure; + pub(crate) struct ParseWasmGlobalsMeasure; + pub(crate) struct ParseWasmTableEntriesMeasure; + pub(crate) struct ParseWasmTypesMeasure; + pub(crate) struct ParseWasmDataSegmentsMeasure; + pub(crate) struct ParseWasmElemSegmentsMeasure; + pub(crate) struct ParseWasmImportsMeasure; + pub(crate) struct ParseWasmExportsMeasure; + pub(crate) struct ParseWasmMemoryPagesMeasure; + + pub(crate) struct InstantiateWasmInstructionsMeasure; + pub(crate) struct InstantiateWasmFunctionsMeasure; + pub(crate) struct InstantiateWasmGlobalsMeasure; + pub(crate) struct InstantiateWasmTableEntriesMeasure; + pub(crate) struct InstantiateWasmTypesMeasure; + pub(crate) struct InstantiateWasmDataSegmentsMeasure; + pub(crate) struct InstantiateWasmElemSegmentsMeasure; + pub(crate) struct InstantiateWasmImportsMeasure; + pub(crate) struct InstantiateWasmExportsMeasure; + pub(crate) struct InstantiateWasmMemoryPagesMeasure; + + // Protocol 21 refined cost model + impl_measurement_for_instantiation_cost_type!( + ParseWasmInstructionsRun, + ParseWasmInstructionsMeasure, + wasm_module_with_n_insns, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmFunctionsRun, + ParseWasmFunctionsMeasure, + wasm_module_with_n_internal_funcs, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmGlobalsRun, + ParseWasmGlobalsMeasure, + wasm_module_with_n_globals, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmTableEntriesRun, + ParseWasmTableEntriesMeasure, + wasm_module_with_n_table_entries, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmTypesRun, + ParseWasmTypesMeasure, + wasm_module_with_n_types, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmDataSegmentsRun, + ParseWasmDataSegmentsMeasure, + wasm_module_with_n_data_segments, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmElemSegmentsRun, + ParseWasmElemSegmentsMeasure, + wasm_module_with_n_elem_segments, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmImportsRun, + ParseWasmImportsMeasure, + wasm_module_with_n_imports, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmExportsRun, + ParseWasmExportsMeasure, + wasm_module_with_n_exports, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + ParseWasmMemoryPagesRun, + ParseWasmMemoryPagesMeasure, + wasm_module_with_n_memory_pages, + true, + 30 + ); + + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmInstructionsRun, + InstantiateWasmInstructionsMeasure, + wasm_module_with_n_insns, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmFunctionsRun, + InstantiateWasmFunctionsMeasure, + wasm_module_with_n_internal_funcs, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmGlobalsRun, + InstantiateWasmGlobalsMeasure, + wasm_module_with_n_globals, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmTableEntriesRun, + InstantiateWasmTableEntriesMeasure, + wasm_module_with_n_table_entries, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmTypesRun, + InstantiateWasmTypesMeasure, + wasm_module_with_n_types, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmDataSegmentsRun, + InstantiateWasmDataSegmentsMeasure, + wasm_module_with_n_data_segments, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmElemSegmentsRun, + InstantiateWasmElemSegmentsMeasure, + wasm_module_with_n_elem_segments, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmImportsRun, + InstantiateWasmImportsMeasure, + wasm_module_with_n_imports, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmExportsRun, + InstantiateWasmExportsMeasure, + wasm_module_with_n_exports, + true, + 30 + ); + impl_measurement_for_instantiation_cost_type!( + InstantiateWasmMemoryPagesRun, + InstantiateWasmMemoryPagesMeasure, + wasm_module_with_n_memory_pages, + true, + 30 + ); +} diff --git a/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs b/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs index d54f4435b..14ab43a81 100644 --- a/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs +++ b/soroban-env-host/benches/common/cost_types/wasm_insn_exec.rs @@ -55,7 +55,7 @@ pub fn wasm_module_with_n_globals(n: usize) -> Vec { pub fn wasm_module_with_n_imports(n: usize) -> Vec { let mut me = ModEmitter::default(); let names = Vm::get_all_host_functions(); - for (module,name,arity) in names.iter().take(n) { + for (module, name, arity) in names.iter().take(n) { if *module == "t" { continue; } @@ -157,7 +157,7 @@ pub fn wasm_module_with_n_types(mut n: usize) -> Vec { } pub fn wasm_module_with_n_elem_segments(n: usize) -> Vec { - let me = ModEmitter::from_configs(1,n as u32); + let me = ModEmitter::from_configs(1, n as u32); let mut fe = me.func(Arity(0), 0); fe.push(Symbol::try_from_small_str("pass").unwrap()); let (mut me, f) = fe.finish(); @@ -179,6 +179,14 @@ pub fn wasm_module_with_n_data_segments(n: usize) -> Vec { me.finish() } +pub fn wasm_module_with_n_memory_pages(n: usize) -> Vec { + let mut me = ModEmitter::from_configs(n as u32, 0); + me.define_data_segment(0, vec![0xff; n * 0x10000]); + let mut fe = me.func(Arity(0), 0); + fe.push(Symbol::try_from_small_str("pass").unwrap()); + fe.finish_and_export("test").finish() +} + fn wasm_module_with_mem_grow(n_pages: usize) -> Vec { let mut fe = ModEmitter::default().func(Arity(0), 0); fe.push(Operand::Const32(n_pages as i32)); @@ -540,7 +548,7 @@ macro_rules! impl_wasm_insn_measure_with_baseline_trap { let insns = 1 + step * Self::STEP_SIZE; let id: Hash = [0; 32].into(); let module = $wasm_gen(insns, rng); - let vm = Vm::new(&host, id, &module.wasm, None).unwrap(); + let vm = Vm::new(&host, id, &module.wasm).unwrap(); WasmInsnSample { vm, insns, @@ -551,7 +559,7 @@ macro_rules! impl_wasm_insn_measure_with_baseline_trap { fn new_baseline_case(host: &Host, _rng: &mut StdRng) -> WasmInsnSample { let module = wasm_module_baseline_trap(); let id: Hash = [0; 32].into(); - let vm = Vm::new(&host, id, &module.wasm, None).unwrap(); + let vm = Vm::new(&host, id, &module.wasm).unwrap(); WasmInsnSample { vm, insns: 0, @@ -578,14 +586,14 @@ macro_rules! impl_wasm_insn_measure_with_baseline_pass { let insns = 1 + step * Self::STEP_SIZE $(* $grow / $shrink)?; let id: Hash = [0; 32].into(); let module = $wasm_gen(insns, rng); - let vm = Vm::new(&host, id, &module.wasm, None).unwrap(); + let vm = Vm::new(&host, id, &module.wasm).unwrap(); WasmInsnSample { vm, insns, overhead: module.overhead } } fn new_baseline_case(host: &Host, _rng: &mut StdRng) -> WasmInsnSample { let module = wasm_module_baseline_pass(); let id: Hash = [0; 32].into(); - let vm = Vm::new(&host, id, &module.wasm, None).unwrap(); + let vm = Vm::new(&host, id, &module.wasm).unwrap(); WasmInsnSample { vm, insns: 0, overhead: module.overhead } } diff --git a/soroban-env-host/benches/common/experimental/vm_ops.rs b/soroban-env-host/benches/common/experimental/vm_ops.rs index a970ec179..aaf042900 100644 --- a/soroban-env-host/benches/common/experimental/vm_ops.rs +++ b/soroban-env-host/benches/common/experimental/vm_ops.rs @@ -18,7 +18,7 @@ impl HostCostMeasurement for VmMemReadMeasure { let buf = vec![0; input as usize]; let id: xdr::Hash = [0; 32].into(); let code = soroban_test_wasms::ADD_I32; - let vm = Vm::new(&host, id, &code, None).unwrap(); + let vm = Vm::new(&host, id, &code).unwrap(); VmMemRunSample { vm, buf } } } @@ -36,7 +36,7 @@ impl HostCostMeasurement for VmMemWriteMeasure { rng.fill_bytes(buf.as_mut_slice()); let id: xdr::Hash = [0; 32].into(); let code = soroban_test_wasms::ADD_I32; - let vm = Vm::new(&host, id, &code, None).unwrap(); + let vm = Vm::new(&host, id, &code).unwrap(); VmMemRunSample { vm, buf } } } diff --git a/soroban-env-host/benches/common/mod.rs b/soroban-env-host/benches/common/mod.rs index 465edab84..5667e83bd 100644 --- a/soroban-env-host/benches/common/mod.rs +++ b/soroban-env-host/benches/common/mod.rs @@ -83,16 +83,30 @@ pub(crate) fn for_each_host_cost_measurement( call_bench::(&mut params)?; call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; - call_bench::(&mut params)?; + #[cfg(feature = "next")] + { + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + call_bench::(&mut params)?; + } // These three mem ones are derived analytically, we do not calibrate them typically if std::env::var("INCLUDE_ANALYTICAL_COSTTYPES").is_ok() { call_bench::(&mut params)?; diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs index e113056d9..7d23c6dc1 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -92,17 +92,47 @@ impl Default for BudgetTracker { ContractCostType::Int256Pow => (), ContractCostType::Int256Shift => (), ContractCostType::ChaCha20DrawBytes => init_input(), // number of random bytes to draw - ContractCostType::VmInstantiateUnknownBytes => init_input(), - ContractCostType::VmInstantiateInstructions => init_input(), - ContractCostType::VmInstantiateFunctions => init_input(), - ContractCostType::VmInstantiateGlobals => init_input(), - ContractCostType::VmInstantiateTableEntries => init_input(), - ContractCostType::VmInstantiateTypes => init_input(), - ContractCostType::VmInstantiateDataSegments => init_input(), - ContractCostType::VmInstantiateElemSegments => init_input(), - ContractCostType::VmInstantiateImports => init_input(), - ContractCostType::VmInstantiateExports => init_input(), - ContractCostType::VmInstantiateMemoryPages => init_input(), + + #[cfg(feature = "next")] + ContractCostType::ParseWasmInstructions => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmFunctions => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmGlobals => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmTableEntries => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmTypes => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmDataSegments => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmElemSegments => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmImports => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmExports => init_input(), + #[cfg(feature = "next")] + ContractCostType::ParseWasmMemoryPages => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmInstructions => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmFunctions => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmGlobals => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmTableEntries => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmTypes => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmDataSegments => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmElemSegments => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmImports => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmExports => init_input(), + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmMemoryPages => init_input(), } } mt @@ -379,47 +409,104 @@ impl Default for BudgetImpl { cpu.const_term = 1058; cpu.lin_term = ScaledU64(501); } - ContractCostType::VmInstantiateUnknownBytes => { + #[cfg(feature = "next")] + ContractCostType::ParseWasmInstructions => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmFunctions => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateInstructions => { + #[cfg(feature = "next")] + ContractCostType::ParseWasmGlobals => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmTableEntries => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmTypes => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmDataSegments => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmElemSegments => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmImports => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmExports => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmMemoryPages => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmInstructions => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateFunctions => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmFunctions => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateGlobals => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmGlobals => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateTableEntries => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmTableEntries => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateTypes => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmTypes => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateDataSegments => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmDataSegments => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateElemSegments => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmElemSegments => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateImports => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmImports => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateExports => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmExports => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateMemoryPages => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmMemoryPages => { cpu.const_term = 0; cpu.lin_term = ScaledU64(1); } @@ -526,47 +613,104 @@ impl Default for BudgetImpl { mem.const_term = 0; mem.lin_term = ScaledU64(0); } - ContractCostType::VmInstantiateUnknownBytes => { + #[cfg(feature = "next")] + ContractCostType::ParseWasmInstructions => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateInstructions => { + #[cfg(feature = "next")] + ContractCostType::ParseWasmFunctions => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmGlobals => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmTableEntries => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmTypes => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmDataSegments => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmElemSegments => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmImports => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmExports => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + #[cfg(feature = "next")] + ContractCostType::ParseWasmMemoryPages => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmInstructions => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateFunctions => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmFunctions => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateGlobals => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmGlobals => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateTableEntries => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmTableEntries => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateTypes => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmTypes => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateDataSegments => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmDataSegments => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateElemSegments => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmElemSegments => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateImports => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmImports => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateExports => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmExports => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } - ContractCostType::VmInstantiateMemoryPages => { + #[cfg(feature = "next")] + ContractCostType::InstantiateWasmMemoryPages => { mem.const_term = 0; mem.lin_term = ScaledU64(1); } diff --git a/soroban-env-host/src/budget/wasmi_helper.rs b/soroban-env-host/src/budget/wasmi_helper.rs index b0c94e379..b976d4c0e 100644 --- a/soroban-env-host/src/budget/wasmi_helper.rs +++ b/soroban-env-host/src/budget/wasmi_helper.rs @@ -1,5 +1,8 @@ use crate::{ - budget::AsBudget, host::error::TryBorrowOrErr, xdr::ContractCostType, Host, HostError, + budget::{AsBudget, Budget}, + host::error::TryBorrowOrErr, + xdr::ContractCostType, + Host, HostError, }; use wasmi::{errors, FuelConsumptionMode, FuelCosts, ResourceLimiter}; @@ -110,9 +113,9 @@ pub(crate) fn load_calibrated_fuel_costs() -> FuelCosts { fuel_costs } -pub(crate) fn get_wasmi_config(host: &Host) -> Result { +pub(crate) fn get_wasmi_config(budget: &Budget) -> Result { let mut config = wasmi::Config::default(); - let fuel_costs = host.as_budget().0.try_borrow_or_err()?.fuel_costs; + let fuel_costs = budget.0.try_borrow_or_err()?.fuel_costs; // Turn off most optional wasm features, leaving on some post-MVP features // commonly enabled by Rust and Clang. Make sure all unused features are diff --git a/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs b/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs index 0ea68bfee..b0a7a57bb 100644 --- a/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs +++ b/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs @@ -1,35 +1,18 @@ use crate::{ cost_runner::{CostRunner, CostType}, - xdr::Hash, - xdr::{ - ContractCodeCostInputs, - ContractCostType::{ - VmInstantiateDataSegments, VmInstantiateElemSegments, VmInstantiateExports, - VmInstantiateFunctions, VmInstantiateGlobals, VmInstantiateImports, - VmInstantiateInstructions, VmInstantiateTableEntries, VmInstantiateTypes, - VmInstantiation, - }, - }, + vm::ParsedModule, + xdr::{ContractCostType::VmInstantiation, Hash}, Vm, }; use std::{hint::black_box, rc::Rc}; pub struct VmInstantiationRun; -pub struct VmInstantiationInstructionsRun; -pub struct VmInstantiationFunctionsRun; -pub struct VmInstantiationGlobalsRun; -pub struct VmInstantiationTableEntriesRun; -pub struct VmInstantiationTypesRun; -pub struct VmInstantiationDataSegmentsRun; -pub struct VmInstantiationElemSegmentsRun; -pub struct VmInstantiationImportsRun; -pub struct VmInstantiationExportsRun; #[derive(Clone)] pub struct VmInstantiationSample { pub id: Option, pub wasm: Vec, - pub cost_inputs: Option, + pub module: Rc, } macro_rules! impl_costrunner_for_instantiation_cost_type { @@ -48,12 +31,17 @@ macro_rules! impl_costrunner_for_instantiation_cost_type { _iter: u64, sample: Self::SampleType, ) -> Self::RecycledType { + #[cfg(feature = "next")] let vm = black_box( - Vm::new( + Vm::from_parsed_module(host, sample.id.unwrap(), sample.module).unwrap(), + ); + #[cfg(not(feature = "next"))] + let vm = black_box( + Vm::new_with_cost_inputs( host, sample.id.unwrap(), &sample.wasm[..], - sample.cost_inputs.clone(), + sample.module.cost_inputs, ) .unwrap(), ); @@ -72,25 +60,124 @@ macro_rules! impl_costrunner_for_instantiation_cost_type { }; } +// Protocol 20 coarse cost model impl_costrunner_for_instantiation_cost_type!(VmInstantiationRun, VmInstantiation); -impl_costrunner_for_instantiation_cost_type!( - VmInstantiationInstructionsRun, - VmInstantiateInstructions -); -impl_costrunner_for_instantiation_cost_type!(VmInstantiationFunctionsRun, VmInstantiateFunctions); -impl_costrunner_for_instantiation_cost_type!(VmInstantiationGlobalsRun, VmInstantiateGlobals); -impl_costrunner_for_instantiation_cost_type!( - VmInstantiationTableEntriesRun, - VmInstantiateTableEntries -); -impl_costrunner_for_instantiation_cost_type!(VmInstantiationTypesRun, VmInstantiateTypes); -impl_costrunner_for_instantiation_cost_type!( - VmInstantiationDataSegmentsRun, - VmInstantiateDataSegments -); -impl_costrunner_for_instantiation_cost_type!( - VmInstantiationElemSegmentsRun, - VmInstantiateElemSegments -); -impl_costrunner_for_instantiation_cost_type!(VmInstantiationImportsRun, VmInstantiateImports); -impl_costrunner_for_instantiation_cost_type!(VmInstantiationExportsRun, VmInstantiateExports); + +// Protocol 21 refined cost model. +#[cfg(feature = "next")] +pub use v21::*; +#[cfg(feature = "next")] +mod v21 { + use super::*; + use crate::vm::ParsedModule; + use crate::xdr::ContractCostType::{ + InstantiateWasmDataSegments, InstantiateWasmElemSegments, InstantiateWasmExports, + InstantiateWasmFunctions, InstantiateWasmGlobals, InstantiateWasmImports, + InstantiateWasmInstructions, InstantiateWasmMemoryPages, InstantiateWasmTableEntries, + InstantiateWasmTypes, ParseWasmDataSegments, ParseWasmElemSegments, ParseWasmExports, + ParseWasmFunctions, ParseWasmGlobals, ParseWasmImports, ParseWasmInstructions, + ParseWasmMemoryPages, ParseWasmTableEntries, ParseWasmTypes, + }; + + macro_rules! impl_costrunner_for_parse_cost_type { + ($RUNNER:ty, $COST:ident) => { + impl CostRunner for $RUNNER { + const COST_TYPE: CostType = CostType::Contract($COST); + + const RUN_ITERATIONS: u64 = 10; + + type SampleType = VmInstantiationSample; + + type RecycledType = (Option>, Vec); + + fn run_iter( + host: &crate::Host, + _iter: u64, + sample: Self::SampleType, + ) -> Self::RecycledType { + let module = black_box(Rc::new( + ParsedModule::new( + host, + sample.module.module.engine(), + &sample.wasm[..], + sample.module.cost_inputs.clone(), + ) + .unwrap(), + )); + (Some(module), sample.wasm) + } + + fn run_baseline_iter( + host: &crate::Host, + _iter: u64, + sample: Self::SampleType, + ) -> Self::RecycledType { + black_box(host.charge_budget($COST, Some(0)).unwrap()); + black_box((None, sample.wasm)) + } + } + }; + } + + pub struct ParseWasmInstructionsRun; + pub struct ParseWasmFunctionsRun; + pub struct ParseWasmGlobalsRun; + pub struct ParseWasmTableEntriesRun; + pub struct ParseWasmTypesRun; + pub struct ParseWasmDataSegmentsRun; + pub struct ParseWasmElemSegmentsRun; + pub struct ParseWasmImportsRun; + pub struct ParseWasmExportsRun; + pub struct ParseWasmMemoryPagesRun; + + pub struct InstantiateWasmInstructionsRun; + pub struct InstantiateWasmFunctionsRun; + pub struct InstantiateWasmGlobalsRun; + pub struct InstantiateWasmTableEntriesRun; + pub struct InstantiateWasmTypesRun; + pub struct InstantiateWasmDataSegmentsRun; + pub struct InstantiateWasmElemSegmentsRun; + pub struct InstantiateWasmImportsRun; + pub struct InstantiateWasmExportsRun; + pub struct InstantiateWasmMemoryPagesRun; + + impl_costrunner_for_parse_cost_type!(ParseWasmInstructionsRun, ParseWasmInstructions); + impl_costrunner_for_parse_cost_type!(ParseWasmFunctionsRun, ParseWasmFunctions); + impl_costrunner_for_parse_cost_type!(ParseWasmGlobalsRun, ParseWasmGlobals); + impl_costrunner_for_parse_cost_type!(ParseWasmTableEntriesRun, ParseWasmTableEntries); + impl_costrunner_for_parse_cost_type!(ParseWasmTypesRun, ParseWasmTypes); + impl_costrunner_for_parse_cost_type!(ParseWasmDataSegmentsRun, ParseWasmDataSegments); + impl_costrunner_for_parse_cost_type!(ParseWasmElemSegmentsRun, ParseWasmElemSegments); + impl_costrunner_for_parse_cost_type!(ParseWasmImportsRun, ParseWasmImports); + impl_costrunner_for_parse_cost_type!(ParseWasmExportsRun, ParseWasmExports); + impl_costrunner_for_parse_cost_type!(ParseWasmMemoryPagesRun, ParseWasmMemoryPages); + + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmInstructionsRun, + InstantiateWasmInstructions + ); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmFunctionsRun, + InstantiateWasmFunctions + ); + impl_costrunner_for_instantiation_cost_type!(InstantiateWasmGlobalsRun, InstantiateWasmGlobals); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmTableEntriesRun, + InstantiateWasmTableEntries + ); + impl_costrunner_for_instantiation_cost_type!(InstantiateWasmTypesRun, InstantiateWasmTypes); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmDataSegmentsRun, + InstantiateWasmDataSegments + ); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmElemSegmentsRun, + InstantiateWasmElemSegments + ); + impl_costrunner_for_instantiation_cost_type!(InstantiateWasmImportsRun, InstantiateWasmImports); + impl_costrunner_for_instantiation_cost_type!(InstantiateWasmExportsRun, InstantiateWasmExports); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmMemoryPagesRun, + InstantiateWasmMemoryPages + ); +} diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index 33b8aecc4..a6a12020a 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -10,6 +10,7 @@ use crate::{ impl_wrapping_obj_to_num, num::*, storage::Storage, + vm::ModuleCache, xdr::{ int128_helpers, AccountId, Asset, ContractCostType, ContractEventType, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress, CreateContractArgs, Duration, Hash, @@ -91,6 +92,7 @@ pub struct CoverageScoreboard { #[derive(Clone, Default)] struct HostImpl { + module_cache: RefCell>, source_account: RefCell>, ledger: RefCell>, objects: RefCell>, @@ -198,7 +200,12 @@ macro_rules! impl_checked_borrow_helpers { } }; } - +impl_checked_borrow_helpers!( + module_cache, + Option, + try_borrow_module_cache, + try_borrow_module_cache_mut +); impl_checked_borrow_helpers!( source_account, Option, @@ -327,6 +334,7 @@ impl Host { #[cfg(all(not(target_family = "wasm"), feature = "tracy"))] let _client = tracy_client::Client::start(); Self(Rc::new(HostImpl { + module_cache: RefCell::new(None), source_account: RefCell::new(None), ledger: RefCell::new(None), objects: Default::default(), @@ -357,6 +365,15 @@ impl Host { })) } + pub fn maybe_add_module_cache(&self) -> Result<(), HostError> { + if cfg!(feature = "next") + && self.get_ledger_protocol_version()? >= ModuleCache::MIN_LEDGER_VERSION + { + *self.try_borrow_module_cache_mut()? = Some(ModuleCache::new(self)?); + } + Ok(()) + } + pub fn set_source_account(&self, source_account: AccountId) -> Result<(), HostError> { *self.try_borrow_source_account_mut()? = Some(source_account); Ok(()) diff --git a/soroban-env-host/src/host/data_helper.rs b/soroban-env-host/src/host/data_helper.rs index 2f5a496e8..a0bee492c 100644 --- a/soroban-env-host/src/host/data_helper.rs +++ b/soroban-env-host/src/host/data_helper.rs @@ -6,14 +6,14 @@ use crate::{ err, host::metered_clone::{MeteredAlloc, MeteredClone}, storage::{InstanceStorageMap, Storage}, + vm::VersionedContractCodeCostInputs, xdr::{ - AccountEntry, AccountId, Asset, BytesM, ContractCodeCostInputs, ContractCodeEntry, - ContractCodeEntryExt, ContractDataDurability, ContractDataEntry, ContractExecutable, - ContractIdPreimage, ExtensionPoint, Hash, HashIdPreimage, HashIdPreimageContractId, - LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, LedgerKeyAccount, - LedgerKeyContractCode, LedgerKeyContractData, LedgerKeyTrustLine, PublicKey, ScAddress, - ScContractInstance, ScErrorCode, ScErrorType, ScMap, ScVal, Signer, SignerKey, - ThresholdIndexes, TrustLineAsset, Uint256, + AccountEntry, AccountId, Asset, BytesM, ContractCodeEntry, ContractDataDurability, + ContractDataEntry, ContractExecutable, ContractIdPreimage, ExtensionPoint, Hash, + HashIdPreimage, HashIdPreimageContractId, LedgerEntry, LedgerEntryData, LedgerEntryExt, + LedgerKey, LedgerKeyAccount, LedgerKeyContractCode, LedgerKeyContractData, + LedgerKeyTrustLine, PublicKey, ScAddress, ScContractInstance, ScErrorCode, ScErrorType, + ScMap, ScVal, Signer, SignerKey, ThresholdIndexes, TrustLineAsset, Uint256, }, AddressObject, Env, Host, HostError, StorageType, U32Val, Val, }; @@ -126,7 +126,7 @@ impl Host { pub(crate) fn retrieve_wasm_from_storage( &self, wasm_hash: &Hash, - ) -> Result<(BytesM, Option), HostError> { + ) -> Result<(BytesM, VersionedContractCodeCostInputs), HostError> { let key = self.contract_code_ledger_key(wasm_hash)?; match &self .try_borrow_storage_mut()? @@ -136,9 +136,16 @@ impl Host { { LedgerEntryData::ContractCode(e) => { let code = e.code.metered_clone(self)?; - let costs = match &e.ext { - ContractCodeEntryExt::V0 => None, - ContractCodeEntryExt::V1(v1) => Some(v1.cost_inputs.clone()), + #[allow(unused_mut)] + let mut costs = VersionedContractCodeCostInputs::V0 { + wasm_bytes: code.len(), + }; + #[cfg(feature = "next")] + match &e.ext { + crate::xdr::ContractCodeEntryExt::V0 => (), + crate::xdr::ContractCodeEntryExt::V1(v1) => { + costs = VersionedContractCodeCostInputs::V1(v1.cost_inputs.clone()) + } }; Ok((code, costs)) } diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs index e2204af22..fea201386 100644 --- a/soroban-env-host/src/host/frame.rs +++ b/soroban-env-host/src/host/frame.rs @@ -640,8 +640,14 @@ impl Host { let args_vec = args.to_vec(); match &instance.executable { ContractExecutable::Wasm(wasm_hash) => { - let (code, costs) = self.retrieve_wasm_from_storage(&wasm_hash)?; - let vm = Vm::new(self, id.metered_clone(self)?, code.as_slice(), costs)?; + let contract_id = id.metered_clone(self)?; + let vm = if let Some(cache) = &*self.try_borrow_module_cache()? { + let module = cache.get_module(self, wasm_hash)?; + Vm::from_parsed_module(self, contract_id, module)? + } else { + let (code, costs) = self.retrieve_wasm_from_storage(&wasm_hash)?; + Vm::new_with_cost_inputs(self, contract_id, code.as_slice(), costs)? + }; let relative_objects = Vec::new(); self.with_frame( Frame::ContractVM { diff --git a/soroban-env-host/src/host/lifecycle.rs b/soroban-env-host/src/host/lifecycle.rs index ca02fec20..921c10200 100644 --- a/soroban-env-host/src/host/lifecycle.rs +++ b/soroban-env-host/src/host/lifecycle.rs @@ -5,13 +5,13 @@ use crate::{ metered_clone::{MeteredAlloc, MeteredClone}, metered_write_xdr, ContractReentryMode, CreateContractArgs, }, + vm::Vm, xdr::{ - Asset, ContractCodeCostInputs, ContractCodeEntry, ContractCodeEntryExt, - ContractCodeEntryV1, ContractDataDurability, ContractExecutable, ContractIdPreimage, + Asset, ContractCodeEntry, ContractDataDurability, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress, ExtensionPoint, Hash, LedgerKey, LedgerKeyContractCode, ScAddress, ScErrorCode, ScErrorType, }, - AddressObject, BytesObject, Host, HostError, Symbol, TryFromVal, Vm, + AddressObject, BytesObject, Host, HostError, Symbol, TryFromVal, }; use std::rc::Rc; @@ -165,7 +165,15 @@ impl Host { ) })?; - let cost_inputs: ContractCodeCostInputs; + // We're going to make a code entry with an ext field which, if zero, + // can be read as either a protocol v20 ExtensionPoint::V0 value, or a + // protocol v21 ContractCodeEntryExt::V0 value, depending on which + // protocol we were compiled with. + #[cfg(not(feature = "next"))] + let ext = ExtensionPoint::V0; + + #[cfg(feature = "next")] + let mut ext = crate::xdr::ContractCodeEntryExt::V0; // Instantiate a temporary / throwaway VM using this wasm. This will do // both quick checks like "does this wasm have the right protocol number @@ -179,26 +187,26 @@ impl Host { // Allow a zero-byte contract when testing, as this is used to make // native test contracts behave like wasm. They will never be // instantiated, this is just to exercise their storage logic. - cost_inputs = ContractCodeCostInputs { - n_instructions: 0, - n_functions: 0, - n_globals: 0, - n_table_entries: 0, - n_types: 0, - n_data_segments: 0, - n_elem_segments: 0, - n_imports: 0, - n_exports: 0, - n_memory_pages: 0, - }; } else { - let check_vm = Vm::new( + let _check_vm = Vm::new( self, Hash(hash_bytes.metered_clone(self)?), wasm_bytes_m.as_slice(), - None, )?; - cost_inputs = check_vm.get_contract_code_cost_inputs(); + #[cfg(feature = "next")] + if self.get_ledger_protocol_version()? >= super::ModuleCache::MIN_LEDGER_VERSION { + // At this point we do a secondary parse on what we've checked to be a valid + // module in order to extract a refined cost model, which we'll store in the + // code entry's ext field, for future parsing and instantiations. + _check_vm.module.cost_inputs.charge_for_parsing(self)?; + ext = crate::xdr::ContractCodeEntryExt::V1(crate::xdr::ContractCodeEntryV1 { + ext: ExtensionPoint::V0, + cost_inputs: crate::vm::ParsedModule::extract_refined_contract_cost_inputs( + self, + wasm_bytes_m.as_slice(), + )?, + }); + } } let hash_obj = self.add_host_object(self.scbytes_from_slice(hash_bytes.as_slice())?)?; @@ -216,10 +224,7 @@ impl Host { self.with_mut_storage(|storage| { let data = ContractCodeEntry { hash: Hash(hash_bytes), - ext: ContractCodeEntryExt::V1(ContractCodeEntryV1 { - ext: ExtensionPoint::V0, - cost_inputs, - }), + ext, code: wasm_bytes_m, }; storage.put( diff --git a/soroban-env-host/src/test/basic.rs b/soroban-env-host/src/test/basic.rs index f742a49ef..4428748a9 100644 --- a/soroban-env-host/src/test/basic.rs +++ b/soroban-env-host/src/test/basic.rs @@ -78,6 +78,6 @@ fn f32_does_not_work() -> Result<(), HostError> { use soroban_env_common::xdr::Hash; let host = observe_host!(Host::default()); let hash = Hash::from([0; 32]); - assert!(crate::vm::Vm::new(&host, hash, soroban_test_wasms::ADD_F32, None).is_err()); + assert!(crate::vm::Vm::new(&host, hash, soroban_test_wasms::ADD_F32).is_err()); Ok(()) } diff --git a/soroban-env-host/src/test/e2e_tests.rs b/soroban-env-host/src/test/e2e_tests.rs index fbfe8b726..932fa1e73 100644 --- a/soroban-env-host/src/test/e2e_tests.rs +++ b/soroban-env-host/src/test/e2e_tests.rs @@ -7,16 +7,16 @@ use crate::{ budget::Budget, e2e_invoke::{invoke_host_function, LedgerEntryChange}, xdr::{ - AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractCodeEntryExt, - ContractDataDurability, ContractDataEntry, ContractEvent, ContractExecutable, - ContractIdPreimage, ContractIdPreimageFromAddress, CreateContractArgs, DiagnosticEvent, - ExtensionPoint, HashIdPreimage, HashIdPreimageContractId, - HashIdPreimageSorobanAuthorization, HostFunction, InvokeContractArgs, LedgerEntry, - LedgerEntryData, LedgerEntryExt, LedgerFootprint, LedgerKey, LedgerKeyContractCode, - LedgerKeyContractData, Limits, PublicKey, ReadXdr, ScAddress, ScBytes, ScContractInstance, - ScErrorCode, ScErrorType, ScMapEntry, ScVal, SequenceNumber, SorobanAuthorizationEntry, - SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, - SorobanResources, Thresholds, TtlEntry, Uint256, WriteXdr, + AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractDataDurability, + ContractDataEntry, ContractEvent, ContractExecutable, ContractIdPreimage, + ContractIdPreimageFromAddress, CreateContractArgs, DiagnosticEvent, ExtensionPoint, + HashIdPreimage, HashIdPreimageContractId, HashIdPreimageSorobanAuthorization, HostFunction, + InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerFootprint, + LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limits, PublicKey, ReadXdr, + ScAddress, ScBytes, ScContractInstance, ScErrorCode, ScErrorType, ScMapEntry, ScVal, + SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction, + SorobanAuthorizedInvocation, SorobanCredentials, SorobanResources, Thresholds, TtlEntry, + Uint256, WriteXdr, }, Host, HostError, LedgerInfo, }; @@ -107,7 +107,10 @@ fn account_entry(account_id: &AccountId) -> LedgerEntry { fn wasm_entry(wasm: &[u8]) -> LedgerEntry { ledger_entry(LedgerEntryData::ContractCode(ContractCodeEntry { - ext: ContractCodeEntryExt::V0, + #[cfg(feature = "next")] + ext: crate::xdr::ContractCodeEntryExt::V0, + #[cfg(not(feature = "next"))] + ext: ExtensionPoint::V0, hash: get_wasm_hash(wasm).try_into().unwrap(), code: wasm.try_into().unwrap(), })) diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index d6b14d99d..8e07289a6 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -11,6 +11,8 @@ mod dispatch; mod fuel_refillable; mod func_info; +mod module_cache; +mod parsed_module; #[cfg(feature = "bench")] pub(crate) use dispatch::dummy0; @@ -19,33 +21,55 @@ pub(crate) use dispatch::protocol_gated_dummy; use crate::{ budget::{get_wasmi_config, AsBudget, Budget}, - err, host::{ error::TryBorrowOrErr, metered_clone::MeteredContainer, metered_hash::{CountingHasher, MeteredHash}, }, - meta::{self, get_ledger_protocol_version}, - xdr::{ - ContractCodeCostInputs, ContractCostType, Hash, Limited, ReadXdr, ScEnvMetaEntry, - ScErrorCode, ScErrorType, - }, + xdr::{ContractCostType, Hash, ScErrorCode, ScErrorType}, ConversionError, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, WasmiMarshal, - DEFAULT_XDR_RW_LIMITS, }; -use std::{cell::RefCell, collections::BTreeSet, io::Cursor, rc::Rc, time::Instant}; +use std::{cell::RefCell, collections::BTreeSet, rc::Rc, time::Instant}; use fuel_refillable::FuelRefillable; use func_info::HOST_FUNCTIONS; -use wasmi::{Engine, Instance, Linker, Memory, Module, Store, Value}; +pub use module_cache::ModuleCache; +pub use parsed_module::{ParsedModule, VersionedContractCodeCostInputs}; + +use wasmi::{Instance, Linker, Memory, Store, Value}; use crate::VmCaller; use wasmi::{Caller, StoreContextMut}; + impl wasmi::core::HostError for HostError {} const MAX_VM_ARGS: usize = 32; +struct VmInstantiationTimer { + host: Host, + #[cfg(not(target_family = "wasm"))] + start: Instant, +} +impl VmInstantiationTimer { + fn new(host: Host) -> Self { + VmInstantiationTimer { + host, + #[cfg(not(target_family = "wasm"))] + start: Instant::now(), + } + } +} +#[cfg(not(target_family = "wasm"))] +impl Drop for VmInstantiationTimer { + fn drop(&mut self) { + let _ = self.host.as_budget().track_time( + ContractCostType::VmInstantiation, + self.start.elapsed().as_nanos() as u64, + ); + } +} + /// A [Vm] is a thin wrapper around an instance of [wasmi::Module]. Multiple /// [Vm]s may be held in a single [Host], and each contains a single WASM module /// instantiation. @@ -59,11 +83,8 @@ const MAX_VM_ARGS: usize = 32; /// will fail. pub struct Vm { pub(crate) contract_id: Hash, - // TODO: consider moving store and possibly module to Host so they can be - // recycled across calls. Or possibly beyond, to be recycled across txs. - // https://github.com/stellar/rs-soroban-env/issues/827 - module: Module, - cost_inputs: ContractCodeCostInputs, + #[allow(dead_code)] + pub(crate) module: Rc, store: RefCell>, instance: Instance, pub(crate) memory: Option, @@ -76,134 +97,6 @@ impl std::hash::Hash for Vm { } impl Vm { - fn check_contract_interface_version( - host: &Host, - interface_version: u64, - ) -> Result<(), HostError> { - let want_proto = { - let ledger_proto = host.get_ledger_protocol_version()?; - let env_proto = get_ledger_protocol_version(meta::INTERFACE_VERSION); - if ledger_proto <= env_proto { - // ledger proto should be before or equal to env proto - ledger_proto - } else { - return Err(err!( - host, - (ScErrorType::Context, ScErrorCode::InternalError), - "ledger protocol number is ahead of supported env protocol number", - ledger_proto, - env_proto - )); - } - }; - - // Not used when "next" is enabled - #[cfg(not(feature = "next"))] - let got_pre = meta::get_pre_release_version(interface_version); - - let got_proto = get_ledger_protocol_version(interface_version); - - if got_proto < want_proto { - // Old protocols are finalized, we only support contracts - // with similarly finalized (zero) prerelease numbers. - // - // Note that we only enable this check if the "next" feature isn't enabled - // because a "next" stellar-core can still run a "curr" test using non-finalized - // test wasms. The "next" feature isn't safe for production and is meant to - // simulate the protocol version after the one currently supported in - // stellar-core, so bypassing this check for "next" is safe. - #[cfg(not(feature = "next"))] - if got_pre != 0 { - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), - "contract pre-release number for old protocol is nonzero", - got_pre - )); - } - } else if got_proto == want_proto { - // Relax this check as well for the "next" feature to allow for flexibility while testing. - // stellar-core can pass in an older protocol version, in which case the pre-release version - // will not match up with the "next" feature (The "next" pre-release version is always 1). - #[cfg(not(feature = "next"))] - { - // Current protocol might have a nonzero prerelease number; we will - // allow it only if it matches the current prerelease exactly. - let want_pre = meta::get_pre_release_version(meta::INTERFACE_VERSION); - if want_pre != got_pre { - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), - "contract pre-release number for current protocol does not match host", - got_pre, - want_pre - )); - } - } - } else { - // Future protocols we don't allow. It might be nice (in the sense - // of "allowing uploads of a future-protocol contract that will go - // live as soon as the network upgrades to it") but there's a risk - // that the "future" protocol semantics baked in to a contract - // differ from the final semantics chosen by the network, so to be - // conservative we avoid even allowing this. - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), - "contract protocol number is newer than host", - got_proto - )); - } - Ok(()) - } - - fn check_meta_section(host: &Host, m: &Module) -> Result { - if let Some(env_meta) = Self::module_custom_section(m, meta::ENV_META_V0_SECTION_NAME) { - let mut limits = DEFAULT_XDR_RW_LIMITS; - limits.len = env_meta.len(); - let mut cursor = Limited::new(Cursor::new(env_meta), limits); - if let Some(env_meta_entry) = ScEnvMetaEntry::read_xdr_iter(&mut cursor).next() { - let ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) = - host.map_err(env_meta_entry)?; - Vm::check_contract_interface_version(host, v)?; - Ok(v) - } else { - Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "contract missing environment interface version", - &[], - )) - } - } else { - Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "contract missing metadata section", - &[], - )) - } - } - - fn check_max_args(host: &Host, m: &Module) -> Result<(), HostError> { - for e in m.exports() { - match e.ty() { - wasmi::ExternType::Func(f) => { - if f.params().len() > MAX_VM_ARGS || f.results().len() > MAX_VM_ARGS { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "Too many arguments or results in wasm export", - &[], - )); - } - } - _ => (), - } - } - Ok(()) - } - #[cfg(feature = "testutils")] pub fn get_all_host_functions() -> Vec<(&'static str, &'static str, u32)> { HOST_FUNCTIONS @@ -212,95 +105,25 @@ impl Vm { .collect() } - /// Instantiates a VM given the arguments provided in [`Self::new`] + /// Instantiates a VM given the arguments provided in [`Self::new`], + /// or [`Self::new_from_module_cache`] fn instantiate( host: &Host, contract_id: Hash, - module_wasm_code: &[u8], - code_cost_inputs: Option, + parsed_module: Rc, ) -> Result, HostError> { - match &code_cost_inputs { - // When we have a set of cost inputs, we charge each of them. This - // is the "cheap" path where we're going to charge a model cost that - // is much closer to the true cost of instantiating the contract, - // modeled as a variety of different linear terms. - Some(inputs) => { - host.charge_budget( - ContractCostType::VmInstantiateInstructions, - Some(inputs.n_instructions as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateFunctions, - Some(inputs.n_functions as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateGlobals, - Some(inputs.n_globals as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateTableEntries, - Some(inputs.n_table_entries as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateTypes, - Some(inputs.n_types as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateDataSegments, - Some(inputs.n_data_segments as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateElemSegments, - Some(inputs.n_elem_segments as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateImports, - Some(inputs.n_imports as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateExports, - Some(inputs.n_exports as u64), - )?; - host.charge_budget( - ContractCostType::VmInstantiateMemoryPages, - Some(inputs.n_memory_pages as u64), - )?; - } - None => { - // When we don't have a set of cost inputs, either because the - // contract predates storing cost inputs or because we're doing - // an upload-time analysis of an unknown contract, we charge - // based on the byte size and assume the worst-case cost for - // each byte. This is the "expensive" path, but we only have to - // charge it during upload, after which we'll store the cost - // inputs for use in future instantiations. - host.charge_budget( - ContractCostType::VmInstantiation, - Some(module_wasm_code.len() as u64), - )?; - } - } + let _span = tracy_span!("Vm::instantiate"); - let config = get_wasmi_config(host)?; - let engine = Engine::new(&config); - let module = { - let _span0 = tracy_span!("parse module"); - host.map_err(Module::new(&engine, module_wasm_code))? - }; + let engine = parsed_module.module.engine(); + let mut linker = >::new(engine); + let mut store = Store::new(engine, host.clone()); - Self::check_max_args(host, &module)?; - let interface_version = Self::check_meta_section(host, &module)?; - let contract_proto = get_ledger_protocol_version(interface_version); + parsed_module.cost_inputs.charge_for_instantiation(host)?; - let cost_inputs = match code_cost_inputs { - Some(inputs) => inputs, - None => Self::extract_contract_cost_inputs(host.clone(), module_wasm_code)?, - }; - - let mut store = Store::new(&engine, host.clone()); store.limiter(|host| host); - let module_imports: BTreeSet<(&str, &str)> = module + let module_imports: BTreeSet<(&str, &str)> = parsed_module + .module .imports() .filter(|i| i.ty().func().is_some()) .map(|i| { @@ -310,8 +133,6 @@ impl Vm { }) .collect(); - let mut linker = >::new(&engine); - { // We perform link-time protocol version gating here. // Reasons for doing link-time instead of run-time check: @@ -333,14 +154,14 @@ impl Vm { let ledger_proto = host.with_ledger_info(|li| Ok(li.protocol_version))?; for hf in HOST_FUNCTIONS { if let Some(min_proto) = hf.min_proto { - if contract_proto < min_proto || ledger_proto < min_proto { + if parsed_module.proto_version < min_proto || ledger_proto < min_proto { // We skip linking this hf instead of returning an error // because we have to support old contracts during replay. continue; } } if let Some(max_proto) = hf.max_proto { - if contract_proto > max_proto || ledger_proto > max_proto { + if parsed_module.proto_version > max_proto || ledger_proto > max_proto { // We skip linking this hf instead of returning an error // because we have to support old contracts during replay. continue; @@ -362,7 +183,7 @@ impl Vm { let not_started_instance = { let _span0 = tracy_span!("instantiate module"); - host.map_err(linker.instantiate(&mut store, &module))? + host.map_err(linker.instantiate(&mut store, &parsed_module.module))? }; let instance = host.map_err( @@ -382,14 +203,23 @@ impl Vm { // boundary. Ok(Rc::new(Self { contract_id, - module, - cost_inputs, + module: parsed_module, store: RefCell::new(store), instance, memory, })) } + pub fn from_parsed_module( + host: &Host, + contract_id: Hash, + parsed_module: Rc, + ) -> Result, HostError> { + let _span = tracy_span!("Vm::from_parsed_module"); + VmInstantiationTimer::new(host.clone()); + Self::instantiate(host, contract_id, parsed_module) + } + /// Constructs a new instance of a [Vm] within the provided [Host], /// establishing a new execution context for a contract identified by /// `contract_id` with WASM bytecode provided in `module_wasm_code`. @@ -408,236 +238,26 @@ impl Vm { /// /// This method is called automatically as part of [Host::invoke_function] /// and does not usually need to be called from outside the crate. - pub fn new( - host: &Host, - contract_id: Hash, - module_wasm_code: &[u8], - code_cost_inputs: Option, - ) -> Result, HostError> { - let _span = tracy_span!("Vm::new"); - - if cfg!(not(target_family = "wasm")) { - let now = Instant::now(); - let vm = Self::instantiate(host, contract_id, module_wasm_code, code_cost_inputs)?; - - host.as_budget().track_time( - ContractCostType::VmInstantiation, - now.elapsed().as_nanos() as u64, - )?; - - Ok(vm) - } else { - Self::instantiate(host, contract_id, module_wasm_code, code_cost_inputs) - } - } - - fn check_const_expr_simple(host: &Host, expr: &wasmparser::ConstExpr) -> Result<(), HostError> { - use wasmparser::Operator::*; - let mut op = expr.get_operators_reader(); - while !op.eof() { - match host.map_err(op.read())? { - I32Const { .. } | I64Const { .. } | RefFunc { .. } | RefNull { .. } | End => (), - _ => { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "unsupported complex wasm constant expression", - &[], - )) - } - } - } - Ok(()) - } - - fn extract_contract_cost_inputs( - host: Host, - wasm: &[u8], - ) -> Result { - use wasmparser::{ElementItems, ElementKind, Parser, Payload::*, TableInit}; - - if !Parser::is_core_wasm(wasm) { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "unsupported non-core wasm module", - &[], - )); - } - let mut costs = ContractCodeCostInputs { - n_instructions: 0, - n_functions: 0, - n_globals: 0, - n_table_entries: 0, - n_types: 0, - n_data_segments: 0, - n_elem_segments: 0, - n_imports: 0, - n_exports: 0, - n_memory_pages: 0, + pub fn new(host: &Host, contract_id: Hash, wasm: &[u8]) -> Result, HostError> { + let cost_inputs = VersionedContractCodeCostInputs::V0 { + wasm_bytes: wasm.len(), }; - - let parser = Parser::new(0); - let mut elements: u32 = 0; - let mut data: u32 = 0; - for section in parser.parse_all(wasm) { - let section = host.map_err(section)?; - match section { - // Ignored sections. - Version { .. } - | DataCountSection { .. } - | CustomSection(_) - | CodeSectionStart { .. } - | End(_) => (), - - // Component-model stuff or other unsupported sections. Error out. - StartSection { .. } - | ModuleSection { .. } - | InstanceSection(_) - | CoreTypeSection(_) - | ComponentSection { .. } - | ComponentInstanceSection(_) - | ComponentAliasSection(_) - | ComponentTypeSection(_) - | ComponentCanonicalSection(_) - | ComponentStartSection { .. } - | ComponentImportSection(_) - | ComponentExportSection(_) - | TagSection(_) - | UnknownSection { .. } => { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "unsupported wasm section", - &[], - )) - } - - MemorySection(s) => { - for mem in s { - let mem = host.map_err(mem)?; - if mem.memory64 { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "unsupported 64-bit memory", - &[], - )); - } - if mem.shared { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "unsupported shared memory", - &[], - )); - } - if mem.initial > 0xffff { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "unsupported memory size", - &[], - )); - } - costs.n_memory_pages = - costs.n_memory_pages.saturating_add(mem.initial as u32); - } - } - - TypeSection(s) => costs.n_types = costs.n_types.saturating_add(s.count()), - ImportSection(s) => costs.n_imports = costs.n_imports.saturating_add(s.count()), - FunctionSection(s) => { - costs.n_functions = costs.n_functions.saturating_add(s.count()) - } - TableSection(s) => { - for table in s { - let table = host.map_err(table)?; - costs.n_table_entries = - costs.n_table_entries.saturating_add(table.ty.initial); - match table.init { - TableInit::RefNull => (), - TableInit::Expr(ref expr) => { - Self::check_const_expr_simple(&host, &expr)?; - } - } - } - } - GlobalSection(s) => { - costs.n_globals = costs.n_globals.saturating_add(s.count()); - for global in s { - let global = host.map_err(global)?; - Self::check_const_expr_simple(&host, &global.init_expr)?; - } - } - ExportSection(s) => costs.n_exports = costs.n_exports.saturating_add(s.count()), - ElementSection(s) => { - costs.n_elem_segments = costs.n_elem_segments.saturating_add(1); - elements = elements.saturating_add(s.count()); - for elem in s { - let elem = host.map_err(elem)?; - match elem.kind { - ElementKind::Declared | ElementKind::Passive => (), - ElementKind::Active { offset_expr, .. } => { - Self::check_const_expr_simple(&host, &offset_expr)? - } - } - match elem.items { - ElementItems::Functions(_) => (), - ElementItems::Expressions(_, exprs) => { - for expr in exprs { - let expr = host.map_err(expr)?; - Self::check_const_expr_simple(&host, &expr)?; - } - } - } - } - } - DataSection(s) => { - costs.n_data_segments = costs.n_data_segments.saturating_add(1); - for d in s { - let d = host.map_err(d)?; - if d.data.len() > u32::MAX as usize { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "data segment too large", - &[], - )); - } - data = data.saturating_add(d.data.len() as u32); - match d.kind { - wasmparser::DataKind::Active { offset_expr, .. } => { - Self::check_const_expr_simple(&host, &offset_expr)? - } - wasmparser::DataKind::Passive => (), - } - } - } - CodeSectionEntry(s) => { - let ops = host.map_err(s.get_operators_reader())?; - for _op in ops { - costs.n_instructions = costs.n_instructions.saturating_add(1); - } - } - } - } - if elements > costs.n_table_entries { - dbg!(elements); - dbg!(costs.n_table_entries); - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "too many elements in wasm elem section(s)", - &[], - )); - } - Ok(costs) + Self::new_with_cost_inputs(host, contract_id, wasm, cost_inputs) } - pub fn get_contract_code_cost_inputs(&self) -> ContractCodeCostInputs { - self.cost_inputs.clone() + pub fn new_with_cost_inputs( + host: &Host, + contract_id: Hash, + wasm: &[u8], + cost_inputs: VersionedContractCodeCostInputs, + ) -> Result, HostError> { + let _span = tracy_span!("Vm::new"); + VmInstantiationTimer::new(host.clone()); + let config = get_wasmi_config(host.as_budget())?; + let engine = wasmi::Engine::new(&config); + let parsed_module = Rc::new(ParsedModule::new(host, &engine, wasm, cost_inputs)?); + Self::instantiate(host, contract_id, parsed_module) } pub(crate) fn get_memory(&self, host: &Host) -> Result { @@ -782,22 +402,6 @@ impl Vm { self.metered_func_call(host, func_sym, wasm_args.as_slice()) } - fn module_custom_section(m: &Module, name: impl AsRef) -> Option<&[u8]> { - m.custom_sections().iter().find_map(|s| { - if &*s.name == name.as_ref() { - Some(&*s.data) - } else { - None - } - }) - } - - /// Returns the raw bytes content of a named custom section from the WASM - /// module loaded into the [Vm], or `None` if no such custom section exists. - pub fn custom_section(&self, name: impl AsRef) -> Option<&[u8]> { - Self::module_custom_section(&self.module, name) - } - /// Utility function that synthesizes a `VmCaller` configured to point /// to this VM's `Store` and `Instance`, and calls the provided function /// back with it. Mainly used for testing. diff --git a/soroban-env-host/src/vm/module_cache.rs b/soroban-env-host/src/vm/module_cache.rs new file mode 100644 index 000000000..43033cc0a --- /dev/null +++ b/soroban-env-host/src/vm/module_cache.rs @@ -0,0 +1,97 @@ +use super::parsed_module::{ParsedModule, VersionedContractCodeCostInputs}; +use crate::{ + budget::{get_wasmi_config, AsBudget}, + xdr::{Hash, ScErrorCode, ScErrorType}, + Host, HostError, +}; +use std::{collections::BTreeMap, rc::Rc}; +use wasmi::Engine; + +/// A [ModuleCache] is a cache of a set of WASM modules that have been parsed +/// but not yet instantiated, along with a shared and reusable [Engine] storing +/// their code. The cache must be populated eagerly with all the contracts in a +/// single [Host]'s lifecycle (at least) added all at once, since each wasmi +/// [Engine] is locked during execution and no new modules can be added to it. +#[derive(Clone, Default)] +pub struct ModuleCache { + pub(crate) engine: Engine, + modules: BTreeMap>, +} + +impl ModuleCache { + // ModuleCache should not be active until protocol version 21. + pub const MIN_LEDGER_VERSION: u32 = 21; + + pub fn new(host: &Host) -> Result { + let config = get_wasmi_config(host.as_budget())?; + let engine = Engine::new(&config); + let modules = BTreeMap::new(); + #[allow(unused_mut)] + let mut cache = Self { engine, modules }; + #[cfg(feature = "next")] + cache.add_stored_contracts(host)?; + Ok(cache) + } + + #[cfg(feature = "next")] + pub fn add_stored_contracts(&mut self, host: &Host) -> Result<(), HostError> { + use crate::xdr::{ContractCodeEntry, ContractCodeEntryExt, LedgerEntryData, LedgerKey}; + for (k, v) in host.try_borrow_storage()?.map.iter(host.as_budget())? { + if let LedgerKey::ContractCode(_) = &**k { + if let Some((e, _)) = v { + if let LedgerEntryData::ContractCode(ContractCodeEntry { code, hash, ext }) = + &e.data + { + let code_cost_inputs = match ext { + ContractCodeEntryExt::V0 => VersionedContractCodeCostInputs::V0 { + wasm_bytes: code.len(), + }, + ContractCodeEntryExt::V1(v1) => { + VersionedContractCodeCostInputs::V1(v1.cost_inputs.clone()) + } + }; + self.parse_and_cache_module(host, hash, code, code_cost_inputs)?; + } + } + } + } + Ok(()) + } + + pub fn parse_and_cache_module( + &mut self, + host: &Host, + contract_id: &Hash, + wasm: &[u8], + cost_inputs: VersionedContractCodeCostInputs, + ) -> Result<(), HostError> { + if self.modules.contains_key(contract_id) { + return Err(host.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "module cache already contains contract", + &[], + )); + } + let parsed_module = Rc::new(ParsedModule::new(host, &self.engine, &wasm, cost_inputs)?); + self.modules.insert(contract_id.clone(), parsed_module); + Ok(()) + } + + pub fn get_module( + &self, + host: &Host, + contract_id: &Hash, + ) -> Result, HostError> { + if let Some(m) = self.modules.get(contract_id) { + return Ok(m.clone()); + } else { + Err(host.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "module cache missing contract", + &[], + )) + } + } +} diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs new file mode 100644 index 000000000..7fc1f303b --- /dev/null +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -0,0 +1,550 @@ +use crate::{ + err, + meta::{self, get_ledger_protocol_version}, + xdr::{ContractCostType, Limited, ReadXdr, ScEnvMetaEntry, ScErrorCode, ScErrorType}, + Host, HostError, DEFAULT_XDR_RW_LIMITS, +}; + +use wasmi::{Engine, Module}; + +use super::MAX_VM_ARGS; +use std::io::Cursor; + +#[derive(Debug, Clone)] +pub enum VersionedContractCodeCostInputs { + V0 { + wasm_bytes: usize, + }, + #[cfg(feature = "next")] + V1(crate::xdr::ContractCodeCostInputs), +} + +impl VersionedContractCodeCostInputs { + pub fn is_v0(&self) -> bool { + match self { + Self::V0 { .. } => true, + #[cfg(feature = "next")] + Self::V1(_) => false, + } + } + pub fn charge_for_parsing(&self, host: &Host) -> Result<(), HostError> { + match self { + Self::V0 { wasm_bytes } => { + host.charge_budget(ContractCostType::VmInstantiation, Some(*wasm_bytes as u64))?; + } + #[cfg(feature = "next")] + Self::V1(inputs) => { + host.charge_budget( + ContractCostType::ParseWasmInstructions, + Some(inputs.n_instructions as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmFunctions, + Some(inputs.n_functions as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmGlobals, + Some(inputs.n_globals as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmTableEntries, + Some(inputs.n_table_entries as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmTypes, + Some(inputs.n_types as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmDataSegments, + Some(inputs.n_data_segments as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmElemSegments, + Some(inputs.n_elem_segments as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmImports, + Some(inputs.n_imports as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmExports, + Some(inputs.n_exports as u64), + )?; + host.charge_budget( + ContractCostType::ParseWasmMemoryPages, + Some(inputs.n_memory_pages as u64), + )?; + } + } + Ok(()) + } + pub fn charge_for_instantiation(&self, _host: &Host) -> Result<(), HostError> { + match self { + Self::V0 { .. } => { + // No-op, already charged when parsing + } + #[cfg(feature = "next")] + Self::V1(inputs) => { + _host.charge_budget( + ContractCostType::InstantiateWasmInstructions, + Some(inputs.n_instructions as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmFunctions, + Some(inputs.n_functions as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmGlobals, + Some(inputs.n_globals as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmTableEntries, + Some(inputs.n_table_entries as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmTypes, + Some(inputs.n_types as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmDataSegments, + Some(inputs.n_data_segments as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmElemSegments, + Some(inputs.n_elem_segments as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmImports, + Some(inputs.n_imports as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmExports, + Some(inputs.n_exports as u64), + )?; + _host.charge_budget( + ContractCostType::InstantiateWasmMemoryPages, + Some(inputs.n_memory_pages as u64), + )?; + } + } + Ok(()) + } +} + +/// A [ParsedModule] contains the parsed [wasmi::Module] for a given wasm blob, +/// as well as a protocol number and set of [ContractCodeCostInputs] extracted +/// from the module when it was parsed. +pub struct ParsedModule { + pub module: Module, + pub proto_version: u32, + pub cost_inputs: VersionedContractCodeCostInputs, +} + +impl ParsedModule { + pub fn new( + host: &Host, + engine: &Engine, + wasm: &[u8], + cost_inputs: VersionedContractCodeCostInputs, + ) -> Result { + cost_inputs.charge_for_parsing(host)?; + let (module, proto_version) = Self::parse_wasm(host, engine, wasm)?; + Ok(Self { + module, + proto_version, + cost_inputs, + }) + } + + #[cfg(any(test, feature = "testutils"))] + pub fn new_with_isolated_engine( + host: &Host, + wasm: &[u8], + cost_inputs: VersionedContractCodeCostInputs, + ) -> Result { + use crate::budget::AsBudget; + let config = crate::vm::get_wasmi_config(host.as_budget())?; + let engine = Engine::new(&config); + cost_inputs.charge_for_parsing(host)?; + let (module, proto_version) = Self::parse_wasm(host, &engine, wasm)?; + Ok(Self { + module, + proto_version, + cost_inputs, + }) + } + + /// Parse the wasm blob into a [Module] and its protocol number, checking check its interface version + fn parse_wasm(host: &Host, engine: &Engine, wasm: &[u8]) -> Result<(Module, u32), HostError> { + let module = { + let _span0 = tracy_span!("parse module"); + host.map_err(Module::new(&engine, wasm))? + }; + + Self::check_max_args(host, &module)?; + let interface_version = Self::check_meta_section(host, &module)?; + let contract_proto = get_ledger_protocol_version(interface_version); + + Ok((module, contract_proto)) + } + + fn check_contract_interface_version( + host: &Host, + interface_version: u64, + ) -> Result<(), HostError> { + let want_proto = { + let ledger_proto = host.get_ledger_protocol_version()?; + let env_proto = get_ledger_protocol_version(meta::INTERFACE_VERSION); + if ledger_proto <= env_proto { + // ledger proto should be before or equal to env proto + ledger_proto + } else { + return Err(err!( + host, + (ScErrorType::Context, ScErrorCode::InternalError), + "ledger protocol number is ahead of supported env protocol number", + ledger_proto, + env_proto + )); + } + }; + + // Not used when "next" is enabled + #[cfg(not(feature = "next"))] + let got_pre = meta::get_pre_release_version(interface_version); + + let got_proto = get_ledger_protocol_version(interface_version); + + if got_proto < want_proto { + // Old protocols are finalized, we only support contracts + // with similarly finalized (zero) prerelease numbers. + // + // Note that we only enable this check if the "next" feature isn't enabled + // because a "next" stellar-core can still run a "curr" test using non-finalized + // test wasms. The "next" feature isn't safe for production and is meant to + // simulate the protocol version after the one currently supported in + // stellar-core, so bypassing this check for "next" is safe. + #[cfg(not(feature = "next"))] + if got_pre != 0 { + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + "contract pre-release number for old protocol is nonzero", + got_pre + )); + } + } else if got_proto == want_proto { + // Relax this check as well for the "next" feature to allow for flexibility while testing. + // stellar-core can pass in an older protocol version, in which case the pre-release version + // will not match up with the "next" feature (The "next" pre-release version is always 1). + #[cfg(not(feature = "next"))] + { + // Current protocol might have a nonzero prerelease number; we will + // allow it only if it matches the current prerelease exactly. + let want_pre = meta::get_pre_release_version(meta::INTERFACE_VERSION); + if want_pre != got_pre { + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + "contract pre-release number for current protocol does not match host", + got_pre, + want_pre + )); + } + } + } else { + // Future protocols we don't allow. It might be nice (in the sense + // of "allowing uploads of a future-protocol contract that will go + // live as soon as the network upgrades to it") but there's a risk + // that the "future" protocol semantics baked in to a contract + // differ from the final semantics chosen by the network, so to be + // conservative we avoid even allowing this. + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + "contract protocol number is newer than host", + got_proto + )); + } + Ok(()) + } + + fn module_custom_section(m: &Module, name: impl AsRef) -> Option<&[u8]> { + m.custom_sections().iter().find_map(|s| { + if &*s.name == name.as_ref() { + Some(&*s.data) + } else { + None + } + }) + } + + /// Returns the raw bytes content of a named custom section from the WASM + /// module loaded into the [Vm], or `None` if no such custom section exists. + #[allow(dead_code)] + pub fn custom_section(&self, name: impl AsRef) -> Option<&[u8]> { + Self::module_custom_section(&self.module, name) + } + + fn check_meta_section(host: &Host, m: &Module) -> Result { + if let Some(env_meta) = Self::module_custom_section(m, meta::ENV_META_V0_SECTION_NAME) { + let mut limits = DEFAULT_XDR_RW_LIMITS; + limits.len = env_meta.len(); + let mut cursor = Limited::new(Cursor::new(env_meta), limits); + if let Some(env_meta_entry) = ScEnvMetaEntry::read_xdr_iter(&mut cursor).next() { + let ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) = + host.map_err(env_meta_entry)?; + Self::check_contract_interface_version(host, v)?; + Ok(v) + } else { + Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "contract missing environment interface version", + &[], + )) + } + } else { + Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "contract missing metadata section", + &[], + )) + } + } + + fn check_max_args(host: &Host, m: &Module) -> Result<(), HostError> { + for e in m.exports() { + match e.ty() { + wasmi::ExternType::Func(f) => { + if f.params().len() > MAX_VM_ARGS || f.results().len() > MAX_VM_ARGS { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "Too many arguments or results in wasm export", + &[], + )); + } + } + _ => (), + } + } + Ok(()) + } + + // Do a second, manual parse of the wasm blob to extract cost parameters we're + // interested in. + #[cfg(feature = "next")] + pub fn extract_refined_contract_cost_inputs( + host: &Host, + wasm: &[u8], + ) -> Result { + use wasmparser::{ElementItems, ElementKind, Parser, Payload::*, TableInit}; + + if !Parser::is_core_wasm(wasm) { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "unsupported non-core wasm module", + &[], + )); + } + + let mut costs = crate::xdr::ContractCodeCostInputs { + n_instructions: 0, + n_functions: 0, + n_globals: 0, + n_table_entries: 0, + n_types: 0, + n_data_segments: 0, + n_elem_segments: 0, + n_imports: 0, + n_exports: 0, + n_memory_pages: 0, + }; + + let parser = Parser::new(0); + let mut elements: u32 = 0; + let mut data: u32 = 0; + for section in parser.parse_all(wasm) { + let section = host.map_err(section)?; + match section { + // Ignored sections. + Version { .. } + | DataCountSection { .. } + | CustomSection(_) + | CodeSectionStart { .. } + | End(_) => (), + + // Component-model stuff or other unsupported sections. Error out. + StartSection { .. } + | ModuleSection { .. } + | InstanceSection(_) + | CoreTypeSection(_) + | ComponentSection { .. } + | ComponentInstanceSection(_) + | ComponentAliasSection(_) + | ComponentTypeSection(_) + | ComponentCanonicalSection(_) + | ComponentStartSection { .. } + | ComponentImportSection(_) + | ComponentExportSection(_) + | TagSection(_) + | UnknownSection { .. } => { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "unsupported wasm section", + &[], + )) + } + + MemorySection(s) => { + for mem in s { + let mem = host.map_err(mem)?; + if mem.memory64 { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "unsupported 64-bit memory", + &[], + )); + } + if mem.shared { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "unsupported shared memory", + &[], + )); + } + if mem.initial > 0xffff { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "unsupported memory size", + &[], + )); + } + costs.n_memory_pages = + costs.n_memory_pages.saturating_add(mem.initial as u32); + } + } + + TypeSection(s) => costs.n_types = costs.n_types.saturating_add(s.count()), + ImportSection(s) => costs.n_imports = costs.n_imports.saturating_add(s.count()), + FunctionSection(s) => { + costs.n_functions = costs.n_functions.saturating_add(s.count()) + } + TableSection(s) => { + for table in s { + let table = host.map_err(table)?; + costs.n_table_entries = + costs.n_table_entries.saturating_add(table.ty.initial); + match table.init { + TableInit::RefNull => (), + TableInit::Expr(ref expr) => { + Self::check_const_expr_simple(&host, &expr)?; + } + } + } + } + GlobalSection(s) => { + costs.n_globals = costs.n_globals.saturating_add(s.count()); + for global in s { + let global = host.map_err(global)?; + Self::check_const_expr_simple(&host, &global.init_expr)?; + } + } + ExportSection(s) => costs.n_exports = costs.n_exports.saturating_add(s.count()), + ElementSection(s) => { + costs.n_elem_segments = costs.n_elem_segments.saturating_add(1); + elements = elements.saturating_add(s.count()); + for elem in s { + let elem = host.map_err(elem)?; + match elem.kind { + ElementKind::Declared | ElementKind::Passive => (), + ElementKind::Active { offset_expr, .. } => { + Self::check_const_expr_simple(&host, &offset_expr)? + } + } + match elem.items { + ElementItems::Functions(_) => (), + ElementItems::Expressions(_, exprs) => { + for expr in exprs { + let expr = host.map_err(expr)?; + Self::check_const_expr_simple(&host, &expr)?; + } + } + } + } + } + DataSection(s) => { + costs.n_data_segments = costs.n_data_segments.saturating_add(1); + for d in s { + let d = host.map_err(d)?; + if d.data.len() > u32::MAX as usize { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "data segment too large", + &[], + )); + } + data = data.saturating_add(d.data.len() as u32); + match d.kind { + wasmparser::DataKind::Active { offset_expr, .. } => { + Self::check_const_expr_simple(&host, &offset_expr)? + } + wasmparser::DataKind::Passive => (), + } + } + } + CodeSectionEntry(s) => { + let ops = host.map_err(s.get_operators_reader())?; + for _op in ops { + costs.n_instructions = costs.n_instructions.saturating_add(1); + } + } + } + } + if data > costs.n_memory_pages.saturating_mul(0x10000) { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "data segments exceed memory size", + &[], + )); + } + if elements > costs.n_table_entries { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "too many elements in wasm elem section(s)", + &[], + )); + } + Ok(costs) + } + + #[cfg(feature = "next")] + fn check_const_expr_simple(host: &Host, expr: &wasmparser::ConstExpr) -> Result<(), HostError> { + use wasmparser::Operator::*; + let mut op = expr.get_operators_reader(); + while !op.eof() { + match host.map_err(op.read())? { + I32Const { .. } | I64Const { .. } | RefFunc { .. } | RefNull { .. } | End => (), + _ => { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "unsupported complex wasm constant expression", + &[], + )) + } + } + } + Ok(()) + } +}