Repository: stellar/rs-soroban-env
Pull request #1184 (2023-11-08 17:43:05)

Storage tests
### What

Write some of the tests mentioned in https://github.com/stellar/rs-soroban-env/issues/1159.


Patch:
diff --git a/soroban-synth-wasm/src/func_emitter.rs b/soroban-synth-wasm/src/func_emitter.rs
index 7f6b0208..51fe15de 100644
--- a/soroban-synth-wasm/src/func_emitter.rs
+++ b/soroban-synth-wasm/src/func_emitter.rs
@@ -393,7 +393,7 @@ i64_mem_insn!(
     (i64_store32, I64Store32)
 );
 
-macro_rules! i64_numeric_insn {
+macro_rules! numeric_insn {
     ( $(($func_name: ident, $insn: ident)),* )
     =>
     {
@@ -406,7 +406,8 @@ macro_rules! i64_numeric_insn {
         }
     }
 }
-i64_numeric_insn!(
+numeric_insn!(
+    (i32_wrap_i64, I32WrapI64),
     (i64_eqz, I64Eqz),
     (i64_eq, I64Eq),
     (i64_ne, I64Ne),


Test patch:
diff --git a/soroban-env-host/src/test.rs b/soroban-env-host/src/test.rs
index 6b93b6a3..de41138a 100644
--- a/soroban-env-host/src/test.rs
+++ b/soroban-env-host/src/test.rs
@@ -1,4 +1,5 @@
 pub(crate) mod util;
+pub(crate) use util::wasm as wasm_util;
 
 mod address;
 mod auth;
diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs
index 9ced1d99..5a969770 100644
--- a/soroban-env-host/src/test/budget_metering.rs
+++ b/soroban-env-host/src/test/budget_metering.rs
@@ -78,7 +78,7 @@ fn vm_hostfn_invocation() -> Result<(), HostError> {
 
 #[test]
 fn test_vm_fuel_metering() -> Result<(), HostError> {
-    use super::util::wasm_module_with_4n_insns;
+    use super::wasm_util::wasm_module_with_4n_insns;
     let host = Host::test_host_with_recording_footprint();
     let id_obj = host.register_test_contract_wasm(&wasm_module_with_4n_insns(1000));
     let sym = Symbol::try_from_small_str("test").unwrap();
diff --git a/soroban-env-host/src/test/hostile.rs b/soroban-env-host/src/test/hostile.rs
index 2c46bc09..77926c2a 100644
--- a/soroban-env-host/src/test/hostile.rs
+++ b/soroban-env-host/src/test/hostile.rs
@@ -3,13 +3,13 @@ use soroban_env_common::{
     xdr::{ContractCostType, ScErrorCode, ScErrorType},
     Env, EnvBase, Symbol, SymbolSmall, Tag, Val, VecObject,
 };
-use soroban_synth_wasm::{Arity, LocalRef, ModEmitter, Operand};
 use soroban_test_wasms::HOSTILE;
 
 use crate::{
     budget::{AsBudget, Budget},
     host_object::HostVec,
     storage::Storage,
+    test::wasm_util,
     DiagnosticLevel, Host, HostError,
 };
 
@@ -210,20 +210,11 @@ fn hostile_forged_objects_trap() -> Result<(), HostError> {
     Ok(())
 }
 
-fn wasm_module_with_mem_grow(n_pages: usize) -> Vec<u8> {
-    let mut fe = ModEmitter::new().func(Arity(0), 0);
-    fe.push(Operand::Const32(n_pages as i32));
-    fe.memory_grow();
-    fe.drop();
-    fe.push(Symbol::try_from_small_str("pass").unwrap());
-    fe.finish_and_export("test").finish()
-}
-
 #[test]
 fn excessive_memory_growth() -> Result<(), HostError> {
     // `memory_grow(32)`, wasmi will desire 33 pages of memory, that includes the
     // initial page.
-    let wasm = wasm_module_with_mem_grow(32);
+    let wasm = wasm_util::wasm_module_with_mem_grow(32);
     let host = Host::test_host_with_recording_footprint();
     let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice());
     let host = host
@@ -297,25 +288,9 @@ fn broken_object() {
     let _args = host.vec_new_from_slice(&[bad_val]);
 }
 
-fn wasm_module_with_linear_memory_logging() -> Vec<u8> {
-    let mut me = ModEmitter::new();
-    // log_from_linear_memory
-    let f0 = me.import_func("x", "_", Arity(4));
-    // the caller
-    let mut fe = me.func(Arity(4), 0);
-    fe.push(Operand::Local(LocalRef(0)));
-    fe.push(Operand::Local(LocalRef(1)));
-    fe.push(Operand::Local(LocalRef(2)));
-    fe.push(Operand::Local(LocalRef(3)));
-    fe.call_func(f0);
-    fe.drop();
-    fe.push(Symbol::try_from_small_str("pass").unwrap());
-    fe.finish_and_export("test").finish()
-}
-
 #[test]
 fn excessive_logging() -> Result<(), HostError> {
-    let wasm = wasm_module_with_linear_memory_logging();
+    let wasm = wasm_util::wasm_module_with_linear_memory_logging();
     let host = Host::test_host_with_recording_footprint();
     host.enable_debug()?;
     let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice());
@@ -416,3 +391,106 @@ fn excessive_logging() -> Result<(), HostError> {
 
     Ok(())
 }
+
+#[test]
+fn test_unreachable_contract_should_fail() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_unreachable();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice());
+
+    host.budget_ref().reset_limits(2_000_000, 500_000)?;
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("test")?,
+        host.test_vec_obj::<u32>(&[])?,
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_indirect_call_via_table_access() -> Result<(), HostError> {
+    // this module contains a table with 128 FuncRef elements, 3 of which are
+    // occuplied with 1 host function 2 contract functions
+    let wasm = wasm_util::wasm_module_with_indirect_call();
+    let host = Host::test_host_with_recording_footprint();
+    let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice());
+    host.budget_ref().reset_unlimited()?;
+
+    let call_fn = |raw_bits: u64| -> Result<Val, HostError> {
+        // construct HostVec directly to avoid the check_val_integrety
+        let hv = HostVec::from_vec(vec![Val::from_payload(raw_bits)])?;
+        let args = host.add_host_object(hv)?;
+        host.call(contract_id_obj, Symbol::try_from_small_str("test")?, args)
+    };
+
+    // three functions matches the correct type, they should succeed
+    for i in 0..=2 {
+        let res = call_fn(i);
+        assert!(res.is_ok());
+    }
+    // between 3 and 128 -- the elements count, the FuncRef will be missing
+    // but they are valid table bounds.
+    // Need to skip the Tag::Object ranges to avoid the internall error converting
+    // object handle.
+    for i in 3..=Tag::ObjectCodeLowerBound as u64 {
+        let res = call_fn(i);
+        assert!(HostError::result_matches_err(
+            res,
+            (ScErrorType::WasmVm, ScErrorCode::MissingValue)
+        ));
+    }
+    // after 127 the index is out of table elements range
+    for i in 128..200 {
+        let res = call_fn(i);
+        assert!(HostError::result_matches_err(
+            res,
+            (ScErrorType::WasmVm, ScErrorCode::IndexBounds)
+        ));
+    }
+    Ok(())
+}
+
+#[test]
+fn test_div_by_zero() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_div_by_zero();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice());
+
+    host.budget_ref().reset_limits(2_000_000, 500_000)?;
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("test")?,
+        host.test_vec_obj::<u32>(&[])?,
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::ArithDomain)
+    ));
+    Ok(())
+}
+
+#[test]
+fn test_integer_overflow() -> Result<(), HostError> {
+    let wasm = wasm_util::wasm_module_with_integer_overflow();
+    let host = Host::test_host_with_recording_footprint();
+    host.enable_debug()?;
+    let contract_id_obj = host.register_test_contract_wasm(wasm.as_slice());
+
+    host.budget_ref().reset_limits(2_000_000, 500_000)?;
+    let res = host.call(
+        contract_id_obj,
+        Symbol::try_from_small_str("test")?,
+        host.test_vec_obj::<u32>(&[])?,
+    );
+    assert!(HostError::result_matches_err(
+        res,
+        (ScErrorType::WasmVm, ScErrorCode::ArithDomain)
+    ));
+    Ok(())
+}
diff --git a/soroban-env-host/src/test/lifecycle.rs b/soroban-env-host/src/test/lifecycle.rs
index 8bdc2037..44a7b496 100644
--- a/soroban-env-host/src/test/lifecycle.rs
+++ b/soroban-env-host/src/test/lifecycle.rs
@@ -14,8 +14,8 @@ use crate::{
 use sha2::{Digest, Sha256};
 use soroban_env_common::xdr::{
     ContractIdPreimage, ContractIdPreimageFromAddress, DepthLimitedWrite, HostFunction, ScAddress,
-    SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanAuthorizedInvocation,
-    SorobanCredentials, VecM, DEFAULT_XDR_RW_DEPTH_LIMIT,
+    ScErrorCode, ScErrorType, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
+    SorobanAuthorizedInvocation, SorobanCredentials, VecM, DEFAULT_XDR_RW_DEPTH_LIMIT,
 };
 use soroban_env_common::{xdr::ScBytes, TryIntoVal, Val};
 use soroban_env_common::{StorageType, VecObject};
@@ -416,12 +416,27 @@ fn test_create_contract_from_source_account_recording_auth() {
 #[test]
 fn test_invalid_contract() {
     let host = Host::test_host_with_recording_footprint();
+    let bytes = [0u8; 32];
 
-    let bytes = [0u8, 32];
+    let err = host
+        .invoke_function(HostFunction::UploadContractWasm(bytes.try_into().unwrap()))
+        .err()
+        .unwrap();
 
-    assert!(host
-        .invoke_function(HostFunction::UploadContractWasm(
-            bytes.to_vec().try_into().unwrap(),
-        ))
-        .is_err());
+    assert!(err.error.is_type(ScErrorType::WasmVm));
+    assert!(err.error.is_code(ScErrorCode::InvalidAction));
+}
+
+#[test]
+fn test_large_contract() {
+    let host = Host::test_host_with_recording_footprint();
+    let bytes = vec![0u8; u32::MAX.try_into().unwrap()];
+
+    let err = host
+        .invoke_function(HostFunction::UploadContractWasm(bytes.try_into().unwrap()))
+        .err()
+        .unwrap();
+
+    assert!(err.error.is_type(ScErrorType::Budget));
+    assert!(err.error.is_code(ScErrorCode::ExceededLimit));
 }
diff --git a/soroban-env-host/src/test/storage.rs b/soroban-env-host/src/test/storage.rs
index e0188fb4..eb792dbe 100644
--- a/soroban-env-host/src/test/storage.rs
+++ b/soroban-env-host/src/test/storage.rs
@@ -347,3 +347,69 @@ fn test_storage_mix() {
     test_storage(&host, contract_id, "temporary");
     test_storage(&host, contract_id, "instance");
 }
+
+#[test]
+#[should_panic(
+    expected = "called `Result::unwrap()` on an `Err` value: Error(Budget, ExceededLimit)"
+)]
+fn test_large_persistent_value() {
+    let host = Host::test_host_with_recording_footprint();
+    let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
+    let key_1 = Symbol::try_from_small_str("key_1").unwrap();
+
+    let bytes = vec![0u8; 100000000];
+    let _ = host.try_call(
+        contract_id,
+        storage_fn_name(&host, "put", "persistent"),
+        host_vec![&host, key_1, bytes].into(),
+    );
+}
+
+#[test]
+#[should_panic(
+    expected = "called `Result::unwrap()` on an `Err` value: Error(Budget, ExceededLimit)"
+)]
+fn test_large_instance_value() {
+    let host = Host::test_host_with_recording_footprint();
+    let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
+    let key_1 = Symbol::try_from_small_str("key_1").unwrap();
+
+    let bytes = vec![0u8; 100000000];
+    let _ = host.try_call(
+        contract_id,
+        storage_fn_name(&host, "put", "instance"),
+        host_vec![&host, key_1, bytes].into(),
+    );
+}
+
+#[test]
+#[should_panic(
+    expected = "called `Result::unwrap()` on an `Err` value: Error(Budget, ExceededLimit)"
+)]
+fn test_large_persistent_key() {
+    let host = Host::test_host_with_recording_footprint();
+    let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
+
+    let key = vec![0u8; 100000000];
+    let _ = host.try_call(
+        contract_id,
+        storage_fn_name(&host, "put", "persistent"),
+        host_vec![&host, key, 1_u64].into(),
+    );
+}
+
+#[test]
+#[should_panic(
+    expected = "called `Result::unwrap()` on an `Err` value: Error(Budget, ExceededLimit)"
+)]
+fn test_large_instance_key() {
+    let host = Host::test_host_with_recording_footprint();
+    let contract_id = host.register_test_contract_wasm(CONTRACT_STORAGE);
+
+    let key = vec![0u8; 100000000];
+    let _ = host.try_call(
+        contract_id,
+        storage_fn_name(&host, "put", "instance"),
+        host_vec![&host, key, 1_u64].into(),
+    );
+}
diff --git a/soroban-env-host/src/test/util.rs b/soroban-env-host/src/test/util.rs
index 9b4ada91..13694f8f 100644
--- a/soroban-env-host/src/test/util.rs
+++ b/soroban-env-host/src/test/util.rs
@@ -1,20 +1,18 @@
 use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
 
 use rand::RngCore;
