From 6081e2478059af2b946e45f7b87339329dd3018e Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 29 Feb 2024 01:45:24 -0800 Subject: [PATCH 01/16] Sketch of cheaper instantiation change --- Cargo.lock | 7 +- Cargo.toml | 13 +- soroban-env-common/Cargo.toml | 3 +- soroban-env-common/src/error.rs | 7 + soroban-env-host/Cargo.toml | 3 +- .../benches/common/cost_types/invoke.rs | 2 +- .../benches/common/cost_types/vm_ops.rs | 173 ++++++++-- .../common/cost_types/wasm_insn_exec.rs | 149 +++++++- .../benches/common/experimental/vm_ops.rs | 5 +- soroban-env-host/benches/common/measure.rs | 9 +- soroban-env-host/benches/common/mod.rs | 11 + soroban-env-host/benches/common/util.rs | 3 + .../benches/worst_case_linear_models.rs | 30 +- soroban-env-host/src/budget.rs | 107 +++++- soroban-env-host/src/budget/dimension.rs | 14 +- soroban-env-host/src/budget/model.rs | 4 +- .../src/cost_runner/cost_types/vm_ops.rs | 93 ++++- soroban-env-host/src/host/data_helper.rs | 27 +- soroban-env-host/src/host/frame.rs | 4 +- soroban-env-host/src/host/lifecycle.rs | 26 +- soroban-env-host/src/test/basic.rs | 2 +- soroban-env-host/src/vm.rs | 317 +++++++++++++++++- soroban-env-host/src/vm/func_info.rs | 30 +- soroban-synth-wasm/Cargo.toml | 2 +- soroban-synth-wasm/src/mod_emitter.rs | 6 + 25 files changed, 927 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef16fdd40..577dc697a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1366,6 +1366,7 @@ dependencies = [ "static_assertions", "stellar-xdr", "tracy-client", + "wasmparser", ] [[package]] @@ -1420,6 +1421,7 @@ dependencies = [ "thousands", "tracy-client", "wasm-encoder", + "wasmparser", "wasmprinter", ] @@ -1470,7 +1472,6 @@ version = "20.3.0" [[package]] name = "soroban-wasmi" version = "0.31.1-soroban.20.0.1" -source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "smallvec", "spin", @@ -1515,7 +1516,7 @@ dependencies = [ [[package]] name = "stellar-xdr" version = "20.1.0" -source = "git+https://github.com/stellar/rs-stellar-xdr?rev=2e0f7f7d42fcd6c3c42eb0d65570fba9f5193d7e#2e0f7f7d42fcd6c3c42eb0d65570fba9f5193d7e" +source = "git+https://github.com/stellar/rs-stellar-xdr?rev=44b7e2d4cdf27a3611663e82828de56c5274cba0#44b7e2d4cdf27a3611663e82828de56c5274cba0" dependencies = [ "arbitrary", "base64 0.13.1", @@ -1857,12 +1858,10 @@ dependencies = [ [[package]] name = "wasmi_arena" version = "0.4.0" -source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" [[package]] name = "wasmi_core" version = "0.13.0" -source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "downcast-rs", "libm", diff --git a/Cargo.toml b/Cargo.toml index 1e74df1e2..3e5fee548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,22 +29,25 @@ soroban-env-guest = { version = "=20.3.0", path = "soroban-env-guest" } soroban-env-host = { version = "=20.3.0", path = "soroban-env-host" } soroban-env-macros = { version = "=20.3.0", path = "soroban-env-macros" } soroban-builtin-sdk-macros = { version = "=20.3.0", path = "soroban-builtin-sdk-macros" } +# NB: this must match the wasmparser version wasmi is using +wasmparser = "=0.116.1" # NB: When updating, also update the version in rs-soroban-env dev-dependencies [workspace.dependencies.stellar-xdr] version = "=20.1.0" git = "https://github.com/stellar/rs-stellar-xdr" -rev = "2e0f7f7d42fcd6c3c42eb0d65570fba9f5193d7e" +rev = "44b7e2d4cdf27a3611663e82828de56c5274cba0" default-features = false [workspace.dependencies.wasmi] package = "soroban-wasmi" version = "=0.31.1-soroban.20.0.1" -git = "https://github.com/stellar/wasmi" -rev = "0ed3f3dee30dc41ebe21972399e0a73a41944aa0" +path = "/src/wasmi/crates/wasmi/" +#git = "https://github.com/stellar/wasmi" +#rev = "0ed3f3dee30dc41ebe21972399e0a73a41944aa0" -# [patch."https://github.com/stellar/rs-stellar-xdr"] -# stellar-xdr = { path = "../rs-stellar-xdr/" } +#[patch."https://github.com/stellar/rs-stellar-xdr"] +# stellar-xdr = { path = "/src/rs-stellar-xdr/" } # [patch."https://github.com/stellar/wasmi"] # soroban-wasmi = { path = "../wasmi/crates/wasmi/" } # soroban-wasmi_core = { path = "../wasmi/crates/core/" } diff --git a/soroban-env-common/Cargo.toml b/soroban-env-common/Cargo.toml index 7d058e7f8..57428f373 100644 --- a/soroban-env-common/Cargo.toml +++ b/soroban-env-common/Cargo.toml @@ -17,6 +17,7 @@ crate-git-revision = "=0.0.6" soroban-env-macros = { workspace = true } stellar-xdr = { workspace = true, default-features = false, features = [ "curr" ] } wasmi = { workspace = true, optional = true } +wasmparser = { workspace = true, optional = true} serde = { version = "=1.0.192", features = ["derive"], optional = true } static_assertions = "=1.1.0" ethnum = "=1.5.0" @@ -34,7 +35,7 @@ num-traits = "=0.2.17" [features] std = ["stellar-xdr/std", "stellar-xdr/base64"] serde = ["dep:serde", "stellar-xdr/serde"] -wasmi = ["dep:wasmi"] +wasmi = ["dep:wasmi", "dep:wasmparser"] testutils = ["dep:arbitrary", "stellar-xdr/arbitrary"] next = ["stellar-xdr/next", "soroban-env-macros/next"] tracy = ["dep:tracy-client"] diff --git a/soroban-env-common/src/error.rs b/soroban-env-common/src/error.rs index 6e83b35c4..e967d4615 100644 --- a/soroban-env-common/src/error.rs +++ b/soroban-env-common/src/error.rs @@ -277,6 +277,13 @@ impl From for Error { } } +#[cfg(feature = "wasmi")] +impl From for Error { + fn from(_: wasmparser::BinaryReaderError) -> Self { + Error::from_type_and_code(ScErrorType::WasmVm, ScErrorCode::InvalidInput) + } +} + impl Error { // NB: we don't provide a "get_type" to avoid casting a bad bit-pattern into // an ScErrorType. Instead we provide an "is_type" to check any specific diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml index 2276dc27a..b9a5318f2 100644 --- a/soroban-env-host/Cargo.toml +++ b/soroban-env-host/Cargo.toml @@ -17,6 +17,7 @@ exclude = ["observations/"] soroban-builtin-sdk-macros = { workspace = true } soroban-env-common = { workspace = true, features = ["std", "wasmi", "shallow-val-hash"] } wasmi = { workspace = true } +wasmparser = { workspace = true } stellar-strkey = "=0.0.8" static_assertions = "=1.1.0" sha2 = "=0.10.8" @@ -86,7 +87,7 @@ rustversion = "1.0" [dev-dependencies.stellar-xdr] version = "=20.1.0" git = "https://github.com/stellar/rs-stellar-xdr" -rev = "2e0f7f7d42fcd6c3c42eb0d65570fba9f5193d7e" +rev = "44b7e2d4cdf27a3611663e82828de56c5274cba0" default-features = false features = ["arbitrary"] diff --git a/soroban-env-host/benches/common/cost_types/invoke.rs b/soroban-env-host/benches/common/cost_types/invoke.rs index d26a92b48..51f0b96ae 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).unwrap(); + let vm = Vm::new(&host, id, &code, None).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 d011ace8f..e31c0bfe7 100644 --- a/soroban-env-host/benches/common/cost_types/vm_ops.rs +++ b/soroban-env-host/benches/common/cost_types/vm_ops.rs @@ -1,45 +1,160 @@ #[allow(unused)] -use super::wasm_insn_exec::{wasm_module_with_4n_insns, wasm_module_with_n_internal_funcs}; +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 crate::common::{util, HostCostMeasurement}; use rand::{rngs::StdRng, Rng}; use soroban_env_host::{ - cost_runner::{VmInstantiationRun, VmInstantiationSample}, - xdr, Host, + cost_runner::{ + VmInstantiationDataSegmentsRun, VmInstantiationElemSegmentsRun, VmInstantiationExportsRun, + VmInstantiationFunctionsRun, VmInstantiationGlobalsRun, VmInstantiationImportsRun, + VmInstantiationInstructionsRun, VmInstantiationRun, VmInstantiationSample, + VmInstantiationTableEntriesRun, VmInstantiationTypesRun, + }, + xdr, Host, Vm, }; 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. -impl HostCostMeasurement for VmInstantiationMeasure { - type Runner = VmInstantiationRun; +macro_rules! impl_measurement_for_instantiation_cost_type { + ($RUNNER:ty, $MEASURE:ty, $BUILD:ident, $HAS_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(); - VmInstantiationSample { id: Some(id), wasm } - } + 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(); + VmInstantiationSample { + id: Some(id), + wasm, + cost_inputs: None, + } + } - fn new_worst_case(_host: &Host, _rng: &mut StdRng, input: u64) -> VmInstantiationSample { - let id: xdr::Hash = [0; 32].into(); - // generate a test wasm contract with many trivial internal functions, - // which represents the worst case in terms of work needed for WASM parsing. - let n = (Self::INPUT_BASE_SIZE + input * 30) as usize; - let wasm = wasm_module_with_n_internal_funcs(n); - // replace the above two lines with these below to test with wasm contracts - // with a single function of many instructions. In both tests the cpu grows - // linearly with the contract size however the slopes are very different. - // let n = (input * 50) as usize; - // let wasm = wasm_module_with_4n_insns(n); - VmInstantiationSample { id: Some(id), wasm } - } + 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 + }; + VmInstantiationSample { + id: Some(id), + wasm, + cost_inputs, + } + } - fn new_random_case(_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 = util::TEST_WASMS[idx].into(); - VmInstantiationSample { id: Some(id), wasm } - } + fn new_random_case( + 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 + }; + VmInstantiationSample { + id: Some(id), + wasm, + cost_inputs, + } + } + } + }; } + +impl_measurement_for_instantiation_cost_type!( + VmInstantiationRun, + VmInstantiationMeasure, + wasm_module_with_n_internal_funcs, + false, + 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 +); 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 4453462b6..d54f4435b 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 @@ -2,6 +2,7 @@ use crate::common::HostCostMeasurement; use rand::{rngs::StdRng, RngCore}; use soroban_env_host::{cost_runner::*, xdr::Hash, Host, Symbol, Vm}; use soroban_synth_wasm::{Arity, GlobalRef, ModEmitter, Operand}; +use wasm_encoder::{ConstExpr, ExportKind, ValType}; // These are fp numbers to minimize rounding during overhead calculation. // The fact they both turned out to be "whole" numbers is pure luck. @@ -25,7 +26,9 @@ pub fn wasm_module_with_n_internal_funcs(n: usize) -> Vec { fe.finish_and_export("test").finish() } -pub fn wasm_module_with_4n_insns(n: usize) -> Vec { +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 arg = fe.args[0]; fe.push(Operand::Const64(1)); @@ -39,6 +42,142 @@ pub fn wasm_module_with_4n_insns(n: usize) -> Vec { fe.push(Symbol::try_from_small_str("pass").unwrap()); fe.finish_and_export("test").finish() } +pub fn wasm_module_with_n_globals(n: usize) -> Vec { + let mut me = ModEmitter::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() +} + +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) { + if *module == "t" { + continue; + } + 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() +} + +pub fn wasm_module_with_n_exports(n: usize) -> Vec { + let me = ModEmitter::default(); + let mut fe = me.func(Arity(0), 0); + fe.push(Symbol::try_from_small_str("pass").unwrap()); + let (mut me, fid) = fe.finish(); + for i in 0..n { + me.export(format!("_{i}").as_str(), ExportKind::Func, fid.0); + } + me.finish() +} + +pub fn wasm_module_with_n_table_entries(n: usize) -> Vec { + 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(); + let funcs = vec![f; n]; + me.define_elem_funcs(&funcs); + me.finish() +} + +pub fn wasm_module_with_n_types(mut n: usize) -> Vec { + let mut me = ModEmitter::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. + let val_types = &[ValType::I32, ValType::I64]; + + 'top: for a in val_types { + for b in val_types { + for c in val_types { + for d in val_types { + for e in val_types { + for f in val_types { + for g in val_types { + for h in val_types { + for i in val_types { + for j in val_types { + for aa in val_types { + for bb in val_types { + for cc in val_types { + for dd in val_types { + for ee in val_types { + for ff in val_types { + for gg in val_types { + for hh in val_types { + for ii in val_types { + for jj in val_types + { + if n == 0 { + break 'top; + } + n -= 1; + let params = &[ + *a, *b, *c, + *d, *e, *f, + *g, *h, *i, + *j, *aa, + *bb, *cc, + *dd, *ee, + *ff, *gg, + *hh, *ii, + *jj, + ]; + me.add_raw_fn_type(params, &[]); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + let mut fe = me.func(Arity(0), 0); + fe.push(Symbol::try_from_small_str("pass").unwrap()); + fe.finish_and_export("test").finish() +} + +pub fn wasm_module_with_n_elem_segments(n: usize) -> Vec { + 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(); + for _ in 0..n { + me.define_elem_funcs(&[f]); + } + me.finish() +} + +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(); + for _ in 0..n { + me.define_data_segment(n as u32 * 1024, vec![1, 2, 3, 4]); + } + me.finish() +} fn wasm_module_with_mem_grow(n_pages: usize) -> Vec { let mut fe = ModEmitter::default().func(Arity(0), 0); @@ -401,7 +540,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).unwrap(); + let vm = Vm::new(&host, id, &module.wasm, None).unwrap(); WasmInsnSample { vm, insns, @@ -412,7 +551,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).unwrap(); + let vm = Vm::new(&host, id, &module.wasm, None).unwrap(); WasmInsnSample { vm, insns: 0, @@ -439,14 +578,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).unwrap(); + let vm = Vm::new(&host, id, &module.wasm, None).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).unwrap(); + let vm = Vm::new(&host, id, &module.wasm, None).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 943a73d61..a970ec179 100644 --- a/soroban-env-host/benches/common/experimental/vm_ops.rs +++ b/soroban-env-host/benches/common/experimental/vm_ops.rs @@ -1,5 +1,4 @@ #[allow(unused)] -use super::wasm_insn_exec::{wasm_module_with_4n_insns, wasm_module_with_n_internal_funcs}; use crate::common::{util, HostCostMeasurement}; use rand::{rngs::StdRng, Rng, RngCore}; use soroban_env_host::{ @@ -19,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).unwrap(); + let vm = Vm::new(&host, id, &code, None).unwrap(); VmMemRunSample { vm, buf } } } @@ -37,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).unwrap(); + let vm = Vm::new(&host, id, &code, None).unwrap(); VmMemRunSample { vm, buf } } } diff --git a/soroban-env-host/benches/common/measure.rs b/soroban-env-host/benches/common/measure.rs index f60c51b4e..51f54300b 100644 --- a/soroban-env-host/benches/common/measure.rs +++ b/soroban-env-host/benches/common/measure.rs @@ -68,10 +68,11 @@ impl Measurements { .iter() .map(|m| { let mut e = m.clone(); - e.inputs = e.inputs.map(|i| i / e.iterations); - e.cpu_insns = e.cpu_insns.saturating_sub(self.baseline.cpu_insns) / e.iterations; - e.mem_bytes = e.mem_bytes.saturating_sub(self.baseline.mem_bytes) / e.iterations; - e.time_nsecs = e.time_nsecs.saturating_sub(self.baseline.time_nsecs) / e.iterations; + let iterations = e.iterations.max(1); + e.inputs = e.inputs.map(|i| i / iterations); + e.cpu_insns = e.cpu_insns.saturating_sub(self.baseline.cpu_insns) / iterations; + e.mem_bytes = e.mem_bytes.saturating_sub(self.baseline.mem_bytes) / iterations; + e.time_nsecs = e.time_nsecs.saturating_sub(self.baseline.time_nsecs) / iterations; e }) .collect() diff --git a/soroban-env-host/benches/common/mod.rs b/soroban-env-host/benches/common/mod.rs index 0d981a384..465edab84 100644 --- a/soroban-env-host/benches/common/mod.rs +++ b/soroban-env-host/benches/common/mod.rs @@ -82,6 +82,17 @@ 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)?; + 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/benches/common/util.rs b/soroban-env-host/benches/common/util.rs index a41c222f7..e3e70a781 100644 --- a/soroban-env-host/benches/common/util.rs +++ b/soroban-env-host/benches/common/util.rs @@ -10,6 +10,9 @@ pub(crate) fn test_host() -> Host { .unwrap(); host.as_budget().reset_unlimited().unwrap(); host.as_budget().reset_fuel_config().unwrap(); + if std::env::var("DEBUG_BENCH_HOST").is_ok() { + host.enable_debug().unwrap(); + } host } diff --git a/soroban-env-host/benches/worst_case_linear_models.rs b/soroban-env-host/benches/worst_case_linear_models.rs index 00c06337f..d8a3c84c0 100644 --- a/soroban-env-host/benches/worst_case_linear_models.rs +++ b/soroban-env-host/benches/worst_case_linear_models.rs @@ -295,22 +295,24 @@ fn process_tier( params_wasm: &BTreeMap, insn_tier: &[WasmInsnType], ) -> u64 { - println!("\n"); - println!("\n{:=<100}", ""); - println!("\"{:?}\" tier", tier); - let (params_tier, ave_cpu_per_fuel) = extract_tier(params_wasm, insn_tier); - let mut tw = TabWriter::new(vec![]) - .padding(5) - .alignment(Alignment::Right); - write_cost_params_table::(&mut tw, ¶ms_tier).unwrap(); - eprintln!("{}", String::from_utf8(tw.into_inner().unwrap()).unwrap()); - println!( - "average cpu insns per fuel for \"{:?}\" tier: {}", - tier, ave_cpu_per_fuel - ); - println!("{:=<100}\n", ""); + if !params_tier.is_empty() { + println!("\n"); + println!("\n{:=<100}", ""); + println!("\"{:?}\" tier", tier); + + let mut tw = TabWriter::new(vec![]) + .padding(5) + .alignment(Alignment::Right); + write_cost_params_table::(&mut tw, ¶ms_tier).unwrap(); + eprintln!("{}", String::from_utf8(tw.into_inner().unwrap()).unwrap()); + println!( + "average cpu insns per fuel for \"{:?}\" tier: {}", + tier, ave_cpu_per_fuel + ); + println!("{:=<100}\n", ""); + } ave_cpu_per_fuel } diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs index 87ad7cd43..e113056d9 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -23,7 +23,7 @@ use crate::{ use dimension::{BudgetDimension, IsCpu, IsShadowMode}; -#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct CostTracker { pub iterations: u64, pub inputs: Option, @@ -46,11 +46,11 @@ struct BudgetTracker { impl Default for BudgetTracker { fn default() -> Self { let mut mt = Self { - cost_tracker: Default::default(), + cost_tracker: [CostTracker::default(); ContractCostType::variants().len()], meter_count: Default::default(), #[cfg(any(test, feature = "testutils", feature = "bench"))] wasm_memory: Default::default(), - time_tracker: Default::default(), + time_tracker: [0_u64; ContractCostType::variants().len()], }; for (ct, tracker) in ContractCostType::variants() .iter() @@ -92,6 +92,17 @@ 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(), } } mt @@ -368,6 +379,50 @@ impl Default for BudgetImpl { cpu.const_term = 1058; cpu.lin_term = ScaledU64(501); } + ContractCostType::VmInstantiateUnknownBytes => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateInstructions => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateFunctions => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateGlobals => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateTableEntries => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateTypes => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateDataSegments => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateElemSegments => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateImports => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateExports => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateMemoryPages => { + cpu.const_term = 0; + cpu.lin_term = ScaledU64(1); + } } // define the memory cost model parameters @@ -471,6 +526,50 @@ impl Default for BudgetImpl { mem.const_term = 0; mem.lin_term = ScaledU64(0); } + ContractCostType::VmInstantiateUnknownBytes => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateInstructions => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateFunctions => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateGlobals => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateTableEntries => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateTypes => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateDataSegments => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateElemSegments => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateImports => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateExports => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } + ContractCostType::VmInstantiateMemoryPages => { + mem.const_term = 0; + mem.lin_term = ScaledU64(1); + } } } @@ -790,7 +889,7 @@ impl Budget { .tracker .cost_tracker .get(ty as usize) - .map(|x| x.clone()) + .map(|x| *x) .ok_or_else(|| (ScErrorType::Budget, ScErrorCode::InternalError).into()) } diff --git a/soroban-env-host/src/budget/dimension.rs b/soroban-env-host/src/budget/dimension.rs index 326b9d667..75a62a081 100644 --- a/soroban-env-host/src/budget/dimension.rs +++ b/soroban-env-host/src/budget/dimension.rs @@ -8,7 +8,7 @@ use core::fmt::Debug; pub(crate) struct IsCpu(pub(crate) bool); pub(crate) struct IsShadowMode(pub(crate) bool); -#[derive(Clone, Default)] +#[derive(Clone)] pub(crate) struct BudgetDimension { /// A set of cost models that map input values (eg. event counts, object /// sizes) from some CostType to whatever concrete resource type is being @@ -36,6 +36,18 @@ pub(crate) struct BudgetDimension { pub(crate) shadow_total_count: u64, } +impl Default for BudgetDimension { + fn default() -> Self { + Self { + cost_models: [MeteredCostComponent::default(); ContractCostType::variants().len()], + limit: Default::default(), + total_count: Default::default(), + shadow_limit: Default::default(), + shadow_total_count: Default::default(), + } + } +} + impl Debug for BudgetDimension { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!( diff --git a/soroban-env-host/src/budget/model.rs b/soroban-env-host/src/budget/model.rs index 39b9c20df..4cda325f1 100644 --- a/soroban-env-host/src/budget/model.rs +++ b/soroban-env-host/src/budget/model.rs @@ -38,7 +38,7 @@ pub trait HostCostModel { const COST_MODEL_LIN_TERM_SCALE_BITS: u32 = 7; /// A helper type that wraps an u64 to signify the wrapped value have been scaled. -#[derive(Clone, Default, Debug)] +#[derive(Clone, Copy, Default, Debug)] pub struct ScaledU64(pub(crate) u64); impl ScaledU64 { @@ -81,7 +81,7 @@ impl From for ScaledU64 { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct MeteredCostComponent { pub const_term: u64, pub lin_term: ScaledU64, 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 b30829405..0ea68bfee 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,39 +1,96 @@ use crate::{ cost_runner::{CostRunner, CostType}, - xdr::ContractCostType::VmInstantiation, xdr::Hash, + xdr::{ + ContractCodeCostInputs, + ContractCostType::{ + VmInstantiateDataSegments, VmInstantiateElemSegments, VmInstantiateExports, + VmInstantiateFunctions, VmInstantiateGlobals, VmInstantiateImports, + VmInstantiateInstructions, VmInstantiateTableEntries, VmInstantiateTypes, + VmInstantiation, + }, + }, 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, } -impl CostRunner for VmInstantiationRun { - const COST_TYPE: CostType = CostType::Contract(VmInstantiation); +macro_rules! impl_costrunner_for_instantiation_cost_type { + ($RUNNER:ty, $COST:ident) => { + impl CostRunner for $RUNNER { + const COST_TYPE: CostType = CostType::Contract($COST); - const RUN_ITERATIONS: u64 = 10; + const RUN_ITERATIONS: u64 = 10; - type SampleType = VmInstantiationSample; + type SampleType = VmInstantiationSample; - type RecycledType = (Option>, Vec); + type RecycledType = (Option>, Vec); - fn run_iter(host: &crate::Host, _iter: u64, sample: Self::SampleType) -> Self::RecycledType { - let vm = black_box(Vm::new(host, sample.id.unwrap(), &sample.wasm[..]).unwrap()); - (Some(vm), sample.wasm) - } + fn run_iter( + host: &crate::Host, + _iter: u64, + sample: Self::SampleType, + ) -> Self::RecycledType { + let vm = black_box( + Vm::new( + host, + sample.id.unwrap(), + &sample.wasm[..], + sample.cost_inputs.clone(), + ) + .unwrap(), + ); + (Some(vm), sample.wasm) + } - fn run_baseline_iter( - host: &crate::Host, - _iter: u64, - sample: Self::SampleType, - ) -> Self::RecycledType { - black_box(host.charge_budget(VmInstantiation, Some(0)).unwrap()); - black_box((None, 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)) + } + } + }; } + +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); diff --git a/soroban-env-host/src/host/data_helper.rs b/soroban-env-host/src/host/data_helper.rs index 89d6858b4..2f5a496e8 100644 --- a/soroban-env-host/src/host/data_helper.rs +++ b/soroban-env-host/src/host/data_helper.rs @@ -7,12 +7,13 @@ use crate::{ host::metered_clone::{MeteredAlloc, MeteredClone}, storage::{InstanceStorageMap, Storage}, xdr::{ - 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, + 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, }, AddressObject, Env, Host, HostError, StorageType, U32Val, Val, }; @@ -122,7 +123,10 @@ impl Host { ) } - pub(crate) fn retrieve_wasm_from_storage(&self, wasm_hash: &Hash) -> Result { + pub(crate) fn retrieve_wasm_from_storage( + &self, + wasm_hash: &Hash, + ) -> Result<(BytesM, Option), HostError> { let key = self.contract_code_ledger_key(wasm_hash)?; match &self .try_borrow_storage_mut()? @@ -130,7 +134,14 @@ impl Host { .map_err(|e| self.decorate_contract_code_storage_error(e, wasm_hash))? .data { - LedgerEntryData::ContractCode(e) => e.code.metered_clone(self), + 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()), + }; + Ok((code, costs)) + } _ => Err(err!( self, (ScErrorType::Storage, ScErrorCode::InternalError), diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs index e3afa2bee..e2204af22 100644 --- a/soroban-env-host/src/host/frame.rs +++ b/soroban-env-host/src/host/frame.rs @@ -640,8 +640,8 @@ impl Host { let args_vec = args.to_vec(); match &instance.executable { ContractExecutable::Wasm(wasm_hash) => { - let code_entry = self.retrieve_wasm_from_storage(&wasm_hash)?; - let vm = Vm::new(self, id.metered_clone(self)?, code_entry.as_slice())?; + let (code, costs) = self.retrieve_wasm_from_storage(&wasm_hash)?; + let vm = Vm::new(self, id.metered_clone(self)?, 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 7259f75ef..ca02fec20 100644 --- a/soroban-env-host/src/host/lifecycle.rs +++ b/soroban-env-host/src/host/lifecycle.rs @@ -6,7 +6,8 @@ use crate::{ metered_write_xdr, ContractReentryMode, CreateContractArgs, }, xdr::{ - Asset, ContractCodeEntry, ContractDataDurability, ContractExecutable, ContractIdPreimage, + Asset, ContractCodeCostInputs, ContractCodeEntry, ContractCodeEntryExt, + ContractCodeEntryV1, ContractDataDurability, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress, ExtensionPoint, Hash, LedgerKey, LedgerKeyContractCode, ScAddress, ScErrorCode, ScErrorType, }, @@ -164,6 +165,8 @@ impl Host { ) })?; + let cost_inputs: ContractCodeCostInputs; + // Instantiate a temporary / throwaway VM using this wasm. This will do // both quick checks like "does this wasm have the right protocol number // to run on this network" and also a full parse-and-link pass to check @@ -176,12 +179,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(); } let hash_obj = self.add_host_object(self.scbytes_from_slice(hash_bytes.as_slice())?)?; @@ -199,7 +216,10 @@ impl Host { self.with_mut_storage(|storage| { let data = ContractCodeEntry { hash: Hash(hash_bytes), - ext: ExtensionPoint::V0, + ext: ContractCodeEntryExt::V1(ContractCodeEntryV1 { + ext: ExtensionPoint::V0, + cost_inputs, + }), 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 4428748a9..f742a49ef 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).is_err()); + assert!(crate::vm::Vm::new(&host, hash, soroban_test_wasms::ADD_F32, None).is_err()); Ok(()) } diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index a469d1cb9..d6b14d99d 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -26,11 +26,14 @@ use crate::{ metered_hash::{CountingHasher, MeteredHash}, }, meta::{self, get_ledger_protocol_version}, - xdr::{ContractCostType, Hash, Limited, ReadXdr, ScEnvMetaEntry, ScErrorCode, ScErrorType}, + xdr::{ + ContractCodeCostInputs, ContractCostType, Hash, Limited, ReadXdr, ScEnvMetaEntry, + ScErrorCode, ScErrorType, + }, ConversionError, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, WasmiMarshal, DEFAULT_XDR_RW_LIMITS, }; -use std::{cell::RefCell, io::Cursor, rc::Rc, time::Instant}; +use std::{cell::RefCell, collections::BTreeSet, io::Cursor, rc::Rc, time::Instant}; use fuel_refillable::FuelRefillable; use func_info::HOST_FUNCTIONS; @@ -60,6 +63,7 @@ pub struct Vm { // 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, store: RefCell>, instance: Instance, pub(crate) memory: Option, @@ -200,16 +204,82 @@ impl Vm { Ok(()) } + #[cfg(feature = "testutils")] + pub fn get_all_host_functions() -> Vec<(&'static str, &'static str, u32)> { + HOST_FUNCTIONS + .iter() + .map(|hf| (hf.mod_str, hf.fn_str, hf.arity)) + .collect() + } + /// Instantiates a VM given the arguments provided in [`Self::new`] fn instantiate( host: &Host, contract_id: Hash, module_wasm_code: &[u8], + code_cost_inputs: Option, ) -> Result, HostError> { - host.charge_budget( - ContractCostType::VmInstantiation, - Some(module_wasm_code.len() as u64), - )?; + 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 config = get_wasmi_config(host)?; let engine = Engine::new(&config); @@ -222,9 +292,24 @@ impl Vm { let interface_version = Self::check_meta_section(host, &module)?; let contract_proto = get_ledger_protocol_version(interface_version); + 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 + .imports() + .filter(|i| i.ty().func().is_some()) + .map(|i| { + let mod_str = i.module(); + let fn_str = i.name(); + (mod_str, fn_str) + }) + .collect(); + let mut linker = >::new(&engine); { @@ -261,6 +346,11 @@ impl Vm { continue; } } + // We only link the functions that are actually used by the + // contract. Linking is quite expensive. + if !module_imports.contains(&(hf.mod_str, hf.fn_str)) { + continue; + } let func = (hf.wrap)(&mut store); host.map_err( linker @@ -293,6 +383,7 @@ impl Vm { Ok(Rc::new(Self { contract_id, module, + cost_inputs, store: RefCell::new(store), instance, memory, @@ -321,12 +412,13 @@ impl Vm { 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)?; + let vm = Self::instantiate(host, contract_id, module_wasm_code, code_cost_inputs)?; host.as_budget().track_time( ContractCostType::VmInstantiation, @@ -335,8 +427,217 @@ impl Vm { Ok(vm) } else { - Self::instantiate(host, contract_id, module_wasm_code) + 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, + }; + + 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) + } + + pub fn get_contract_code_cost_inputs(&self) -> ContractCodeCostInputs { + self.cost_inputs.clone() } pub(crate) fn get_memory(&self, host: &Host) -> Result { diff --git a/soroban-env-host/src/vm/func_info.rs b/soroban-env-host/src/vm/func_info.rs index 48123d70f..7914828bb 100644 --- a/soroban-env-host/src/vm/func_info.rs +++ b/soroban-env-host/src/vm/func_info.rs @@ -11,6 +11,10 @@ pub(crate) struct HostFuncInfo { /// as. pub(crate) fn_str: &'static str, + /// Number of I64-typed wasm arguments the function takes. + #[allow(dead_code)] + pub(crate) arity: u32, + /// Function that takes a wasmi::Store and _wraps_ a dispatch function /// for this host function, with the specific type of the dispatch function, /// into a Func in the Store. @@ -23,38 +27,54 @@ pub(crate) struct HostFuncInfo { pub(crate) max_proto: Option, } +macro_rules! fn_arity { + (($($args:ident : $tys:ident),*)) => { + fn_arity!(@count_args 0, $($args:$tys)*) + }; + (@count_args $n:expr, ) => { + $n + }; + (@count_args $n:expr, $arg:ident:$ty:ident $($args:ident:$tys:ident)*) => { + fn_arity!(@count_args $n+1, $($args:$tys)*) + }; +} + macro_rules! host_function_info_helper { - {$mod_str:literal, $fn_id:literal, $func_id:ident, $min_proto:literal, $max_proto:literal} => { + {$mod_str:literal, $fn_id:literal, $args:tt, $func_id:ident, $min_proto:literal, $max_proto:literal} => { HostFuncInfo { mod_str: $mod_str, fn_str: $fn_id, + arity: fn_arity!($args), wrap: |store| Func::wrap(store, dispatch::$func_id), min_proto: Some($min_proto), max_proto: Some($max_proto), } }; - {$mod_str:literal, $fn_id:literal, $func_id:ident, $min_proto:literal, } => { + {$mod_str:literal, $fn_id:literal, $args:tt, $func_id:ident, $min_proto:literal, } => { HostFuncInfo { mod_str: $mod_str, fn_str: $fn_id, + arity: fn_arity!($args), wrap: |store| Func::wrap(store, dispatch::$func_id), min_proto: Some($min_proto), max_proto: None, } }; - {$mod_str:literal, $fn_id:literal, $func_id:ident, , $max_proto:literal} => { + {$mod_str:literal, $fn_id:literal, $args:tt, $func_id:ident, , $max_proto:literal} => { HostFuncInfo { mod_str: $mod_str, fn_str: $fn_id, + arity: fn_arity!($args), wrap: |store| Func::wrap(store, dispatch::$func_id), min_proto: None, max_proto: Some($max_proto), } }; - {$mod_str:literal, $fn_id:literal, $func_id:ident, , } => { + {$mod_str:literal, $fn_id:literal, $args:tt, $func_id:ident, , } => { HostFuncInfo { mod_str: $mod_str, fn_str: $fn_id, + arity: fn_arity!($args), wrap: |store| Func::wrap(store, dispatch::$func_id), min_proto: None, max_proto: None, @@ -119,7 +139,7 @@ macro_rules! generate_host_function_infos { // block repetition-level from the outer pattern in the // expansion, flattening all functions from all 'mod' blocks // into the a single array of HostFuncInfo structs. - host_function_info_helper!{$mod_str, $fn_id, $func_id, $($min_proto)?, $($max_proto)?}, + host_function_info_helper!{$mod_str, $fn_id, $args, $func_id, $($min_proto)?, $($max_proto)?}, )* )* ]; diff --git a/soroban-synth-wasm/Cargo.toml b/soroban-synth-wasm/Cargo.toml index 4b1ea2eb6..6729abf87 100644 --- a/soroban-synth-wasm/Cargo.toml +++ b/soroban-synth-wasm/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] wasm-encoder = "=0.36.2" -wasmparser = "=0.116.1" +wasmparser = { workspace = true } soroban-env-common = { workspace = true } arbitrary = { version = "=1.3.2", features = ["derive"] } soroban-env-macros = { workspace = true } diff --git a/soroban-synth-wasm/src/mod_emitter.rs b/soroban-synth-wasm/src/mod_emitter.rs index 76ae0aa04..ea504e58e 100644 --- a/soroban-synth-wasm/src/mod_emitter.rs +++ b/soroban-synth-wasm/src/mod_emitter.rs @@ -225,6 +225,12 @@ impl ModEmitter { } } + #[cfg(feature = "adversarial")] + pub fn add_raw_fn_type(&mut self, params: &[ValType], results: &[ValType]) { + self.types + .function(params.iter().cloned(), results.iter().cloned()); + } + #[cfg(feature = "adversarial")] pub fn add_fn_type_no_check(&mut self, arity: Arity, ret: Arity) -> TypeRef { let params: Vec<_> = std::iter::repeat(ValType::I64) From 0492b274d6b4ae7874ce8d875cc8969eed5dd1ae Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 7 Mar 2024 13:14:48 -0800 Subject: [PATCH 02/16] 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/vm.rs | 546 +++-------------- soroban-env-host/src/vm/module_cache.rs | 97 +++ soroban-env-host/src/vm/parsed_module.rs | 550 ++++++++++++++++++ 17 files changed, 1401 insertions(+), 712 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 b9a5318f2..e0f1f952c 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 ecd9a5118..c9d07f8c2 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, @@ -79,6 +80,7 @@ pub struct CoverageScoreboard { #[derive(Clone, Default)] struct HostImpl { + module_cache: RefCell>, source_account: RefCell>, ledger: RefCell>, objects: RefCell>, @@ -186,7 +188,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, @@ -315,6 +322,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(), @@ -345,6 +353,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/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(()) + } +} From 84ec25758520199be5019098ad762028226a7d8b Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 14 Mar 2024 20:19:55 -0700 Subject: [PATCH 03/16] Charge for cached-module instantiation even in coarse model, fix calibration --- .../benches/common/cost_types/vm_ops.rs | 73 +++------- soroban-env-host/benches/common/mod.rs | 2 + .../src/cost_runner/cost_types/vm_ops.rs | 126 +++++++++++------- soroban-env-host/src/e2e_testutils.rs | 5 + soroban-env-host/src/vm/parsed_module.rs | 7 +- 5 files changed, 106 insertions(+), 107 deletions(-) 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 45d9869a6..2dcbdd18f 100644 --- a/soroban-env-host/benches/common/cost_types/vm_ops.rs +++ b/soroban-env-host/benches/common/cost_types/vm_ops.rs @@ -1,6 +1,6 @@ use super::wasm_insn_exec::wasm_module_with_n_internal_funcs; -use crate::common::{util, HostCostMeasurement}; -use rand::{rngs::StdRng, Rng}; +use crate::common::HostCostMeasurement; +use rand::rngs::StdRng; use soroban_env_host::{ cost_runner::{VmInstantiationRun, VmInstantiationSample}, vm::{ParsedModule, VersionedContractCodeCostInputs}, @@ -18,24 +18,7 @@ macro_rules! impl_measurement_for_instantiation_cost_type { 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, - module, - } - } - - fn new_worst_case( + fn new_random_case( _host: &Host, _rng: &mut StdRng, input: u64, @@ -67,44 +50,11 @@ macro_rules! impl_measurement_for_instantiation_cost_type { module, } } - - fn new_random_case( - _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(); - #[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, - module, - } - } } }; } -// Protocol 20 coarse cost model +// Protocol 20 coarse unified, or protocol 21 coarse parse-phase cost model impl_measurement_for_instantiation_cost_type!( VmInstantiationRun, VmInstantiationMeasure, @@ -113,7 +63,7 @@ impl_measurement_for_instantiation_cost_type!( 30 ); -// Protocol 21 refined cost model. +// Protocol 21 cost models. #[cfg(feature = "next")] pub(crate) use v21::*; #[cfg(feature = "next")] @@ -135,11 +85,13 @@ mod v21 { ParseWasmDataSegmentsRun, ParseWasmElemSegmentsRun, ParseWasmExportsRun, ParseWasmFunctionsRun, ParseWasmGlobalsRun, ParseWasmImportsRun, ParseWasmInstructionsRun, ParseWasmMemoryPagesRun, ParseWasmTableEntriesRun, - ParseWasmTypesRun, VmInstantiationSample, + ParseWasmTypesRun, VmCachedInstantiationRun, VmInstantiationSample, }, xdr, Host, }; + pub(crate) struct VmCachedInstantiationMeasure; + pub(crate) struct ParseWasmInstructionsMeasure; pub(crate) struct ParseWasmFunctionsMeasure; pub(crate) struct ParseWasmGlobalsMeasure; @@ -162,6 +114,15 @@ mod v21 { pub(crate) struct InstantiateWasmExportsMeasure; pub(crate) struct InstantiateWasmMemoryPagesMeasure; + // Protocol 21 coarse instantiation-phase cost model + impl_measurement_for_instantiation_cost_type!( + VmCachedInstantiationRun, + VmCachedInstantiationMeasure, + wasm_module_with_n_internal_funcs, + false, + 30 + ); + // Protocol 21 refined cost model impl_measurement_for_instantiation_cost_type!( ParseWasmInstructionsRun, diff --git a/soroban-env-host/benches/common/mod.rs b/soroban-env-host/benches/common/mod.rs index 5667e83bd..1cb3e035e 100644 --- a/soroban-env-host/benches/common/mod.rs +++ b/soroban-env-host/benches/common/mod.rs @@ -85,6 +85,8 @@ pub(crate) fn for_each_host_cost_measurement( #[cfg(feature = "next")] { + call_bench::(&mut params)?; + call_bench::(&mut params)?; call_bench::(&mut params)?; call_bench::(&mut params)?; 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 b0a7a57bb..fdd19cd9e 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 @@ -6,8 +6,6 @@ use crate::{ }; use std::{hint::black_box, rc::Rc}; -pub struct VmInstantiationRun; - #[derive(Clone)] pub struct VmInstantiationSample { pub id: Option, @@ -15,55 +13,44 @@ pub struct VmInstantiationSample { pub module: Rc, } -macro_rules! impl_costrunner_for_instantiation_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 { - #[cfg(feature = "next")] - let vm = black_box( - 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.module.cost_inputs, - ) - .unwrap(), - ); - (Some(vm), sample.wasm) - } +// Protocol 20 coarse and unified cost model +#[cfg(not(feature = "next"))] +pub struct VmInstantiationRun; - 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)) - } - } - }; -} +#[cfg(not(feature = "next"))] +impl CostRunner for VmInstantiationRun { + const COST_TYPE: CostType = CostType::Contract(VmInstantiation); + + 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 vm = black_box( + Vm::new_with_cost_inputs( + host, + sample.id.unwrap(), + &sample.wasm[..], + sample.module.cost_inputs.clone(), + ) + .unwrap(), + ); + (Some(vm), sample.wasm) + } -// Protocol 20 coarse cost model -impl_costrunner_for_instantiation_cost_type!(VmInstantiationRun, VmInstantiation); + fn run_baseline_iter( + host: &crate::Host, + _iter: u64, + sample: Self::SampleType, + ) -> Self::RecycledType { + black_box(host.charge_budget(VmInstantiation, Some(0)).unwrap()); + black_box((None, sample.wasm)) + } +} -// Protocol 21 refined cost model. +// Protocol 21 refined and split/caching cost model. #[cfg(feature = "next")] pub use v21::*; #[cfg(feature = "next")] @@ -76,7 +63,7 @@ mod v21 { InstantiateWasmInstructions, InstantiateWasmMemoryPages, InstantiateWasmTableEntries, InstantiateWasmTypes, ParseWasmDataSegments, ParseWasmElemSegments, ParseWasmExports, ParseWasmFunctions, ParseWasmGlobals, ParseWasmImports, ParseWasmInstructions, - ParseWasmMemoryPages, ParseWasmTableEntries, ParseWasmTypes, + ParseWasmMemoryPages, ParseWasmTableEntries, ParseWasmTypes, VmCachedInstantiation, }; macro_rules! impl_costrunner_for_parse_cost_type { @@ -119,6 +106,45 @@ mod v21 { }; } + macro_rules! impl_costrunner_for_instantiation_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 vm = black_box( + Vm::from_parsed_module(host, sample.id.unwrap(), sample.module).unwrap(), + ); + (Some(vm), 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)) + } + } + }; + } + + // This cost-type is recycled as unrefined-model, parse-only phase. + pub struct VmInstantiationRun; + // This cost-type is recycled as unrefined-model, instantiate-only phase. + pub struct VmCachedInstantiationRun; + pub struct ParseWasmInstructionsRun; pub struct ParseWasmFunctionsRun; pub struct ParseWasmGlobalsRun; @@ -141,6 +167,7 @@ mod v21 { pub struct InstantiateWasmExportsRun; pub struct InstantiateWasmMemoryPagesRun; + impl_costrunner_for_parse_cost_type!(VmInstantiationRun, VmInstantiation); impl_costrunner_for_parse_cost_type!(ParseWasmInstructionsRun, ParseWasmInstructions); impl_costrunner_for_parse_cost_type!(ParseWasmFunctionsRun, ParseWasmFunctions); impl_costrunner_for_parse_cost_type!(ParseWasmGlobalsRun, ParseWasmGlobals); @@ -152,6 +179,7 @@ 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!( InstantiateWasmInstructionsRun, InstantiateWasmInstructions diff --git a/soroban-env-host/src/e2e_testutils.rs b/soroban-env-host/src/e2e_testutils.rs index 284262a7b..ef749f57e 100644 --- a/soroban-env-host/src/e2e_testutils.rs +++ b/soroban-env-host/src/e2e_testutils.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "next")] +use crate::xdr::ContractCodeEntryExt; use crate::xdr::{ AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractDataDurability, ContractDataEntry, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress, @@ -47,7 +49,10 @@ pub fn bytes_sc_val(bytes: &[u8]) -> ScVal { pub fn wasm_entry(wasm: &[u8]) -> LedgerEntry { ledger_entry(LedgerEntryData::ContractCode(ContractCodeEntry { + #[cfg(not(feature = "next"))] ext: ExtensionPoint::V0, + #[cfg(feature = "next")] + ext: ContractCodeEntryExt::V0, hash: get_wasm_hash(wasm).try_into().unwrap(), code: wasm.try_into().unwrap(), })) diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 7fc1f303b..818ab6ffa 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -80,8 +80,11 @@ impl VersionedContractCodeCostInputs { } pub fn charge_for_instantiation(&self, _host: &Host) -> Result<(), HostError> { match self { - Self::V0 { .. } => { - // No-op, already charged when parsing + Self::V0 { wasm_bytes } => { + _host.charge_budget( + ContractCostType::VmCachedInstantiation, + Some(*wasm_bytes as u64), + )?; } #[cfg(feature = "next")] Self::V1(inputs) => { From 1ce06296930986832db48a416c70c5794985bf7f Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 14 Mar 2024 22:36:46 -0700 Subject: [PATCH 04/16] Address review comments and CAP updates --- .../benches/common/cost_types/vm_ops.rs | 4 +- .../common/cost_types/wasm_insn_exec.rs | 4 +- soroban-env-host/src/host/lifecycle.rs | 44 +++++++++---- soroban-env-host/src/vm.rs | 1 + soroban-env-host/src/vm/module_cache.rs | 19 +++--- soroban-env-host/src/vm/parsed_module.rs | 63 ++++++++++++------- 6 files changed, 88 insertions(+), 47 deletions(-) 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 2dcbdd18f..af4e2334d 100644 --- a/soroban-env-host/benches/common/cost_types/vm_ops.rs +++ b/soroban-env-host/benches/common/cost_types/vm_ops.rs @@ -11,8 +11,8 @@ use std::rc::Rc; // Protocol 20 coarse cost model. pub(crate) struct VmInstantiationMeasure; -// This measures the cost of parsing wasm and/or instantiating a host::Vm on a -// variety of possible wasm modules, of different sizes. +// 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, $USE_REFINED_INPUTS:expr, $MAGNITUDE:expr) => { impl HostCostMeasurement for $MEASURE { 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 14ab43a81..9b90938ba 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 @@ -198,7 +198,7 @@ fn wasm_module_with_mem_grow(n_pages: usize) -> Vec { fe.finish_and_export("test").finish() } -// A wasm module with a single const to serve as the baseline +// A Wasm module with a single const to serve as the baseline fn wasm_module_baseline_pass() -> WasmModule { let mut fe = ModEmitter::default().func(Arity(0), 0); fe.push(Symbol::try_from_small_str("pass").unwrap()); @@ -206,7 +206,7 @@ fn wasm_module_baseline_pass() -> WasmModule { WasmModule { wasm, overhead: 0 } } -// A wasm module with a single trap to serve as the baseline +// A Wasm module with a single trap to serve as the baseline fn wasm_module_baseline_trap() -> WasmModule { let mut fe = ModEmitter::default().func(Arity(0), 0); fe.trap(); diff --git a/soroban-env-host/src/host/lifecycle.rs b/soroban-env-host/src/host/lifecycle.rs index 921c10200..b7ec02638 100644 --- a/soroban-env-host/src/host/lifecycle.rs +++ b/soroban-env-host/src/host/lifecycle.rs @@ -216,24 +216,46 @@ impl Host { }), self, )?; - if !self - .try_borrow_storage_mut()? + + let mut storage = self.try_borrow_storage_mut()?; + + // We will definitely put the contract in the ledger if it isn't there yet. + #[allow(unused_mut)] + let mut should_put_contract = !storage .has(&code_key, self.as_budget()) - .map_err(|e| self.decorate_contract_code_storage_error(e, &Hash(hash_bytes)))? + .map_err(|e| self.decorate_contract_code_storage_error(e, &Hash(hash_bytes)))?; + + // We may also, in the cache-supporting protocol, overwrite the contract if its ext field changed. + #[cfg(feature = "next")] + if !should_put_contract + && self.get_ledger_protocol_version()? >= super::ModuleCache::MIN_LEDGER_VERSION { - self.with_mut_storage(|storage| { - let data = ContractCodeEntry { - hash: Hash(hash_bytes), - ext, - code: wasm_bytes_m, - }; - storage.put( + let entry = storage + .get(&code_key, self.as_budget()) + .map_err(|e| self.decorate_contract_code_storage_error(e, &Hash(hash_bytes)))?; + if let crate::xdr::LedgerEntryData::ContractCode(ContractCodeEntry { + ext: old_ext, + .. + }) = &entry.data + { + should_put_contract = *old_ext != ext; + } + } + + if should_put_contract { + let data = ContractCodeEntry { + hash: Hash(hash_bytes), + ext, + code: wasm_bytes_m, + }; + storage + .put( &code_key, &Host::new_contract_code(self, data)?, Some(self.get_min_live_until_ledger(ContractDataDurability::Persistent)?), self.as_budget(), ) - })?; + .map_err(|e| self.decorate_contract_code_storage_error(e, &Hash(hash_bytes)))?; } Ok(hash_obj) } diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 8e07289a6..13ad6cccf 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; +const WASM_STD_MEM_PAGE_SIZE_IN_BYTES: u32 = 0x10000; struct VmInstantiationTimer { host: Host, diff --git a/soroban-env-host/src/vm/module_cache.rs b/soroban-env-host/src/vm/module_cache.rs index 43033cc0a..66999c647 100644 --- a/soroban-env-host/src/vm/module_cache.rs +++ b/soroban-env-host/src/vm/module_cache.rs @@ -1,13 +1,14 @@ use super::parsed_module::{ParsedModule, VersionedContractCodeCostInputs}; use crate::{ budget::{get_wasmi_config, AsBudget}, + host::metered_clone::MeteredClone, xdr::{Hash, ScErrorCode, ScErrorType}, - Host, HostError, + Host, HostError, MeteredOrdMap, }; -use std::{collections::BTreeMap, rc::Rc}; +use std::rc::Rc; use wasmi::Engine; -/// A [ModuleCache] is a cache of a set of WASM modules that have been parsed +/// 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 @@ -15,7 +16,7 @@ use wasmi::Engine; #[derive(Clone, Default)] pub struct ModuleCache { pub(crate) engine: Engine, - modules: BTreeMap>, + modules: MeteredOrdMap, Host>, } impl ModuleCache { @@ -25,7 +26,7 @@ impl ModuleCache { pub fn new(host: &Host) -> Result { let config = get_wasmi_config(host.as_budget())?; let engine = Engine::new(&config); - let modules = BTreeMap::new(); + let modules = MeteredOrdMap::new(); #[allow(unused_mut)] let mut cache = Self { engine, modules }; #[cfg(feature = "next")] @@ -65,7 +66,7 @@ impl ModuleCache { wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, ) -> Result<(), HostError> { - if self.modules.contains_key(contract_id) { + if self.modules.contains_key(contract_id, host)? { return Err(host.err( ScErrorType::Context, ScErrorCode::InternalError, @@ -74,7 +75,9 @@ impl ModuleCache { )); } let parsed_module = Rc::new(ParsedModule::new(host, &self.engine, &wasm, cost_inputs)?); - self.modules.insert(contract_id.clone(), parsed_module); + self.modules = + self.modules + .insert(contract_id.metered_clone(host)?, parsed_module, host)?; Ok(()) } @@ -83,7 +86,7 @@ impl ModuleCache { host: &Host, contract_id: &Hash, ) -> Result, HostError> { - if let Some(m) = self.modules.get(contract_id) { + if let Some(m) = self.modules.get(contract_id, host)? { return Ok(m.clone()); } else { Err(host.err( diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 818ab6ffa..9b1e6512c 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -1,6 +1,7 @@ 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, }; @@ -134,7 +135,7 @@ impl VersionedContractCodeCostInputs { } } -/// A [ParsedModule] contains the parsed [wasmi::Module] for a given wasm blob, +/// 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 { @@ -177,7 +178,7 @@ impl ParsedModule { }) } - /// Parse the wasm blob into a [Module] and its protocol number, checking check its interface version + /// Parse the Wasm blob into a [Module] and its protocol number, checking its interface version fn parse_wasm(host: &Host, engine: &Engine, wasm: &[u8]) -> Result<(Module, u32), HostError> { let module = { let _span0 = tracy_span!("parse module"); @@ -224,7 +225,7 @@ impl ParsedModule { // // 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 + // 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"))] @@ -282,7 +283,7 @@ impl ParsedModule { }) } - /// Returns the raw bytes content of a named custom section from the WASM + /// 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]> { @@ -321,12 +322,20 @@ impl ParsedModule { 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", - &[], + if f.results().len() > MAX_VM_ARGS { + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + "Too many return values in Wasm export", + f.results().len() + )); + } + if f.params().len() > MAX_VM_ARGS { + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + "Too many arguments Wasm export", + f.params().len() )); } } @@ -336,7 +345,7 @@ impl ParsedModule { Ok(()) } - // Do a second, manual parse of the wasm blob to extract cost parameters we're + // 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( @@ -355,6 +364,7 @@ impl ParsedModule { } let mut costs = crate::xdr::ContractCodeCostInputs { + ext: crate::xdr::ExtensionPoint::V0, n_instructions: 0, n_functions: 0, n_globals: 0, @@ -398,7 +408,7 @@ impl ParsedModule { return Err(host.err( ScErrorType::WasmVm, ScErrorCode::InvalidInput, - "unsupported wasm section", + "unsupported wasm section type", &[], )) } @@ -491,7 +501,7 @@ impl ParsedModule { return Err(host.err( ScErrorType::WasmVm, ScErrorCode::InvalidInput, - "data segment too large", + "data segment exceeds u32::MAX", &[], )); } @@ -512,20 +522,25 @@ impl ParsedModule { } } } - if data > costs.n_memory_pages.saturating_mul(0x10000) { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, + let available_memory = costs + .n_memory_pages + .saturating_mul(WASM_STD_MEM_PAGE_SIZE_IN_BYTES); + if data > available_memory { + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), "data segments exceed memory size", - &[], + data, + available_memory )); } if elements > costs.n_table_entries { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, - "too many elements in wasm elem section(s)", - &[], + return Err(err!( + host, + (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + "too many elements in Wasm elem section(s)", + elements, + costs.n_table_entries )); } Ok(costs) @@ -542,7 +557,7 @@ impl ParsedModule { return Err(host.err( ScErrorType::WasmVm, ScErrorCode::InvalidInput, - "unsupported complex wasm constant expression", + "unsupported complex Wasm constant expression", &[], )) } From e527da104446143f55f10a8a2877766b23ecea73 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 15 Mar 2024 21:21:55 -0700 Subject: [PATCH 05/16] Work around declared sizes of replaced ExtensionPoints, horribly. --- soroban-env-host/src/host/data_helper.rs | 4 +- soroban-env-host/src/host/declared_size.rs | 109 +++++++++++++++++++++ soroban-env-host/src/host/metered_clone.rs | 12 +++ soroban-env-host/src/vm/module_cache.rs | 6 +- 4 files changed, 127 insertions(+), 4 deletions(-) diff --git a/soroban-env-host/src/host/data_helper.rs b/soroban-env-host/src/host/data_helper.rs index a0bee492c..cada88a83 100644 --- a/soroban-env-host/src/host/data_helper.rs +++ b/soroban-env-host/src/host/data_helper.rs @@ -144,7 +144,9 @@ impl Host { match &e.ext { crate::xdr::ContractCodeEntryExt::V0 => (), crate::xdr::ContractCodeEntryExt::V1(v1) => { - costs = VersionedContractCodeCostInputs::V1(v1.cost_inputs.clone()) + costs = VersionedContractCodeCostInputs::V1( + v1.cost_inputs.metered_clone(self.as_budget())?, + ) } }; Ok((code, costs)) diff --git a/soroban-env-host/src/host/declared_size.rs b/soroban-env-host/src/host/declared_size.rs index d8ad6f5db..49948fbd0 100644 --- a/soroban-env-host/src/host/declared_size.rs +++ b/soroban-env-host/src/host/declared_size.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "next")] +use crate::xdr::{ContractCodeCostInputs, ContractCodeEntryV1}; use crate::{ auth::{ AccountAuthorizationTracker, AccountAuthorizationTrackerSnapshot, AuthorizedInvocation, @@ -157,7 +159,11 @@ impl_declared_size_type!(LedgerKeyContractCode, 36); impl_declared_size_type!(LedgerEntryExt, 33); impl_declared_size_type!(AccountEntry, 216); impl_declared_size_type!(TrustLineEntry, 128); +#[cfg(feature = "next")] +impl_declared_size_type!(ContractCodeCostInputs, 40); impl_declared_size_type!(ContractCodeEntry, 64); +#[cfg(feature = "next")] +impl_declared_size_type!(ContractCodeEntryV1, 40); // TtlEntry must be declared as it's used in e2e to build // The TtlEntryMap, but is not otherwise cloned anywhere. impl_declared_size_type!(TtlEntry, 36); @@ -172,7 +178,80 @@ impl_declared_size_type!(CreateContractArgs, 98); impl_declared_size_type!(InvokeContractArgs, 88); impl_declared_size_type!(ContractIdPreimage, 65); impl_declared_size_type!(ContractDataDurability, 4); + +// NB: ExtensionPoint is a 1-variant enum with no payload, which Rust optimizes +// to take zero bytes of memroy -- but in XDR it's a 4-byte type like any other +// union. +// +// It exists to help allow the protocol to evolve, as a placeholder type in XDR. +// ExtensionPoints are sprinkled around the XDR definitions where we expect to +// need to add stuff in the future. Any field with ExtensionPoint as its type +// can be _replaced_ in subsequent protocols with some _other_ union type that +// has a variant with a zero discriminant, and some other non-zero-discriminant +// variants with new payloads. If old data (written when the field was +// ExtensionPoint) is read by new code (which knows about the new union type), +// the new code will just interpret the zero-discriminant old value as the zero +// case in the new type. All well and good! +// +// But none of this works in Rust, because Rust doesn't treat unions (enums) as +// variable size: it allocates space for the largest variant that can occupy the +// enum, and it allocates zero bytes for ExtensionPoints. So when you upgrade a +// union field in XDR, if the new variant is bigger than all existing variants, +// the Rust size of the field will just change (and since ExtensionPoint is +// zero-sized, this happens _any_ time you replace an ExtensionPoint with some +// nonempty type). There's no real getting around this. +// +// "Luckily" (from the perspective of deterministic replay) we don't charge the +// cost model based on Rust's "real sizes" of anything; we charge the cost model +// based on stable _declared_ sizes we write down explicitly here. So what +// happens when you upgrade a union field in a way that changes the Rust size of +// a type is that the Rust size _diverges a bit_ from the declared size: in +// other words the metering based on declared sizes gets a bit inaccurate, and +// the test below (`test_declared_size`) that checks real sizes are less than or +// equal to declared sizes needs to have an exception written into it to handle +// this divergence. +// +// This is not ideal. It means in some cases the metering will be off, at least +// when reading old data into the new enum (and, say, cloning it) we'll treat it +// as small when really it is a bit bigger. +// +// We can recover a degree of correctness at least in the cases where we're +// writing new data (occupying the new variants of the extended enum) by +// essentially pretending that the ext field is a sort of magical smart pointer +// that occupies zero bytes of its own but points to the new variant as "deep +// substructure" (the same way we do with real pointer fields). +// +// In other words, we can use the following idiom: +// +// - Assume the enclosing struct was `S`, and it previously had an +// `ExtensionPoint` `S.ext`, and now it has some other type in `S.ext`. Let's +// call the new type we want to add `T` and say we added it by replacing the +// `ExtensionPoint` with an enum `E` with variants `V0` (covering the old +// `ExtensionPoint`) and `V1(T)`. +// +// - Previously we charged nothing to clone `S.ext`, because it was zero-sized. +// So we have to continue to charge nothing when cloning the new field when +// it is in its `E::V0` state: that's what the old `ExtensionPoint` data will +// be interpreted as when it's deserialized, so we have to treat it as we did +// before. This is the case where metering has become incorrect, because `E` +// is larger than `ExtensionPoint` but we can't acknowledge that fact. +// +// - To charge nothing for `E::V0`, we actually have to _not_ declare `E` as a +// `MeteredClone` / `declared_size` type itself. Rather, inside the body of +// `S::charge_for_substructure` we match on `S.ext` and do nothing in the +// `E::V0` case, only do nonzero work in the `E::V1` case. +// +// - In the `E::V1` case we can call through to `T::charge_for_substructure`, +// treating it almost like it was a separate heap allocation. It still gets +// charged for, just "as if" it were out-of-line, rather than inline with `S` +// where it happens to be located. +// +// This is all a bit weird and awful. It's a mistake that we only noticed after +// we finalized the metering system of Soroban. We're stuck with it at this +// point. Hopefully most instances will be fairly benign (and if types get +// upgraded to their new variants, hopefully also transient!) impl_declared_size_type!(ExtensionPoint, 0); + impl_declared_size_type!(ScContractInstance, 64); impl_declared_size_type!(SorobanAuthorizationEntry, 240); impl_declared_size_type!(SorobanAuthorizedInvocation, 128); @@ -382,7 +461,17 @@ mod test { expect!["33"].assert_eq(size_of::().to_string().as_str()); expect!["216"].assert_eq(size_of::().to_string().as_str()); expect!["128"].assert_eq(size_of::().to_string().as_str()); + #[cfg(feature = "next")] + expect!["40"].assert_eq(size_of::().to_string().as_str()); + #[cfg(not(feature = "next"))] expect!["56"].assert_eq(size_of::().to_string().as_str()); + // ContractCodeEntry had an ExtensionPoint added to it and is now 40 + // bytes larger than its original size (and for some reason its declared + // size was 64 bytes, even though its original size wasonly 56 bytes) + #[cfg(feature = "next")] + expect!["104"].assert_eq(size_of::().to_string().as_str()); + #[cfg(feature = "next")] + expect!["40"].assert_eq(size_of::().to_string().as_str()); expect!["36"].assert_eq(size_of::().to_string().as_str()); // NB: a couple structs shrank between rust 1.75 and 1.76 but this is harmless @@ -448,6 +537,19 @@ mod test { <$t as DeclaredSizeForMetering>::DECLARED_SIZE ); }; + // This variant allows accounting for the case where an + // ExtensionPoint has been upgraded to some other type, causing the + // enclosing type to exceed its declared size: you can include an + // extension type as a second argument and it will be added to the + // declared size for the sake of retaining _some_ check here. + // Unfortunately this is kinda the best we can do. See the long + // comment above around the zero declared_size of ExtensionPoint. + ($t:ty, $ext:ty) => { + ma::assert_le!( + size_of::<$t>() as u64, + <$t as DeclaredSizeForMetering>::DECLARED_SIZE + (size_of::<$ext>() as u64) + ); + }; } // primitive types assert_mem_size_le_declared_size!(bool); @@ -556,7 +658,14 @@ mod test { assert_mem_size_le_declared_size!(LedgerEntryExt); assert_mem_size_le_declared_size!(AccountEntry); assert_mem_size_le_declared_size!(TrustLineEntry); + #[cfg(feature = "next")] + assert_mem_size_le_declared_size!(ContractCodeCostInputs); + #[cfg(not(feature = "next"))] assert_mem_size_le_declared_size!(ContractCodeEntry); + #[cfg(feature = "next")] + assert_mem_size_le_declared_size!(ContractCodeEntry, ContractCodeEntryV1); + #[cfg(feature = "next")] + assert_mem_size_le_declared_size!(ContractCodeEntryV1); assert_mem_size_le_declared_size!(TtlEntry); assert_mem_size_le_declared_size!(LedgerKey); assert_mem_size_le_declared_size!(LedgerEntry); diff --git a/soroban-env-host/src/host/metered_clone.rs b/soroban-env-host/src/host/metered_clone.rs index b29be1f5b..1b3c24bda 100644 --- a/soroban-env-host/src/host/metered_clone.rs +++ b/soroban-env-host/src/host/metered_clone.rs @@ -18,6 +18,8 @@ use std::{cell::RefCell, iter::FromIterator, mem, rc::Rc}; +#[cfg(feature = "next")] +use crate::xdr::{ContractCodeCostInputs, ContractCodeEntryExt, ContractCodeEntryV1}; use crate::{ budget::{AsBudget, DepthLimiter}, builtin_contracts::base_types::Address, @@ -314,6 +316,10 @@ impl MeteredClone for TimePoint {} impl MeteredClone for Duration {} impl MeteredClone for Hash {} impl MeteredClone for Uint256 {} +#[cfg(feature = "next")] +impl MeteredClone for ContractCodeCostInputs {} +#[cfg(feature = "next")] +impl MeteredClone for ContractCodeEntryV1 {} impl MeteredClone for ContractExecutable {} impl MeteredClone for AccountId {} impl MeteredClone for ScAddress {} @@ -557,6 +563,12 @@ impl MeteredClone for ContractCodeEntry { const IS_SHALLOW: bool = false; fn charge_for_substructure(&self, budget: impl AsBudget) -> Result<(), HostError> { + #[cfg(feature = "next")] + // self.ext is a former ExtensionEntry; see note on ExtensionEntry in declared_size.rs + match &self.ext { + ContractCodeEntryExt::V0 => (), + ContractCodeEntryExt::V1(v1) => v1.charge_for_substructure(budget.clone())?, + } self.code.charge_for_substructure(budget) } } diff --git a/soroban-env-host/src/vm/module_cache.rs b/soroban-env-host/src/vm/module_cache.rs index 66999c647..81facb2c4 100644 --- a/soroban-env-host/src/vm/module_cache.rs +++ b/soroban-env-host/src/vm/module_cache.rs @@ -47,9 +47,9 @@ impl ModuleCache { ContractCodeEntryExt::V0 => VersionedContractCodeCostInputs::V0 { wasm_bytes: code.len(), }, - ContractCodeEntryExt::V1(v1) => { - VersionedContractCodeCostInputs::V1(v1.cost_inputs.clone()) - } + ContractCodeEntryExt::V1(v1) => VersionedContractCodeCostInputs::V1( + v1.cost_inputs.metered_clone(host.as_budget())?, + ), }; self.parse_and_cache_module(host, hash, code, code_cost_inputs)?; } From 2713f51cd07dc5345a56a450d8c7ff5c5231b12b Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 15 Mar 2024 21:51:48 -0700 Subject: [PATCH 06/16] Fix initial page size maximum --- soroban-env-host/src/vm/parsed_module.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 9b1e6512c..4c28b28f7 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -432,7 +432,10 @@ impl ParsedModule { &[], )); } - if mem.initial > 0xffff { + if (mem.initial as u64) + .saturating_mul(crate::vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES as u64) + > u32::MAX as u64 + { return Err(host.err( ScErrorType::WasmVm, ScErrorCode::InvalidInput, From 3e52bddb391ddca7a06cc554957bafd60395289b Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Mon, 18 Mar 2024 21:35:36 -0700 Subject: [PATCH 07/16] Add tests and calibration for new VM instantiation cost model --- .../benches/common/cost_types/vm_ops.rs | 41 +- .../common/cost_types/wasm_insn_exec.rs | 83 ++-- soroban-env-host/benches/common/mod.rs | 4 +- .../benches/worst_case_linear_models.rs | 91 ++++- soroban-env-host/src/budget.rs | 115 +++--- .../src/cost_runner/cost_types/vm_ops.rs | 77 ++-- soroban-env-host/src/test/lifecycle.rs | 377 ++++++++++++++++++ soroban-env-host/src/testutils.rs | 26 ++ soroban-env-host/src/vm.rs | 1 + soroban-env-host/src/vm/parsed_module.rs | 74 ++-- 10 files changed, 720 insertions(+), 169 deletions(-) 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 af4e2334d..a71c9db84 100644 --- a/soroban-env-host/benches/common/cost_types/vm_ops.rs +++ b/soroban-env-host/benches/common/cost_types/vm_ops.rs @@ -69,23 +69,22 @@ 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, + wasm_module_with_n_data_segment_bytes, 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::*; use soroban_env_host::{ cost_runner::{ - InstantiateWasmDataSegmentsRun, InstantiateWasmElemSegmentsRun, - InstantiateWasmExportsRun, InstantiateWasmFunctionsRun, InstantiateWasmGlobalsRun, - InstantiateWasmImportsRun, InstantiateWasmInstructionsRun, - InstantiateWasmMemoryPagesRun, InstantiateWasmTableEntriesRun, InstantiateWasmTypesRun, + InstantiateWasmDataSegmentBytesRun, InstantiateWasmDataSegmentsRun, + InstantiateWasmElemSegmentsRun, InstantiateWasmExportsRun, InstantiateWasmFunctionsRun, + InstantiateWasmGlobalsRun, InstantiateWasmImportsRun, InstantiateWasmInstructionsRun, + InstantiateWasmTableEntriesRun, InstantiateWasmTypesRun, ParseWasmDataSegmentBytesRun, ParseWasmDataSegmentsRun, ParseWasmElemSegmentsRun, ParseWasmExportsRun, ParseWasmFunctionsRun, ParseWasmGlobalsRun, ParseWasmImportsRun, - ParseWasmInstructionsRun, ParseWasmMemoryPagesRun, ParseWasmTableEntriesRun, - ParseWasmTypesRun, VmCachedInstantiationRun, VmInstantiationSample, + ParseWasmInstructionsRun, ParseWasmTableEntriesRun, ParseWasmTypesRun, + VmCachedInstantiationRun, VmInstantiationSample, }, xdr, Host, }; @@ -101,7 +100,7 @@ mod v21 { pub(crate) struct ParseWasmElemSegmentsMeasure; pub(crate) struct ParseWasmImportsMeasure; pub(crate) struct ParseWasmExportsMeasure; - pub(crate) struct ParseWasmMemoryPagesMeasure; + pub(crate) struct ParseWasmDataSegmentBytesMeasure; pub(crate) struct InstantiateWasmInstructionsMeasure; pub(crate) struct InstantiateWasmFunctionsMeasure; @@ -112,7 +111,7 @@ mod v21 { pub(crate) struct InstantiateWasmElemSegmentsMeasure; pub(crate) struct InstantiateWasmImportsMeasure; pub(crate) struct InstantiateWasmExportsMeasure; - pub(crate) struct InstantiateWasmMemoryPagesMeasure; + pub(crate) struct InstantiateWasmDataSegmentBytesMeasure; // Protocol 21 coarse instantiation-phase cost model impl_measurement_for_instantiation_cost_type!( @@ -188,11 +187,11 @@ mod v21 { 30 ); impl_measurement_for_instantiation_cost_type!( - ParseWasmMemoryPagesRun, - ParseWasmMemoryPagesMeasure, - wasm_module_with_n_memory_pages, + ParseWasmDataSegmentBytesRun, + ParseWasmDataSegmentBytesMeasure, + wasm_module_with_n_data_segment_bytes, true, - 30 + 200000 ); impl_measurement_for_instantiation_cost_type!( @@ -259,10 +258,10 @@ mod v21 { 30 ); impl_measurement_for_instantiation_cost_type!( - InstantiateWasmMemoryPagesRun, - InstantiateWasmMemoryPagesMeasure, - wasm_module_with_n_memory_pages, + InstantiateWasmDataSegmentBytesRun, + InstantiateWasmDataSegmentBytesMeasure, + wasm_module_with_n_data_segment_bytes, true, - 30 + 200000 ); } 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..67cf4f345 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,22 +197,17 @@ 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]); } 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() +pub fn wasm_module_with_n_data_segment_bytes(n: usize) -> Vec { + let mut me = ModEmitter::bench_from_configs(1 + (n / 0x10000) as u32, 0); + me.define_data_segment(0, vec![0xff; n]); + me.finish() } fn wasm_module_with_mem_grow(n_pages: usize) -> Vec { diff --git a/soroban-env-host/benches/common/mod.rs b/soroban-env-host/benches/common/mod.rs index 1cb3e035e..249d88c9e 100644 --- a/soroban-env-host/benches/common/mod.rs +++ b/soroban-env-host/benches/common/mod.rs @@ -96,7 +96,7 @@ 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)?; @@ -107,7 +107,7 @@ 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)?; } // These three mem ones are derived analytically, we do not calibrate them typically if std::env::var("INCLUDE_ANALYTICAL_COSTTYPES").is_ok() { diff --git a/soroban-env-host/benches/worst_case_linear_models.rs b/soroban-env-host/benches/worst_case_linear_models.rs index d8a3c84c0..87a5f188f 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, + ParseWasmDataSegmentBytes, + ], + &[ + InstantiateWasmInstructions, + InstantiateWasmFunctions, + InstantiateWasmGlobals, + InstantiateWasmTableEntries, + InstantiateWasmTypes, + InstantiateWasmDataSegments, + InstantiateWasmElemSegments, + InstantiateWasmImports, + InstantiateWasmExports, + InstantiateWasmDataSegmentBytes, + ], + ]; + 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() { + for_each_wasm_insn_measurement::()? + } else { + BTreeMap::new() + }; + + 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..8b3e1ddbb 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -112,9 +112,9 @@ impl Default for BudgetTracker { #[cfg(feature = "next")] ContractCostType::ParseWasmExports => init_input(), #[cfg(feature = "next")] - ContractCostType::ParseWasmMemoryPages => init_input(), + ContractCostType::ParseWasmDataSegmentBytes => 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")] @@ -132,7 +132,7 @@ impl Default for BudgetTracker { #[cfg(feature = "next")] ContractCostType::InstantiateWasmExports => init_input(), #[cfg(feature = "next")] - ContractCostType::InstantiateWasmMemoryPages => init_input(), + ContractCostType::InstantiateWasmDataSegmentBytes => init_input(), } } mt @@ -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); + ContractCostType::ParseWasmDataSegmentBytes => { + cpu.const_term = 66075; + cpu.lin_term = ScaledU64(28); } - #[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); + ContractCostType::InstantiateWasmDataSegmentBytes => { + cpu.const_term = 25191; + cpu.lin_term = ScaledU64(14); } } @@ -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); + ContractCostType::ParseWasmDataSegmentBytes => { + mem.const_term = 17580; + mem.lin_term = ScaledU64(257); } #[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); + ContractCostType::InstantiateWasmDataSegmentBytes => { + mem.const_term = 69256; + mem.lin_term = ScaledU64(126); } } } 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..b21d02c0e 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 @@ -58,12 +58,13 @@ 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, VmCachedInstantiation, + InstantiateWasmDataSegmentBytes, InstantiateWasmDataSegments, InstantiateWasmElemSegments, + InstantiateWasmExports, InstantiateWasmFunctions, InstantiateWasmGlobals, + InstantiateWasmImports, InstantiateWasmInstructions, InstantiateWasmTableEntries, + InstantiateWasmTypes, ParseWasmDataSegmentBytes, ParseWasmDataSegments, + ParseWasmElemSegments, ParseWasmExports, ParseWasmFunctions, ParseWasmGlobals, + ParseWasmImports, ParseWasmInstructions, ParseWasmTableEntries, ParseWasmTypes, + VmCachedInstantiation, }; macro_rules! impl_costrunner_for_parse_cost_type { @@ -107,7 +108,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 +134,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)) } } @@ -154,7 +159,7 @@ mod v21 { pub struct ParseWasmElemSegmentsRun; pub struct ParseWasmImportsRun; pub struct ParseWasmExportsRun; - pub struct ParseWasmMemoryPagesRun; + pub struct ParseWasmDataSegmentBytesRun; pub struct InstantiateWasmInstructionsRun; pub struct InstantiateWasmFunctionsRun; @@ -165,7 +170,7 @@ mod v21 { pub struct InstantiateWasmElemSegmentsRun; pub struct InstantiateWasmImportsRun; pub struct InstantiateWasmExportsRun; - pub struct InstantiateWasmMemoryPagesRun; + pub struct InstantiateWasmDataSegmentBytesRun; impl_costrunner_for_parse_cost_type!(VmInstantiationRun, VmInstantiation); impl_costrunner_for_parse_cost_type!(ParseWasmInstructionsRun, ParseWasmInstructions); @@ -177,35 +182,61 @@ mod v21 { 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_parse_cost_type!(ParseWasmDataSegmentBytesRun, ParseWasmDataSegmentBytes); - 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 + InstantiateWasmDataSegmentBytesRun, + InstantiateWasmDataSegmentBytes, + false ); } diff --git a/soroban-env-host/src/test/lifecycle.rs b/soroban-env-host/src/test/lifecycle.rs index 10ecd31be..bc4f64171 100644 --- a/soroban-env-host/src/test/lifecycle.rs +++ b/soroban-env-host/src/test/lifecycle.rs @@ -566,3 +566,380 @@ 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, + ParseWasmDataSegmentBytes, + InstantiateWasmInstructions, + InstantiateWasmFunctions, + InstantiateWasmGlobals, + InstantiateWasmTableEntries, + InstantiateWasmTypes, + InstantiateWasmDataSegments, + InstantiateWasmElemSegments, + InstantiateWasmImports, + InstantiateWasmExports, + InstantiateWasmDataSegmentBytes, + ]; + + 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, + )?; + assert_eq!(budget.get_tracker(VmInstantiation)?.cpu, 0); + assert_eq!(budget.get_tracker(VmCachedInstantiation)?.cpu, 0); + for ct in NEW_COST_TYPES { + 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..018cac0e1 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -1,14 +1,13 @@ 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, }; use wasmi::{Engine, Module}; -use super::MAX_VM_ARGS; +use super::{ModuleCache, MAX_VM_ARGS}; use std::io::Cursor; #[derive(Debug, Clone)] @@ -72,8 +71,8 @@ impl VersionedContractCodeCostInputs { Some(inputs.n_exports as u64), )?; host.charge_budget( - ContractCostType::ParseWasmMemoryPages, - Some(inputs.n_memory_pages as u64), + ContractCostType::ParseWasmDataSegmentBytes, + Some(inputs.n_data_segment_bytes as u64), )?; } } @@ -82,17 +81,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 +111,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), @@ -126,8 +129,8 @@ impl VersionedContractCodeCostInputs { Some(inputs.n_exports as u64), )?; _host.charge_budget( - ContractCostType::InstantiateWasmMemoryPages, - Some(inputs.n_memory_pages as u64), + ContractCostType::InstantiateWasmDataSegmentBytes, + Some(inputs.n_data_segment_bytes as u64), )?; } } @@ -374,12 +377,12 @@ impl ParsedModule { n_elem_segments: 0, n_imports: 0, n_exports: 0, - n_memory_pages: 0, + n_data_segment_bytes: 0, }; let parser = Parser::new(0); let mut elements: u32 = 0; - let mut data: u32 = 0; + let mut available_memory: u32 = 0; for section in parser.parse_all(wasm) { let section = host.map_err(section)?; match section { @@ -443,8 +446,10 @@ impl ParsedModule { &[], )); } - costs.n_memory_pages = - costs.n_memory_pages.saturating_add(mem.initial as u32); + available_memory = available_memory.saturating_add( + (mem.initial as u32) + .saturating_mul(crate::vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES), + ); } } @@ -475,8 +480,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 +490,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 +504,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 { @@ -508,7 +515,9 @@ impl ParsedModule { &[], )); } - data = data.saturating_add(d.data.len() as u32); + costs.n_data_segment_bytes = costs + .n_data_segment_bytes + .saturating_add(d.data.len() as u32); match d.kind { wasmparser::DataKind::Active { offset_expr, .. } => { Self::check_const_expr_simple(&host, &offset_expr)? @@ -525,15 +534,12 @@ impl ParsedModule { } } } - let available_memory = costs - .n_memory_pages - .saturating_mul(WASM_STD_MEM_PAGE_SIZE_IN_BYTES); - if data > available_memory { + if costs.n_data_segment_bytes > available_memory { return Err(err!( host, (ScErrorType::WasmVm, ScErrorCode::InvalidInput), - "data segments exceed memory size", - data, + "data segment(s) content exceeds memory size", + costs.n_data_segment_bytes, available_memory )); } @@ -541,7 +547,7 @@ impl ParsedModule { return Err(err!( host, (ScErrorType::WasmVm, ScErrorCode::InvalidInput), - "too many elements in Wasm elem section(s)", + "elem segments(s) content exceeds table size", elements, costs.n_table_entries )); From 728c1be04a0dcab2cbe545d0db76622ddd1ac116 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 22 Mar 2024 22:40:47 -0700 Subject: [PATCH 08/16] Fix vnext test deviations --- soroban-env-host/src/test/budget_metering.rs | 67 +++++++++++++++++++- soroban-env-host/src/test/bytes.rs | 8 ++- soroban-env-host/src/test/hostile.rs | 55 ++++++++++++++++ soroban-env-host/src/test/invocation.rs | 4 +- soroban-env-host/src/test/map.rs | 6 +- soroban-env-host/src/test/vec.rs | 6 +- 6 files changed, 137 insertions(+), 9 deletions(-) diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs index 396d1f4f5..ac623a982 100644 --- a/soroban-env-host/src/test/budget_metering.rs +++ b/soroban-env-host/src/test/budget_metering.rs @@ -383,7 +383,8 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> { } let actual = format!("{:?}", host.as_budget()); - expect![[r#" + #[cfg(not(feature = "next"))] + let expected = expect![[r#" ===================================================================================================================================================================== Cpu limit: 100000000; used: 13060190 Mem limit: 41943040; used: 273960 @@ -419,8 +420,68 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> { Shadow mem limit: 41943040; used: 273960 ===================================================================================================================================================================== - "#]] - .assert_eq(&actual); + "#]]; + #[cfg(feature = "next")] + let expected = expect![ + r#" + ===================================================================================================================================================================== + Cpu limit: 100000000; used: 13060190 + Mem limit: 41943040; used: 273960 + ===================================================================================================================================================================== + CostType iterations input cpu_insns mem_bytes const_term_cpu lin_term_cpu const_term_mem lin_term_mem + WasmInsnExec 246 None 984 0 4 0 0 0 + MemAlloc 1 Some(152) 453 168 434 16 16 128 + MemCpy 1 Some(65) 50 0 42 16 0 0 + MemCmp 1 Some(74) 53 0 44 16 0 0 + DispatchHostFunction 176 None 54560 0 310 0 0 0 + VisitObject 97 None 5917 0 61 0 0 0 + ValSer 1 Some(49) 241 389 230 29 242 384 + ValDeser 1 Some(103) 62271 309 59052 4001 0 384 + ComputeSha256Hash 1 Some(193) 14310 0 3738 7012 0 0 + ComputeEd25519PubKey 226 None 9097178 0 40253 0 0 0 + VerifyEd25519Sig 1 Some(227) 384738 0 377524 4068 0 0 + VmInstantiation 1 Some(147) 503770 135880 451626 45405 130065 5064 + VmCachedInstantiation 1 Some(147) 503770 135880 451626 45405 130065 5064 + InvokeVmFunction 47 None 91556 658 1948 0 14 0 + ComputeKeccak256Hash 1 Some(1) 3812 0 3766 5969 0 0 + ComputeEcdsaSecp256k1Sig 1 None 710 0 710 0 0 0 + RecoverEcdsaSecp256k1Key 1 None 2315295 181 2315295 0 181 0 + Int256AddSub 1 None 4404 99 4404 0 99 0 + Int256Mul 1 None 4947 99 4947 0 99 0 + Int256Div 1 None 4911 99 4911 0 99 0 + Int256Pow 1 None 4286 99 4286 0 99 0 + Int256Shift 1 None 913 99 913 0 99 0 + ChaCha20DrawBytes 1 Some(1) 1061 0 1058 501 0 0 + ParseWasmInstructions 0 Some(0) 0 0 72736 25420 17564 6457 + ParseWasmFunctions 0 Some(0) 0 0 0 536688 0 47464 + ParseWasmGlobals 0 Some(0) 0 0 0 176902 0 13420 + ParseWasmTableEntries 0 Some(0) 0 0 0 29639 0 6285 + ParseWasmTypes 0 Some(0) 0 0 0 1048891 0 64670 + ParseWasmDataSegments 0 Some(0) 0 0 0 236970 0 29074 + ParseWasmElemSegments 0 Some(0) 0 0 0 317249 0 48095 + ParseWasmImports 0 Some(0) 0 0 0 694667 0 102890 + ParseWasmExports 0 Some(0) 0 0 0 427037 0 36394 + ParseWasmDataSegmentBytes0 Some(0) 0 0 66075 28 17580 257 + InstantiateWasmInstructions0 None 0 0 25059 0 70192 0 + InstantiateWasmFunctions 0 Some(0) 0 0 0 7503 0 14613 + InstantiateWasmGlobals 0 Some(0) 0 0 0 10761 0 6833 + InstantiateWasmTableEntries0 Some(0) 0 0 0 3211 0 1025 + InstantiateWasmTypes 0 None 0 0 0 0 0 0 + InstantiateWasmDataSegments0 Some(0) 0 0 0 16370 0 129632 + InstantiateWasmElemSegments0 Some(0) 0 0 0 28309 0 13665 + InstantiateWasmImports 0 Some(0) 0 0 0 683461 0 77273 + InstantiateWasmExports 0 Some(0) 0 0 0 297065 0 9176 + InstantiateWasmDataSegmentBytes0 Some(0) 0 0 25191 14 69256 126 + ===================================================================================================================================================================== + Internal details (diagnostics info, does not affect fees) + Total # times meter was called: 23 + Shadow cpu limit: 100000000; used: 13060190 + Shadow mem limit: 41943040; used: 273960 + ===================================================================================================================================================================== + + "# + ]; + expected.assert_eq(&actual); assert_eq!( host.as_budget().get_cpu_insns_consumed()?, diff --git a/soroban-env-host/src/test/bytes.rs b/soroban-env-host/src/test/bytes.rs index 5f7d1a51c..855b840c9 100644 --- a/soroban-env-host/src/test/bytes.rs +++ b/soroban-env-host/src/test/bytes.rs @@ -490,8 +490,12 @@ fn instantiate_oversized_bytes_from_linear_memory() -> Result<(), HostError> { U32Val::from(100).to_val().get_payload() ); - // constructing a big map will cause budget limit exceeded error - let wasm_long = wasm::wasm_module_with_large_bytes_from_linear_memory(480000, 7); + // constructing a big bytes will cause budget limit exceeded error + #[cfg(not(feature = "next"))] + const TOO_BIG: u32 = 480000; + #[cfg(feature = "next")] + const TOO_BIG: u32 = 8_000_000; + let wasm_long = wasm::wasm_module_with_large_bytes_from_linear_memory(TOO_BIG, 7); host.budget_ref().reset_unlimited()?; let contract_id_obj2 = host.register_test_contract_wasm(&wasm_long.as_slice()); host.budget_ref().reset_default()?; diff --git a/soroban-env-host/src/test/hostile.rs b/soroban-env-host/src/test/hostile.rs index e6873adf1..e0c1417a7 100644 --- a/soroban-env-host/src/test/hostile.rs +++ b/soroban-env-host/src/test/hostile.rs @@ -527,6 +527,61 @@ fn excessive_logging() -> Result<(), HostError> { host.enable_debug()?; let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice()); + #[cfg(feature = "next")] + let expected_budget = expect![[r#" + ======================================================= + Cpu limit: 2000000; used: 284819 + Mem limit: 500000; used: 252830 + ======================================================= + CostType cpu_insns mem_bytes + WasmInsnExec 300 0 + MemAlloc 15750 67248 + MemCpy 2345 0 + MemCmp 696 0 + DispatchHostFunction 310 0 + VisitObject 244 0 + ValSer 0 0 + ValDeser 0 0 + ComputeSha256Hash 3738 0 + ComputeEd25519PubKey 0 0 + VerifyEd25519Sig 0 0 + VmInstantiation 0 0 + VmCachedInstantiation 0 0 + InvokeVmFunction 1948 14 + ComputeKeccak256Hash 0 0 + ComputeEcdsaSecp256k1Sig 0 0 + RecoverEcdsaSecp256k1Key 0 0 + Int256AddSub 0 0 + Int256Mul 0 0 + Int256Div 0 0 + Int256Pow 0 0 + Int256Shift 0 0 + ChaCha20DrawBytes 0 0 + ParseWasmInstructions 74324 17967 + ParseWasmFunctions 4192 370 + ParseWasmGlobals 1382 104 + ParseWasmTableEntries 29639 6285 + ParseWasmTypes 8194 505 + ParseWasmDataSegments 0 0 + ParseWasmElemSegments 0 0 + ParseWasmImports 5427 803 + ParseWasmExports 6672 568 + ParseWasmDataSegmentBytes66075 17580 + InstantiateWasmInstructions25059 70192 + InstantiateWasmFunctions 58 114 + InstantiateWasmGlobals 84 53 + InstantiateWasmTableEntries3211 1025 + InstantiateWasmTypes 0 0 + InstantiateWasmDataSegments0 0 + InstantiateWasmElemSegments0 0 + InstantiateWasmImports 5339 603 + InstantiateWasmExports 4641 143 + InstantiateWasmDataSegmentBytes25191 69256 + ======================================================= + + "#]]; + + #[cfg(not(feature = "next"))] let expected_budget = expect![[r#" ======================================================= Cpu limit: 2000000; used: 522315 diff --git a/soroban-env-host/src/test/invocation.rs b/soroban-env-host/src/test/invocation.rs index dd418a11d..fe58cc5c1 100644 --- a/soroban-env-host/src/test/invocation.rs +++ b/soroban-env-host/src/test/invocation.rs @@ -67,9 +67,9 @@ fn invoke_alloc() -> Result<(), HostError> { // pages or about 1.3 MiB, plus the initial 17 pages (1.1MiB) plus some more // slop from general host machinery allocations, plus allocating a VM once // during upload and once during execution we get around 2.5MiB. Call - // is "less than 4MiB". + // is "less than 5MiB". assert!(used_bytes > (128 * 4096)); - assert!(used_bytes < 0x40_0000); + assert!(used_bytes < 0x50_0000); Ok(()) } diff --git a/soroban-env-host/src/test/map.rs b/soroban-env-host/src/test/map.rs index 0c6558ee2..4e2bb7e57 100644 --- a/soroban-env-host/src/test/map.rs +++ b/soroban-env-host/src/test/map.rs @@ -484,8 +484,12 @@ fn instantiate_oversized_map_from_linear_memory() -> Result<(), HostError> { ); // constructing a big map will cause budget limit exceeded error + #[cfg(not(feature = "next"))] + const TOO_BIG: u32 = 20_000; + #[cfg(feature = "next")] + const TOO_BIG: u32 = 1_000_000; let wasm_long = - wasm::wasm_module_with_large_map_from_linear_memory(20000, U32Val::from(7).to_val()); + wasm::wasm_module_with_large_map_from_linear_memory(TOO_BIG, U32Val::from(7).to_val()); host.budget_ref().reset_unlimited()?; let contract_id_obj2 = host.register_test_contract_wasm(&wasm_long.as_slice()); host.budget_ref().reset_default()?; diff --git a/soroban-env-host/src/test/vec.rs b/soroban-env-host/src/test/vec.rs index 637f44dec..6481aaca3 100644 --- a/soroban-env-host/src/test/vec.rs +++ b/soroban-env-host/src/test/vec.rs @@ -453,8 +453,12 @@ fn instantiate_oversized_vec_from_linear_memory() -> Result<(), HostError> { ); // constructing a big map will cause budget limit exceeded error + #[cfg(not(feature = "next"))] + const TOO_BIG: u32 = 60_000; + #[cfg(feature = "next")] + const TOO_BIG: u32 = 1_000_000; let wasm_long = - wasm::wasm_module_with_large_vector_from_linear_memory(60000, U32Val::from(7).to_val()); + wasm::wasm_module_with_large_vector_from_linear_memory(TOO_BIG, U32Val::from(7).to_val()); host.budget_ref().reset_unlimited()?; let contract_id_obj2 = host.register_test_contract_wasm(&wasm_long.as_slice()); host.budget_ref().reset_default()?; From 2e314e6b6abf1b3c73980661ff1c95186cf8be4d Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 22 Mar 2024 23:31:51 -0700 Subject: [PATCH 09/16] fix some clippy issues --- soroban-env-host/src/vm.rs | 12 +++++++----- soroban-env-host/src/vm/parsed_module.rs | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 74bd8e4a0..8ce6c9ffa 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -29,7 +29,7 @@ use crate::{ xdr::{ContractCostType, Hash, ScErrorCode, ScErrorType}, ConversionError, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, WasmiMarshal, }; -use std::{cell::RefCell, collections::BTreeSet, rc::Rc, time::Instant}; +use std::{cell::RefCell, collections::BTreeSet, rc::Rc}; use fuel_refillable::FuelRefillable; use func_info::HOST_FUNCTIONS; @@ -49,16 +49,18 @@ const MAX_VM_ARGS: usize = 32; const WASM_STD_MEM_PAGE_SIZE_IN_BYTES: u32 = 0x10000; struct VmInstantiationTimer { + #[cfg(not(target_family = "wasm"))] host: Host, #[cfg(not(target_family = "wasm"))] - start: Instant, + start: std::time::Instant, } impl VmInstantiationTimer { - fn new(host: Host) -> Self { + fn new(_host: Host) -> Self { VmInstantiationTimer { - host, #[cfg(not(target_family = "wasm"))] - start: Instant::now(), + host: _host, + #[cfg(not(target_family = "wasm"))] + start: std::time::Instant::now(), } } } diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 018cac0e1..096a88e4e 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -435,7 +435,8 @@ impl ParsedModule { &[], )); } - if (mem.initial as u64) + if mem + .initial .saturating_mul(crate::vm::WASM_STD_MEM_PAGE_SIZE_IN_BYTES as u64) > u32::MAX as u64 { From d9d5c7d0e37b21988d489d1e5e0bbc39c2a59175 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 22 Mar 2024 23:38:29 -0700 Subject: [PATCH 10/16] Switch back to release wasmi --- Cargo.lock | 3 +++ Cargo.toml | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 577dc697a..4c7bc59af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1472,6 +1472,7 @@ version = "20.3.0" [[package]] name = "soroban-wasmi" version = "0.31.1-soroban.20.0.1" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "smallvec", "spin", @@ -1858,10 +1859,12 @@ dependencies = [ [[package]] name = "wasmi_arena" version = "0.4.0" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" [[package]] name = "wasmi_core" version = "0.13.0" +source = "git+https://github.com/stellar/wasmi?rev=0ed3f3dee30dc41ebe21972399e0a73a41944aa0#0ed3f3dee30dc41ebe21972399e0a73a41944aa0" dependencies = [ "downcast-rs", "libm", diff --git a/Cargo.toml b/Cargo.toml index 3e5fee548..67f6d126f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,8 @@ default-features = false [workspace.dependencies.wasmi] package = "soroban-wasmi" version = "=0.31.1-soroban.20.0.1" -path = "/src/wasmi/crates/wasmi/" -#git = "https://github.com/stellar/wasmi" -#rev = "0ed3f3dee30dc41ebe21972399e0a73a41944aa0" +git = "https://github.com/stellar/wasmi" +rev = "0ed3f3dee30dc41ebe21972399e0a73a41944aa0" #[patch."https://github.com/stellar/rs-stellar-xdr"] # stellar-xdr = { path = "/src/rs-stellar-xdr/" } From 5d68c095e274d24be93ec1a06eaf5e7c1d713184 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 22 Mar 2024 23:49:06 -0700 Subject: [PATCH 11/16] Fix semver violation --- soroban-env-host/src/vm.rs | 6 ++++++ soroban-env-host/src/vm/parsed_module.rs | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 8ce6c9ffa..b48208c2e 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -406,6 +406,12 @@ impl Vm { self.metered_func_call(host, func_sym, wasm_args.as_slice()) } + /// 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(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/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 096a88e4e..eff960075 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -288,7 +288,6 @@ impl ParsedModule { /// 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) } From b5ed3a0d8e12766fa8b0bd8c9aab5fd53e9bf4af Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Sat, 23 Mar 2024 00:10:28 -0700 Subject: [PATCH 12/16] Fix latest-clippy issue --- soroban-env-host/src/testutils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soroban-env-host/src/testutils.rs b/soroban-env-host/src/testutils.rs index 82b826a4e..c305cfa8d 100644 --- a/soroban-env-host/src/testutils.rs +++ b/soroban-env-host/src/testutils.rs @@ -33,7 +33,7 @@ where C: FnOnce() -> R + UnwindSafe, { thread_local! { - static TEST_CONTRACT_CALL_COUNT: Cell = Cell::new(0); + static TEST_CONTRACT_CALL_COUNT: Cell = const { Cell::new(0) }; } static WRAP_PANIC_HOOK: Once = Once::new(); From dc409fd380eecfb10179c4d7d4148a32db1e2bdf Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Sat, 23 Mar 2024 00:10:49 -0700 Subject: [PATCH 13/16] Revert Cargo.toml churn --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 67f6d126f..fc3ae86ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,8 @@ version = "=0.31.1-soroban.20.0.1" git = "https://github.com/stellar/wasmi" rev = "0ed3f3dee30dc41ebe21972399e0a73a41944aa0" -#[patch."https://github.com/stellar/rs-stellar-xdr"] -# stellar-xdr = { path = "/src/rs-stellar-xdr/" } +# [patch."https://github.com/stellar/rs-stellar-xdr"] +# stellar-xdr = { path = "../rs-stellar-xdr/" } # [patch."https://github.com/stellar/wasmi"] # soroban-wasmi = { path = "../wasmi/crates/wasmi/" } # soroban-wasmi_core = { path = "../wasmi/crates/core/" } From dd55466a102c8dcb9830dd735c364329ab894fe1 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Sat, 23 Mar 2024 00:20:15 -0700 Subject: [PATCH 14/16] Bump cargo-deny, old version appears to be crashing? --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3c934a384..5977dffb8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,7 +44,7 @@ jobs: continue-on-error: ${{ matrix.checks == 'advisories' }} steps: - uses: actions/checkout@v3 - - uses: EmbarkStudios/cargo-deny-action@e0a440755b184aa50374330fa75cca0f84fcb59a + - uses: EmbarkStudios/cargo-deny-action@b01e7a8cfb1f496c52d77361e84c1840d8246393 with: command: check ${{ matrix.checks }} From 041473fa402d8997f84a21d4f4ce6374cabbbf2b Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Sat, 23 Mar 2024 00:28:11 -0700 Subject: [PATCH 15/16] Fix latest-clippy issue --- soroban-bench-utils/src/tracker.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soroban-bench-utils/src/tracker.rs b/soroban-bench-utils/src/tracker.rs index c77c88b02..0875c39ff 100644 --- a/soroban-bench-utils/src/tracker.rs +++ b/soroban-bench-utils/src/tracker.rs @@ -111,10 +111,10 @@ mod cpu { #[cfg(not(any(target_os = "linux", target_os = "macos")))] mod cpu { - pub struct InstructionCounter(u64); + pub struct InstructionCounter; impl InstructionCounter { pub fn new() -> Self { - InstructionCounter(0) + InstructionCounter } pub fn begin(&mut self) {} pub fn end_and_count(&mut self) -> u64 { From 61bd6f5ade2c44e35be713e0d75e60206c111c19 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Sat, 23 Mar 2024 00:51:12 -0700 Subject: [PATCH 16/16] Fix Rust 1.77 size-change in test_expected_size --- soroban-env-host/src/host/declared_size.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/soroban-env-host/src/host/declared_size.rs b/soroban-env-host/src/host/declared_size.rs index 49948fbd0..d6ab4a431 100644 --- a/soroban-env-host/src/host/declared_size.rs +++ b/soroban-env-host/src/host/declared_size.rs @@ -391,8 +391,21 @@ mod test { expect!["8"].assert_eq(size_of::().to_string().as_str()); expect!["32"].assert_eq(size_of::().to_string().as_str()); expect!["32"].assert_eq(size_of::().to_string().as_str()); + + #[rustversion::before(1.77)] + #[cfg(target_arch = "x86_64")] + fn check_x64_host_object_size_that_changed_at_rust_1_77() { + expect!["40"].assert_eq(size_of::().to_string().as_str()); + } + #[rustversion::since(1.77)] #[cfg(target_arch = "x86_64")] - expect!["40"].assert_eq(size_of::().to_string().as_str()); + fn check_x64_host_object_size_that_changed_at_rust_1_77() { + expect!["48"].assert_eq(size_of::().to_string().as_str()); + } + + #[cfg(target_arch = "x86_64")] + check_x64_host_object_size_that_changed_at_rust_1_77(); + #[cfg(target_arch = "aarch64")] expect!["48"].assert_eq(size_of::().to_string().as_str()); expect!["16"].assert_eq(size_of::().to_string().as_str());