diff --git a/clar2wasm/src/tools.rs b/clar2wasm/src/tools.rs index 69b420f4..414b3fa7 100644 --- a/clar2wasm/src/tools.rs +++ b/clar2wasm/src/tools.rs @@ -27,8 +27,8 @@ use crate::initialize::initialize_contract; #[derive(Clone)] pub struct TestEnvironment { contract_contexts: HashMap, - epoch: StacksEpochId, - version: ClarityVersion, + pub epoch: StacksEpochId, + pub version: ClarityVersion, datastore: Datastore, burn_datastore: BurnDatastore, cost_tracker: LimitedCostTracker, @@ -373,7 +373,7 @@ pub fn interpret(snippet: &str) -> Result, Error> { interpret_at(snippet, StacksEpochId::latest(), ClarityVersion::latest()) } -struct TestConfig; +pub struct TestConfig; impl TestConfig { /// Select a Clarity version based on enabled features. diff --git a/clar2wasm/tests/oom-checker/main.rs b/clar2wasm/tests/oom-checker/main.rs new file mode 100644 index 00000000..8ab52f9a --- /dev/null +++ b/clar2wasm/tests/oom-checker/main.rs @@ -0,0 +1,132 @@ +#![cfg(test)] +pub mod unit_tests; + +use clar2wasm::compile; +use clar2wasm::datastore::Datastore; +use clar2wasm::tools::{crosscheck, crosscheck_with_env, TestConfig, TestEnvironment}; +use clar2wasm::wasm_utils::get_type_in_memory_size; +use clarity::types::StacksEpochId; +use clarity::vm::costs::LimitedCostTracker; +use clarity::vm::errors::{CheckErrors, Error}; +use clarity::vm::types::{ + ListTypeData, QualifiedContractIdentifier, StandardPrincipalData, TypeSignature, +}; +use clarity::vm::{ClarityVersion, Value}; + +/// Name of the buffer that will fill the empty space. +const IGNORE_BUFFER_NAME: &str = "ignore"; +/// Size in memory for the buffer that will fill the empty space's (offset, len). +const IGNORE_BUFFER_SIZE: usize = 8; +/// Minimum size needed in memory to create a filling buffer +const IGNORE_BUFFER_MIN_SIZE_NEEDED: usize = IGNORE_BUFFER_SIZE + IGNORE_BUFFER_NAME.len(); + +/// Size of a page in Wasm +const WASM_PAGE_SIZE: usize = 65536; + +#[allow(clippy::expect_used)] +fn as_oom_check_snippet( + snippet: &str, + args_types: &[TypeSignature], + epoch: StacksEpochId, + version: ClarityVersion, +) -> String { + let compiled_module = Datastore::new() + .as_analysis_db() + .execute(|analysis_db| { + compile( + snippet, + &QualifiedContractIdentifier::new( + StandardPrincipalData::transient(), + ("foo").into(), + ), + LimitedCostTracker::new_free(), + version, + epoch, + analysis_db, + ) + .map_err(|e| CheckErrors::Expects(format!("Compilation failure {e:?}"))) + }) + .expect("Could not compile snippet") + .module; + + // we look for the total number of pages that were allocated for the module. + let memory_pages = compiled_module + .memories + .iter() + .next() + .expect("Couldn't find a memory") + .initial as usize; + // we look for the first byte in memory which doesn't contain useful data. + let stack_pointer_value = match compiled_module + .globals + .iter() + .find(|g| g.name.as_ref().is_some_and(|name| name == "stack-pointer")) + .expect("Couldn't find stack-pointer global") + .kind + { + walrus::GlobalKind::Local(walrus::InitExpr::Value(walrus::ir::Value::I32(val))) => { + val as usize + } + _ => unreachable!("stack-pointer should be a locally declared global with a i32 value"), + }; + + // WORKAROUND: this is to ignore arguments that are computed at runtime and should be removed after fixing + // [issue #587](https://github.com/stacks-network/clarity-wasm/issues/587) + let args_space_needed = args_types + .iter() + .map(|ty| get_type_in_memory_size(ty, false)) + .sum::() as usize; + + // the free space on the last page that we want to fill is the substraction of the total number of bytes + // for all the available pages and the last byte which will contain useful data. + let mut free_space_on_memory_page = memory_pages * WASM_PAGE_SIZE - stack_pointer_value; + + let total_space_needed = IGNORE_BUFFER_MIN_SIZE_NEEDED + args_space_needed; + if free_space_on_memory_page < total_space_needed { + free_space_on_memory_page += WASM_PAGE_SIZE; + } + + format!( + "(define-constant {IGNORE_BUFFER_NAME} 0x{})\n{snippet}", + "00".repeat(free_space_on_memory_page - total_space_needed) + ) +} + +// TODO: deprecate after fixing [issue #587](https://github.com/stacks-network/clarity-wasm/issues/587) +pub fn crosscheck_oom_with_non_literal_args( + snippet: &str, + args_types: &[TypeSignature], + expected: Result, Error>, +) { + crosscheck( + &as_oom_check_snippet( + snippet, + args_types, + TestConfig::latest_epoch(), + TestConfig::clarity_version(), + ), + expected, + ); +} + +pub fn crosscheck_oom(snippet: &str, expected: Result, Error>) { + crosscheck_oom_with_non_literal_args(snippet, &[], expected) +} + +pub fn crosscheck_oom_with_env( + snippet: &str, + expected: Result, Error>, + env: TestEnvironment, +) { + crosscheck_with_env( + &as_oom_check_snippet(snippet, &[], env.epoch, env.version), + expected, + env, + ); +} + +pub(crate) fn list_of(elem: TypeSignature, max_len: u32) -> TypeSignature { + TypeSignature::SequenceType(clarity::vm::types::SequenceSubtype::ListType( + ListTypeData::new_list(elem, max_len).unwrap(), + )) +} diff --git a/clar2wasm/tests/oom-checker/unit_tests.rs b/clar2wasm/tests/oom-checker/unit_tests.rs new file mode 100644 index 00000000..c777b3ce --- /dev/null +++ b/clar2wasm/tests/oom-checker/unit_tests.rs @@ -0,0 +1,285 @@ +use clar2wasm::tools::TestEnvironment; +use clarity::vm::types::{PrincipalData, TypeSignature}; +use clarity::vm::Value; + +use crate::{ + crosscheck_oom, crosscheck_oom_with_env, crosscheck_oom_with_non_literal_args, list_of, +}; + +#[test] +#[ignore = "issue #585"] +fn principal_of_oom() { + crosscheck_oom( + "(principal-of? 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110)", + Ok(Some( + Value::okay( + PrincipalData::parse("ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP") + .unwrap() + .into(), + ) + .unwrap(), + )), + ) +} + +#[test] +fn list_oom() { + crosscheck_oom( + "(list 1 2 3)", + Ok(Some( + Value::cons_list_unsanitized(vec![Value::Int(1), Value::Int(2), Value::Int(3)]) + .unwrap(), + )), + ); +} + +#[test] +fn append_oom() { + crosscheck_oom_with_non_literal_args( + "(append (list 1 2 3) 4)", + &[list_of(TypeSignature::IntType, 3)], + Ok(Some( + Value::cons_list_unsanitized(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(3), + Value::Int(4), + ]) + .unwrap(), + )), + ); +} + +#[test] +fn concat_oom() { + crosscheck_oom_with_non_literal_args( + "(concat (list 1 2 3) (list 4 5))", + &[ + list_of(TypeSignature::IntType, 3), + list_of(TypeSignature::IntType, 2), + ], + Ok(Some( + Value::cons_list_unsanitized(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(3), + Value::Int(4), + Value::Int(5), + ]) + .unwrap(), + )), + ); +} + +#[cfg(not(feature = "test-clarity-v1"))] +#[test] +fn replace_at_oom() { + crosscheck_oom_with_non_literal_args( + "(replace-at? (list 1 2 3) u0 42)", + &[list_of(TypeSignature::IntType, 3)], + Ok(Some( + Value::some( + Value::cons_list_unsanitized(vec![Value::Int(42), Value::Int(2), Value::Int(3)]) + .unwrap(), + ) + .unwrap(), + )), + ); +} + +#[test] +fn map_oom() { + crosscheck_oom_with_non_literal_args( + "(define-private (foo (b bool)) (if b u1 u0)) (map foo (list true true false))", + &[list_of(TypeSignature::BoolType, 3)], + Ok(Some( + Value::cons_list_unsanitized(vec![Value::UInt(1), Value::UInt(1), Value::UInt(0)]) + .unwrap(), + )), + ) +} + +#[test] +fn fold_oom() { + let snippet = r#" +(define-private (concat-buff (a (buff 1)) (b (buff 3))) + (unwrap-panic (as-max-len? (concat a b) u3))) +(fold concat-buff 0x010203 0x) + "#; + crosscheck_oom(snippet, Ok(Some(Value::buff_from(vec![3, 2, 1]).unwrap()))); +} + +#[test] +fn get_block_info_burnchain_header_hash_oom() { + let mut env = TestEnvironment::new( + clarity::types::StacksEpochId::Epoch25, + clarity::vm::ClarityVersion::Clarity2, + ); + env.advance_chain_tip(1); + + crosscheck_oom_with_env( + "(get-block-info? burnchain-header-hash u0)", + Ok(Some( + Value::some(Value::buff_from(vec![0; 32]).unwrap()).unwrap(), + )), + env, + ); +} + +#[test] +fn get_block_info_id_header_hash_oom() { + let mut env = TestEnvironment::new( + clarity::types::StacksEpochId::Epoch25, + clarity::vm::ClarityVersion::Clarity2, + ); + env.advance_chain_tip(1); + + crosscheck_oom_with_env( + "(get-block-info? id-header-hash u0)", + Ok(Some( + Value::some( + // same result as in get_block_info_header_hash() test + Value::buff_from(vec![ + 181, 224, 118, 171, 118, 9, 199, 248, 199, 99, 181, 197, 113, 208, 122, 234, + 128, 176, 107, 65, 69, 34, 49, 177, 67, 115, 112, 244, 150, 78, 214, 110, + ]) + .unwrap(), + ) + .unwrap(), + )), + env, + ); +} + +#[test] +fn get_block_info_header_hash_oom() { + let mut env = TestEnvironment::new( + clarity::types::StacksEpochId::Epoch25, + clarity::vm::ClarityVersion::Clarity2, + ); + env.advance_chain_tip(1); + + crosscheck_oom_with_env( + "(get-block-info? header-hash u0)", + Ok(Some( + Value::some(Value::buff_from(vec![0; 32]).unwrap()).unwrap(), + )), + env, + ); +} + +#[test] +fn get_block_info_miner_address_oom() { + let mut env = TestEnvironment::new( + clarity::types::StacksEpochId::Epoch25, + clarity::vm::ClarityVersion::Clarity2, + ); + env.advance_chain_tip(1); + + crosscheck_oom_with_env( + "(get-block-info? miner-address u0)", + Ok(Some( + Value::some(Value::Principal( + PrincipalData::parse("ST000000000000000000002AMW42H").unwrap(), + )) + .unwrap(), + )), + env, + ); +} + +#[test] +fn get_burn_block_info_header_hash_oom() { + let mut env = TestEnvironment::new( + clarity::types::StacksEpochId::Epoch25, + clarity::vm::ClarityVersion::Clarity2, + ); + env.advance_chain_tip(1); + + crosscheck_oom_with_env( + "(get-burn-block-info? header-hash u0)", + Ok(Some( + Value::some(Value::buff_from(vec![0; 32]).unwrap()).unwrap(), + )), + env, + ); +} + +#[test] +fn get_burn_block_info_pox_addrs_oom() { + let mut env = TestEnvironment::new( + clarity::types::StacksEpochId::Epoch25, + clarity::vm::ClarityVersion::Clarity2, + ); + env.advance_chain_tip(1); + + crosscheck_oom_with_env( + "(get-burn-block-info? pox-addrs u0)", + Ok(Some( + Value::some( + clarity::vm::types::TupleData::from_data(vec![ + ( + "addrs".into(), + Value::cons_list_unsanitized(vec![ + clarity::vm::types::TupleData::from_data(vec![ + ( + "hashbytes".into(), + Value::buff_from([0; 32].to_vec()).unwrap(), + ), + ("version".into(), Value::buff_from_byte(0)), + ]) + .unwrap() + .into(), + ]) + .unwrap(), + ), + ("payout".into(), Value::UInt(0)), + ]) + .unwrap() + .into(), + ) + .unwrap(), + )), + env, + ); +} + +#[test] +#[ignore = "issue #592"] +fn int_to_ascii_oom() { + crosscheck_oom( + "(int-to-ascii 42)", + Ok(Some( + Value::string_ascii_from_bytes(b"42".to_vec()).unwrap(), + )), + ); +} + +#[test] +#[ignore = "issue #592"] +fn int_to_utf8_oom() { + crosscheck_oom( + "(int-to-utf8 42)", + Ok(Some(Value::string_utf8_from_bytes(b"42".to_vec()).unwrap())), + ); +} + +#[test] +fn data_var_oom() { + crosscheck_oom( + r#" + (define-data-var n (buff 1) 0x) + (var-set n 0x42) + (var-get n) + "#, + Ok(Some(Value::buff_from_byte(0x42))), + ); +} + +#[test] +fn secp256k1_recover_oom() { + crosscheck_oom( + "(secp256k1-recover? 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a1301)", + Ok(Some(Value::okay(Value::buff_from(vec![3, 173, 184, 222, 75, 251, 101, 219, 44, 253, 97, 32, 213, 92, 101, 38, 174, 156, 82, 230, 117, 219, 126, 71, 48, 134, 54, 83, 75, 167, 120, 97, 16]).unwrap()).unwrap())), + ); +}