-use soroban_env_common::{
-    xdr::{
-        AccountEntry, AccountId, ContractCostType, LedgerEntry, LedgerEntryData, LedgerKey,
-        PublicKey, ScAddress, ScErrorCode, ScErrorType, ScVal, ScVec, Uint256,
-    },
-    AddressObject, BytesObject, Env, EnvBase, Symbol, Val, VecObject,
-};
-use soroban_synth_wasm::{Arity, ModEmitter, Operand};
 
 use crate::{
     budget::{AsBudget, Budget},
     host::HostLifecycleEvent,
     storage::{SnapshotSource, Storage},
-    xdr, Error, Host, HostError, LedgerInfo,
+    xdr,
+    xdr::{
+        AccountEntry, AccountId, ContractCostType, LedgerEntry, LedgerEntryData, LedgerKey,
+        PublicKey, ScAddress, ScErrorCode, ScErrorType, ScVal, ScVec, Uint256,
+    },
+    AddressObject, BytesObject, Env, EnvBase, Error, Host, HostError, LedgerInfo, Symbol, Val,
+    VecObject,
 };
 
 use soroban_bench_utils::HostTracker;
@@ -36,6 +34,18 @@ impl AsScVal for i32 {
     }
 }
 
+impl AsScVal for u64 {
+    fn as_scval(&self) -> ScVal {
+        ScVal::U64(*self)
+    }
+}
+
+impl AsScVal for i64 {
+    fn as_scval(&self) -> ScVal {
+        ScVal::I64(*self)
+    }
+}
+
 impl AsScVal for ScVec {
     fn as_scval(&self) -> ScVal {
         ScVal::Vec(Some(self.clone()))
@@ -55,21 +65,6 @@ pub(crate) fn generate_bytes_array(host: &Host) -> [u8; 32] {
     bytes
 }
 
-pub(crate) fn wasm_module_with_4n_insns(n: usize) -> Vec<u8> {
-    let mut fe = ModEmitter::new().func(Arity(1), 0);
-    let arg = fe.args[0];
-    fe.push(Operand::Const64(1));
-    for i in 0..n {
-        fe.push(arg);
-        fe.push(Operand::Const64(i as i64));
-        fe.i64_mul();
-        fe.i64_add();
-    }
-    fe.drop();
-    fe.push(Symbol::try_from_small_str("pass").unwrap());
-    fe.finish_and_export("test").finish()
-}
-
 pub(crate) struct MockSnapshotSource(BTreeMap<Rc<LedgerKey>, (Rc<LedgerEntry>, Option<u32>)>);
 
 impl MockSnapshotSource {
@@ -316,3 +311,100 @@ impl Host {
         val
     }
 }
+
+pub(crate) mod wasm {
+    use crate::Symbol;
+    use soroban_synth_wasm::{Arity, LocalRef, ModEmitter, Operand};
+
+    pub(crate) fn wasm_module_with_4n_insns(n: usize) -> Vec<u8> {
+        let mut fe = ModEmitter::new().func(Arity(1), 0);
+        let arg = fe.args[0];
+        fe.push(Operand::Const64(1));
+        for i in 0..n {
+            fe.push(arg);
+            fe.push(Operand::Const64(i as i64));
+            fe.i64_mul();
+            fe.i64_add();
+        }
+        fe.drop();
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_mem_grow(n_pages: usize) -> Vec<u8> {
+        let mut fe = ModEmitter::new().func(Arity(0), 0);
+        fe.push(Operand::Const32(n_pages as i32));
+        fe.memory_grow();
+        fe.drop();
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_linear_memory_logging() -> Vec<u8> {
+        let mut me = ModEmitter::new();
+        // log_from_linear_memory
+        let f0 = me.import_func("x", "_", Arity(4));
+        // the caller
+        let mut fe = me.func(Arity(4), 0);
+        fe.push(Operand::Local(LocalRef(0)));
+        fe.push(Operand::Local(LocalRef(1)));
+        fe.push(Operand::Local(LocalRef(2)));
+        fe.push(Operand::Local(LocalRef(3)));
+        fe.call_func(f0);
+        fe.drop();
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_unreachable() -> Vec<u8> {
+        let me = ModEmitter::new();
+        let mut fe = me.func(Arity(0), 0);
+        fe.trap();
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_indirect_call() -> Vec<u8> {
+        let mut me = ModEmitter::new();
+        // an imported function
+        let f0 = me.import_func("t", "_", Arity(0));
+        // a local wasm function
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Symbol::try_from_small_str("pass").unwrap());
+        let (me, f1) = fe.finish();
+        // another local wasm function
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Symbol::try_from_small_str("pass2").unwrap());
+        let (mut me, f2) = fe.finish();
+        // store in table
+        me.define_elems(&[f0, f1, f2]);
+        let ty = me.get_fn_type(Arity(0));
+        // the caller
+        fe = me.func(Arity(1), 0);
+        fe.local_get(LocalRef(0));
+        fe.i32_wrap_i64();
+        // fe.i32_const(0);
+        fe.call_func_indirect(ty);
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_div_by_zero() -> Vec<u8> {
+        let me = ModEmitter::new();
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Operand::Const64(123));
+        fe.push(Operand::Const64(0));
+        fe.i64_div_s();
+        fe.finish_and_export("test").finish()
+    }
+
+    pub(crate) fn wasm_module_with_integer_overflow() -> Vec<u8> {
+        let me = ModEmitter::new();
+        let mut fe = me.func(Arity(0), 0);
+        fe.push(Operand::Const64(i64::MIN));
+        fe.push(Operand::Const64(-1));
+        // interestingly the only operation that can trigger `IntegerOverflow`
+        // is an overflowing division. Other arithmatic opeartions add, sub, mul
+        // are wrapping.
+        fe.i64_div_s();
+        fe.finish_and_export("test").finish()
+    }
+}