From 43abc75eab8fdb517e4ab2b902f9a5281133fd3c Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 25 Aug 2026 17:27:12 +0300 Subject: [PATCH 1/6] Add fuzzing implementation * add randomness implementation * add fuzz macro implementation * add fuzzing impelmentation, which can generate arguments and witness to inject values into program * edit interface for program creation (we have Into and Into instead of Box) * extend ui tests to include randomness * continue work onto this pr from b2c4bfd4 to simplify rebasing --- Cargo.lock | 174 ++++++++- Cargo.toml | 2 + crates/build/Cargo.toml | 2 +- crates/build/src/generator.rs | 16 +- crates/build/src/macros/codegen.rs | 108 +++++- crates/build/src/macros/core.rs | 19 +- crates/build/src/macros/types.rs | 57 ++- crates/cli/assets/Simplex.default.toml | 6 + crates/cli/src/commands/core.rs | 3 + crates/cli/src/commands/test.rs | 3 + crates/macros/src/lib.rs | 12 +- crates/sdk/Cargo.toml | 3 +- crates/sdk/src/program/arguments.rs | 9 +- crates/sdk/src/program/core.rs | 68 +++- crates/sdk/src/program/mod.rs | 6 +- crates/sdk/src/program/witness.rs | 9 +- crates/sdk/src/signer/core.rs | 36 +- .../sdk/src/transaction/final_transaction.rs | 4 +- crates/sdk/src/transaction/partial_input.rs | 17 +- crates/simplex/Cargo.toml | 2 + crates/simplex/src/lib.rs | 5 +- crates/simplex/tests/ui/array_tr_storage.rs | 160 ++++++++- crates/simplex/tests/ui/bytes32_tr_storage.rs | 162 ++++++++- .../simplex/tests/ui/dual_currency_deposit.rs | 166 ++++++++- .../tests/ui/either_with_single_witness.rs | 169 ++++++++- crates/simplex/tests/ui/exotic_values.rs | 162 ++++++++- crates/simplex/tests/ui/list_check.rs | 177 +++++++++- crates/simplex/tests/ui/option_offer.rs | 159 ++++++++- crates/simplex/tests/ui/options.rs | 159 ++++++++- crates/simplex/tests/ui/simple_storage.rs | 159 ++++++++- crates/simplex/tests/ui/single_bit.rs | 213 +++++++---- crates/test/Cargo.toml | 3 + crates/test/src/config.rs | 10 + crates/test/src/error.rs | 20 ++ crates/test/src/fuzz/args_strategy.rs | 7 + crates/test/src/fuzz/args_strategy/random.rs | 55 +++ .../fuzz/args_strategy/random_interesting.rs | 332 ++++++++++++++++++ .../src/fuzz/args_strategy/random_pool.rs | 154 ++++++++ crates/test/src/fuzz/builders.rs | 3 + .../src/fuzz/builders/final_tx_builder.rs | 274 +++++++++++++++ crates/test/src/fuzz/core.rs | 82 +++++ crates/test/src/fuzz/engine.rs | 314 +++++++++++++++++ crates/test/src/fuzz/mod.rs | 12 + crates/test/src/fuzz/utils.rs | 45 +++ crates/test/src/lib.rs | 1 + crates/test/src/macros/core.rs | 64 +++- crates/test/src/macros/mod.rs | 2 +- crates/wasm/src/lib.rs | 18 +- examples/basic/Cargo.lock | 174 ++++++++- examples/basic/Simplex.toml | 6 + examples/basic/tests/basic_test.rs | 4 +- fixtures/Cargo.lock | 174 ++++++++- fixtures/Simplex.toml | 6 + fixtures/simf/failure_test.simf | 13 + fixtures/simf/simple_storage.simf | 105 ++++++ fixtures/tests/basic_test.rs | 4 +- fixtures/tests/log_level.rs | 4 +- fixtures/tests/metadata_cmr.rs | 2 +- fixtures/tests/multidep_test.rs | 4 +- fixtures/tests/nested_sig.rs | 4 +- fixtures/tests/prop_testing.rs | 187 ++++++++++ fixtures/tests/tapleaf_test.rs | 4 +- 62 files changed, 4171 insertions(+), 163 deletions(-) create mode 100644 crates/test/src/fuzz/args_strategy.rs create mode 100644 crates/test/src/fuzz/args_strategy/random.rs create mode 100644 crates/test/src/fuzz/args_strategy/random_interesting.rs create mode 100644 crates/test/src/fuzz/args_strategy/random_pool.rs create mode 100644 crates/test/src/fuzz/builders.rs create mode 100644 crates/test/src/fuzz/builders/final_tx_builder.rs create mode 100644 crates/test/src/fuzz/core.rs create mode 100644 crates/test/src/fuzz/engine.rs create mode 100644 crates/test/src/fuzz/mod.rs create mode 100644 crates/test/src/fuzz/utils.rs create mode 100644 fixtures/simf/failure_test.simf create mode 100644 fixtures/simf/simple_storage.simf create mode 100644 fixtures/tests/prop_testing.rs diff --git a/Cargo.lock b/Cargo.lock index 2f0ec7f2..9c047783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,12 +161,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand", - "rand_core", + "rand 0.8.8", + "rand_core 0.6.4", "serde", "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin" version = "0.32.102" @@ -603,6 +618,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -638,6 +659,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -646,7 +679,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -933,6 +966,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objc2" version = "0.6.4" @@ -1015,6 +1057,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.11", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.32" @@ -1025,6 +1086,12 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -1034,6 +1101,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1047,8 +1120,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1058,7 +1141,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1070,6 +1163,24 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "regex-automata" version = "0.3.9" @@ -1186,6 +1297,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1202,7 +1325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand", + "rand 0.8.8", "secp256k1-sys", "serde", ] @@ -1223,7 +1346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" dependencies = [ "bitcoin-private", - "rand", + "rand 0.8.8", "secp256k1", "secp256k1-zkp-sys", "serde", @@ -1434,6 +1557,7 @@ dependencies = [ "elements-miniscript", "hex", "minreq 3.0.0", + "rand_core 0.9.5", "serde", "serde_json", "sha2", @@ -1446,6 +1570,8 @@ name = "smplx-std" version = "0.0.10" dependencies = [ "either", + "rand 0.9.5", + "rand_core 0.9.5", "serde", "simplicityhl", "smplx-macros", @@ -1460,7 +1586,9 @@ version = "0.0.10" dependencies = [ "electrsd", "proc-macro2", + "proptest", "quote", + "rand 0.9.5", "serde", "simplicityhl", "smplx-regtest", @@ -1689,6 +1817,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1734,6 +1868,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1750,6 +1893,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1950,6 +2102,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 9a096f51..3619dbba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ smplx-sdk = { path = "./crates/sdk", version = "0.0.10", default-features = fals smplx-std = { path = "./crates/simplex", version = "0.0.10" } smplx-wasm = { path = "./crates/wasm", version = "0.0.10" } +rand_core = { version = "0.9.5" } +rand = { version = "0.9.4" } serde = { version = "1.0.228", features = ["derive"] } hex = { version = "0.4.3" } hmac = { version = "0.12.1" } diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index adf63903..f2939d63 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -21,5 +21,5 @@ quote = { version = "1.0.44" } pathdiff = { version = "0.2.3" } prettyplease = { version = "0.2.37" } glob = { version = "0.3.3"} -globwalk = { version = "0.9.1"} +globwalk = { version = "0.9.1" } serde_json = { version = "1.0.151" } diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index e5f27a25..80f60c66 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -19,8 +19,7 @@ use simplicityhl::source::CanonSourceFile; use crate::contract_id::ContractId; use crate::macros::codegen::{ - convert_contract_name_to_contract_module, convert_contract_name_to_contract_source_const, - convert_contract_name_to_struct_name, + construct_program_name, convert_contract_name_to_contract_module, convert_contract_name_to_contract_source_const, }; use crate::macros::parse::SimfContent; @@ -279,22 +278,19 @@ impl ArtifactsGenerator { } fn generate_simf_binding_code(contract_name: &str, target_simf: &Path) -> Result { - let program_name = { - let base_name = convert_contract_name_to_struct_name(contract_name); - format_ident!("{base_name}Program") - }; - + let program_name = construct_program_name(contract_name); let include_simf_source_const = convert_contract_name_to_contract_source_const(contract_name); let include_simf_module = convert_contract_name_to_contract_module(contract_name); let target_simf_str = target_simf.to_string_lossy().into_owned(); let code = quote! { use simplex::include_simf; - use simplex::program::{ArgumentsTrait, Program}; + use simplex::program::{Program}; use simplex::provider::SimplicityNetwork; use simplex::simplicityhl::elements::Script; use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; + #[derive(Clone)] pub struct #program_name { program: Program, } @@ -303,9 +299,9 @@ impl ArtifactsGenerator { pub const SOURCE: &'static str = #include_simf_module::#include_simf_source_const; #[must_use] - pub fn new(arguments: &impl ArgumentsTrait) -> Self { + pub fn new(arguments: impl Into) -> Self { Self { - program: Program::new(Self::SOURCE, arguments), + program: Program::new(Self::SOURCE, arguments.into()), } } diff --git a/crates/build/src/macros/codegen.rs b/crates/build/src/macros/codegen.rs index 2936e9be..923b6a91 100644 --- a/crates/build/src/macros/codegen.rs +++ b/crates/build/src/macros/codegen.rs @@ -8,6 +8,7 @@ use crate::macros::types::RustType; pub struct SimfContractMeta { pub contract_source_const_name: proc_macro2::Ident, + pub program_struct_name: proc_macro2::Ident, pub args_struct: WitnessStruct, pub witness_struct: WitnessStruct, pub simf_content: SimfContent, @@ -26,6 +27,11 @@ pub struct GeneratedWitnessTokens { pub struct_impl: proc_macro2::TokenStream, } +pub struct GeneratedProgramTraitHelperTokens { + pub imports: proc_macro2::TokenStream, + pub helper_impls: proc_macro2::TokenStream, +} + pub struct WitnessField { witness_simf_name: String, struct_rust_field: proc_macro2::Ident, @@ -47,15 +53,41 @@ impl SimfContractMeta { let witness_struct = WitnessStruct::generate_witness_struct(&simf_content.contract_name, &abi_meta.witness_types)?; let contract_source_const_name = convert_contract_name_to_contract_source_const(&simf_content.contract_name); - + let program_struct_name = construct_program_name(&simf_content.contract_name); Ok(SimfContractMeta { contract_source_const_name, + program_struct_name, args_struct, witness_struct, simf_content, abi_meta, }) } + + /// Generates code necessary for creating mutant testing using simplex. + pub fn generate_program_trait_helpers_impl(&self) -> syn::Result { + let args_struct_name = &self.args_struct.struct_name; + let program_name = &self.program_struct_name; + + let program_helpers_impl = quote! { + impl ProgramFactory<#program_name> for #program_name { + fn instantiate_program(args: impl Into) -> Box<#program_name> { + Box::new(#program_name::new(args)) + } + } + }; + + Ok(GeneratedProgramTraitHelperTokens { + imports: quote! { + use super::{super::#program_name, #args_struct_name}; + use simplex::program::{Program, ProgramFactory}; + use simplex::simplicityhl::{Arguments}; + }, + helper_impls: quote! { + #program_helpers_impl + }, + }) + } } impl WitnessField { @@ -105,6 +137,7 @@ impl WitnessStruct { proc_macro2::TokenStream, proc_macro2::TokenStream, ) = self.generate_from_args_conversion_with_param_name("args"); + let rand_mapping: proc_macro2::TokenStream = self.generate_rand_mapping(); let default_mapping: proc_macro2::TokenStream = self.generate_default_mapping(); Ok(GeneratedArgumentTokens { @@ -117,28 +150,34 @@ impl WitnessStruct { use simplex::simplicityhl::types::TypeConstructible; use simplex::simplicityhl::value::ValueConstructible; use simplex::program::ArgumentsTrait; + use simplex::rand_core::{RngCore}; + use simplex::rand::Rng; }, struct_token_stream: quote! { #generated_struct }, struct_impl: quote! { impl #struct_name { - /// Build struct from Simplicity Arguments. + /// Build struct from Simplicity `Arguments`. /// /// # Errors /// - /// Returns error if any required witness is missing, has wrong type, or has invalid value. + /// Returns error if any required witness is missing, has the wrong type, or has an invalid value. pub fn from_arguments(args: &Arguments) -> Result { #arguments_conversion_from_args_map Ok(#struct_to_return) } + /// Generate a random Arguments struct instance using the provided RNG. + pub fn generate_arguments_raw(rng: &mut R) -> Self + { + #rand_mapping + } } impl simplex::program::ArgumentsTrait for #struct_name { /// Build Simplicity arguments for contract instantiation. - #[must_use] fn build_arguments(&self) -> simplex::simplicityhl::Arguments { simplex::simplicityhl::Arguments::from(HashMap::from([ #(#tuples),* @@ -165,11 +204,24 @@ impl WitnessStruct { } } + impl simplex::program::RandomArguments for #struct_name { + fn generate_arguments(rng: &mut dyn RngCore) -> simplex::simplicityhl::Arguments + { + Self::generate_arguments_raw(rng).build_arguments() + } + } + impl core::default::Default for #struct_name { fn default() -> Self { #default_mapping } } + + impl From<#struct_name> for simplex::simplicityhl::Arguments { + fn from(val: #struct_name) -> simplex::simplicityhl::Arguments { + val.build_arguments() + } + } }, }) } @@ -187,6 +239,7 @@ impl WitnessStruct { proc_macro2::TokenStream, ) = self.generate_from_args_conversion_with_param_name("witness"); let default_mapping: proc_macro2::TokenStream = self.generate_default_mapping(); + let rand_mapping: proc_macro2::TokenStream = self.generate_rand_mapping(); Ok(GeneratedWitnessTokens { imports: quote! { @@ -198,13 +251,15 @@ impl WitnessStruct { use simplex::simplicityhl::types::TypeConstructible; use simplex::simplicityhl::value::ValueConstructible; use simplex::program::WitnessTrait; + use simplex::rand_core::{RngCore}; + use simplex::rand::Rng; }, struct_token_stream: quote! { #generated_struct }, struct_impl: quote! { impl #struct_name { - /// Build struct from Simplicity WitnessValues. + /// Build struct from Simplicity `WitnessValues`. /// /// # Errors /// @@ -214,11 +269,16 @@ impl WitnessStruct { Ok(#struct_to_return) } + + /// Generate a random Witness struct instance using the provided RNG. + pub fn generate_witness_raw(rng: &mut R) -> Self + { + #rand_mapping + } } impl simplex::program::WitnessTrait for #struct_name { /// Build Simplicity witness values for contract execution. - #[must_use] fn build_witness(&self) -> simplex::simplicityhl::WitnessValues { simplex::simplicityhl::WitnessValues::from(HashMap::from([ #(#tuples),* @@ -245,11 +305,24 @@ impl WitnessStruct { } } + impl simplex::program::RandomWitness for #struct_name { + fn generate_witness(rng: &mut dyn RngCore) -> simplex::simplicityhl::WitnessValues + { + Self::generate_witness_raw(rng).build_witness() + } + } + impl core::default::Default for #struct_name { fn default() -> Self { #default_mapping } } + + impl From<#struct_name> for simplex::simplicityhl::WitnessValues { + fn from(val: #struct_name) -> simplex::simplicityhl::WitnessValues { + val.build_witness() + } + } }, }) } @@ -300,6 +373,24 @@ impl WitnessStruct { } } + fn generate_rand_mapping(&self) -> proc_macro2::TokenStream { + let name = format_ident!("{}", self.struct_name); + let fields: Vec = self + .witness_values + .iter() + .map(|field| { + let field_name = format_ident!("{}", field.struct_rust_field); + let field_default_value = field.rust_type.get_random_value(); + quote! { #field_name: #field_default_value } + }) + .collect(); + quote! { + #name { + #(#fields),* + } + } + } + fn generate_default_mapping(&self) -> proc_macro2::TokenStream { let name = format_ident!("{}", self.struct_name); let fields: Vec = self @@ -367,6 +458,11 @@ impl WitnessStruct { } } +pub fn construct_program_name(contract_name: &str) -> proc_macro2::Ident { + let base_name = convert_contract_name_to_struct_name(contract_name); + format_ident!("{base_name}Program") +} + pub fn convert_contract_name_to_struct_name(contract_name: &str) -> String { let words: Vec = contract_name .split('_') diff --git a/crates/build/src/macros/core.rs b/crates/build/src/macros/core.rs index 12f6d5b5..e50d29b2 100644 --- a/crates/build/src/macros/core.rs +++ b/crates/build/src/macros/core.rs @@ -7,7 +7,8 @@ use simplicityhl::ast::ElementsJetHinter; use simplicityhl::{AbiMeta, TemplateProgram, UnstableFeatures}; use super::codegen::{ - GeneratedArgumentTokens, GeneratedWitnessTokens, SimfContractMeta, convert_contract_name_to_contract_module, + GeneratedArgumentTokens, GeneratedProgramTraitHelperTokens, GeneratedWitnessTokens, SimfContractMeta, + convert_contract_name_to_contract_module, }; use super::parse::{SimfContent, SynFilePath}; @@ -27,6 +28,7 @@ fn expand_inner(simf_content: SimfContent, meta: AbiMeta) -> Result Result Result> { .generate_abi_meta()?, ) } + +fn construct_program_trait_helpers(derived_meta: &SimfContractMeta) -> syn::Result { + let GeneratedProgramTraitHelperTokens { imports, helper_impls } = + derived_meta.generate_program_trait_helpers_impl()?; + + Ok(quote! { + mod program_helpers { + #imports + + #helper_impls + } + }) +} diff --git a/crates/build/src/macros/types.rs b/crates/build/src/macros/types.rs index 00fbcb68..8bca1f8e 100644 --- a/crates/build/src/macros/types.rs +++ b/crates/build/src/macros/types.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use quote::quote; +use quote::{format_ident, quote}; use simplicityhl::ResolvedType; @@ -60,6 +60,61 @@ impl RustTypeContext { } impl RustType { + pub fn get_random_value(&self) -> proc_macro2::TokenStream { + match self { + RustType::Bool => quote! { rng.random() }, + RustType::U1 => quote! { rng.random::() as u8 }, + RustType::U2 => quote! { rng.random::() & 0x03 }, + RustType::U4 => quote! { rng.random::() & 0x0F }, + RustType::U8 | RustType::U16 | RustType::U32 | RustType::U64 | RustType::U128 => quote! { rng.random() }, + RustType::U256Array => quote! { rng.random() }, + RustType::Array(element, size) => { + let elements = vec![element.get_random_value(); *size]; + quote! { [#(#elements),*] } + } + RustType::Tuple(elements) => { + let element_values: Vec<_> = elements.iter().map(RustType::get_random_value).collect(); + quote! { (#(#element_values),*) } + } + RustType::Either(left, right) => { + let left_val = left.get_random_value(); + let right_val = right.get_random_value(); + quote! { if rng.random() { simplex::either::Either::Left(#left_val) } else { simplex::either::Either::Right(#right_val) } } + } + RustType::Option(inner) => { + let inner_val = inner.get_random_value(); + quote! { if rng.random() { Some(#inner_val) } else { None } } + } + RustType::List(element, size) => { + let (generated_fn, generated_fn_name) = { + let generated_fn_name = format_ident!("__gen_list_path"); + let rust_ret_type = element.to_type_token_stream(); + let rand_element_generation = element.get_random_value(); + + let generated_fn = quote! { + fn #generated_fn_name(rng: &mut R) -> #rust_ret_type{ + #rand_element_generation + } + }; + (generated_fn, generated_fn_name) + }; + + quote! { + { + #generated_fn + + let random_size: u32 = rng.random_range(0..#size as u32); + let mut res = Vec::with_capacity(random_size as usize); + for s in 0..random_size { + res.push(#generated_fn_name(rng)); + } + res + } + } + } + } + } + pub fn get_default_value(&self) -> proc_macro2::TokenStream { match self { RustType::Bool => quote! { Default::default() }, diff --git a/crates/cli/assets/Simplex.default.toml b/crates/cli/assets/Simplex.default.toml index 5ab8f8bb..3286a810 100644 --- a/crates/cli/assets/Simplex.default.toml +++ b/crates/cli/assets/Simplex.default.toml @@ -25,6 +25,12 @@ # url = "" # network = "" +# [test.fuzz] +# cases = 10000 +# max_shrink_iters = 100 +# max_global_rejects = 100 +# max_local_rejects = 100 + # [test.rpc] # url = "" # username = "" diff --git a/crates/cli/src/commands/core.rs b/crates/cli/src/commands/core.rs index 3302e246..d9b66105 100644 --- a/crates/cli/src/commands/core.rs +++ b/crates/cli/src/commands/core.rs @@ -70,6 +70,9 @@ pub struct TestFlags { /// Run non-simplex tests (may be used for running unit tests) #[arg(long = "no-simplex")] pub no_simplex: bool, + /// Perform fuzzing via simplex + #[arg(long = "fuzz")] + pub fuzz: bool, } #[allow(clippy::struct_excessive_bools)] diff --git a/crates/cli/src/commands/test.rs b/crates/cli/src/commands/test.rs index bb624526..ea9225b3 100644 --- a/crates/cli/src/commands/test.rs +++ b/crates/cli/src/commands/test.rs @@ -9,6 +9,7 @@ use super::error::CommandError; /// Nextest dsl variable to filter and use only simplex tests const SMPLX_NEXTEST_DSL_TEST_MARKER: &str = concat!("test(/", smplx_test_marker!(), "$/)"); +const SMPLX_NEXTEST_FUZZ_DSL_TEST_MARKER: &str = concat!("test(/", smplx_test_marker!(fuzz), "$/)"); const DEFAULT_THREADS_NUMBER: usize = 1; pub struct Test {} @@ -84,6 +85,8 @@ impl Test { let dsl_marker = if flags.no_simplex { format!("not {SMPLX_NEXTEST_DSL_TEST_MARKER}") + } else if flags.fuzz { + format!("not {SMPLX_NEXTEST_DSL_TEST_MARKER} and {SMPLX_NEXTEST_FUZZ_DSL_TEST_MARKER}") } else { SMPLX_NEXTEST_DSL_TEST_MARKER.into() }; diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 15136045..6842b40f 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -16,7 +16,17 @@ pub fn include_simf(tokenstream: TokenStream) -> TokenStream { pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemFn); - match smplx_test::macros::expand(args.into(), input) { + match smplx_test::macros::expand_test(args.into(), input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[proc_macro_attribute] +pub fn fuzz(args: TokenStream, input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::ItemFn); + + match smplx_test::macros::expand_fuzz(args.into(), input) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 2c5d6bdc..f319aa90 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -22,9 +22,10 @@ simplicityhl = { workspace = true } bitcoincore-rpc = { workspace = true, optional = true } serde = { workspace = true } hex = { workspace = true } +rand_core = { workspace = true } serde_json = { version = "1.0" } elements-miniscript = { version = "0.4", features = ["base64", "serde"] } bip39 = { version = "2.0.0", features = ["rand"] } dyn-clone = { version = "1.0.20" } -bitcoin_hashes = { version = "0.14.1" } +bitcoin_hashes = { version = "0.14.101" } diff --git a/crates/sdk/src/program/arguments.rs b/crates/sdk/src/program/arguments.rs index 61d734dd..9c891ad9 100644 --- a/crates/sdk/src/program/arguments.rs +++ b/crates/sdk/src/program/arguments.rs @@ -2,10 +2,17 @@ use dyn_clone::DynClone; use simplicityhl::Arguments; /// An interface for structs capable of generating static argument mapping for Simplicity programs. -/// See the `include_simc!()` macro, which generates automatic `ArgumentsTrait` implementation. +/// See the `include_simf!()` macro, which generates automatic `ArgumentsTrait` implementation. pub trait ArgumentsTrait: DynClone { /// Compiles and returns the bound `Arguments` dict required to instantiate a program. fn build_arguments(&self) -> Arguments; } dyn_clone::clone_trait_object!(ArgumentsTrait); + +/// An interface for the struct capable of generating proper `Arguments` mappings using the provided RNG. +/// See the ` include_simf!()` macro, which generates an automatic `ArgumentsTrait` implementation. +pub trait RandomArguments: ArgumentsTrait { + /// Generates a random `Arguments` instance using the provided RNG. + fn generate_arguments(rng: &mut dyn rand_core::RngCore) -> Arguments; +} diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index c37cb251..10406815 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -15,7 +15,6 @@ use simplicityhl::{CompiledProgram, UnstableFeatures}; use crate::global::GlobalConfig; use crate::program::logger::ProgramLogger; -use super::arguments::ArgumentsTrait; use super::error::ProgramError; use crate::provider::SimplicityNetwork; @@ -200,14 +199,53 @@ impl ProgramTrait for Program { } } +impl ProgramTrait for &T { + fn get_argument_types(&self) -> Result { + (*self).get_argument_types() + } + + fn get_witness_types(&self) -> Result { + (*self).get_witness_types() + } + + fn get_env( + &self, + pst: &PartiallySignedTransaction, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result>, ProgramError> { + (*self).get_env(pst, input_index, network) + } + + fn execute( + &self, + pst: &PartiallySignedTransaction, + witness: &WitnessValues, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result<(Arc, Value), ProgramError> { + (*self).execute(pst, witness, input_index, network) + } + + fn finalize( + &self, + pst: &PartiallySignedTransaction, + witness: &WitnessValues, + input_index: usize, + network: &SimplicityNetwork, + ) -> Result>, ProgramError> { + (*self).finalize(pst, witness, input_index, network) + } +} + impl Program { /// Creates a new instance of the struct with the provided source string and arguments. #[must_use] - pub fn new(source: impl Into>, arguments: &dyn ArgumentsTrait) -> Self { + pub fn new(source: impl Into>, arguments: impl Into) -> Self { Self { source: source.into(), pub_key: tr_unspendable_key(), - arguments: arguments.build_arguments(), + arguments: arguments.into(), storage: Vec::new(), include_debug_symbols: None, compiled: Arc::new(OnceLock::new()), @@ -347,7 +385,11 @@ impl Program { Ok(abi_meta.witness_types) } - fn load(&self) -> Result<&CompiledProgram, ProgramError> { + /// Compiles program by providing saved `Arguments`. + /// + /// # Errors + /// Retruns an error we have problems in a program compilation. + pub fn load(&self) -> Result<&CompiledProgram, ProgramError> { // Check cache first if let Some(compiled) = self.compiled.get() { return Ok(compiled); @@ -418,6 +460,14 @@ impl Program { } } +/// A trait for creating instances of a program. The `ProgramFactory` trait defines a mechanism +/// for constructing and returning a program instance of a type that implements `AsRef`. +/// Even only by generic struct name we have a possibility to create an instance of a program. +pub trait ProgramFactory + Sized> { + /// Instantiates a program instance with the given arguments. + fn instantiate_program(args: impl Into) -> Box

; +} + #[cfg(test)] mod tests { use simplicityhl::{ @@ -427,6 +477,8 @@ mod tests { use super::*; + use crate::program::ArgumentsTrait; + // simplicityhl/examples/cat.simf const DUMMY_PROGRAM: &str = r" fn main() { @@ -448,12 +500,18 @@ mod tests { } } + impl From for Arguments { + fn from(val: EmptyArguments) -> Self { + val.build_arguments() + } + } + fn dummy_asset_id(byte: u8) -> AssetId { AssetId::from_slice(&[byte; 32]).unwrap() } fn dummy_program() -> Program { - Program::new(DUMMY_PROGRAM, &EmptyArguments) + Program::new(DUMMY_PROGRAM, EmptyArguments {}) } fn dummy_network() -> SimplicityNetwork { diff --git a/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs index 8722cf7e..2246c34a 100644 --- a/crates/sdk/src/program/mod.rs +++ b/crates/sdk/src/program/mod.rs @@ -9,8 +9,8 @@ pub mod logger; /// Definitions and traits for resolving and satisfying execution witnesses for Simplicity programs. pub mod witness; -pub use arguments::ArgumentsTrait; -pub use core::{Program, ProgramTrait}; +pub use arguments::{ArgumentsTrait, RandomArguments}; +pub use core::{Program, ProgramFactory, ProgramTrait}; pub use error::ProgramError; pub use simplicityhl::tracker::TrackerLogLevel; -pub use witness::WitnessTrait; +pub use witness::{RandomWitness, WitnessTrait}; diff --git a/crates/sdk/src/program/witness.rs b/crates/sdk/src/program/witness.rs index 7bf47777..eaa451b0 100644 --- a/crates/sdk/src/program/witness.rs +++ b/crates/sdk/src/program/witness.rs @@ -2,10 +2,17 @@ use dyn_clone::DynClone; use simplicityhl::WitnessValues; /// An interface for structs capable of generating Simplicity program witness mappings. -/// See the ` include_simc!()` macro, which generates an automatic `WitnessTrait` implementation. +/// See the ` include_simf!()` macro, which generates an automatic `WitnessTrait` implementation. pub trait WitnessTrait: DynClone { /// Compiles and generates the fully populated `WitnessValues` map for execution. fn build_witness(&self) -> WitnessValues; } dyn_clone::clone_trait_object!(WitnessTrait); + +/// An interface for the struct capable of generating proper `WitnessValues` mappings using the provided RNG. +/// See the ` include_simf!()` macro, which generates an automatic `RandomWitness` implementation. +pub trait RandomWitness: WitnessTrait { + /// Generates a random `WitnessValues` instance using the provided RNG. + fn generate_witness(rng: &mut dyn rand_core::RngCore) -> WitnessValues; +} diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index 2f39c569..f302c19b 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -530,7 +530,7 @@ impl Signer { self.network.policy_asset(), )); - let final_tx = self.sign_tx(&fee_tx)?; + let final_tx = self.sign_and_extract_tx(&fee_tx)?; let fee = fee_tx.calculate_fee(final_tx.discount_weight(), fee_rate); if available_delta > fee && available_delta - fee >= MIN_FEE { @@ -540,7 +540,7 @@ impl Signer { outputs[outputs.len() - 2].amount = available_delta - fee; outputs[outputs.len() - 1].amount = fee; - let final_tx = self.sign_tx(&fee_tx)?; + let final_tx = self.sign_and_extract_tx(&fee_tx)?; return Ok(Estimate::Success(final_tx, fee)); } @@ -552,7 +552,7 @@ impl Signer { // Fix this by adding a dummy change or throwing a clear error. fee_tx.remove_output(fee_tx.n_outputs() - 2); - let final_tx = self.sign_tx(&fee_tx)?; + let final_tx = self.sign_and_extract_tx(&fee_tx)?; let fee = fee_tx.calculate_fee(final_tx.discount_weight(), fee_rate); if available_delta < fee { @@ -565,12 +565,19 @@ impl Signer { outputs[outputs.len() - 1].amount = available_delta; // Finalize the tx with fee and without the change - let final_tx = self.sign_tx(&fee_tx)?; + let final_tx = self.sign_and_extract_tx(&fee_tx)?; Ok(Estimate::Success(final_tx, fee)) } - fn sign_tx(&self, tx: &FinalTransaction) -> Result { + /// Signs transaction in raw format for easy processing later in a format of `PartiallySignedTransaction`. + /// + /// # Errors + /// Returns a `SignerError` if we have an error in singing and constructing program witness. + /// + /// # Panics + /// Throws a panic if we failed to sign a program witness. + pub fn sign_tx(&self, tx: &FinalTransaction) -> Result { let (mut pst, secrets) = tx.extract_pst(); let inputs = tx.inputs(); @@ -597,7 +604,7 @@ impl Signer { Some((witness_name, sig_path, message)) => Ok(self.get_signed_program_witness( &pst, program_input.program.as_ref(), - &program_input.witness.build_witness(), + &program_input.witness, witness_name, sig_path, index, @@ -605,7 +612,7 @@ impl Signer { message, )?), // Just build the witness - None => Ok(program_input.witness.build_witness()), + None => Ok(program_input.witness.shallow_clone()), }; let pruned_witness = program_input @@ -631,11 +638,21 @@ impl Signer { } } - Ok(pst.extract_tx()?) + Ok(pst) } + fn sign_and_extract_tx(&self, tx: &FinalTransaction) -> Result { + Ok(self.sign_tx(tx)?.extract_tx()?) + } + + /// Signs and inserts a signature into appropriate witness value. + /// + /// # Errors + /// Returns a `SignerError` if signing the program fails, if the witness types cannot be + /// retrieved from the program, if `witness_name` is not present among the program's + /// witness fields, or if injecting the signature into the witness value at `sig_path` fails. #[allow(clippy::too_many_arguments)] - fn get_signed_program_witness( + pub fn get_signed_program_witness( &self, pst: &PartiallySignedTransaction, program: &dyn ProgramTrait, @@ -657,6 +674,7 @@ impl Signer { .get(&WitnessName::from_str_unchecked(witness_name)) .ok_or(SignerError::WtnsFieldNotFound(witness_name.to_string()))?; + #[allow(clippy::missing_panics_doc)] let local_wtns = Arc::new( witness .get(&WitnessName::from_str_unchecked(witness_name)) diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index 2a00cebf..07fead1f 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -30,7 +30,7 @@ pub struct IssuanceDetails { } /// Represents the final input structure put into a `FinalTransaction` for processing. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct FinalInput { /// Holds the base input data required for the operation. pub partial_input: PartialInput, @@ -140,7 +140,7 @@ impl FinalInput { } /// A struct representing a final (but not yet signed) transaction. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct FinalTransaction { inputs: Vec, outputs: Vec, diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index c59e4fd5..e6776f0f 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -1,8 +1,10 @@ use std::fmt; +use std::fmt::{Debug, Formatter}; use std::sync::Arc; use elements_miniscript::bitcoin::bip32::DerivationPath; +use simplicityhl::WitnessValues; use simplicityhl::elements::confidential::{Asset, Value}; use simplicityhl::elements::pset::Input; use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, TxOutSecrets, Txid}; @@ -135,7 +137,13 @@ pub struct ProgramInput { /// The compiled program interface associated with the input. pub program: Box, /// The witness values required to satisfy the program. - pub witness: Box, + pub witness: WitnessValues, +} + +impl Debug for ProgramInput { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.witness) + } } /// Represents an input designated for asset issuance or reissuance. @@ -251,8 +259,11 @@ impl PartialInput { impl ProgramInput { /// Creates a new `ProgramInput` from a `ProgramTrait` and its associated `WitnessTrait`. #[must_use] - pub fn new(program: Box, witness: Box) -> Self { - Self { program, witness } + pub fn new(program: Box, witness: impl Into) -> Self { + Self { + program, + witness: witness.into(), + } } } diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index 61abf849..eeb68f67 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -24,6 +24,8 @@ smplx-sdk = { workspace = true, features = ["provider"] } serde = { workspace = true } simplicityhl = { workspace = true, features = ["serde"] } +rand_core = { workspace = true } +rand = { workspace = true } either = { version = "1.15.0", features = ["serde"] } diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index 4e3525b9..5b7f598a 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -1,4 +1,6 @@ pub use either; +pub use rand; +pub use rand_core; pub use serde; pub use simplicityhl; @@ -6,6 +8,7 @@ pub use smplx_sdk::*; pub use smplx_test::config::TestConfig; pub use smplx_test::context::TestContext; +pub use smplx_test::fuzz; pub use smplx_macros; -pub use smplx_macros::{include_simf, test}; +pub use smplx_macros::{fuzz, include_simf, test}; diff --git a/crates/simplex/tests/ui/array_tr_storage.rs b/crates/simplex/tests/ui/array_tr_storage.rs index 26ad3d9a..112a9149 100644 --- a/crates/simplex/tests/ui/array_tr_storage.rs +++ b/crates/simplex/tests/ui/array_tr_storage.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{ArgumentsTrait, WitnessTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct ArrayTrStorageProgram { + program: Program, +} +impl ArrayTrStorageProgram { + pub const SOURCE: &'static str = derived_array_tr_storage::ARRAY_TR_STORAGE_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for ArrayTrStorageProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for ArrayTrStorageProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/array_tr_storage.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_array_tr_storage::ArrayTrStorageWitness::default(); let witness_values = original_witness.build_witness(); @@ -18,3 +91,88 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_array_tr_storage::ArrayTrStorageWitness::default(), + derived_array_tr_storage::ArrayTrStorageWitness::default() + ); + assert_eq!( + derived_array_tr_storage::ArrayTrStorageArguments::default(), + derived_array_tr_storage::ArrayTrStorageArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_array_tr_storage::ArrayTrStorageWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_array_tr_storage::ArrayTrStorageWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_array_tr_storage::ArrayTrStorageWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_array_tr_storage::ArrayTrStorageArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_array_tr_storage::ArrayTrStorageArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_array_tr_storage::ArrayTrStorageArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_array_tr_storage::ArrayTrStorageWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_array_tr_storage::ArrayTrStorageWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_array_tr_storage::ArrayTrStorageArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_array_tr_storage::ArrayTrStorageArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} \ No newline at end of file diff --git a/crates/simplex/tests/ui/bytes32_tr_storage.rs b/crates/simplex/tests/ui/bytes32_tr_storage.rs index de30a3f4..ba438a7a 100644 --- a/crates/simplex/tests/ui/bytes32_tr_storage.rs +++ b/crates/simplex/tests/ui/bytes32_tr_storage.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct Bytes32TrStorageProgram { + program: Program, +} +impl Bytes32TrStorageProgram { + pub const SOURCE: &'static str = derived_bytes32_tr_storage::BYTES32_TR_STORAGE_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for Bytes32TrStorageProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for Bytes32TrStorageProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/bytes32_tr_storage.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_bytes32_tr_storage::Bytes32TrStorageWitness::default(); let witness_values = original_witness.build_witness(); @@ -18,3 +91,90 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_bytes32_tr_storage::Bytes32TrStorageWitness::default(), + derived_bytes32_tr_storage::Bytes32TrStorageWitness::default() + ); + assert_eq!( + derived_bytes32_tr_storage::Bytes32TrStorageArguments::default(), + derived_bytes32_tr_storage::Bytes32TrStorageArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_bytes32_tr_storage::Bytes32TrStorageWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_bytes32_tr_storage::Bytes32TrStorageWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_bytes32_tr_storage::Bytes32TrStorageWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = + derived_bytes32_tr_storage::Bytes32TrStorageArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = + derived_bytes32_tr_storage::Bytes32TrStorageArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = + derived_bytes32_tr_storage::Bytes32TrStorageArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_bytes32_tr_storage::Bytes32TrStorageWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_bytes32_tr_storage::Bytes32TrStorageWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_bytes32_tr_storage::Bytes32TrStorageArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_bytes32_tr_storage::Bytes32TrStorageArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/dual_currency_deposit.rs b/crates/simplex/tests/ui/dual_currency_deposit.rs index a1395759..d016d1ba 100644 --- a/crates/simplex/tests/ui/dual_currency_deposit.rs +++ b/crates/simplex/tests/ui/dual_currency_deposit.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct DualCurrencyDepositProgram { + program: Program, +} +impl DualCurrencyDepositProgram { + pub const SOURCE: &'static str = derived_dual_currency_deposit::DUAL_CURRENCY_DEPOSIT_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for DualCurrencyDepositProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for DualCurrencyDepositProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/dual_currency_deposit.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_dual_currency_deposit::DualCurrencyDepositWitness::default(); let witness_values = original_witness.build_witness(); @@ -19,3 +92,94 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_dual_currency_deposit::DualCurrencyDepositWitness::default(), + derived_dual_currency_deposit::DualCurrencyDepositWitness::default() + ); + assert_eq!( + derived_dual_currency_deposit::DualCurrencyDepositArguments::default(), + derived_dual_currency_deposit::DualCurrencyDepositArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = + derived_dual_currency_deposit::DualCurrencyDepositWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = + derived_dual_currency_deposit::DualCurrencyDepositWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = + derived_dual_currency_deposit::DualCurrencyDepositWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = + derived_dual_currency_deposit::DualCurrencyDepositArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = + derived_dual_currency_deposit::DualCurrencyDepositArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = + derived_dual_currency_deposit::DualCurrencyDepositArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_dual_currency_deposit::DualCurrencyDepositWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_dual_currency_deposit::DualCurrencyDepositWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = + derived_dual_currency_deposit::DualCurrencyDepositArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_dual_currency_deposit::DualCurrencyDepositArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/either_with_single_witness.rs b/crates/simplex/tests/ui/either_with_single_witness.rs index 9f1186ef..5755d79d 100644 --- a/crates/simplex/tests/ui/either_with_single_witness.rs +++ b/crates/simplex/tests/ui/either_with_single_witness.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct EitherWithSingleWitnessProgram { + program: Program, +} +impl EitherWithSingleWitnessProgram { + pub const SOURCE: &'static str = derived_either_with_single_witness::EITHER_WITH_SINGLE_WITNESS_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for EitherWithSingleWitnessProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for EitherWithSingleWitnessProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/either_with_single_witness.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_either_with_single_witness::EitherWithSingleWitnessWitness::default(); let witness_values = original_witness.build_witness(); @@ -20,3 +93,97 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_either_with_single_witness::EitherWithSingleWitnessWitness::default(), + derived_either_with_single_witness::EitherWithSingleWitnessWitness::default() + ); + assert_eq!( + derived_either_with_single_witness::EitherWithSingleWitnessArguments::default(), + derived_either_with_single_witness::EitherWithSingleWitnessArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = + derived_either_with_single_witness::EitherWithSingleWitnessWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = + derived_either_with_single_witness::EitherWithSingleWitnessWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = + derived_either_with_single_witness::EitherWithSingleWitnessWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = + derived_either_with_single_witness::EitherWithSingleWitnessArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = + derived_either_with_single_witness::EitherWithSingleWitnessArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = + derived_either_with_single_witness::EitherWithSingleWitnessArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = + derived_either_with_single_witness::EitherWithSingleWitnessWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_either_with_single_witness::EitherWithSingleWitnessWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = + derived_either_with_single_witness::EitherWithSingleWitnessArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_either_with_single_witness::EitherWithSingleWitnessArguments::from_arguments( + &arguments_values, + )?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/exotic_values.rs b/crates/simplex/tests/ui/exotic_values.rs index e0aab6a7..03ddf9e2 100644 --- a/crates/simplex/tests/ui/exotic_values.rs +++ b/crates/simplex/tests/ui/exotic_values.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct ExoticValuesProgram { + program: Program, +} +impl ExoticValuesProgram { + pub const SOURCE: &'static str = derived_exotic_values::EXOTIC_VALUES_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for ExoticValuesProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for ExoticValuesProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/exotic_values.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_exotic_values::ExoticValuesWitness::default(); let witness_values = original_witness.build_witness(); @@ -13,9 +86,92 @@ fn main() -> Result<(), String> { let original_arguments = derived_exotic_values::ExoticValuesArguments::default(); let arguments_values = original_arguments.build_arguments(); - let recovered_arguments = - derived_exotic_values::ExoticValuesArguments::from_arguments(&arguments_values)?; + let recovered_arguments = derived_exotic_values::ExoticValuesArguments::from_arguments(&arguments_values)?; assert_eq!(original_arguments, recovered_arguments); Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_exotic_values::ExoticValuesWitness::default(), + derived_exotic_values::ExoticValuesWitness::default() + ); + assert_eq!( + derived_exotic_values::ExoticValuesArguments::default(), + derived_exotic_values::ExoticValuesArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_exotic_values::ExoticValuesWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_exotic_values::ExoticValuesWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_exotic_values::ExoticValuesWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_exotic_values::ExoticValuesArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_exotic_values::ExoticValuesArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_exotic_values::ExoticValuesArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_exotic_values::ExoticValuesWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_exotic_values::ExoticValuesWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_exotic_values::ExoticValuesArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_exotic_values::ExoticValuesArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/list_check.rs b/crates/simplex/tests/ui/list_check.rs index a36079a0..155a368e 100644 --- a/crates/simplex/tests/ui/list_check.rs +++ b/crates/simplex/tests/ui/list_check.rs @@ -1,9 +1,84 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct ListCheckProgram { + program: Program, +} +impl ListCheckProgram { + pub const SOURCE: &'static str = derived_list_check::LIST_CHECK_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for ListCheckProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for ListCheckProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/list_check.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_build_panic()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_list_check::ListCheckWitness { draft: vec![], path: simplex::either::Left(vec![ @@ -17,12 +92,16 @@ fn main() -> Result<(), String> { let recovered_witness = derived_list_check::ListCheckWitness::from_witness(&witness_values)?; assert_eq!(original_witness, recovered_witness); - let original_arguments = derived_list_check::ListCheckArguments {}; + let original_arguments = derived_list_check::ListCheckArguments::default(); let arguments_values = original_arguments.build_arguments(); let recovered_arguments = derived_list_check::ListCheckArguments::from_arguments(&arguments_values)?; assert_eq!(original_arguments, recovered_arguments); + Ok(()) +} + +fn test_build_panic() -> Result<(), String> { // Build Witness, which would panic on building let original_witness = derived_list_check::ListCheckWitness { draft: vec![], @@ -37,13 +116,99 @@ fn main() -> Result<(), String> { // Register panic hook to reduce warnings let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(|_| {})); - let result = std::panic::catch_unwind(|| { - original_witness.build_witness() - }); + let result = std::panic::catch_unwind(|| original_witness.build_witness()); std::panic::set_hook(default_hook); - assert!(result.is_err(), "Expected build_witness to panic, as we have Vec size equal to list size, but it succeeded."); + assert!( + result.is_err(), + "Expected build_witness to panic, as we have Vec size equal to list size, but it succeeded." + ); + + Ok(()) +} + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_list_check::ListCheckWitness::default(), + derived_list_check::ListCheckWitness::default() + ); + assert_eq!( + derived_list_check::ListCheckArguments::default(), + derived_list_check::ListCheckArguments::default() + ); + + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_list_check::ListCheckWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_list_check::ListCheckWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_list_check::ListCheckWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_list_check::ListCheckArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_list_check::ListCheckArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_list_check::ListCheckArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_list_check::ListCheckWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_list_check::ListCheckWitness::from_witness(&witness_values)?; + } + { + let arguments_values = derived_list_check::ListCheckArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_list_check::ListCheckArguments::from_arguments(&arguments_values)?; + } + } Ok(()) } +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/option_offer.rs b/crates/simplex/tests/ui/option_offer.rs index 3ace0aeb..f1a10cbc 100644 --- a/crates/simplex/tests/ui/option_offer.rs +++ b/crates/simplex/tests/ui/option_offer.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct OptionOfferProgram { + program: Program, +} +impl OptionOfferProgram { + pub const SOURCE: &'static str = derived_option_offer::OPTION_OFFER_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for OptionOfferProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for OptionOfferProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/option_offer.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_option_offer::OptionOfferWitness::default(); let witness_values = original_witness.build_witness(); @@ -18,3 +91,87 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_option_offer::OptionOfferWitness::default(), + derived_option_offer::OptionOfferWitness::default() + ); + assert_eq!( + derived_option_offer::OptionOfferArguments::default(), + derived_option_offer::OptionOfferArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_option_offer::OptionOfferWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_option_offer::OptionOfferWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_option_offer::OptionOfferWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_option_offer::OptionOfferArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_option_offer::OptionOfferArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_option_offer::OptionOfferArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_option_offer::OptionOfferWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_option_offer::OptionOfferWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_option_offer::OptionOfferArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_option_offer::OptionOfferArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/options.rs b/crates/simplex/tests/ui/options.rs index 9dad90af..d2b8d48f 100644 --- a/crates/simplex/tests/ui/options.rs +++ b/crates/simplex/tests/ui/options.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct OptionsProgram { + program: Program, +} +impl OptionsProgram { + pub const SOURCE: &'static str = derived_options::OPTIONS_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for OptionsProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for OptionsProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/options.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_options::OptionsWitness::default(); let witness_values = original_witness.build_witness(); @@ -18,3 +91,87 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_options::OptionsWitness::default(), + derived_options::OptionsWitness::default() + ); + assert_eq!( + derived_options::OptionsArguments::default(), + derived_options::OptionsArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_options::OptionsWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_options::OptionsWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_options::OptionsWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_options::OptionsArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_options::OptionsArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_options::OptionsArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_options::OptionsWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_options::OptionsWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_options::OptionsArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_options::OptionsArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/simple_storage.rs b/crates/simplex/tests/ui/simple_storage.rs index 34147700..58d1d5b8 100644 --- a/crates/simplex/tests/ui/simple_storage.rs +++ b/crates/simplex/tests/ui/simple_storage.rs @@ -1,9 +1,82 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct SimpleStorageProgram { + program: Program, +} +impl SimpleStorageProgram { + pub const SOURCE: &'static str = derived_simple_storage::SIMPLE_STORAGE_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for SimpleStorageProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for SimpleStorageProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/simple_storage.simf"); fn main() -> Result<(), String> { + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; + + Ok(()) +} + +fn test_e2e_behaviour() -> Result<(), String> { let original_witness = derived_simple_storage::SimpleStorageWitness::default(); let witness_values = original_witness.build_witness(); @@ -18,3 +91,87 @@ fn main() -> Result<(), String> { Ok(()) } + +fn test_default() -> Result<(), String> { + assert_eq!( + derived_simple_storage::SimpleStorageWitness::default(), + derived_simple_storage::SimpleStorageWitness::default() + ); + assert_eq!( + derived_simple_storage::SimpleStorageArguments::default(), + derived_simple_storage::SimpleStorageArguments::default() + ); + Ok(()) +} + +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + let original_witness = derived_simple_storage::SimpleStorageWitness::generate_witness_raw(&mut rng); + + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_simple_storage::SimpleStorageWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_simple_storage::SimpleStorageWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); + + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_simple_storage::SimpleStorageArguments::generate_arguments_raw(&mut rng); + + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_simple_storage::SimpleStorageArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_simple_storage::SimpleStorageArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } + Ok(()) +} + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_simple_storage::SimpleStorageWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_simple_storage::SimpleStorageWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_simple_storage::SimpleStorageArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_simple_storage::SimpleStorageArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/simplex/tests/ui/single_bit.rs b/crates/simplex/tests/ui/single_bit.rs index d1397cc2..153c360d 100644 --- a/crates/simplex/tests/ui/single_bit.rs +++ b/crates/simplex/tests/ui/single_bit.rs @@ -1,84 +1,179 @@ use simplex::include_simf; -use simplex::program::{WitnessTrait, ArgumentsTrait}; +use simplex::fuzz::{generate_value_by_ty}; +use simplex::program::Program; +use simplex::program::{ArgumentsTrait, RandomArguments, RandomWitness, WitnessTrait}; +use simplex::provider::SimplicityNetwork; +use simplex::simplicityhl::elements::secp256k1_zkp::XOnlyPublicKey; +use simplex::simplicityhl::elements::Script; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::rand::RngCore; + +#[derive(Clone)] +pub struct SingleBitProgram { + program: Program, +} +impl SingleBitProgram { + pub const SOURCE: &'static str = derived_single_bit::SINGLE_BIT_CONTRACT_SOURCE; + #[must_use] + pub fn new(arguments: impl Into) -> Self { + Self { + program: Program::new(Self::SOURCE, arguments.into()), + } + } + #[must_use] + pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { + self.program = self.program.with_taproot_pubkey(pub_key); + self + } + #[must_use] + pub fn with_storage_capacity(mut self, capacity: usize) -> Self { + self.program = self.program.with_storage_capacity(capacity); + self + } + pub fn set_storage_at(&mut self, index: usize, new_value: impl Into>) { + self.program.set_storage_at(index, new_value); + } + #[must_use] + pub fn get_storage_len(&self) -> usize { + self.program.get_storage_len() + } + #[must_use] + pub fn get_storage(&self) -> &[Vec] { + self.program.get_storage() + } + #[must_use] + pub fn get_storage_at(&self, index: usize) -> Vec { + self.program.get_storage_at(index) + } + #[must_use] + pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { + self.program.get_script_pubkey(network) + } + #[must_use] + pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { + self.program.get_script_hash(network) + } +} +impl AsRef for SingleBitProgram { + fn as_ref(&self) -> &Program { + &self.program + } +} +impl AsMut for SingleBitProgram { + fn as_mut(&mut self) -> &mut Program { + &mut self.program + } +} include_simf!("../../../../crates/simplex/tests/ui_simfs/single_bit.simf"); fn main() -> Result<(), String> { - // Testing values with (FLAG = 1, BIT = 1) - let original_witness = derived_single_bit::SingleBitWitness { bit: 1 }; + let _ = test_e2e_behaviour()?; + let _ = test_default()?; + let _ = test_e2e_random_behaviour()?; + let _ = test_e2e_random_value_generation_behaviour()?; - let witness_values = original_witness.build_witness(); - let recovered_witness = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; - assert_eq!( - original_witness, recovered_witness, - "Testing values with (FLAG = 1, BIT = 1) (Witness)" - ); + Ok(()) +} - let original_arguments = derived_single_bit::SingleBitArguments { flag: 1 }; +fn test_e2e_behaviour() -> Result<(), String> { + for (bit, flag) in [(1, 1), (1, 0), (0, 1), (0, 0)] { + let original_witness = derived_single_bit::SingleBitWitness { bit }; - let arguments_values = original_arguments.build_arguments(); - let recovered_arguments = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; - assert_eq!( - original_arguments, recovered_arguments, - "Testing values with (FLAG = 1, BIT = 1) (Arguments)" - ); + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); - // Testing values with (FLAG = 0, BIT = 1) - let original_witness = derived_single_bit::SingleBitWitness { bit: 1 }; + let original_arguments = derived_single_bit::SingleBitArguments { flag }; - let witness_values = original_witness.build_witness(); - let recovered_witness = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; - assert_eq!( - original_witness, recovered_witness, - "Testing values with (FLAG = 0, BIT = 1) (Witness)" - ); + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + } - let original_arguments = derived_single_bit::SingleBitArguments { flag: 0 }; + Ok(()) +} - let arguments_values = original_arguments.build_arguments(); - let recovered_arguments = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; +fn test_default() -> Result<(), String> { assert_eq!( - original_arguments, recovered_arguments, - "Testing values with (FLAG = 0, BIT = 1) (Arguments)" + derived_single_bit::SingleBitWitness::default(), + derived_single_bit::SingleBitWitness::default() ); - - // Testing values with (FLAG = 1, BIT = 0) - let original_witness = derived_single_bit::SingleBitWitness { bit: 0 }; - - let witness_values = original_witness.build_witness(); - let recovered_witness = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; assert_eq!( - original_witness, recovered_witness, - "Testing values with (FLAG = 1, BIT = 0) (Witness)" + derived_single_bit::SingleBitArguments::default(), + derived_single_bit::SingleBitArguments::default() ); + Ok(()) +} - let original_arguments = derived_single_bit::SingleBitArguments { flag: 1 }; +fn test_e2e_random_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; - let arguments_values = original_arguments.build_arguments(); - let recovered_arguments = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; - assert_eq!( - original_arguments, recovered_arguments, - "Testing values with (FLAG = 1, BIT = 0) (Arguments)" - ); + let mut rng = StdRng::seed_from_u64(seed); - // Testing values with (FLAG = 0, BIT = 0) - let original_witness = derived_single_bit::SingleBitWitness { bit: 0 }; + let original_witness = derived_single_bit::SingleBitWitness::generate_witness_raw(&mut rng); - let witness_values = original_witness.build_witness(); - let recovered_witness = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; - assert_eq!( - original_witness, recovered_witness, - "Testing values with (FLAG = 0, BIT = 0) (Witness)" - ); + let witness_values = original_witness.build_witness(); + let recovered_witness = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; + assert_eq!(original_witness, recovered_witness); - let original_arguments = derived_single_bit::SingleBitArguments { flag: 0 }; + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_single_bit::SingleBitWitness::generate_witness(&mut rng); + assert_eq!(witness_values, rand_raw_witness_values); - let arguments_values = original_arguments.build_arguments(); - let recovered_arguments = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; - assert_eq!( - original_arguments, recovered_arguments, - "Testing values with (FLAG = 0, BIT = 0) (Arguments)" - ); + rng = StdRng::seed_from_u64(seed); + let original_arguments = derived_single_bit::SingleBitArguments::generate_arguments_raw(&mut rng); + let arguments_values = original_arguments.build_arguments(); + let recovered_arguments = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; + assert_eq!(original_arguments, recovered_arguments); + + rng = StdRng::seed_from_u64(seed); + let rand_raw_witness_values = derived_single_bit::SingleBitArguments::generate_arguments(&mut rng); + assert_eq!(arguments_values, rand_raw_witness_values); + } Ok(()) } + +fn test_e2e_random_value_generation_behaviour() -> Result<(), String> { + for seed in 0..32 { + use simplex::rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::seed_from_u64(seed); + + { + let witness_values = derived_single_bit::SingleBitWitness::generate_witness(&mut rng); + + let witness_values = regenerate_witness_values(&witness_values, &mut rng); + let _ = derived_single_bit::SingleBitWitness::from_witness(&witness_values)?; + } + + { + let arguments_values = derived_single_bit::SingleBitArguments::generate_arguments(&mut rng); + + let arguments_values = regenerate_arguments_values(&arguments_values, &mut rng); + let _ = derived_single_bit::SingleBitArguments::from_arguments(&arguments_values)?; + } + } + Ok(()) +} + +fn regenerate_witness_values(wit: &WitnessValues, rng: &mut dyn RngCore) -> WitnessValues { + use std::collections::HashMap; + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in wit.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + WitnessValues::from(map) +} + +fn regenerate_arguments_values(args: &Arguments, rng: &mut dyn RngCore) -> Arguments { + use std::collections::HashMap; + + let mut map: HashMap<_,_> = Default::default(); + for (name, val) in args.iter() { + map.insert(name.clone(), generate_value_by_ty(val.ty(), rng)); + } + Arguments::from(map) +} diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 6beff889..00fd54f1 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -19,7 +19,10 @@ simplicityhl = { workspace = true } electrsd = { workspace = true } serde = { workspace = true } toml = { workspace = true } +rand = { workspace = true } syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } proc-macro2 = { version = "1.0.106", features = ["span-locations"] } quote = { version = "1.0.44" } +proptest = { version = "1.11.0", features = ["std"] } + diff --git a/crates/test/src/config.rs b/crates/test/src/config.rs index bacb551d..ca336821 100644 --- a/crates/test/src/config.rs +++ b/crates/test/src/config.rs @@ -22,6 +22,7 @@ pub struct TestConfig { pub bitcoins: u64, pub esplora: Option, pub rpc: Option, + pub proptest: Option, pub verbosity: Verbosity, } @@ -38,6 +39,14 @@ pub struct RpcConfig { pub password: String, } +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ProptestConfig { + pub cases: Option, + pub max_shrink_iters: Option, + pub max_global_rejects: Option, + pub max_local_rejects: Option, +} + impl TestConfig { pub fn from_file(path: impl AsRef) -> Result { let mut content = String::new(); @@ -80,6 +89,7 @@ impl Default for TestConfig { bitcoins: DEFAULT_BITCOINS, esplora: None, rpc: None, + proptest: None, verbosity: Verbosity::None, } } diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index dcb52145..1c999cd7 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -4,6 +4,8 @@ use smplx_sdk::provider::ProviderError; use smplx_regtest::error::RegtestError; +use crate::fuzz::builders::ProgramTarget; + #[derive(thiserror::Error, Debug)] pub enum TestError { #[error(transparent)] @@ -33,3 +35,21 @@ pub enum NetworkUtilsError { #[error("Unsuccessful action completion, err: '{0}'")] UnsuccessfulSync(String), } + +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum FuzzError { + #[error("At least one program target is required")] + NoProgramTargets, + + #[error("Duplicate program target: {0:?}")] + DuplicateProgramTarget(ProgramTarget), + + #[error("Program input target index {index} is out of bounds for {input_count} inputs")] + InputTargetOutOfBounds { index: usize, input_count: usize }, + + #[error("Program output target index {index} is out of bounds for {output_count} outputs")] + OutputTargetOutOfBounds { index: usize, output_count: usize }, + + #[error("Fuzz transaction post hook failed: {0}")] + PostHook(String), +} diff --git a/crates/test/src/fuzz/args_strategy.rs b/crates/test/src/fuzz/args_strategy.rs new file mode 100644 index 00000000..ae56c221 --- /dev/null +++ b/crates/test/src/fuzz/args_strategy.rs @@ -0,0 +1,7 @@ +mod random; +mod random_interesting; +mod random_pool; + +pub use random::*; +pub use random_interesting::*; +pub use random_pool::*; diff --git a/crates/test/src/fuzz/args_strategy/random.rs b/crates/test/src/fuzz/args_strategy/random.rs new file mode 100644 index 00000000..0b59cec0 --- /dev/null +++ b/crates/test/src/fuzz/args_strategy/random.rs @@ -0,0 +1,55 @@ +use std::fmt::{Debug, Formatter}; +use std::marker::PhantomData; + +use simplicityhl::{Arguments, WitnessValues}; + +use proptest::prelude::Strategy; +use proptest::strategy::{NewTree, ValueTree}; +use proptest::test_runner::TestRunner; + +use smplx_sdk::program::{RandomArguments, RandomWitness}; + +pub struct Random { + phantom_data: PhantomData<(Args, Wit)>, +} + +impl Default for Random { + fn default() -> Self { + Self { + phantom_data: PhantomData, + } + } +} + +impl Debug for Random { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Random trees...") + } +} + +pub struct RandomValueTree(T); + +impl ValueTree for RandomValueTree { + type Value = T; + fn current(&self) -> T { + self.0.clone() + } + fn simplify(&mut self) -> bool { + false + } + fn complicate(&mut self) -> bool { + false + } +} + +impl Strategy for Random { + type Tree = RandomValueTree<(Arguments, WitnessValues)>; + type Value = (Arguments, WitnessValues); + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + Ok(RandomValueTree(( + Args::generate_arguments(runner.rng()), + Wit::generate_witness(runner.rng()), + ))) + } +} diff --git a/crates/test/src/fuzz/args_strategy/random_interesting.rs b/crates/test/src/fuzz/args_strategy/random_interesting.rs new file mode 100644 index 00000000..ae1956a3 --- /dev/null +++ b/crates/test/src/fuzz/args_strategy/random_interesting.rs @@ -0,0 +1,332 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::marker::PhantomData; + +use simplicityhl::num::U256; +use simplicityhl::types::{TypeInner, UIntType}; +use simplicityhl::value::ValueConstructible; +use simplicityhl::{Arguments, ResolvedType, Value, WitnessValues}; + +use proptest::prelude::Rng; +use proptest::prelude::Strategy; +use proptest::strategy::{NewTree, ValueTree}; +use proptest::test_runner::{TestRng, TestRunner}; +use rand::prelude::IndexedRandom; + +use smplx_sdk::program::{RandomArguments, RandomWitness}; + +static INTERESTING_U4: &[u8] = &[0, 1, 2, 7, 8, 14, 15]; + +static INTERESTING_U8: &[u8] = &[0, 1, 2, 16, 32, 64, 100, (1 << 7) - 1, 1 << 7, u8::MAX - 1, u8::MAX]; + +static INTERESTING_U16: &[u16] = &[ + 0, + 1, + 2, + 16, + 32, + 64, + 100, + 1000, + 1024, + 4096, + (1 << 7) - 1, + 1 << 7, + u8::MAX as u16, + 1 << 8, + (1 << 15) - 1, + 1 << 15, + u16::MAX - 1, + u16::MAX, +]; + +static INTERESTING_U32: &[u32] = &[ + 0, + 1, + 2, + 16, + 32, + 64, + 100, + 1000, + 1024, + 4096, + (1 << 7) - 1, + 1 << 7, + u8::MAX as u32, + 1 << 8, + (1 << 15) - 1, + 1 << 15, + u16::MAX as u32, + 1 << 16, + (1 << 31) - 1, + 1 << 31, + u32::MAX - 1, + u32::MAX, +]; + +static INTERESTING_U64: &[u64] = &[ + 0, + 1, + 2, + 16, + 32, + 64, + 100, + 1000, + 1024, + 4096, + (1 << 7) - 1, + 1 << 7, + u8::MAX as u64, + 1 << 8, + (1 << 15) - 1, + 1 << 15, + u16::MAX as u64, + 1 << 16, + (1 << 31) - 1, + 1 << 31, + u32::MAX as u64, + 1 << 32, + (1 << 63) - 1, + 1 << 63, + u64::MAX - 1, + u64::MAX, +]; + +static INTERESTING_U128: &[u128] = &[ + 0, + 1, + 2, + 16, + 32, + 64, + 100, + 1000, + 1024, + 4096, + (1 << 7) - 1, + 1 << 7, + u8::MAX as u128, + 1 << 8, + (1 << 15) - 1, + 1 << 15, + u16::MAX as u128, + 1 << 16, + (1 << 31) - 1, + 1 << 31, + u32::MAX as u128, + 1 << 32, + (1 << 63) - 1, + 1 << 63, + u64::MAX as u128, + 1 << 64, + (1_u128 << 127) - 1, + 1_u128 << 127, + u128::MAX - 1, + u128::MAX, +]; + +pub struct InterestingRandom { + phantom_data: PhantomData<(Args, Wit)>, +} + +impl Default for InterestingRandom { + fn default() -> Self { + Self { + phantom_data: PhantomData, + } + } +} + +impl Debug for InterestingRandom { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "InterestingRandom trees...") + } +} + +pub struct InterestingRandomValueTree(T); + +impl ValueTree for InterestingRandomValueTree { + type Value = T; + fn current(&self) -> T { + self.0.clone() + } + fn simplify(&mut self) -> bool { + false + } + fn complicate(&mut self) -> bool { + false + } +} + +impl Strategy + for InterestingRandom +{ + type Tree = InterestingRandomValueTree<(Arguments, WitnessValues)>; + type Value = (Arguments, WitnessValues); + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + let args = Args::generate_arguments(runner.rng()); + let wit = Wit::generate_witness(runner.rng()); + + let mut args_map = HashMap::new(); + for (name, val) in args.iter() { + args_map.insert( + name.clone(), + generate_interesting_or_scratch_by_ty(val.ty(), runner.rng()), + ); + } + + let mut wit_map = HashMap::new(); + for (name, val) in wit.iter() { + wit_map.insert( + name.clone(), + generate_interesting_or_scratch_by_ty(val.ty(), runner.rng()), + ); + } + + Ok(InterestingRandomValueTree(( + Arguments::from(args_map), + WitnessValues::from(wit_map), + ))) + } +} + +pub fn generate_interesting_or_scratch_by_ty(ty: &ResolvedType, rng: &mut TestRng) -> Value { + match ty.as_inner() { + TypeInner::Either(left_ty, right_ty) => { + let left_v: bool = rng.random(); + match left_v { + true => Value::left( + generate_interesting_or_scratch_by_ty(left_ty, rng), + (**right_ty).clone(), + ), + false => Value::right( + (**left_ty).clone(), + generate_interesting_or_scratch_by_ty(right_ty, rng), + ), + } + } + TypeInner::Option(option_ty) => { + let is_some: bool = rng.random(); + match is_some { + true => Value::some(generate_interesting_or_scratch_by_ty(option_ty, rng)), + false => Value::none((**option_ty).clone()), + } + } + TypeInner::Boolean => Value::from(rng.random::()), + TypeInner::UInt(x) => { + let use_interesting: bool = rng.random(); + if use_interesting { + match x { + UIntType::U1 => Value::u1(rng.random::() & 0x01), + UIntType::U2 => Value::u2(rng.random::() & 0x03), + UIntType::U4 => { + let val = *INTERESTING_U4.choose(rng).unwrap(); + Value::u4(val) + } + UIntType::U8 => { + let val = *INTERESTING_U8.choose(rng).unwrap(); + Value::u8(val) + } + UIntType::U16 => { + let val = *INTERESTING_U16.choose(rng).unwrap(); + Value::u16(val) + } + UIntType::U32 => { + let val = *INTERESTING_U32.choose(rng).unwrap(); + Value::u32(val) + } + UIntType::U64 => { + let val = *INTERESTING_U64.choose(rng).unwrap(); + Value::u64(val) + } + UIntType::U128 => { + let val = *INTERESTING_U128.choose(rng).unwrap(); + Value::u128(val) + } + UIntType::U256 => { + let val = *INTERESTING_U128.choose(rng).unwrap(); + let mut bytes = [0u8; 32]; + let val_bytes = val.to_be_bytes(); + bytes[16..32].copy_from_slice(&val_bytes); + Value::u256(U256::from_byte_array(bytes)) + } + } + } else { + match x { + UIntType::U1 => Value::u1(rng.random::() & 0x0001), + UIntType::U2 => Value::u2(rng.random::() & 0x0003), + UIntType::U4 => Value::u4(rng.random::() & 0x000F), + UIntType::U8 => Value::u8(rng.random::()), + UIntType::U16 => Value::u16(rng.random::()), + UIntType::U32 => Value::u32(rng.random::()), + UIntType::U64 => Value::u64(rng.random::()), + UIntType::U128 => Value::u128(rng.random::()), + UIntType::U256 => Value::u256(U256::from_byte_array(rng.random())), + } + } + } + TypeInner::Tuple(tuple_ty) => { + Value::tuple(tuple_ty.iter().map(|x| generate_interesting_or_scratch_by_ty(x, rng))) + } + TypeInner::Array(array_ty, size) => Value::array( + (0..*size).map(|_| generate_interesting_or_scratch_by_ty(array_ty, rng)), + (**array_ty).clone(), + ), + TypeInner::List(list_ty, size_pow_2) => { + let size = rng.random_range(0..size_pow_2.get()); + Value::list( + (0..size).map(|_| generate_interesting_or_scratch_by_ty(list_ty, rng)), + (**list_ty).clone(), + *size_pow_2, + ) + } + _ => Value::unit(), + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct InterestingU64Strategy; + +impl Strategy for InterestingU64Strategy { + type Tree = InterestingU64ValueTree; + type Value = u64; + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + let use_interesting: bool = runner.rng().random(); + let val = if use_interesting { + *INTERESTING_U64.choose(runner.rng()).unwrap() + } else { + runner.rng().random::() + }; + Ok(InterestingU64ValueTree { val }) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct InterestingU64ValueTree { + val: u64, +} + +impl ValueTree for InterestingU64ValueTree { + type Value = u64; + + fn current(&self) -> Self::Value { + self.val + } + + fn simplify(&mut self) -> bool { + if self.val > 0 { + self.val /= 2; + true + } else { + false + } + } + + fn complicate(&mut self) -> bool { + false + } +} diff --git a/crates/test/src/fuzz/args_strategy/random_pool.rs b/crates/test/src/fuzz/args_strategy/random_pool.rs new file mode 100644 index 00000000..42281f9a --- /dev/null +++ b/crates/test/src/fuzz/args_strategy/random_pool.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::marker::PhantomData; + +use simplicityhl::str::WitnessName; +use simplicityhl::{Arguments, ResolvedType, Value, WitnessValues}; + +use proptest::prelude::Rng; +use proptest::prelude::Strategy; +use proptest::strategy::{NewTree, ValueTree}; +use proptest::test_runner::{TestRng, TestRunner}; + +use smplx_sdk::program::{RandomArguments, RandomWitness}; + +pub struct RandomValuePool { + phantom_data: PhantomData<(Args, Wit)>, + _value_pool: ValuePool, +} + +impl Default for RandomValuePool { + fn default() -> Self { + Self { + phantom_data: PhantomData, + _value_pool: ValuePool::default(), + } + } +} + +impl Debug for RandomValuePool { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "RandomValuePool trees...") + } +} + +pub struct ValuePoolValueTree { + current: T, + value_pool: ValuePool, + rng: TestRng, + cnt: usize, + max_bound: usize, +} + +impl ValuePoolValueTree { + pub fn check_utilization(&self) -> bool { + self.cnt < self.max_bound + } +} + +impl ValueTree for ValuePoolValueTree<(Arguments, WitnessValues)> { + type Value = (Arguments, WitnessValues); + fn current(&self) -> Self::Value { + self.current.clone() + } + fn simplify(&mut self) -> bool { + let modified_witness = self + .value_pool + .probabilistically_replace(self.current.1.clone(), &mut self.rng); + self.current.1 = modified_witness; + self.cnt += 1; + self.check_utilization() + } + fn complicate(&mut self) -> bool { + self.simplify() + } +} + +impl Strategy + for RandomValuePool +{ + type Tree = ValuePoolValueTree<(Arguments, WitnessValues)>; + type Value = (Arguments, WitnessValues); + + fn new_tree(&self, runner: &mut TestRunner) -> NewTree { + let args = Args::generate_arguments(runner.rng()); + let wit = Wit::generate_witness(runner.rng()); + let pool = ValuePool::new(&wit.clone(), &args.clone()); + let wit = pool.probabilistically_replace(wit, runner.rng()); + + Ok(ValuePoolValueTree { + current: (args, wit), + value_pool: pool, + rng: runner.rng().clone(), + cnt: 0, + max_bound: 50, + }) + } +} + +#[derive(Default)] +pub struct ValuePool { + pool: HashMap>, + witness_structure: HashMap, +} + +impl ValuePool { + pub fn new(wit: &WitnessValues, args: &Arguments) -> Self { + let mut pool: HashMap> = HashMap::new(); + let mut witness_structure: HashMap = HashMap::new(); + + for (name, val) in wit.iter() { + witness_structure.insert(name.clone(), val.ty().clone()); + pool.entry(val.ty().clone()) + .and_modify(|counter| counter.push(val.clone())) + .or_insert(vec![val.clone()]); + } + + for (_, val) in args.iter() { + pool.entry(val.ty().clone()) + .and_modify(|counter| counter.push(val.clone())) + .or_insert(vec![val.clone()]); + } + // TODO: add possibility in simplex to generate any kind of type + + Self { + pool, + witness_structure, + } + } + + pub fn sample(&self, ty: &ResolvedType, rng: &mut TestRng) -> Option { + self.pool.get(ty).and_then(|values| { + if values.is_empty() { + None + } else { + let idx = rng.random_range(0..values.len()); + Some(values[idx].clone()) + } + }) + } + + pub fn generate_witness(&self, rng: &mut TestRng) -> WitnessValues { + let mut map = HashMap::new(); + for (name, ty) in &self.witness_structure { + if let Some(val) = self.sample(ty, rng) { + map.insert(name.clone(), val); + } + } + WitnessValues::from(map) + } + + pub fn probabilistically_replace(&self, wit: WitnessValues, rng: &mut TestRng) -> WitnessValues { + let mut map = HashMap::new(); + for (name, val) in wit.iter() { + let should_replace: bool = rng.random(); + if should_replace { + let sampled = self.sample(val.ty(), rng).unwrap_or_else(|| val.clone()); + map.insert(name.clone(), sampled); + } else { + map.insert(name.clone(), val.clone()); + } + } + WitnessValues::from(map) + } +} diff --git a/crates/test/src/fuzz/builders.rs b/crates/test/src/fuzz/builders.rs new file mode 100644 index 00000000..43f873f4 --- /dev/null +++ b/crates/test/src/fuzz/builders.rs @@ -0,0 +1,3 @@ +mod final_tx_builder; + +pub use final_tx_builder::*; diff --git a/crates/test/src/fuzz/builders/final_tx_builder.rs b/crates/test/src/fuzz/builders/final_tx_builder.rs new file mode 100644 index 00000000..6ec3bc21 --- /dev/null +++ b/crates/test/src/fuzz/builders/final_tx_builder.rs @@ -0,0 +1,274 @@ +use std::collections::HashSet; + +use simplicityhl::elements::Script; +use simplicityhl::{Arguments, WitnessValues}; + +use smplx_sdk::program::Program; +use smplx_sdk::transaction::{FinalTransaction, ProgramInput}; + +use crate::error::FuzzError; + +type PostHook = dyn Fn(&mut FinalTransaction, &Script, &Arguments, &WitnessValues) -> Result<(), FuzzError>; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ProgramTarget { + Input(usize), + Output(usize), +} + +/// Object, which builds an initial transaction in a fuzzing step to make it valid after +/// generating of script hashes after program compiling. +pub struct FinalTransactionBuilder { + initial_tx: FinalTransaction, + targets: Vec, + post_hook: Option>, +} + +impl FinalTransactionBuilder { + /// Creates a reusable fuzz transaction blueprint. + /// + /// # Errors + /// Returns an error when there are no targets, a target is duplicated, or a target index is out of bounds. + pub fn new( + initial_tx: FinalTransaction, + targets: impl IntoIterator, + ) -> Result { + let targets = targets.into_iter().collect::>(); + + Self::validate_targets(&initial_tx, &targets)?; + + Ok(Self { + initial_tx, + targets, + post_hook: None, + }) + } + + #[must_use] + pub fn with_post_hook( + mut self, + hook: impl Fn(&mut FinalTransaction, &Script, &Arguments, &WitnessValues) -> Result<(), FuzzError> + 'static, + ) -> Self { + self.post_hook = Some(Box::new(hook)); + self + } + + #[must_use] + pub fn targets(&self) -> &[ProgramTarget] { + &self.targets + } + + /// Replaces the scriptPubKey at a selected input or output. + /// + /// # Errors + /// Returns an error when the selected target index is out of bounds. + pub fn set_program_script( + tx: &mut FinalTransaction, + target: ProgramTarget, + script: &Script, + ) -> Result<(), FuzzError> { + match target { + ProgramTarget::Input(index) => { + let input_count = tx.n_inputs(); + let input = tx + .inputs_mut() + .get_mut(index) + .ok_or(FuzzError::InputTargetOutOfBounds { index, input_count })?; + input.partial_input.witness_utxo.script_pubkey = script.clone(); + } + ProgramTarget::Output(index) => { + let output_count = tx.n_outputs(); + let output = tx + .outputs_mut() + .get_mut(index) + .ok_or(FuzzError::OutputTargetOutOfBounds { index, output_count })?; + output.script_pubkey = script.clone(); + } + } + + Ok(()) + } + + pub(crate) fn prepare_transaction( + &self, + program: &Program, + script: &Script, + arguments: &Arguments, + witness: &WitnessValues, + ) -> Result { + let mut tx = self.initial_tx.clone(); + + for target in self.targets.iter().copied() { + Self::set_program_script(&mut tx, target, script)?; + + if let ProgramTarget::Input(index) = target { + let input_count = tx.n_inputs(); + let input = tx + .inputs_mut() + .get_mut(index) + .ok_or(FuzzError::InputTargetOutOfBounds { index, input_count })?; + input.program_input = Some(ProgramInput::new(Box::new(program.clone()), witness.clone())); + } + } + + if let Some(post_hook) = &self.post_hook { + post_hook(&mut tx, script, arguments, witness)?; + } + + Ok(tx) + } + + fn validate_targets(initial_tx: &FinalTransaction, targets: &[ProgramTarget]) -> Result<(), FuzzError> { + if targets.is_empty() { + return Err(FuzzError::NoProgramTargets); + } + + let mut unique_targets = HashSet::with_capacity(targets.len()); + + for target in targets.iter().copied() { + if !unique_targets.insert(target) { + return Err(FuzzError::DuplicateProgramTarget(target)); + } + + match target { + ProgramTarget::Input(index) if index >= initial_tx.n_inputs() => { + return Err(FuzzError::InputTargetOutOfBounds { + index, + input_count: initial_tx.n_inputs(), + }); + } + ProgramTarget::Output(index) if index >= initial_tx.n_outputs() => { + return Err(FuzzError::OutputTargetOutOfBounds { + index, + output_count: initial_tx.n_outputs(), + }); + } + ProgramTarget::Input(_) | ProgramTarget::Output(_) => {} + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::rc::Rc; + + use simplicityhl::elements::hashes::Hash; + use simplicityhl::elements::{OutPoint, Script, TxOut, Txid}; + + use smplx_sdk::provider::SimplicityNetwork; + use smplx_sdk::transaction::{PartialInput, PartialOutput, RequiredSignature, UTXO}; + + use super::*; + + const DUMMY_PROGRAM: &str = r" + fn main() { + assert!(true); + } + "; + + fn initial_transaction() -> FinalTransaction { + let network = SimplicityNetwork::default_regtest(); + let mut tx = FinalTransaction::new(); + + for vout in 0..2 { + let utxo = UTXO { + outpoint: OutPoint::new(Txid::all_zeros(), vout), + txout: TxOut::new_fee(1_000, network.policy_asset()), + secrets: None, + }; + tx.add_input(PartialInput::new(utxo), RequiredSignature::None); + tx.add_output(PartialOutput::new(Script::new(), 500, network.policy_asset())); + } + + tx + } + + fn prepared_program() -> (Program, Script) { + let program = Program::new(DUMMY_PROGRAM, Arguments::default()); + let script = program.get_script_pubkey(&SimplicityNetwork::default_regtest()); + (program, script) + } + + #[test] + fn rejects_empty_duplicate_and_out_of_bounds_targets() { + assert_eq!( + FinalTransactionBuilder::new(initial_transaction(), []).err(), + Some(FuzzError::NoProgramTargets) + ); + assert_eq!( + FinalTransactionBuilder::new( + initial_transaction(), + [ProgramTarget::Input(0), ProgramTarget::Input(0)], + ) + .err(), + Some(FuzzError::DuplicateProgramTarget(ProgramTarget::Input(0))) + ); + assert_eq!( + FinalTransactionBuilder::new(initial_transaction(), [ProgramTarget::Output(2)]).err(), + Some(FuzzError::OutputTargetOutOfBounds { + index: 2, + output_count: 2, + }) + ); + } + + #[test] + fn injects_program_into_all_targets_without_consuming_them() { + let builder = FinalTransactionBuilder::new( + initial_transaction(), + [ProgramTarget::Input(0), ProgramTarget::Output(1)], + ) + .unwrap(); + let (program, script) = prepared_program(); + let arguments = Arguments::default(); + let witness = WitnessValues::default(); + + let first = builder + .prepare_transaction(&program, &script, &arguments, &witness) + .unwrap(); + let second = builder + .prepare_transaction(&program, &script, &arguments, &witness) + .unwrap(); + + assert_eq!(builder.targets().len(), 2); + assert!(first.inputs()[0].program_input.is_some()); + assert_eq!(first.inputs()[0].partial_input.witness_utxo.script_pubkey, script); + assert_eq!(first.outputs()[1].script_pubkey, script); + assert!(second.inputs()[0].program_input.is_some()); + assert_eq!(second.outputs()[1].script_pubkey, script); + } + + #[test] + fn runs_post_hook_after_program_injection_and_propagates_errors() { + let hook_ran = Rc::new(Cell::new(false)); + let hook_ran_in_callback = Rc::clone(&hook_ran); + let builder = FinalTransactionBuilder::new(initial_transaction(), [ProgramTarget::Input(0)]) + .unwrap() + .with_post_hook(move |tx, script, _, _| { + assert_eq!(tx.inputs()[0].partial_input.witness_utxo.script_pubkey, *script); + hook_ran_in_callback.set(true); + FinalTransactionBuilder::set_program_script(tx, ProgramTarget::Input(0), &Script::new()) + }); + let (program, script) = prepared_program(); + + let prepared = builder + .prepare_transaction(&program, &script, &Arguments::default(), &WitnessValues::default()) + .unwrap(); + + assert!(hook_ran.get()); + assert!(prepared.inputs()[0].partial_input.witness_utxo.script_pubkey.is_empty()); + + let failing_builder = FinalTransactionBuilder::new(initial_transaction(), [ProgramTarget::Input(0)]) + .unwrap() + .with_post_hook(|_, _, _, _| Err(FuzzError::PostHook("hook failed".to_string()))); + let error = failing_builder + .prepare_transaction(&program, &script, &Arguments::default(), &WitnessValues::default()) + .unwrap_err(); + + assert_eq!(error, FuzzError::PostHook("hook failed".to_string())); + } +} diff --git a/crates/test/src/fuzz/core.rs b/crates/test/src/fuzz/core.rs new file mode 100644 index 00000000..22af9c5b --- /dev/null +++ b/crates/test/src/fuzz/core.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use simplicityhl::elements::Script; +use simplicityhl::elements::pset::PartiallySignedTransaction; +use simplicityhl::simplicity::{RedeemNode, Value}; +use simplicityhl::{Arguments, WitnessValues}; + +use smplx_sdk::program::{Program, ProgramError, ProgramFactory}; +use smplx_sdk::provider::{ProviderTrait, SimplicityNetwork}; +use smplx_sdk::signer::{Signer, SignerError}; +use smplx_sdk::transaction::FinalTransaction; + +use crate::context::TestContext; +#[derive(Clone, Debug)] +pub(crate) enum SignerOption { + DefaultTestConfigSigner, + CustomSigner, + NoSigning, +} + +#[derive(Clone)] +pub struct FuzzContext { + pub(crate) signer: Arc>, + pub mock_provider: Arc, + /// Used for inserting signer + pub(crate) test_context: Arc>, + pub(crate) signer_option: SignerOption, + pub network: SimplicityNetwork, +} + +pub type ProgramExecResult = Result<(Arc, Value), ProgramError>; + +pub trait FuzzableProgram>: AsRef { + fn build_program(args: impl Into, network: &SimplicityNetwork) -> (Box

, Script); +} + +impl + ProgramFactory

> FuzzableProgram

for P { + fn build_program(args: impl Into, network: &SimplicityNetwork) -> (Box

, Script) { + let prog = P::instantiate_program(args); + let script = prog.as_ref().as_ref().get_script_pubkey(network); + (prog, script) + } +} + +impl FuzzContext { + pub fn get_signer(&self) -> Option<&Signer> { + match self.signer_option { + SignerOption::DefaultTestConfigSigner => Some( + self.test_context + .as_ref() + .as_ref() + .expect("TestContext has to be unempty in order to get a default signer") + .get_default_signer(), + ), + SignerOption::CustomSigner => self.signer.as_ref().as_ref(), + SignerOption::NoSigning => None, + } + } + + #[inline] + pub fn sign_or_extract(&self, ft: &FinalTransaction) -> Result { + match &self.signer_option { + SignerOption::DefaultTestConfigSigner | SignerOption::CustomSigner => { + let signer = self.get_signer(); + Ok(signer.unwrap().sign_tx(ft)?) + } + SignerOption::NoSigning => Ok(ft.extract_pst().0), + } + } +} + +pub trait ProgramCheck { + fn call( + &self, + ctx: &FuzzContext, + tx: &PartiallySignedTransaction, + arguments: &Arguments, + witness: &WitnessValues, + input_index: usize, + program_exec_result: ProgramExecResult, + ) -> Result<(), String>; +} diff --git a/crates/test/src/fuzz/engine.rs b/crates/test/src/fuzz/engine.rs new file mode 100644 index 00000000..775accb2 --- /dev/null +++ b/crates/test/src/fuzz/engine.rs @@ -0,0 +1,314 @@ +use std::fmt::Debug; +use std::marker::PhantomData; +use std::sync::Arc; + +use simplicityhl::{Arguments, WitnessValues}; + +use proptest::prelude::{BoxedStrategy, TestCaseError}; +use proptest::strategy::{Strategy, ValueTree}; +use proptest::test_runner::TestRunner; + +use smplx_sdk::program::{ArgumentsTrait, ProgramFactory, ProgramTrait, RandomArguments, RandomWitness, WitnessTrait}; +use smplx_sdk::provider::{EsploraProvider, ProviderTrait, SimplicityNetwork}; +use smplx_sdk::signer::Signer; + +use crate::context::TestContext; +use crate::fuzz::args_strategy::{InterestingRandom, Random, RandomValuePool}; +use crate::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; +use crate::fuzz::core::{FuzzContext, FuzzableProgram, SignerOption}; +use crate::fuzz::{ProgramCheck, ProgramExecResult}; + +pub struct SimplexFuzzEngine { + runner: TestRunner, + fuzz_context: FuzzContext, + local_fuzz_config: LocalFuzzConfig, + strategy_storage: BoxedStrategy<(Arguments, WitnessValues)>, + blueprint: FinalTransactionBuilder, + _phantom: PhantomData<(Program, Args, Wit)>, +} + +pub struct LocalFuzzConfig { + runs: usize, +} + +pub struct FuzzEngineBuilder { + proptest_config: proptest::test_runner::Config, + pub(crate) signer_option: SignerOption, + pub(crate) signer: Option, + test_context: Option, + mock_provider: Arc, + network: SimplicityNetwork, + _phantom: PhantomData<(Program, Args, Wit)>, +} + +impl FuzzEngineBuilder +where + Program: FuzzableProgram + ProgramFactory + Clone + 'static, + Args: ArgumentsTrait + RandomArguments + std::fmt::Debug + Clone + 'static, + Wit: WitnessTrait + RandomWitness + std::fmt::Debug + Clone + 'static, +{ + pub fn new(config: proptest::test_runner::Config) -> Self { + let default_network = SimplicityNetwork::default_regtest(); + + Self { + proptest_config: config, + test_context: None, + signer_option: SignerOption::NoSigning, + signer: None, + mock_provider: Arc::new(Self::get_default_provider(default_network)), + network: default_network, + _phantom: PhantomData, + } + } + + pub fn from_context(mut config: proptest::test_runner::Config, test_context: TestContext) -> Self { + let default_network = SimplicityNetwork::default_regtest(); + let smplx_test_context = test_context.get_config(); + if let Some(proptest_conf) = smplx_test_context.proptest.as_ref() { + if let Some(cases) = proptest_conf.cases { + config.cases = cases; + } + + if let Some(max_global_rejects) = proptest_conf.max_global_rejects { + config.max_global_rejects = max_global_rejects; + } + + if let Some(max_shrink_iters) = proptest_conf.max_shrink_iters { + config.max_shrink_iters = max_shrink_iters; + } + + if let Some(max_local_rejects) = proptest_conf.max_local_rejects { + config.max_local_rejects = max_local_rejects; + } + + config.verbose = smplx_test_context.verbosity as u32; + } + + Self { + proptest_config: config, + test_context: Some(test_context), + signer_option: SignerOption::NoSigning, + signer: None, + mock_provider: Arc::new(Self::get_default_provider(default_network)), + network: default_network, + _phantom: PhantomData, + } + } + + fn get_default_provider(default_network: SimplicityNetwork) -> EsploraProvider { + EsploraProvider::new("default_web_page.com".into(), default_network) + } +} + +impl FuzzEngineBuilder +where + Program: FuzzableProgram + ProgramFactory + Clone + 'static, + Args: ArgumentsTrait + RandomArguments + std::fmt::Debug + Clone + 'static, + Wit: WitnessTrait + RandomWitness + std::fmt::Debug + Clone + 'static, +{ + pub fn with_signer(mut self, signer: Signer) -> Self { + self.signer_option = SignerOption::CustomSigner; + self.signer = Some(signer); + self + } + + pub fn with_default_signer(mut self) -> Self { + self.signer_option = SignerOption::DefaultTestConfigSigner; + self + } + + pub fn build( + self, + strategy_storage: impl Strategy + 'static, + blueprint: FinalTransactionBuilder, + ) -> SimplexFuzzEngine { + // TODO: add fetching of failure values to feed correct seed into TestRunner on creation + + SimplexFuzzEngine { + runner: proptest::test_runner::TestRunner::new(self.proptest_config), + fuzz_context: FuzzContext { + #[allow(clippy::arc_with_non_send_sync)] + signer: Arc::new(self.signer), + mock_provider: self.mock_provider, + #[allow(clippy::arc_with_non_send_sync)] + test_context: Arc::new(self.test_context), + signer_option: self.signer_option, + network: self.network, + }, + local_fuzz_config: LocalFuzzConfig { runs: 500 }, + strategy_storage: strategy_storage.boxed(), + blueprint, + _phantom: PhantomData, + } + } +} + +pub struct FuzzStrategyBuilder { + base_strat: Option, + _placeholder: PhantomData<(Args, Wit)>, +} + +impl FuzzStrategyBuilder { + pub fn new() -> Self { + Self { + base_strat: None, + _placeholder: Default::default(), + } + } +} + +impl Default for FuzzStrategyBuilder { + fn default() -> Self { + Self::new() + } +} + +impl FuzzStrategyBuilder { + pub fn with_random(self) -> FuzzStrategyBuilder> { + FuzzStrategyBuilder { + base_strat: Some(Random::::default()), + _placeholder: Default::default(), + } + } + + pub fn with_random_pool(self) -> FuzzStrategyBuilder> { + FuzzStrategyBuilder { + base_strat: Some(RandomValuePool::::default()), + _placeholder: Default::default(), + } + } + + pub fn with_custom_strategy(self, custom_strat: NewStrat) -> FuzzStrategyBuilder + where + NewStrat: Strategy + 'static, + { + FuzzStrategyBuilder { + base_strat: Some(custom_strat), + _placeholder: Default::default(), + } + } + + pub fn with_random_interesting_values(self) -> FuzzStrategyBuilder> { + FuzzStrategyBuilder { + base_strat: Some(InterestingRandom::::default()), + _placeholder: Default::default(), + } + } +} + +impl FuzzStrategyBuilder +where + BaseStrat: Strategy + 'static, +{ + pub fn build(self) -> BoxedStrategy<(Arguments, WitnessValues)> { + let base = self + .base_strat + .expect("Base strategy is mandatory. Call with_random() or similar."); + base.boxed() + } +} + +// TODO: create a shared state with Atomic counter (for multiple runners) + +#[derive(Debug)] +pub struct CaseOutcome {} + +/// Returned by a single fuzz when a counterexample has been discovered +#[derive(Debug)] +pub struct CounterExampleOutcome { + pub args: Arguments, + pub wit: WitnessValues, +} + +/// Outcome of a single fuzz +#[derive(Debug)] +pub enum FuzzOutcome { + Case(CaseOutcome), + CounterExample(CounterExampleOutcome), +} + +impl SimplexFuzzEngine +where + Program: FuzzableProgram + ProgramFactory + Clone + 'static, +{ + pub fn run_with_check(self, program_post_hook: impl ProgramCheck) { + let mut runner = self.runner; + let context = self.fuzz_context; + let blueprint = self.blueprint; + let strategy_storage = self.strategy_storage; + + // TODO: add collected strategies usage to modify FinalTransaction or inputs into `proptest::prop_oneof[ strategy list ... ]` + + let mut runner_cnt = 0; + while runner_cnt < self.local_fuzz_config.runs { + // TODO: If counterexample recorded, replay it first, without incrementing runs. + // TODO: evaluate incremental runs + let mut inc_runs = || { + debug_assert!( + runner_cnt <= self.local_fuzz_config.runs, + "worker runs were not distributed correctly" + ); + runner_cnt += 1; + }; + + match Self::single_fuzz(&mut runner, &context, &blueprint, &program_post_hook, &strategy_storage) { + Ok(fuzz_outcome) => match fuzz_outcome { + FuzzOutcome::Case(_case) => { + inc_runs(); + } + FuzzOutcome::CounterExample(CounterExampleOutcome { args, wit }) => { + panic!("program failed tith these arguments: {args} and witness: {wit}"); + } + }, + Err(err) => match err { + TestCaseError::Fail(e) => { + panic!("{e}"); + } + TestCaseError::Reject(e) => { + panic!("{e}"); + } + }, + } + } + } + + fn single_fuzz( + test_runner: &mut TestRunner, + fuzz_context: &FuzzContext, + initial_tx: &FinalTransactionBuilder, + program_post_hook: &impl ProgramCheck, + strategy: &BoxedStrategy<(Arguments, WitnessValues)>, + ) -> Result { + // TODO: extract seed in order to save it in failure case + let (arguments, witness) = strategy + .new_tree(test_runner) + .map_err(|error| TestCaseError::fail(format!("failed to generate fuzz inputs: {error}")))? + .current(); + let (program, script) = Program::build_program(arguments.clone(), &fuzz_context.network); + let final_transaction = initial_tx + .prepare_transaction(program.as_ref().as_ref(), &script, &arguments, &witness) + .map_err(|error| TestCaseError::fail(format!("failed to prepare fuzz transaction: {error}")))?; + let pst = fuzz_context.sign_or_extract(&final_transaction)?; + + // Iterate over program inputs to check contract execution + for target in initial_tx.targets().iter().copied() { + let ProgramTarget::Input(input_index) = target else { + continue; + }; + + let exec_result: ProgramExecResult = + program + .as_ref() + .as_ref() + .execute(&pst, &witness, input_index, &fuzz_context.network); + if let Err(error) = + program_post_hook.call(fuzz_context, &pst, &arguments, &witness, input_index, exec_result) + { + return Err(TestCaseError::fail(format!( + "{error}, args: {arguments}, wit: {witness}" + ))); + } + } + Ok(FuzzOutcome::Case(CaseOutcome {})) + } +} diff --git a/crates/test/src/fuzz/mod.rs b/crates/test/src/fuzz/mod.rs new file mode 100644 index 00000000..c9a01d32 --- /dev/null +++ b/crates/test/src/fuzz/mod.rs @@ -0,0 +1,12 @@ +pub mod args_strategy; +pub mod builders; +pub mod core; +pub mod engine; +pub mod utils; + +pub use proptest; + +pub use crate::error::FuzzError; +pub use core::{FuzzContext, FuzzableProgram, ProgramCheck, ProgramExecResult}; +pub use engine::{FuzzEngineBuilder, SimplexFuzzEngine}; +pub use utils::generate_value_by_ty; diff --git a/crates/test/src/fuzz/utils.rs b/crates/test/src/fuzz/utils.rs new file mode 100644 index 00000000..7214e37a --- /dev/null +++ b/crates/test/src/fuzz/utils.rs @@ -0,0 +1,45 @@ +use simplicityhl::num::U256; +use simplicityhl::types::{TypeInner, UIntType}; +use simplicityhl::value::ValueConstructible; +use simplicityhl::{ResolvedType, Value}; + +use rand::Rng; + +pub fn generate_value_by_ty(ty: &ResolvedType, rng: &mut R) -> Value { + match ty.as_inner() { + TypeInner::Either(left_ty, right_ty) => match rng.random::() { + true => Value::left(generate_value_by_ty(left_ty, rng), (**right_ty).clone()), + false => Value::right((**left_ty).clone(), generate_value_by_ty(right_ty, rng)), + }, + TypeInner::Option(option_ty) => match rng.random::() { + true => Value::some(generate_value_by_ty(option_ty, rng)), + false => Value::none((**option_ty).clone()), + }, + TypeInner::Boolean => Value::from(rng.random::()), + TypeInner::UInt(x) => match x { + UIntType::U1 => Value::u1(rng.random::() & 0x0001), + UIntType::U2 => Value::u2(rng.random::() & 0x0003), + UIntType::U4 => Value::u4(rng.random::() & 0x000F), + UIntType::U8 => Value::u8(rng.random::()), + UIntType::U16 => Value::u16(rng.random::()), + UIntType::U32 => Value::u32(rng.random::()), + UIntType::U64 => Value::u64(rng.random::()), + UIntType::U128 => Value::u128(rng.random::()), + UIntType::U256 => Value::u256(U256::from_byte_array(rng.random())), + }, + TypeInner::Tuple(tuple_ty) => Value::tuple(tuple_ty.iter().map(|x| generate_value_by_ty(x, rng))), + TypeInner::Array(array_ty, size) => Value::array( + (0..*size).map(|_| generate_value_by_ty(array_ty, rng)), + (**array_ty).clone(), + ), + TypeInner::List(list_ty, size_pow_2) => { + let size = rng.random_range(0..size_pow_2.get()); + Value::list( + (0..size).map(|_| generate_value_by_ty(list_ty, rng)), + (**list_ty).clone(), + *size_pow_2, + ) + } + _ => Value::unit(), + } +} diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 2725aa8a..f3a2ab01 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -1,6 +1,7 @@ pub mod config; pub mod context; pub mod error; +pub mod fuzz; pub mod macros; pub mod network_utils; diff --git a/crates/test/src/macros/core.rs b/crates/test/src/macros/core.rs index c6ecbbaf..949de3eb 100644 --- a/crates/test/src/macros/core.rs +++ b/crates/test/src/macros/core.rs @@ -8,19 +8,30 @@ macro_rules! smplx_test_marker { () => { "_smplx_test" }; + (fuzz) => { + "_smplx_fuzz" + }; } pub const SMPLX_TEST_MARKER: &str = smplx_test_marker!(); +pub const SMPLX_FUZZ_MARKER: &str = smplx_test_marker!(fuzz); type AttributeArgs = syn::punctuated::Punctuated; -pub fn expand(args: TokenStream, input: syn::ItemFn) -> syn::Result { +pub fn expand_test(args: TokenStream, input: syn::ItemFn) -> syn::Result { let parser = AttributeArgs::parse_terminated; let args = parser.parse2(args)?; expand_inner(&input, args) } +pub fn expand_fuzz(args: TokenStream, input: syn::ItemFn) -> syn::Result { + let parser = AttributeArgs::parse_terminated; + let args = parser.parse2(args)?; + + expand_fuzz_inner(&input, args) +} + // TODO: args? fn expand_inner(input: &syn::ItemFn, _args: AttributeArgs) -> syn::Result { let ret = &input.sig.output; @@ -57,3 +68,54 @@ fn expand_inner(input: &syn::ItemFn, _args: AttributeArgs) -> syn::Result syn::Result { + let ret = &input.sig.output; + let name = quote::format_ident!("{}_{}", &input.sig.ident.to_string(), SMPLX_FUZZ_MARKER); + let inputs = &input.sig.inputs; + let body = &input.block; + let attrs = &input.attrs; + + let simplex_test_env = TEST_ENV_NAME; + + let expansion = quote::quote! { + #[::core::prelude::v1::test] + #(#attrs)* + fn #name() #ret { + use std::path::PathBuf; + use simplex::TestContext; + + fn #name(#inputs) #ret { + #body + } + + let config = fuzz::proptest::test_runner::Config { + test_name: ::core::option::Option::Some(::core::concat!( + ::core::module_path!(), + "::", + ::core::stringify!(#name) + )), + source_file: Some(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/", + stringify!(#name), + ".txt" + )), + ..Default::default() + }; + let test_context = match std::env::var(#simplex_test_env) { + Err(_) => { + panic!("Failed to run this test, required to use `simplex test`"); + }, + Ok(path) => { + TestContext::new(PathBuf::from(path)).unwrap() + } + }; + let fuzz_context_builder = FuzzEngineBuilder::from_context(config, test_context); + #name(fuzz_context_builder) + } + }; + + Ok(expansion) +} diff --git a/crates/test/src/macros/mod.rs b/crates/test/src/macros/mod.rs index 9e9d50f0..f266cc16 100644 --- a/crates/test/src/macros/mod.rs +++ b/crates/test/src/macros/mod.rs @@ -1,3 +1,3 @@ pub mod core; -pub use core::expand; +pub use core::{expand_fuzz, expand_test}; diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 275c3b1e..0198788d 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -86,6 +86,12 @@ impl ArgumentsTrait for FixedArguments { } } +impl From for Arguments { + fn from(val: FixedArguments) -> Self { + val.build_arguments() + } +} + /// Witness values for a covenant input, resolved before the transaction is assembled. #[derive(Clone)] struct FixedWitness(WitnessValues); @@ -96,6 +102,12 @@ impl WitnessTrait for FixedWitness { } } +impl From for WitnessValues { + fn from(val: FixedWitness) -> Self { + val.build_witness() + } +} + /// A compiled `SimplicityHL` covenant. #[wasm_bindgen] pub struct Covenant { @@ -189,7 +201,7 @@ impl Covenant { _ => Arguments::default(), }; - let mut program = Program::new(Arc::::from(source), &FixedArguments(arguments)); + let mut program = Program::new(Arc::::from(source), FixedArguments(arguments)); if let Some(include) = include_debug_symbols { program = program.with_debug_symbols(include); @@ -587,7 +599,7 @@ impl TransactionBuilder { program_input .program - .execute(&pst, &program_input.witness.build_witness(), input_index, &network) + .execute(&pst, &program_input.witness, input_index, &network) .map_err(|e| JsError::new(&format!("Input {input_index} did not execute: {e}")))?; Ok(()) @@ -674,7 +686,7 @@ impl TransactionBuilder { Ok(ProgramInput { program: Box::new(Covenant::new(source, arguments_json, extra_leaves_json, include_debug_symbols)?.program), - witness: Box::new(FixedWitness(witness)), + witness: FixedWitness(witness).into(), }) } } diff --git a/examples/basic/Cargo.lock b/examples/basic/Cargo.lock index 72ee221c..e2440c05 100644 --- a/examples/basic/Cargo.lock +++ b/examples/basic/Cargo.lock @@ -161,12 +161,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand", - "rand_core", + "rand 0.8.8", + "rand_core 0.6.4", "serde", "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin" version = "0.32.102" @@ -546,6 +561,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -605,6 +626,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -613,7 +646,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -883,6 +916,15 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.39.1" @@ -956,6 +998,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.11", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.32" @@ -966,6 +1027,12 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -975,6 +1042,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -988,8 +1061,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -999,7 +1082,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1011,6 +1104,24 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "regex-automata" version = "0.3.9" @@ -1127,6 +1238,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1143,7 +1266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand", + "rand 0.8.8", "secp256k1-sys", "serde", ] @@ -1164,7 +1287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" dependencies = [ "bitcoin-private", - "rand", + "rand 0.8.8", "secp256k1", "secp256k1-zkp-sys", "serde", @@ -1369,6 +1492,7 @@ dependencies = [ "elements-miniscript", "hex", "minreq 3.0.0", + "rand_core 0.9.5", "serde", "serde_json", "sha2", @@ -1381,6 +1505,8 @@ name = "smplx-std" version = "0.0.10" dependencies = [ "either", + "rand 0.9.4", + "rand_core 0.9.5", "serde", "simplicityhl", "smplx-macros", @@ -1394,7 +1520,9 @@ version = "0.0.10" dependencies = [ "electrsd", "proc-macro2", + "proptest", "quote", + "rand 0.9.4", "serde", "simplicityhl", "smplx-regtest", @@ -1566,6 +1694,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1611,6 +1745,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1627,6 +1770,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -1814,6 +1966,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "yansi" version = "1.0.1" diff --git a/examples/basic/Simplex.toml b/examples/basic/Simplex.toml index c522f64c..f3ae2c0b 100644 --- a/examples/basic/Simplex.toml +++ b/examples/basic/Simplex.toml @@ -21,6 +21,12 @@ # bitcoins = 10_000_000 # verbosity = 0 +# [test.fuzz] +# cases = 10000 +# max_shrink_iters = 100 +# max_global_rejects = 100 +# max_local_rejects = 100 + # [test.esplora] # url = "" # network = "" diff --git a/examples/basic/tests/basic_test.rs b/examples/basic/tests/basic_test.rs index 9253626a..ed219376 100644 --- a/examples/basic/tests/basic_test.rs +++ b/examples/basic/tests/basic_test.rs @@ -12,7 +12,7 @@ fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { public_key: signer.get_schnorr_public_key().serialize(), }; - let p2pk = P2pkProgram::new(&arguments); + let p2pk = P2pkProgram::new(arguments); let p2pk_script = p2pk.get_script_pubkey(context.get_network()); (p2pk, p2pk_script) @@ -43,7 +43,7 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result> { ft.add_program_input( PartialInput::new(p2pk_utxos[0].clone()), - ProgramInput::new(Box::new(p2pk.as_ref().clone()), Box::new(witness.clone())), + ProgramInput::new(Box::new(p2pk.as_ref().clone()), witness.clone()), RequiredSignature::Witness("SIGNATURE".to_string()), ); diff --git a/fixtures/Cargo.lock b/fixtures/Cargo.lock index 6431877d..5ed8bbfa 100644 --- a/fixtures/Cargo.lock +++ b/fixtures/Cargo.lock @@ -161,12 +161,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ "bitcoin_hashes", - "rand", - "rand_core", + "rand 0.8.8", + "rand_core 0.6.4", "serde", "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin" version = "0.32.102" @@ -546,6 +561,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -605,6 +626,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -613,7 +646,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -883,6 +916,15 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.39.1" @@ -956,6 +998,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.11", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.32" @@ -966,6 +1027,12 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -975,6 +1042,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -988,8 +1061,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -999,7 +1082,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1011,6 +1104,24 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "regex-automata" version = "0.3.9" @@ -1127,6 +1238,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1143,7 +1266,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand", + "rand 0.8.8", "secp256k1-sys", "serde", ] @@ -1164,7 +1287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" dependencies = [ "bitcoin-private", - "rand", + "rand 0.8.8", "secp256k1", "secp256k1-zkp-sys", "serde", @@ -1370,6 +1493,7 @@ dependencies = [ "elements-miniscript", "hex", "minreq 3.0.0", + "rand_core 0.9.5", "serde", "serde_json", "sha2", @@ -1382,6 +1506,8 @@ name = "smplx-std" version = "0.0.10" dependencies = [ "either", + "rand 0.9.4", + "rand_core 0.9.5", "serde", "simplicityhl", "smplx-macros", @@ -1395,7 +1521,9 @@ version = "0.0.10" dependencies = [ "electrsd", "proc-macro2", + "proptest", "quote", + "rand 0.9.4", "serde", "simplicityhl", "smplx-regtest", @@ -1567,6 +1695,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1612,6 +1746,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1628,6 +1771,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -1815,6 +1967,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "yansi" version = "1.0.1" diff --git a/fixtures/Simplex.toml b/fixtures/Simplex.toml index 32d71802..7cf3c1ac 100644 --- a/fixtures/Simplex.toml +++ b/fixtures/Simplex.toml @@ -24,6 +24,12 @@ std = { git = "https://github.com/BlockstreamResearch/simplicityhl-std.git", tag # bitcoins = 10_000_000 # verbosity = 0 +# [test.fuzz] +# cases = 10000 +# max_shrink_iters = 100 +# max_global_rejects = 100 +# max_local_rejects = 100 + # [test.esplora] # url = "" # network = "" diff --git a/fixtures/simf/failure_test.simf b/fixtures/simf/failure_test.simf new file mode 100644 index 00000000..ea5e1861 --- /dev/null +++ b/fixtures/simf/failure_test.simf @@ -0,0 +1,13 @@ +fn not(bit: bool) -> bool { + ::into(jet::complement_1(::into(bit))) +} + +fn main() { + let failure_value: u256 = param::FAILURE_VALUE; + let cmp_value: u256 = witness::CMP_VALUE; + + assert!(not(jet::eq_256( + failure_value, + cmp_value + ))); +} \ No newline at end of file diff --git a/fixtures/simf/simple_storage.simf b/fixtures/simf/simple_storage.simf new file mode 100644 index 00000000..cf2cbb35 --- /dev/null +++ b/fixtures/simf/simple_storage.simf @@ -0,0 +1,105 @@ +/* + * Simple Storage Program for Liquid + * + * Only the owner of the storage can modify the value. + * + * ==== IMPORTANT ==== + * + * Based on the following resources: + * https://github.com/ElementsProject/elements/blob/master/src/consensus/amount.h + * https://github.com/ElementsProject/rust-elements/blob/f6ffc7800df14b81c0f5ae1c94368a78b99612b9/src/blind.rs#L471 + * + * The maximum allowed amount is 2,100,000,000,000,000 + * (i.e., 21,000,000 × 10^8), which is approximately 51 bits. + */ + +fn checksig(pk: Pubkey, sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((pk, msg), sig); +} + +fn ensure_current_index_eq(expected_index: u32){ + assert!(jet::eq_32(jet::current_index(), expected_index)); +} + +fn ensure_input_and_output_script_hash_eq(index: u32) { + assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index)))); +} + +fn ensure_output_is_op_return(index: u32) { + match jet::output_null_datum(index, 0) { + Some(entry: Option>>) => (), + None => panic!(), + } +} + +fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + +fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) { + let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index)); + let (asset, amount): (Asset1, Amount1) = pair; + + let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset); + let amount: u64 = unwrap_right::<(u1, u256)>(amount); + (asset_bits, amount) +} + + +fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) { + let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index)); + assert!(jet::eq_256(asset, expected_bits)); + assert!(jet::eq_64(amount, expected_amount)); +} + +fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 1)); } +fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(::into(bit), 0)); } + +fn increment_by(index: u32, amount: u32) -> u32 { + let (carry, result): (bool, u32) = dbg!(jet::add_32(index, amount)); + ensure_zero_bit(carry); + result +} + +fn enforce_stage_checks(index: u32, new_value: u64) { + ensure_input_and_output_script_hash_eq(index); + + let (asset_bits, old_value): (u256, u64) = get_input_explicit_asset_amount(index); + assert!(jet::eq_256(asset_bits, param::SLOT_ID)); + + ensure_output_asset_with_amount_eq(index, param::SLOT_ID, new_value); + + match dbg!(jet::lt_64(new_value, old_value)) { + // burn + true => { + let burn_output_index: u32 = increment_by(index, 1); + + let (carry, amount_to_burn): (bool, u64) = jet::subtract_64(old_value, new_value); + ensure_zero_bit(carry); + + ensure_output_is_op_return(burn_output_index); + ensure_output_asset_with_amount_eq(burn_output_index, param::SLOT_ID, amount_to_burn); + }, + // mint + false => { + let reissuance_output_index: u32 = increment_by(index, 1); + ensure_input_and_output_script_hash_eq(reissuance_output_index); + }, + }; +} + +fn main() { + let index: u32 = 0; + let n1: u32 = dbg!(111); + enforce_stage_checks(index, witness::NEW_VALUE); + + let n2: u32 = dbg!(222); + checksig(param::USER, witness::USER_SIGNATURE); + let n3: u32 = dbg!(333); +} diff --git a/fixtures/tests/basic_test.rs b/fixtures/tests/basic_test.rs index 21c2e540..c5b0b521 100644 --- a/fixtures/tests/basic_test.rs +++ b/fixtures/tests/basic_test.rs @@ -12,7 +12,7 @@ fn get_p2pk(context: &simplex::TestContext) -> (P2pkProgram, Script) { public_key: signer.get_schnorr_public_key().serialize(), }; - let p2pk = P2pkProgram::new(&arguments); + let p2pk = P2pkProgram::new(arguments); let p2pk_script = p2pk.get_script_pubkey(context.get_network()); (p2pk, p2pk_script) @@ -43,7 +43,7 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result<()> { ft.add_program_input( PartialInput::new(p2pk_utxos[0].clone()), - ProgramInput::new(Box::new(p2pk.as_ref().clone()), Box::new(witness.clone())), + ProgramInput::new(Box::new(p2pk.as_ref().clone()), witness), RequiredSignature::Witness("SIGNATURE".to_string()), ); diff --git a/fixtures/tests/log_level.rs b/fixtures/tests/log_level.rs index c575e2c9..e680c71a 100644 --- a/fixtures/tests/log_level.rs +++ b/fixtures/tests/log_level.rs @@ -7,7 +7,7 @@ fn setup_dummy(context: &simplex::TestContext) -> (DummyPanicProgram, simplex::s let signer = context.get_default_signer(); let dummy = - DummyPanicProgram::new(&DummyPanicArguments::default()).with_taproot_pubkey(signer.get_schnorr_public_key()); + DummyPanicProgram::new(DummyPanicArguments::default()).with_taproot_pubkey(signer.get_schnorr_public_key()); let script = dummy.get_script_pubkey(context.get_network()); @@ -30,7 +30,7 @@ fn dummy_log_level(context: simplex::TestContext) -> anyhow::Result<()> { ft.add_program_input( PartialInput::new(utxos[0].clone()), - ProgramInput::new(Box::new(dummy.as_ref().clone()), Box::new(DummyPanicWitness::default())), + ProgramInput::new(Box::new(dummy.as_ref().clone()), DummyPanicWitness::default()), RequiredSignature::None, ); diff --git a/fixtures/tests/metadata_cmr.rs b/fixtures/tests/metadata_cmr.rs index 1e5ecfcf..9c45fbae 100644 --- a/fixtures/tests/metadata_cmr.rs +++ b/fixtures/tests/metadata_cmr.rs @@ -33,7 +33,7 @@ fn expected_cmr<'a>(metadata: &'a Value, source: &str) -> &'a str { macro_rules! assert_metadata_cmr { ($metadata:expr, $source:literal, $program:ty, $arguments:ty) => {{ - let program = <$program>::new(&<$arguments>::default()); + let program = <$program>::new(<$arguments>::default()); assert_eq!( to_hex(program.get_cmr()), diff --git a/fixtures/tests/multidep_test.rs b/fixtures/tests/multidep_test.rs index fe6bd860..56947db6 100644 --- a/fixtures/tests/multidep_test.rs +++ b/fixtures/tests/multidep_test.rs @@ -8,7 +8,7 @@ use simplex_fixtures::artifacts::imports::multidep::derived_multidep::{MultidepA fn get_multidep(context: &simplex::TestContext) -> (MultidepProgram, Script) { let arguments = MultidepArguments { prev_hash: 5 }; - let p2pk = MultidepProgram::new(&arguments); + let p2pk = MultidepProgram::new(arguments); let p2pk_script = p2pk.get_script_pubkey(context.get_network()); (p2pk, p2pk_script) @@ -39,7 +39,7 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result<()> { ft.add_program_input( PartialInput::new(utxos[0].clone()), - ProgramInput::new(Box::new(program.as_ref().clone()), Box::new(witness.clone())), + ProgramInput::new(Box::new(program.as_ref().clone()), witness), RequiredSignature::None, ); diff --git a/fixtures/tests/nested_sig.rs b/fixtures/tests/nested_sig.rs index dcfb54a7..fd2a208b 100644 --- a/fixtures/tests/nested_sig.rs +++ b/fixtures/tests/nested_sig.rs @@ -12,7 +12,7 @@ fn get_nested_sig(context: &simplex::TestContext) -> (NestedSigProgram, Script) public_key: signer.get_schnorr_public_key().serialize(), }; - let program = NestedSigProgram::new(&arguments); + let program = NestedSigProgram::new(arguments); let script = program.get_script_pubkey(context.get_network()); (program, script) @@ -44,7 +44,7 @@ fn spend_nested_sig( ft.add_program_input( PartialInput::new(utxos[0].clone()), - ProgramInput::new(Box::new(program.as_ref().clone()), Box::new(witness)), + ProgramInput::new(Box::new(program.as_ref().clone()), witness), RequiredSignature::witness_with_path("INHERIT_OR_NOT", sig_path), ); diff --git a/fixtures/tests/prop_testing.rs b/fixtures/tests/prop_testing.rs new file mode 100644 index 00000000..bce16c88 --- /dev/null +++ b/fixtures/tests/prop_testing.rs @@ -0,0 +1,187 @@ +mod failure_test_prop { + use simplex::fuzz; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::core::FuzzContext; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::{FuzzEngineBuilder, FuzzError, ProgramCheck, ProgramExecResult}; + use simplex::program::ProgramError; + use simplex::provider::SimplicityNetwork; + use simplex::simplicityhl::elements::hashes::Hash; + use simplex::simplicityhl::elements::pset::PartiallySignedTransaction; + use simplex::simplicityhl::elements::{OutPoint, Script, TxOut, Txid}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + use simplex_fixtures::artifacts::failure_test::FailureTestProgram; + use simplex_fixtures::artifacts::failure_test::derived_failure_test::{FailureTestArguments, FailureTestWitness}; + + struct FailureTestCheck; + + const FAILURE_PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + + fn failure_transaction_builder() -> Result { + let network = SimplicityNetwork::default_regtest(); + let utxo = UTXO { + outpoint: OutPoint::new(Txid::all_zeros(), 0), + txout: TxOut::new_fee(0, network.policy_asset()), + secrets: None, + }; + let mut transaction = FinalTransaction::new(); + transaction.add_input(PartialInput::new(utxo), RequiredSignature::None); + + FinalTransactionBuilder::new(transaction, [FAILURE_PROGRAM_TARGET]) + } + + impl ProgramCheck for FailureTestCheck { + fn call( + &self, + _ctx: &FuzzContext, + _tx: &PartiallySignedTransaction, + _arguments: &Arguments, + _witness: &WitnessValues, + _input_index: usize, + program_exec_result: ProgramExecResult, + ) -> Result<(), String> { + let args = FailureTestArguments::from_arguments(_arguments)?; + let witness = FailureTestWitness::from_witness(_witness)?; + if args.failure_value == witness.cmp_value { + return Err(format!( + "Failed contract, failure_value == cmp_value , {program_exec_result:?}" + )); + } + if program_exec_result.is_err() { + println!("error: {program_exec_result:?}"); + return Err(format!("Failed contract, error: {program_exec_result:?}")); + } + Ok(()) + } + } + + struct ScriptMismatchCheck; + + impl ProgramCheck for ScriptMismatchCheck { + fn call( + &self, + _ctx: &FuzzContext, + _tx: &PartiallySignedTransaction, + _arguments: &Arguments, + _witness: &WitnessValues, + _input_index: usize, + program_exec_result: ProgramExecResult, + ) -> Result<(), String> { + match program_exec_result { + Err(ProgramError::ScriptPubkeyMismatch { .. }) => Ok(()), + Err(error) => Err(format!("expected a script pubkey mismatch, got {error}")), + Ok(_) => Err("expected execution to fail after replacing the program script".to_string()), + } + } + } + + #[ignore] + #[test] + fn possible_interface_failure_program() -> anyhow::Result<()> { + let config = fuzz::proptest::test_runner::Config { + test_name: ::core::option::Option::Some(::core::concat!( + ::core::module_path!(), + "::", + ::core::stringify!(possible_interface_failure_program) + )), + source_file: Some(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/", + stringify!(possible_interface_failure_program), + ".txt" + )), + ..Default::default() + }; + + let fuzz_engine = + FuzzEngineBuilder::::new(config); + + let strategy_storage = FuzzStrategyBuilder::::new() + .with_random() + .build(); + let transaction_builder = failure_transaction_builder()?; + let runner = fuzz_engine.build(strategy_storage, transaction_builder); + runner.run_with_check(FailureTestCheck); + + Ok(()) + } + + #[test] + fn possible_interface_failure_program_with_pool() -> anyhow::Result<()> { + let result = std::panic::catch_unwind(|| { + let config = fuzz::proptest::test_runner::Config { + test_name: ::core::option::Option::Some(::core::concat!( + ::core::module_path!(), + "::", + ::core::stringify!(possible_interface_failure_program_with_pool) + )), + source_file: Some(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/", + stringify!(possible_interface_failure_program_with_pool), + ".txt" + )), + ..Default::default() + }; + + let fuzz_engine_builder = + FuzzEngineBuilder::::new(config); + + // TODO: Add additional strategies to builder to make proper proptest + let strategy_storage = FuzzStrategyBuilder::::new() + .with_random_pool() + .build(); + let transaction_builder = failure_transaction_builder().unwrap(); + let runner = fuzz_engine_builder.build(strategy_storage, transaction_builder); + runner.run_with_check(FailureTestCheck); + }); + assert!(result.is_err()); + + Ok(()) + } + + #[ignore] + #[test] + fn post_hook_can_replace_one_program_script() -> anyhow::Result<()> { + let config = fuzz::proptest::test_runner::Config { + test_name: Some(concat!( + module_path!(), + "::", + stringify!(post_hook_can_replace_one_program_script) + )), + ..Default::default() + }; + let fuzz_engine = + FuzzEngineBuilder::::new(config); + let strategy_storage = FuzzStrategyBuilder::::new() + .with_random() + .build(); + let transaction_builder = + failure_transaction_builder()?.with_post_hook(|transaction, _program_script, _arguments, _witness| { + FinalTransactionBuilder::set_program_script(transaction, FAILURE_PROGRAM_TARGET, &Script::new()) + }); + + fuzz_engine + .build(strategy_storage, transaction_builder) + .run_with_check(ScriptMismatchCheck); + + Ok(()) + } + + #[simplex::fuzz] + fn possible_interface_failure_program_with_interesting_values( + fuzz_engine_builder: FuzzEngineBuilder, + ) -> anyhow::Result<()> { + // TODO: Add additional strategies to builder to make proper proptest + let strategy_storage = FuzzStrategyBuilder::::new() + .with_random_interesting_values() + .build(); + let transaction_builder = failure_transaction_builder()?; + let runner = fuzz_engine_builder.build(strategy_storage, transaction_builder); + runner.run_with_check(FailureTestCheck); + + Ok(()) + } +} diff --git a/fixtures/tests/tapleaf_test.rs b/fixtures/tests/tapleaf_test.rs index dd67bbe8..6e560d46 100644 --- a/fixtures/tests/tapleaf_test.rs +++ b/fixtures/tests/tapleaf_test.rs @@ -8,7 +8,7 @@ fn program_tapleaf_test(context: simplex::TestContext) -> anyhow::Result<()> { let signer = context.get_default_signer(); let provider = context.get_default_provider(); - let tapleaf_check = TapleafCheckProgram::new(&TapleafCheckArguments::default()); + let tapleaf_check = TapleafCheckProgram::new(TapleafCheckArguments::default()); let tapleaf_check_script = tapleaf_check.get_script_pubkey(context.get_network()); let tx_receipt = signer.send(tapleaf_check_script.clone(), 50)?; @@ -24,7 +24,7 @@ fn program_tapleaf_test(context: simplex::TestContext) -> anyhow::Result<()> { ft.add_program_input( PartialInput::new(tapleaf_check_utxo), - ProgramInput::new(Box::new(tapleaf_check.as_ref().clone()), Box::new(witness.clone())), + ProgramInput::new(Box::new(tapleaf_check.as_ref().clone()), witness), RequiredSignature::None, ); From a87f08496b2eb06d29e8f3cf0aae6637e95d7c82 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 25 Aug 2026 17:56:28 +0300 Subject: [PATCH 2/6] Remove Interestingu64 unused struct --- .../fuzz/args_strategy/random_interesting.rs | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/crates/test/src/fuzz/args_strategy/random_interesting.rs b/crates/test/src/fuzz/args_strategy/random_interesting.rs index ae1956a3..908087a9 100644 --- a/crates/test/src/fuzz/args_strategy/random_interesting.rs +++ b/crates/test/src/fuzz/args_strategy/random_interesting.rs @@ -286,47 +286,3 @@ pub fn generate_interesting_or_scratch_by_ty(ty: &ResolvedType, rng: &mut TestRn _ => Value::unit(), } } - -#[derive(Clone, Copy, Debug, Default)] -pub struct InterestingU64Strategy; - -impl Strategy for InterestingU64Strategy { - type Tree = InterestingU64ValueTree; - type Value = u64; - - fn new_tree(&self, runner: &mut TestRunner) -> NewTree { - let use_interesting: bool = runner.rng().random(); - let val = if use_interesting { - *INTERESTING_U64.choose(runner.rng()).unwrap() - } else { - runner.rng().random::() - }; - Ok(InterestingU64ValueTree { val }) - } -} - -#[derive(Clone, Copy, Debug)] -pub struct InterestingU64ValueTree { - val: u64, -} - -impl ValueTree for InterestingU64ValueTree { - type Value = u64; - - fn current(&self) -> Self::Value { - self.val - } - - fn simplify(&mut self) -> bool { - if self.val > 0 { - self.val /= 2; - true - } else { - false - } - } - - fn complicate(&mut self) -> bool { - false - } -} From 734be18ce43487ba2c879ece04aeededbb15076a Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 27 Aug 2026 22:40:33 +0300 Subject: [PATCH 3/6] refactor: add mods --- .../test/src/fuzz/{args_strategy.rs => args_strategy/mod.rs} | 0 crates/test/src/fuzz/{builders.rs => builders/mod.rs} | 0 crates/test/src/fuzz/engine.rs | 4 +++- 3 files changed, 3 insertions(+), 1 deletion(-) rename crates/test/src/fuzz/{args_strategy.rs => args_strategy/mod.rs} (100%) rename crates/test/src/fuzz/{builders.rs => builders/mod.rs} (100%) diff --git a/crates/test/src/fuzz/args_strategy.rs b/crates/test/src/fuzz/args_strategy/mod.rs similarity index 100% rename from crates/test/src/fuzz/args_strategy.rs rename to crates/test/src/fuzz/args_strategy/mod.rs diff --git a/crates/test/src/fuzz/builders.rs b/crates/test/src/fuzz/builders/mod.rs similarity index 100% rename from crates/test/src/fuzz/builders.rs rename to crates/test/src/fuzz/builders/mod.rs diff --git a/crates/test/src/fuzz/engine.rs b/crates/test/src/fuzz/engine.rs index 775accb2..6fc2a655 100644 --- a/crates/test/src/fuzz/engine.rs +++ b/crates/test/src/fuzz/engine.rs @@ -8,7 +8,9 @@ use proptest::prelude::{BoxedStrategy, TestCaseError}; use proptest::strategy::{Strategy, ValueTree}; use proptest::test_runner::TestRunner; -use smplx_sdk::program::{ArgumentsTrait, ProgramFactory, ProgramTrait, RandomArguments, RandomWitness, WitnessTrait}; +use smplx_sdk::program::{ + ArgumentsTrait, Program, ProgramFactory, ProgramTrait, RandomArguments, RandomWitness, WitnessTrait, +}; use smplx_sdk::provider::{EsploraProvider, ProviderTrait, SimplicityNetwork}; use smplx_sdk::signer::Signer; From 3a2a8ac0bee8b3993cf5d99bf61ffe7d48ce86eb Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 27 Aug 2026 23:20:34 +0300 Subject: [PATCH 4/6] refactor: change default behaviour for FuzzStrategyBuilder --- crates/test/src/fuzz/engine.rs | 20 +++++++++----------- fixtures/tests/prop_testing.rs | 4 +--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/test/src/fuzz/engine.rs b/crates/test/src/fuzz/engine.rs index 6fc2a655..e1d2118f 100644 --- a/crates/test/src/fuzz/engine.rs +++ b/crates/test/src/fuzz/engine.rs @@ -8,9 +8,7 @@ use proptest::prelude::{BoxedStrategy, TestCaseError}; use proptest::strategy::{Strategy, ValueTree}; use proptest::test_runner::TestRunner; -use smplx_sdk::program::{ - ArgumentsTrait, Program, ProgramFactory, ProgramTrait, RandomArguments, RandomWitness, WitnessTrait, -}; +use smplx_sdk::program::{ArgumentsTrait, ProgramFactory, ProgramTrait, RandomArguments, RandomWitness, WitnessTrait}; use smplx_sdk::provider::{EsploraProvider, ProviderTrait, SimplicityNetwork}; use smplx_sdk::signer::Signer; @@ -145,23 +143,23 @@ where } } -pub struct FuzzStrategyBuilder { +pub struct FuzzStrategyBuilder> { base_strat: Option, _placeholder: PhantomData<(Args, Wit)>, } -impl FuzzStrategyBuilder { +impl FuzzStrategyBuilder { pub fn new() -> Self { - Self { - base_strat: None, - _placeholder: Default::default(), - } + Self::default() } } -impl Default for FuzzStrategyBuilder { +impl Default for FuzzStrategyBuilder { fn default() -> Self { - Self::new() + Self { + base_strat: Some(InterestingRandom::default()), + _placeholder: Default::default(), + } } } diff --git a/fixtures/tests/prop_testing.rs b/fixtures/tests/prop_testing.rs index bce16c88..e3c4812c 100644 --- a/fixtures/tests/prop_testing.rs +++ b/fixtures/tests/prop_testing.rs @@ -175,9 +175,7 @@ mod failure_test_prop { fuzz_engine_builder: FuzzEngineBuilder, ) -> anyhow::Result<()> { // TODO: Add additional strategies to builder to make proper proptest - let strategy_storage = FuzzStrategyBuilder::::new() - .with_random_interesting_values() - .build(); + let strategy_storage = FuzzStrategyBuilder::::new().build(); let transaction_builder = failure_transaction_builder()?; let runner = fuzz_engine_builder.build(strategy_storage, transaction_builder); runner.run_with_check(FailureTestCheck); From acab7466d84c507d70e4e7a60eaf060bda03addf Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 27 Aug 2026 23:27:32 +0300 Subject: [PATCH 5/6] refactor: add default in UTXO --- crates/sdk/src/transaction/utxo.rs | 10 ++++++++++ fixtures/tests/prop_testing.rs | 12 ++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs index d4423bcd..65444396 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -11,6 +11,16 @@ pub struct UTXO { pub secrets: Option, } +impl Default for UTXO { + fn default() -> Self { + Self { + outpoint: OutPoint::default(), + txout: TxOut::new_fee(0, AssetId::default()), + secrets: None, + } + } +} + impl UTXO { /// Retrieves the explicit `AssetId` from the transaction output (`txout`). /// diff --git a/fixtures/tests/prop_testing.rs b/fixtures/tests/prop_testing.rs index e3c4812c..d3262d23 100644 --- a/fixtures/tests/prop_testing.rs +++ b/fixtures/tests/prop_testing.rs @@ -5,10 +5,8 @@ mod failure_test_prop { use simplex::fuzz::engine::FuzzStrategyBuilder; use simplex::fuzz::{FuzzEngineBuilder, FuzzError, ProgramCheck, ProgramExecResult}; use simplex::program::ProgramError; - use simplex::provider::SimplicityNetwork; - use simplex::simplicityhl::elements::hashes::Hash; + use simplex::simplicityhl::elements::Script; use simplex::simplicityhl::elements::pset::PartiallySignedTransaction; - use simplex::simplicityhl::elements::{OutPoint, Script, TxOut, Txid}; use simplex::simplicityhl::{Arguments, WitnessValues}; use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; @@ -20,14 +18,8 @@ mod failure_test_prop { const FAILURE_PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); fn failure_transaction_builder() -> Result { - let network = SimplicityNetwork::default_regtest(); - let utxo = UTXO { - outpoint: OutPoint::new(Txid::all_zeros(), 0), - txout: TxOut::new_fee(0, network.policy_asset()), - secrets: None, - }; let mut transaction = FinalTransaction::new(); - transaction.add_input(PartialInput::new(utxo), RequiredSignature::None); + transaction.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); FinalTransactionBuilder::new(transaction, [FAILURE_PROGRAM_TARGET]) } From b2cd2d868607b07980827a8fabb9ed2303f2a046 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 3 Sep 2026 15:07:16 +0300 Subject: [PATCH 6/6] fix lint --- crates/sdk/src/transaction/partial_input.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index e6776f0f..24d938cc 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -11,7 +11,6 @@ use simplicityhl::elements::{AssetId, LockTime, OutPoint, Sequence, TxOut, TxOut use simplicityhl::simplicity::hashes::Hash; use crate::program::ProgramTrait; -use crate::program::WitnessTrait; use crate::utils::tagged_hash; use super::UTXO;