From ecb144ca55e51405f624c2ff7b2236937f138e67 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Thu, 29 Feb 2024 01:45:24 -0800 Subject: [PATCH] Sketch of cheaper instantiation change --- Cargo.lock | 7 +- Cargo.toml | 12 +- soroban-env-common/Cargo.toml | 3 +- soroban-env-common/src/error.rs | 7 + soroban-env-host/Cargo.toml | 2 + .../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/test/e2e_tests.rs | 22 +- 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 + 26 files changed, 937 insertions(+), 130 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee623ca82..5e4c1e3fa 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", ] @@ -1467,7 +1469,6 @@ version = "20.2.2" [[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", @@ -1512,8 +1513,6 @@ dependencies = [ [[package]] name = "stellar-xdr" version = "20.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e59cdf3eb4467fb5a4b00b52e7de6dca72f67fac6f9b700f55c95a5d86f09c9d" dependencies = [ "arbitrary", "base64 0.13.1", @@ -1849,12 +1848,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 827308dce..4ffffa0f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,13 @@ soroban-env-guest = { version = "=20.2.2", path = "soroban-env-guest" } soroban-env-host = { version = "=20.2.2", path = "soroban-env-host" } soroban-env-macros = { version = "=20.2.2", path = "soroban-env-macros" } soroban-builtin-sdk-macros = { version = "=20.2.2", 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" +path = "/src/rs-stellar-xdr/" # git = "https://github.com/stellar/rs-stellar-xdr" # rev = "8b9d623ef40423a8462442b86997155f2c04d3a1" default-features = false @@ -37,11 +40,12 @@ 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 85258797b..52e9c108f 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" @@ -85,6 +86,7 @@ rustversion = "1.0" [dev-dependencies.stellar-xdr] version = "=20.1.0" +path = "/src/rs-stellar-xdr" # git = "https://github.com/stellar/rs-stellar-xdr" # rev = "8b9d623ef40423a8462442b86997155f2c04d3a1" default-features = false 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/test/e2e_tests.rs b/soroban-env-host/src/test/e2e_tests.rs index b198898c0..fbfe8b726 100644 --- a/soroban-env-host/src/test/e2e_tests.rs +++ b/soroban-env-host/src/test/e2e_tests.rs @@ -7,16 +7,16 @@ use crate::{ budget::Budget, e2e_invoke::{invoke_host_function, LedgerEntryChange}, xdr::{ - AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractDataDurability, - ContractDataEntry, ContractEvent, ContractExecutable, ContractIdPreimage, - ContractIdPreimageFromAddress, CreateContractArgs, DiagnosticEvent, ExtensionPoint, - HashIdPreimage, HashIdPreimageContractId, HashIdPreimageSorobanAuthorization, HostFunction, - InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerFootprint, - LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limits, PublicKey, ReadXdr, - ScAddress, ScBytes, ScContractInstance, ScErrorCode, ScErrorType, ScMapEntry, ScVal, - SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction, - SorobanAuthorizedInvocation, SorobanCredentials, SorobanResources, Thresholds, TtlEntry, - Uint256, WriteXdr, + AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractCodeEntryExt, + ContractDataDurability, ContractDataEntry, ContractEvent, ContractExecutable, + ContractIdPreimage, ContractIdPreimageFromAddress, CreateContractArgs, DiagnosticEvent, + ExtensionPoint, HashIdPreimage, HashIdPreimageContractId, + HashIdPreimageSorobanAuthorization, HostFunction, InvokeContractArgs, LedgerEntry, + LedgerEntryData, LedgerEntryExt, LedgerFootprint, LedgerKey, LedgerKeyContractCode, + LedgerKeyContractData, Limits, PublicKey, ReadXdr, ScAddress, ScBytes, ScContractInstance, + ScErrorCode, ScErrorType, ScMapEntry, ScVal, SequenceNumber, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, + SorobanResources, Thresholds, TtlEntry, Uint256, WriteXdr, }, Host, HostError, LedgerInfo, }; @@ -107,7 +107,7 @@ fn account_entry(account_id: &AccountId) -> LedgerEntry { fn wasm_entry(wasm: &[u8]) -> LedgerEntry { ledger_entry(LedgerEntryData::ContractCode(ContractCodeEntry { - ext: ExtensionPoint::V0, + ext: ContractCodeEntryExt::V0, hash: get_wasm_hash(wasm).try_into().unwrap(), code: wasm.try_into().unwrap(), })) diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 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)