From bf8bcf2e1ec75fcf64dbb73d3350e790517a26f2 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Mon, 18 Mar 2024 21:35:36 -0700 Subject: [PATCH] Add tests and calibration for new VM instantiation cost model --- .../common/cost_types/wasm_insn_exec.rs | 79 ++-- .../benches/worst_case_linear_models.rs | 91 ++++- soroban-env-host/src/budget.rs | 95 ++--- .../src/cost_runner/cost_types/vm_ops.rs | 56 ++- soroban-env-host/src/test/lifecycle.rs | 381 ++++++++++++++++++ soroban-env-host/src/testutils.rs | 26 ++ soroban-env-host/src/vm.rs | 1 + soroban-env-host/src/vm/parsed_module.rs | 44 +- 8 files changed, 665 insertions(+), 108 deletions(-) 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 9b90938ba..329736ecd 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 @@ -14,22 +14,56 @@ struct WasmModule { overhead: u64, } +// ModEmitter's default constructors are a little too spartan for our needs, we +// want our benchmarks to all have at least one imported function and at least +// one defined and exported function, so we're in the right performance tier. +// But we also don't want to go changing those constructors since it'll perturb +// a lot of non-benchmark users. +trait ModEmitterExt { + fn bench_default() -> Self; + fn bench_from_configs(mem_pages: u32, elem_count: u32) -> Self; + fn add_bench_import(self) -> Self; + fn add_bench_export(self) -> Self; + fn add_bench_baseline_material(self) -> Self; +} + +impl ModEmitterExt for ModEmitter { + fn add_bench_import(mut self) -> Self { + self.import_func("t", "_", Arity(0)); + self + } + fn add_bench_export(self) -> Self { + let mut fe = self.func(Arity(0), 0); + fe.push(Symbol::try_from_small_str("pass").unwrap()); + fe.finish_and_export("default") + } + fn add_bench_baseline_material(self) -> Self { + self.add_bench_import().add_bench_export() + } + + fn bench_default() -> Self { + Self::add_bench_baseline_material(ModEmitter::default()) + } + + fn bench_from_configs(mem_pages: u32, elem_count: u32) -> Self { + Self::add_bench_baseline_material(ModEmitter::from_configs(mem_pages, elem_count)) + } +} + pub fn wasm_module_with_n_internal_funcs(n: usize) -> Vec { - let mut me = ModEmitter::default(); + let mut me = ModEmitter::bench_default(); for _ in 0..n { let mut fe = me.func(Arity(0), 0); fe.push(Symbol::try_from_small_str("pass").unwrap()); (me, _) = fe.finish(); } - let mut fe = me.func(Arity(0), 0); - fe.push(Symbol::try_from_small_str("pass").unwrap()); - fe.finish_and_export("test").finish() + me.finish() } pub fn wasm_module_with_n_insns(n: usize) -> Vec { // We actually emit 4 instructions per loop iteration, so we need to divide by 4. let n = 1 + (n / 4); - let mut fe = ModEmitter::default().func(Arity(1), 0); + let mut fe = ModEmitter::bench_default().func(Arity(1), 0); let arg = fe.args[0]; fe.push(Operand::Const64(1)); for i in 0..n { @@ -43,17 +77,15 @@ pub fn wasm_module_with_n_insns(n: usize) -> Vec { fe.finish_and_export("test").finish() } pub fn wasm_module_with_n_globals(n: usize) -> Vec { - let mut me = ModEmitter::default(); + let mut me = ModEmitter::bench_default(); for i in 0..n { me.global(ValType::I64, true, &ConstExpr::i64_const(i as i64)); } - let mut fe = me.func(Arity(0), 0); - fe.push(Symbol::try_from_small_str("pass").unwrap()); - fe.finish_and_export("test").finish() + me.finish() } pub fn wasm_module_with_n_imports(n: usize) -> Vec { - let mut me = ModEmitter::default(); + let mut me = ModEmitter::default().add_bench_import(); let names = Vm::get_all_host_functions(); for (module, name, arity) in names.iter().take(n) { if *module == "t" { @@ -61,13 +93,11 @@ pub fn wasm_module_with_n_imports(n: usize) -> Vec { } me.import_func(module, name, Arity(*arity)); } - let mut fe = me.func(Arity(0), 0); - fe.push(Symbol::try_from_small_str("pass").unwrap()); - fe.finish_and_export("test").finish() + me.add_bench_export().finish() } pub fn wasm_module_with_n_exports(n: usize) -> Vec { - let me = ModEmitter::default(); + let me = ModEmitter::bench_default(); let mut fe = me.func(Arity(0), 0); fe.push(Symbol::try_from_small_str("pass").unwrap()); let (mut me, fid) = fe.finish(); @@ -78,7 +108,7 @@ pub fn wasm_module_with_n_exports(n: usize) -> Vec { } pub fn wasm_module_with_n_table_entries(n: usize) -> Vec { - let me = ModEmitter::from_configs(1, n as u32); + let me = ModEmitter::bench_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(); @@ -88,7 +118,7 @@ pub fn wasm_module_with_n_table_entries(n: usize) -> Vec { } pub fn wasm_module_with_n_types(mut n: usize) -> Vec { - let mut me = ModEmitter::default(); + let mut me = ModEmitter::bench_default(); // There's a max of 1,000,000 types, so we just make a loop // that covers more than that many combinations, and break when we've got // to the requested number. @@ -151,13 +181,11 @@ pub fn wasm_module_with_n_types(mut n: usize) -> Vec { } } } - let mut fe = me.func(Arity(0), 0); - fe.push(Symbol::try_from_small_str("pass").unwrap()); - fe.finish_and_export("test").finish() + me.finish() } pub fn wasm_module_with_n_elem_segments(n: usize) -> Vec { - let me = ModEmitter::from_configs(1, n as u32); + let me = ModEmitter::bench_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(); @@ -169,10 +197,7 @@ pub fn wasm_module_with_n_elem_segments(n: usize) -> Vec { pub fn wasm_module_with_n_data_segments(n: usize) -> Vec { let mem_offset = n as u32 * 1024; - let me = ModEmitter::from_configs(1 + mem_offset / 65536, 0); - let mut fe = me.func(Arity(0), 0); - fe.push(Symbol::try_from_small_str("pass").unwrap()); - let (mut me, _) = fe.finish(); + let mut me = ModEmitter::bench_from_configs(1 + mem_offset / 65536, 0); for _ in 0..n { me.define_data_segment(n as u32 * 1024, vec![1, 2, 3, 4]); } @@ -180,11 +205,9 @@ pub fn wasm_module_with_n_data_segments(n: usize) -> Vec { } pub fn wasm_module_with_n_memory_pages(n: usize) -> Vec { - let mut me = ModEmitter::from_configs(n as u32, 0); + let mut me = ModEmitter::bench_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() + me.finish() } fn wasm_module_with_mem_grow(n_pages: usize) -> Vec { diff --git a/soroban-env-host/benches/worst_case_linear_models.rs b/soroban-env-host/benches/worst_case_linear_models.rs index d8a3c84c0..16181c91a 100644 --- a/soroban-env-host/benches/worst_case_linear_models.rs +++ b/soroban-env-host/benches/worst_case_linear_models.rs @@ -68,6 +68,87 @@ fn write_cost_params_table( tw.flush() } +fn correct_multi_variable_models( + params: &mut BTreeMap, +) { + // Several cost types actually represent additional terms a cost model that + // we're decomposing into multiple variables, such as the cost of VM + // instantiation. When we charge these costs, we charge each variable + // separately, i.e. to charge a 5-variable cost we'll make 5 calls to the + // budget. Only the first of these 5 calls should have a constant factor, + // the rest should have zero as their constant (since they only contribute a + // new linear term), but the calibration code will have put the same (or + // nearly-the-same) nonzero constant term in each `CostComponent`. We + // correct this here by zeroing out the constant term in all but the first + // `CostComponent` of each set, (and attempting to confirm that they all + // have roughly-the-same constant term). + use ContractCostType::*; + const MULTI_VARIABLE_COST_GROUPS: &[&[ContractCostType]] = &[ + &[ + ParseWasmInstructions, + ParseWasmFunctions, + ParseWasmGlobals, + ParseWasmTableEntries, + ParseWasmTypes, + ParseWasmDataSegments, + ParseWasmElemSegments, + ParseWasmImports, + ParseWasmExports, + ParseWasmMemoryPages, + ], + &[ + InstantiateWasmInstructions, + InstantiateWasmFunctions, + InstantiateWasmGlobals, + InstantiateWasmTableEntries, + InstantiateWasmTypes, + InstantiateWasmDataSegments, + InstantiateWasmElemSegments, + InstantiateWasmImports, + InstantiateWasmExports, + InstantiateWasmMemoryPages, + ], + ]; + for group in MULTI_VARIABLE_COST_GROUPS { + let mut iter = group.iter(); + if let Some(first) = iter.next() { + let Some((first_cpu, first_mem)) = params.get(&CostType::Contract(*first)).cloned() + else { + continue; + }; + for ty in iter { + let Some((cpu, mem)) = params.get_mut(&CostType::Contract(*ty)) else { + continue; + }; + let cpu_const_diff_ratio = (cpu.const_term as f64 - first_cpu.const_term as f64) + / first_cpu.const_term as f64; + let mem_const_diff_ratio = (mem.const_term as f64 - first_mem.const_term as f64) + / first_mem.const_term as f64; + assert!( + cpu_const_diff_ratio < 0.25, + "cost type {:?} has too large a constant CPU term over {:?}: {:?} vs. {:?} ({:?} diff)", + ty, + first, + cpu.const_term, + first_cpu.const_term, + cpu_const_diff_ratio + ); + assert!( + mem_const_diff_ratio < 0.25, + "cost type {:?} has too large a constant memory term over {:?}: {:?} vs. {:?} ({:?} diff)", + ty, + first, + mem.const_term, + first_mem.const_term, + mem_const_diff_ratio + ); + cpu.const_term = 0; + mem.const_term = 0; + } + } + } +} + fn write_budget_params_code( params: &BTreeMap, wasm_tier_cost: &BTreeMap, @@ -335,12 +416,18 @@ fn extract_wasmi_fuel_costs( #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] fn main() -> std::io::Result<()> { - let params = if std::env::var("RUN_EXPERIMENT").is_err() { + let mut params = if std::env::var("RUN_EXPERIMENT").is_err() { for_each_host_cost_measurement::()? } else { for_each_experimental_cost_measurement::()? }; - let params_wasm = for_each_wasm_insn_measurement::()?; + let params_wasm = if std::env::var("SKIP_WASM_INSNS").is_err() { + BTreeMap::new() + } else { + for_each_wasm_insn_measurement::()? + }; + + correct_multi_variable_models(&mut params); let mut tw = TabWriter::new(vec![]) .padding(5) diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs index 7d23c6dc1..8dd5e465d 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -114,7 +114,7 @@ impl Default for BudgetTracker { #[cfg(feature = "next")] ContractCostType::ParseWasmMemoryPages => init_input(), #[cfg(feature = "next")] - ContractCostType::InstantiateWasmInstructions => init_input(), + ContractCostType::InstantiateWasmInstructions => (), #[cfg(feature = "next")] ContractCostType::InstantiateWasmFunctions => init_input(), #[cfg(feature = "next")] @@ -122,7 +122,7 @@ impl Default for BudgetTracker { #[cfg(feature = "next")] ContractCostType::InstantiateWasmTableEntries => init_input(), #[cfg(feature = "next")] - ContractCostType::InstantiateWasmTypes => init_input(), + ContractCostType::InstantiateWasmTypes => (), #[cfg(feature = "next")] ContractCostType::InstantiateWasmDataSegments => init_input(), #[cfg(feature = "next")] @@ -409,106 +409,106 @@ impl Default for BudgetImpl { cpu.const_term = 1058; cpu.lin_term = ScaledU64(501); } + #[cfg(feature = "next")] ContractCostType::ParseWasmInstructions => { - cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.const_term = 72736; + cpu.lin_term = ScaledU64(25420); } #[cfg(feature = "next")] ContractCostType::ParseWasmFunctions => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(536688); } #[cfg(feature = "next")] ContractCostType::ParseWasmGlobals => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(176902); } #[cfg(feature = "next")] ContractCostType::ParseWasmTableEntries => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(29639); } #[cfg(feature = "next")] ContractCostType::ParseWasmTypes => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(1048891); } #[cfg(feature = "next")] ContractCostType::ParseWasmDataSegments => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(236970); } #[cfg(feature = "next")] ContractCostType::ParseWasmElemSegments => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(317249); } #[cfg(feature = "next")] ContractCostType::ParseWasmImports => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(694667); } #[cfg(feature = "next")] ContractCostType::ParseWasmExports => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(427037); } #[cfg(feature = "next")] ContractCostType::ParseWasmMemoryPages => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(1475718); } - #[cfg(feature = "next")] ContractCostType::InstantiateWasmInstructions => { - cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.const_term = 25059; + cpu.lin_term = ScaledU64(0); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmFunctions => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(7503); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmGlobals => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(10761); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmTableEntries => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(3211); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmTypes => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(0); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmDataSegments => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(16370); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmElemSegments => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(28309); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmImports => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(683461); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmExports => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(297065); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmMemoryPages => { cpu.const_term = 0; - cpu.lin_term = ScaledU64(1); + cpu.lin_term = ScaledU64(738966); } } @@ -613,106 +613,107 @@ impl Default for BudgetImpl { mem.const_term = 0; mem.lin_term = ScaledU64(0); } + #[cfg(feature = "next")] ContractCostType::ParseWasmInstructions => { - mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.const_term = 17564; + mem.lin_term = ScaledU64(6457); } #[cfg(feature = "next")] ContractCostType::ParseWasmFunctions => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(47464); } #[cfg(feature = "next")] ContractCostType::ParseWasmGlobals => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(13420); } #[cfg(feature = "next")] ContractCostType::ParseWasmTableEntries => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(6285); } #[cfg(feature = "next")] ContractCostType::ParseWasmTypes => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(64670); } #[cfg(feature = "next")] ContractCostType::ParseWasmDataSegments => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(29074); } #[cfg(feature = "next")] ContractCostType::ParseWasmElemSegments => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(48095); } #[cfg(feature = "next")] ContractCostType::ParseWasmImports => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(102890); } #[cfg(feature = "next")] ContractCostType::ParseWasmExports => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(36394); } #[cfg(feature = "next")] ContractCostType::ParseWasmMemoryPages => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(16777245); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmInstructions => { - mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.const_term = 70192; + mem.lin_term = ScaledU64(0); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmFunctions => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(14613); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmGlobals => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(6833); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmTableEntries => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(1025); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmTypes => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(0); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmDataSegments => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(129632); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmElemSegments => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(13665); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmImports => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(77273); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmExports => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(9176); } #[cfg(feature = "next")] ContractCostType::InstantiateWasmMemoryPages => { mem.const_term = 0; - mem.lin_term = ScaledU64(1); + mem.lin_term = ScaledU64(8388609); } } } 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 fdd19cd9e..b08cc01ac 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 @@ -107,7 +107,7 @@ mod v21 { } macro_rules! impl_costrunner_for_instantiation_cost_type { - ($RUNNER:ty, $COST:ident) => { + ($RUNNER:ty, $COST:ident, $IS_CONST:expr) => { impl CostRunner for $RUNNER { const COST_TYPE: CostType = CostType::Contract($COST); @@ -133,7 +133,11 @@ mod v21 { _iter: u64, sample: Self::SampleType, ) -> Self::RecycledType { - black_box(host.charge_budget($COST, Some(0)).unwrap()); + if $IS_CONST { + black_box(host.charge_budget($COST, None).unwrap()); + } else { + black_box(host.charge_budget($COST, Some(0)).unwrap()); + } black_box((None, sample.wasm)) } } @@ -179,33 +183,59 @@ mod v21 { impl_costrunner_for_parse_cost_type!(ParseWasmExportsRun, ParseWasmExports); impl_costrunner_for_parse_cost_type!(ParseWasmMemoryPagesRun, ParseWasmMemoryPages); - impl_costrunner_for_instantiation_cost_type!(VmCachedInstantiationRun, VmCachedInstantiation); + impl_costrunner_for_instantiation_cost_type!( + VmCachedInstantiationRun, + VmCachedInstantiation, + false + ); impl_costrunner_for_instantiation_cost_type!( InstantiateWasmInstructionsRun, - InstantiateWasmInstructions + InstantiateWasmInstructions, + true ); impl_costrunner_for_instantiation_cost_type!( InstantiateWasmFunctionsRun, - InstantiateWasmFunctions + InstantiateWasmFunctions, + false + ); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmGlobalsRun, + InstantiateWasmGlobals, + false ); - impl_costrunner_for_instantiation_cost_type!(InstantiateWasmGlobalsRun, InstantiateWasmGlobals); impl_costrunner_for_instantiation_cost_type!( InstantiateWasmTableEntriesRun, - InstantiateWasmTableEntries + InstantiateWasmTableEntries, + false + ); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmTypesRun, + InstantiateWasmTypes, + true ); - impl_costrunner_for_instantiation_cost_type!(InstantiateWasmTypesRun, InstantiateWasmTypes); impl_costrunner_for_instantiation_cost_type!( InstantiateWasmDataSegmentsRun, - InstantiateWasmDataSegments + InstantiateWasmDataSegments, + false ); impl_costrunner_for_instantiation_cost_type!( InstantiateWasmElemSegmentsRun, - InstantiateWasmElemSegments + InstantiateWasmElemSegments, + false + ); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmImportsRun, + InstantiateWasmImports, + false + ); + impl_costrunner_for_instantiation_cost_type!( + InstantiateWasmExportsRun, + InstantiateWasmExports, + false ); - impl_costrunner_for_instantiation_cost_type!(InstantiateWasmImportsRun, InstantiateWasmImports); - impl_costrunner_for_instantiation_cost_type!(InstantiateWasmExportsRun, InstantiateWasmExports); impl_costrunner_for_instantiation_cost_type!( InstantiateWasmMemoryPagesRun, - InstantiateWasmMemoryPages + InstantiateWasmMemoryPages, + false ); } diff --git a/soroban-env-host/src/test/lifecycle.rs b/soroban-env-host/src/test/lifecycle.rs index 10ecd31be..c558a3e41 100644 --- a/soroban-env-host/src/test/lifecycle.rs +++ b/soroban-env-host/src/test/lifecycle.rs @@ -566,3 +566,384 @@ fn test_large_contract() { assert!(err.error.is_type(ScErrorType::Budget)); assert!(err.error.is_code(ScErrorCode::ExceededLimit)); } + +#[cfg(feature = "next")] +#[allow(dead_code)] +mod cap_54_55_56 { + + use super::*; + use crate::{ + storage::{FootprintMap, StorageMap}, + test::observe::ObservedHost, + testutils::wasm::wasm_module_with_a_bit_of_everything, + vm::ModuleCache, + xdr::{ + ContractCostType::{self, *}, + LedgerEntry, LedgerKey, + }, + AddressObject, HostError, + }; + use std::rc::Rc; + + const V_NEW: u32 = ModuleCache::MIN_LEDGER_VERSION; + const V_OLD: u32 = V_NEW - 1; + const NEW_COST_TYPES: &'static [ContractCostType] = &[ + ParseWasmInstructions, + ParseWasmFunctions, + ParseWasmGlobals, + ParseWasmTableEntries, + ParseWasmTypes, + ParseWasmDataSegments, + ParseWasmElemSegments, + ParseWasmImports, + ParseWasmExports, + ParseWasmMemoryPages, + InstantiateWasmInstructions, + InstantiateWasmFunctions, + InstantiateWasmGlobals, + InstantiateWasmTableEntries, + InstantiateWasmTypes, + InstantiateWasmDataSegments, + InstantiateWasmElemSegments, + InstantiateWasmImports, + InstantiateWasmExports, + InstantiateWasmMemoryPages, + ]; + + fn new_host_with_protocol_and_uploaded_contract( + hostname: &'static str, + proto: u32, + ) -> Result<(ObservedHost, AddressObject), HostError> { + let host = Host::test_host_with_recording_footprint(); + let host = ObservedHost::new(hostname, host); + host.with_mut_ledger_info(|ledger_info| ledger_info.protocol_version = proto)?; + let contract_addr_obj = + host.register_test_contract_wasm(&wasm_module_with_a_bit_of_everything(proto)); + Ok((host, contract_addr_obj)) + } + + struct ContractAndWasmEntries { + contract_key: Rc, + contract_entry: Rc, + wasm_key: Rc, + wasm_entry: Rc, + } + + impl ContractAndWasmEntries { + fn from_contract_addr( + host: &Host, + contract_addr_obj: AddressObject, + ) -> Result { + let contract_id = host.contract_id_from_address(contract_addr_obj)?; + Self::from_contract_id(host, contract_id) + } + fn reload(self, host: &Host) -> Result { + host.with_mut_storage(|storage| { + let budget = host.budget_cloned(); + let contract_entry = storage.get(&self.contract_key, &budget)?; + let wasm_entry = storage.get(&self.wasm_key, &budget)?; + Ok(ContractAndWasmEntries { + contract_key: self.contract_key, + contract_entry, + wasm_key: self.wasm_key, + wasm_entry, + }) + }) + } + fn from_contract_id(host: &Host, contract_id: Hash) -> Result { + let contract_key = host.contract_instance_ledger_key(&contract_id)?; + let wasm_hash = get_contract_wasm_ref(host, contract_id); + let wasm_key = host.contract_code_ledger_key(&wasm_hash)?; + + host.with_mut_storage(|storage| { + let budget = host.budget_cloned(); + let contract_entry = storage.get(&contract_key, &budget)?; + let wasm_entry = storage.get(&wasm_key, &budget)?; + Ok(ContractAndWasmEntries { + contract_key, + contract_entry, + wasm_key, + wasm_entry, + }) + }) + } + fn read_only_footprint(&self, budget: &Budget) -> Footprint { + Footprint( + FootprintMap::new() + .insert(self.contract_key.clone(), AccessType::ReadOnly, budget) + .unwrap() + .insert(self.wasm_key.clone(), AccessType::ReadOnly, budget) + .unwrap(), + ) + } + fn wasm_writing_footprint(&self, budget: &Budget) -> Footprint { + Footprint( + FootprintMap::new() + .insert(self.contract_key.clone(), AccessType::ReadOnly, budget) + .unwrap() + .insert(self.wasm_key.clone(), AccessType::ReadWrite, budget) + .unwrap(), + ) + } + fn storage_map(&self, budget: &Budget) -> StorageMap { + StorageMap::new() + .insert( + self.contract_key.clone(), + Some((self.contract_entry.clone(), Some(99999))), + budget, + ) + .unwrap() + .insert( + self.wasm_key.clone(), + Some((self.wasm_entry.clone(), Some(99999))), + budget, + ) + .unwrap() + } + fn read_only_storage(&self, budget: &Budget) -> Storage { + Storage::with_enforcing_footprint_and_map( + self.read_only_footprint(budget), + self.storage_map(budget), + ) + } + fn wasm_writing_storage(&self, budget: &Budget) -> Storage { + Storage::with_enforcing_footprint_and_map( + self.wasm_writing_footprint(budget), + self.storage_map(budget), + ) + } + } + + fn upload_and_get_contract_and_wasm_entries( + upload_hostname: &'static str, + upload_proto: u32, + ) -> Result { + let (host, contract) = + new_host_with_protocol_and_uploaded_contract(upload_hostname, upload_proto)?; + ContractAndWasmEntries::from_contract_addr(&host, contract) + } + + fn upload_and_call( + upload_hostname: &'static str, + upload_proto: u32, + call_hostname: &'static str, + call_proto: u32, + ) -> Result<(Budget, Storage), HostError> { + // Phase 1: upload contract, tear down host, "close the ledger" and possibly change protocol. + let (host, contract) = + new_host_with_protocol_and_uploaded_contract(upload_hostname, upload_proto)?; + let contract_id = host.contract_id_from_address(contract)?; + let realhost = host.clone(); + drop(host); + let (storage, _events) = realhost.try_finish()?; + + // Phase 2: build new host with previous ledger output as storage, call contract. Possibly on new protocol. + let host = Host::with_storage_and_budget(storage, Budget::default()); + host.enable_debug()?; + let host = ObservedHost::new(call_hostname, host); + host.set_ledger_info(LedgerInfo { + protocol_version: call_proto, + ..Default::default() + })?; + let contract = host.add_host_object(crate::xdr::ScAddress::Contract(contract_id))?; + let _ = host.call( + contract, + Symbol::try_from_small_str("test").unwrap(), + host.vec_new()?, + )?; + let realhost = host.clone(); + drop(host); + let budget = realhost.budget_cloned(); + let (storage, _events) = realhost.try_finish()?; + Ok((budget, storage)) + } + + fn code_entry_has_cost_inputs(entry: &Rc) -> bool { + match &entry.data { + LedgerEntryData::ContractCode(cce) => match &cce.ext { + crate::xdr::ContractCodeEntryExt::V1(_v1) => return true, + _ => (), + }, + _ => panic!("expected LedgerEntryData::ContractCode"), + } + false + } + + // Test that running on protocol vOld only charges the VmInstantiation cost + // type. + #[test] + fn test_v_old_only_charges_vm_instantiation() -> Result<(), HostError> { + let (budget, _storage) = upload_and_call( + "test_v_old_only_charges_vminstantiation_upload", + V_OLD, + "test_v_old_only_charges_vm_instantiation_call", + V_OLD, + )?; + assert_ne!(budget.get_tracker(VmInstantiation)?.cpu, 0); + assert_eq!(budget.get_tracker(VmCachedInstantiation)?.cpu, 0); + for ct in NEW_COST_TYPES { + assert_eq!(budget.get_tracker(*ct)?.cpu, 0); + } + Ok(()) + } + + // Test that running on protocol vNew on a ContractCode LE that does not have + // ContractCodeCostInputs charges the VmInstantiation and VmCachedInstantiation + // cost types. + #[test] + fn test_v_new_no_contract_code_cost_inputs() -> Result<(), HostError> { + let (budget, _storage) = upload_and_call( + "test_v_new_no_contract_code_cost_inputs_upload", + V_OLD, + "test_v_new_no_contract_code_cost_inputs_call", + V_NEW, + )?; + assert_ne!(budget.get_tracker(VmInstantiation)?.cpu, 0); + assert_ne!(budget.get_tracker(VmCachedInstantiation)?.cpu, 0); + for ct in NEW_COST_TYPES { + assert_eq!(budget.get_tracker(*ct)?.cpu, 0); + } + Ok(()) + } + + // Test that running on protocol vNew does add ContractCodeCostInputs to a + // newly uploaded ContractCode LE. + #[test] + fn test_v_new_gets_contract_code_cost_inputs() -> Result<(), HostError> { + let entries = upload_and_get_contract_and_wasm_entries( + "test_v_new_gets_contract_code_cost_inputs_upload", + V_NEW, + )?; + assert!(code_entry_has_cost_inputs(&entries.wasm_entry)); + Ok(()) + } + + // Test that running on protocol vNew on a ContractCode LE that does have + // ContractCodeCostInputs charges the new cost model types nonzero costs + // (both parsing and instantiation). + #[test] + fn test_v_new_with_contract_code_cost_inputs_causes_nonzero_costs() -> Result<(), HostError> { + let (budget, _storage) = upload_and_call( + "test_v_new_with_contract_code_cost_inputs_causes_nonzero_costs_upload", + V_NEW, + "test_v_new_with_contract_code_cost_inputs_causes_nonzero_costs_call", + V_NEW, + )?; + for (k, v) in _storage.map.iter(&budget)? { + dbg!(k, v); + } + assert_eq!(budget.get_tracker(VmInstantiation)?.cpu, 0); + assert_eq!(budget.get_tracker(VmCachedInstantiation)?.cpu, 0); + for ct in NEW_COST_TYPES { + dbg!(ct); + if *ct == InstantiateWasmTypes { + // This is a zero-cost type in the current calibration of the + // new model -- and exceptional case in this test -- though we + // keep it in case it becomes nonzero at some point (it's + // credible that it would). + continue; + } + assert_ne!(budget.get_tracker(*ct)?.cpu, 0); + } + Ok(()) + } + + // Test that running on protocol vOld does not add ContractCodeCostInputs to a + // newly uploaded ContractCode LE. + #[test] + fn test_v_old_no_contract_code_cost_inputs() -> Result<(), HostError> { + let entries = upload_and_get_contract_and_wasm_entries( + "test_v_old_no_contract_code_cost_inputs_upload", + V_OLD, + )?; + assert!(!code_entry_has_cost_inputs(&entries.wasm_entry)); + Ok(()) + } + + // Test that running on protocol vOld does not rewrite a ContractCode LE when it + // already exists. + #[test] + fn test_v_old_no_rewrite() -> Result<(), HostError> { + let entries = + upload_and_get_contract_and_wasm_entries("test_v_old_no_rewrite_upload", V_OLD)?; + // make a new storage map for a new run + let budget = Budget::default(); + let storage = entries.read_only_storage(&budget); + let host = Host::with_storage_and_budget(storage, budget); + let host = ObservedHost::new("test_v_old_no_rewrite_call", host); + host.set_ledger_info(LedgerInfo { + protocol_version: V_OLD, + ..Default::default() + })?; + host.upload_contract_wasm(wasm_module_with_a_bit_of_everything(V_OLD))?; + Ok(()) + } + + // Test that running on protocol vNew does rewrite a ContractCode LE when it + // already exists but doesn't yet have ContractCodeCostInputs. + #[test] + fn test_v_new_rewrite() -> Result<(), HostError> { + let entries = upload_and_get_contract_and_wasm_entries("test_v_new_rewrite_upload", V_OLD)?; + assert!(!code_entry_has_cost_inputs(&entries.wasm_entry)); + + // make a new storage map for a new upload but with read-only footprint -- this should fail + let budget = Budget::default(); + let storage = entries.read_only_storage(&budget); + let host = Host::with_storage_and_budget(storage, budget); + let host = ObservedHost::new("test_v_new_rewrite_call_fail", host); + host.set_ledger_info(LedgerInfo { + protocol_version: V_NEW, + ..Default::default() + })?; + let wasm_blob = match &entries.wasm_entry.data { + LedgerEntryData::ContractCode(cce) => cce.code.to_vec(), + _ => panic!("expected ContractCode"), + }; + assert!(host.upload_contract_wasm(wasm_blob.clone()).is_err()); + let entries = entries.reload(&host)?; + assert!(!code_entry_has_cost_inputs(&entries.wasm_entry)); + + // make a new storage map for a new upload but with read-write footprint -- this should pass + let budget = Budget::default(); + let storage = entries.wasm_writing_storage(&budget); + let host = Host::with_storage_and_budget(storage, budget); + let host = ObservedHost::new("test_v_new_rewrite_call_succeed", host); + host.set_ledger_info(LedgerInfo { + protocol_version: V_NEW, + ..Default::default() + })?; + host.upload_contract_wasm(wasm_blob)?; + let entries = entries.reload(&host)?; + assert!(code_entry_has_cost_inputs(&entries.wasm_entry)); + + Ok(()) + } + + // Test that running on protocol vNew does not rewrite a ContractCode LE when it + // already exists and already has ContractCodeCostInputs. + #[test] + fn test_v_new_no_rewrite() -> Result<(), HostError> { + let entries = + upload_and_get_contract_and_wasm_entries("test_v_new_no_rewrite_upload", V_NEW)?; + assert!(code_entry_has_cost_inputs(&entries.wasm_entry)); + + // make a new storage map for a new upload but with read-only footprint -- this should pass + let budget = Budget::default(); + let storage = entries.read_only_storage(&budget); + let host = Host::with_storage_and_budget(storage, budget); + let host = ObservedHost::new("test_v_new_no_rewrite_call_pass", host); + host.set_ledger_info(LedgerInfo { + protocol_version: V_NEW, + ..Default::default() + })?; + let wasm_blob = match &entries.wasm_entry.data { + LedgerEntryData::ContractCode(cce) => cce.code.to_vec(), + _ => panic!("expected ContractCode"), + }; + host.upload_contract_wasm(wasm_blob.clone())?; + let entries = entries.reload(&host)?; + assert!(code_entry_has_cost_inputs(&entries.wasm_entry)); + + Ok(()) + } +} diff --git a/soroban-env-host/src/testutils.rs b/soroban-env-host/src/testutils.rs index 5e914e194..82b826a4e 100644 --- a/soroban-env-host/src/testutils.rs +++ b/soroban-env-host/src/testutils.rs @@ -1110,4 +1110,30 @@ pub(crate) mod wasm { fe.call_func(f0); fe.finish_and_export("test").finish() } + + #[cfg(feature = "next")] + pub(crate) fn wasm_module_with_a_bit_of_everything(wasm_proto: u32) -> Vec { + let mut me = ModEmitter::new(); + let pre = get_pre_release_version(INTERFACE_VERSION); + me.custom_section( + &"contractenvmetav0", + interface_meta_with_custom_versions(wasm_proto, pre).as_slice(), + ); + me.table(RefType::FUNCREF, 128, None); + me.memory(1, None, false, false); + me.global(wasm_encoder::ValType::I64, true, &ConstExpr::i64_const(42)); + me.export("memory", wasm_encoder::ExportKind::Memory, 0); + let _f0 = me.import_func("t", "_", Arity(0)); + let mut fe = me.func(Arity(0), 0); + fe.push(Operand::Const64(1)); + fe.push(Operand::Const64(2)); + fe.i64_add(); + fe.drop(); + fe.push(Symbol::try_from_small_str("pass").unwrap()); + let (mut me, fid) = fe.finish(); + me.export_func(fid, "test"); + me.define_elem_funcs(&[fid]); + me.define_data_segment(0x1234, vec![0; 8]); + me.finish() + } } diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 13ad6cccf..74bd8e4a0 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -45,6 +45,7 @@ use wasmi::{Caller, StoreContextMut}; impl wasmi::core::HostError for HostError {} const MAX_VM_ARGS: usize = 32; +#[cfg(feature = "next")] const WASM_STD_MEM_PAGE_SIZE_IN_BYTES: u32 = 0x10000; struct VmInstantiationTimer { diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 4c28b28f7..3932de691 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -1,14 +1,16 @@ use crate::{ err, meta::{self, get_ledger_protocol_version}, - vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES, xdr::{ContractCostType, Limited, ReadXdr, ScEnvMetaEntry, ScErrorCode, ScErrorType}, Host, HostError, DEFAULT_XDR_RW_LIMITS, }; +#[cfg(feature = "next")] +use crate::vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES; + use wasmi::{Engine, Module}; -use super::MAX_VM_ARGS; +use super::{ModuleCache, MAX_VM_ARGS}; use std::io::Cursor; #[derive(Debug, Clone)] @@ -82,17 +84,24 @@ impl VersionedContractCodeCostInputs { pub fn charge_for_instantiation(&self, _host: &Host) -> Result<(), HostError> { match self { Self::V0 { wasm_bytes } => { - _host.charge_budget( - ContractCostType::VmCachedInstantiation, - Some(*wasm_bytes as u64), - )?; + // Before soroban supported cached instantiation, the full cost + // of parsing-and-instantiation was charged to the + // VmInstantiation cost type and we already charged it by the + // time we got here, in `charge_for_parsing` above. At-and-after + // the protocol that enabled cached instantiation, the + // VmInstantiation cost type was repurposed to only cover the + // cost of parsing, so we have to charge the "second half" cost + // of instantiaiton separately here. + if _host.get_ledger_protocol_version()? >= ModuleCache::MIN_LEDGER_VERSION { + _host.charge_budget( + ContractCostType::VmCachedInstantiation, + Some(*wasm_bytes as u64), + )?; + } } #[cfg(feature = "next")] Self::V1(inputs) => { - _host.charge_budget( - ContractCostType::InstantiateWasmInstructions, - Some(inputs.n_instructions as u64), - )?; + _host.charge_budget(ContractCostType::InstantiateWasmInstructions, None)?; _host.charge_budget( ContractCostType::InstantiateWasmFunctions, Some(inputs.n_functions as u64), @@ -105,10 +114,7 @@ impl VersionedContractCodeCostInputs { ContractCostType::InstantiateWasmTableEntries, Some(inputs.n_table_entries as u64), )?; - _host.charge_budget( - ContractCostType::InstantiateWasmTypes, - Some(inputs.n_types as u64), - )?; + _host.charge_budget(ContractCostType::InstantiateWasmTypes, None)?; _host.charge_budget( ContractCostType::InstantiateWasmDataSegments, Some(inputs.n_data_segments as u64), @@ -475,8 +481,7 @@ impl ParsedModule { } 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()); + costs.n_elem_segments = costs.n_elem_segments.saturating_add(s.count()); for elem in s { let elem = host.map_err(elem)?; match elem.kind { @@ -486,8 +491,11 @@ impl ParsedModule { } } match elem.items { - ElementItems::Functions(_) => (), + ElementItems::Functions(fs) => { + elements = elements.saturating_add(fs.count()); + } ElementItems::Expressions(_, exprs) => { + elements = elements.saturating_add(exprs.count()); for expr in exprs { let expr = host.map_err(expr)?; Self::check_const_expr_simple(&host, &expr)?; @@ -497,7 +505,7 @@ impl ParsedModule { } } DataSection(s) => { - costs.n_data_segments = costs.n_data_segments.saturating_add(1); + costs.n_data_segments = costs.n_data_segments.saturating_add(s.count()); for d in s { let d = host.map_err(d)?; if d.data.len() > u32::MAX as usize {