diff --git a/Cargo.lock b/Cargo.lock index 7eb322e..b4c1e0e 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" @@ -598,11 +613,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.8", "rustc-hex", "static_assertions", ] +[[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" @@ -668,6 +689,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" @@ -676,7 +709,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -1130,6 +1163,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.2", + "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" @@ -1140,6 +1192,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" @@ -1149,6 +1207,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" @@ -1168,8 +1232,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]] @@ -1179,7 +1253,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]] @@ -1191,6 +1275,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" @@ -1313,6 +1415,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" @@ -1329,7 +1443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand", + "rand 0.8.8", "secp256k1-sys", "serde", ] @@ -1350,7 +1464,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", @@ -1496,7 +1610,7 @@ dependencies = [ "num-bigint", "num-traits", "primitive-types", - "rand", + "rand 0.8.8", "secp256k1-zkp", "smplx-std", ] @@ -1510,8 +1624,6 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smplx-build" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1538c2de418c94bfec9824b140a0f8bc2f2542cde0f18ef1b2e3a23e5c9eb1" dependencies = [ "glob", "globwalk", @@ -1531,8 +1643,6 @@ dependencies = [ [[package]] name = "smplx-macros" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb08baf344083ccad07886156cbd92feb2d66b1886b5b73e61a9e03db2ef8f7" dependencies = [ "smplx-build", "smplx-test", @@ -1542,8 +1652,6 @@ dependencies = [ [[package]] name = "smplx-regtest" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d419c9a387fcd2944ac487dff8ced762fc1d52e21186366ab70e7c899931a6" dependencies = [ "electrsd", "hex", @@ -1558,8 +1666,6 @@ dependencies = [ [[package]] name = "smplx-sdk" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ac052541ca5611bcdd23d46317970b2065c73ebc7864e53ff5607b30c96dfa9" dependencies = [ "bip39", "bitcoin_hashes", @@ -1568,6 +1674,7 @@ dependencies = [ "elements-miniscript", "hex", "minreq 3.0.0", + "rand_core 0.9.5", "serde", "serde_json", "sha2", @@ -1578,10 +1685,10 @@ dependencies = [ [[package]] name = "smplx-std" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a9e1bdf1d8139fbdf2be662fdcd11761ae61604aeb78628ced8506f7c6a7b9" dependencies = [ "either", + "rand 0.9.5", + "rand_core 0.9.5", "serde", "simplicityhl", "smplx-macros", @@ -1592,12 +1699,12 @@ dependencies = [ [[package]] name = "smplx-test" version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bd10a1047d731372fc1589ebf2a76444ad8c65d352f84115718e6345f3aa0c" dependencies = [ "electrsd", "proc-macro2", + "proptest", "quote", + "rand 0.9.5", "serde", "simplicityhl", "smplx-regtest", @@ -1793,6 +1900,12 @@ dependencies = [ "static_assertions", ] +[[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" @@ -1844,6 +1957,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" @@ -1860,6 +1982,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.128" @@ -2047,6 +2178,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 = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index d54cc7f..e9f7d3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["simplicity", "liquid", "elements", "smart-contracts"] categories = ["cryptography::cryptocurrencies"] [dependencies] -smplx-std = { version = "0.0.11" } +smplx-std = { path = "../simplex/crates/simplex" } [dev-dependencies] anyhow = { version = "1.0.101" } diff --git a/Simplex.toml b/Simplex.toml index c522f64..8038bcc 100644 --- a/Simplex.toml +++ b/Simplex.toml @@ -29,3 +29,6 @@ # url = "" # username = "" # password = "" + +# [test.fuzz] +# cases = 256 diff --git a/tests/stdlib/asserts.rs b/tests/stdlib/asserts.rs index b3ca487..d329c3f 100644 --- a/tests/stdlib/asserts.rs +++ b/tests/stdlib/asserts.rs @@ -11,6 +11,7 @@ use simplicityhl_std::artifacts::tests::asserts::derived_asserts::{ // simf/tests/asserts.simf. use FunctionToTest::*; +#[derive(Clone, Copy)] enum FunctionToTest { AssertEq1, AssertEq8, @@ -38,7 +39,7 @@ const DEFAULT_SOME_U128: Option = Some(0); const DEFAULT_SOME_U256: Option<[u8; 32]> = Some([0; 32]); fn program() -> AssertsTestProgram { - AssertsTestProgram::new(&AssertsTestArguments {}) + AssertsTestProgram::new(AssertsTestArguments {}) } /// Returns two values in `[min, max]` that are equal when `same`, distinct otherwise. @@ -63,11 +64,9 @@ pub fn generate_uints_in_one_range(same: bool, min_val: u128, max_val: u128) -> (some_u, other_u) } -/// Builds the witness for one assert call. `same` controls the two `assert_eq` -/// args; `none` makes the single `assert_none` arg `None`. -fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTestWitness { - let mut witness = AssertsTestWitness { - function_index: 0, +fn default_witness(function_index: u8) -> AssertsTestWitness { + AssertsTestWitness { + function_index, first_arg_u1: DEFAULT_SOME_U8, // u1 in Simplicity is represented as u8 second_arg_u1: DEFAULT_SOME_U8, first_arg_u8: DEFAULT_SOME_U8, @@ -82,7 +81,13 @@ fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTes second_arg_u128: DEFAULT_SOME_U128, first_arg_u256: DEFAULT_SOME_U256, second_arg_u256: DEFAULT_SOME_U256, - }; + } +} + +/// Builds the witness for one assert call. `same` controls the two `assert_eq` +/// args; `none` makes the single `assert_none` arg `None`. +fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTestWitness { + let mut witness = default_witness(function as u8); match function { FunctionToTest::AssertEq1 => { @@ -155,7 +160,6 @@ fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTes } } - witness.function_index = function as u8; witness } @@ -350,3 +354,660 @@ fn assert_none_256_happy_path(context: simplex::TestContext) -> anyhow::Result<( fn assert_none_256_unhappy_path(context: simplex::TestContext) -> anyhow::Result<()> { case(AssertNone256).expecting(&context, Expect::AssertFailed) } + +mod asserts_test_fuzz { + use super::*; + + use std::fmt::Debug; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + + type AssertsFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u1() -> impl Strategy { + any::().prop_map(u8::from) + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>() + } + + type ArgumentFields = fn(&mut AssertsTestWitness) -> (&mut Option, &mut Option); + + fn equal_values_strategy( + function_index: u8, + values: impl Strategy + 'static, + fields: ArgumentFields, + ) -> BoxedStrategy + where + T: Clone + Debug + Into + 'static, + V: 'static, + { + values + .prop_map(move |value| { + let mut witness = default_witness(function_index); + let first: V = value.clone().into(); + let second: V = value.into(); + + Case::set_arguments(fields(&mut witness), first, second); + witness + }) + .boxed() + } + + fn distinct_values_strategy( + function_index: u8, + values: impl Strategy + 'static, + fields: ArgumentFields, + ) -> BoxedStrategy + where + T: PartialEq + Debug + Into + 'static, + V: 'static, + { + let values = values.boxed(); + + (values.clone(), values) + .prop_filter("assert_eq arguments must be distinct", |(first, second)| { + first != second + }) + .prop_map(move |(first, second)| { + let mut witness = default_witness(function_index); + let first: V = first.into(); + let second: V = second.into(); + + Case::set_arguments(fields(&mut witness), first, second); + witness + }) + .boxed() + } + + fn none_strategy( + none: bool, + function_index: u8, + values: impl Strategy + 'static, + fields: ArgumentFields, + ) -> BoxedStrategy { + values + .prop_map(move |value| { + let mut witness = default_witness(function_index); + + let (first, second) = if none { + (None, Some(value)) + } else { + (Some(value), Some(T::default())) + }; + + Case::set_arguments(fields(&mut witness), first, second); + witness + }) + .boxed() + } + + impl Case { + fn set_arguments( + fields: (&mut Option, &mut Option), + first: impl Into>, + second: impl Into>, + ) { + *fields.0 = first.into(); + *fields.1 = second.into(); + } + + fn u1_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u1, &mut witness.second_arg_u1) + } + + fn u8_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u8, &mut witness.second_arg_u8) + } + + fn u16_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u16, &mut witness.second_arg_u16) + } + + fn u32_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u32, &mut witness.second_arg_u32) + } + + fn u64_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u64, &mut witness.second_arg_u64) + } + + fn u128_arguments( + witness: &mut AssertsTestWitness, + ) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u128, &mut witness.second_arg_u128) + } + + fn u256_arguments( + witness: &mut AssertsTestWitness, + ) -> (&mut Option<[u8; 32]>, &mut Option<[u8; 32]>) { + (&mut witness.first_arg_u256, &mut witness.second_arg_u256) + } + + fn strategy(self) -> BoxedStrategy { + match (self.function, self.same) { + ( + AssertNone1 | AssertNone8 | AssertNone16 | AssertNone32 | AssertNone64 + | AssertNone128 | AssertNone256, + _, + ) => self.strategy_with_none_values(), + (_, true) => self.strategy_with_same_values(), + (_, false) => self.strategy_with_distinct_values(), + } + } + + fn strategy_with_none_values(&self) -> BoxedStrategy { + let function_index = self.function as u8; + + match self.function { + AssertNone1 => { + none_strategy(self.none, function_index, arb_u1(), Self::u1_arguments) + } + AssertNone8 => { + none_strategy(self.none, function_index, arb_u8(), Self::u8_arguments) + } + AssertNone16 => { + none_strategy(self.none, function_index, arb_u16(), Self::u16_arguments) + } + AssertNone32 => { + none_strategy(self.none, function_index, arb_u32(), Self::u32_arguments) + } + AssertNone64 => { + none_strategy(self.none, function_index, arb_u64(), Self::u64_arguments) + } + AssertNone128 => { + none_strategy(self.none, function_index, arb_u128(), Self::u128_arguments) + } + AssertNone256 => { + none_strategy(self.none, function_index, arb_u256(), Self::u256_arguments) + } + _ => unreachable!("assert_eq cases aren't handled"), + } + } + + fn strategy_with_same_values(&self) -> BoxedStrategy { + let function_index = self.function as u8; + + match self.function { + AssertEq1 => equal_values_strategy(function_index, arb_u1(), Self::u1_arguments), + AssertEq8 => equal_values_strategy(function_index, arb_u8(), Self::u8_arguments), + AssertEq16 => equal_values_strategy(function_index, arb_u16(), Self::u16_arguments), + AssertEq32 => equal_values_strategy(function_index, arb_u32(), Self::u32_arguments), + AssertEq64 => equal_values_strategy(function_index, arb_u64(), Self::u64_arguments), + AssertEq128 => { + equal_values_strategy(function_index, arb_u128(), Self::u128_arguments) + } + AssertEq256 => { + equal_values_strategy(function_index, arb_u256(), Self::u256_arguments) + } + AssertEqBool => { + equal_values_strategy(function_index, arb_bool(), Self::u1_arguments) + } + _ => unreachable!("assert_none cases aren't handled"), + } + } + + fn strategy_with_distinct_values(&self) -> BoxedStrategy { + let function_index = self.function as u8; + + match self.function { + AssertEq1 => distinct_values_strategy(function_index, arb_u1(), Self::u1_arguments), + AssertEq8 => distinct_values_strategy(function_index, arb_u8(), Self::u8_arguments), + AssertEq16 => { + distinct_values_strategy(function_index, arb_u16(), Self::u16_arguments) + } + AssertEq32 => { + distinct_values_strategy(function_index, arb_u32(), Self::u32_arguments) + } + AssertEq64 => { + distinct_values_strategy(function_index, arb_u64(), Self::u64_arguments) + } + AssertEq128 => { + distinct_values_strategy(function_index, arb_u128(), Self::u128_arguments) + } + AssertEq256 => { + distinct_values_strategy(function_index, arb_u256(), Self::u256_arguments) + } + AssertEqBool => { + distinct_values_strategy(function_index, arb_bool(), Self::u1_arguments) + } + _ => unreachable!("assert_none cases aren't handled"), + } + } + } + + struct CaseFuzz { + case: Case, + builder: AssertsFuzzEngineBuilder, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: AssertsFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn equal(mut self) -> Self { + self.case = self.case.equal(); + self + } + + fn none(mut self) -> Self { + self.case = self.case.none(); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let strategy = self + .case + .strategy() + .prop_map(|witness| { + let arguments: Arguments = AssertsTestArguments {}.into(); + let witness: WitnessValues = witness.into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(CaseFuzz::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn assert_eq_1_happy_path(fuzz_engine_builder: AssertsFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(AssertEq1, fuzz_engine_builder, "assert_eq_1_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_1_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq1, fuzz_engine_builder, "assert_eq_1_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_8_happy_path(fuzz_engine_builder: AssertsFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(AssertEq8, fuzz_engine_builder, "assert_eq_8_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_8_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq8, fuzz_engine_builder, "assert_eq_8_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_16_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq16, fuzz_engine_builder, "assert_eq_16_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_16_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq16, fuzz_engine_builder, "assert_eq_16_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_32_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq32, fuzz_engine_builder, "assert_eq_32_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_32_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq32, fuzz_engine_builder, "assert_eq_32_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_64_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq64, fuzz_engine_builder, "assert_eq_64_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_64_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq64, fuzz_engine_builder, "assert_eq_64_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_128_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq128, fuzz_engine_builder, "assert_eq_128_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_128_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertEq128, + fuzz_engine_builder, + "assert_eq_128_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_256_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertEq256, fuzz_engine_builder, "assert_eq_256_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_256_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertEq256, + fuzz_engine_builder, + "assert_eq_256_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_bool_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertEqBool, + fuzz_engine_builder, + "assert_eq_bool_happy_path", + ) + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_bool_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertEqBool, + fuzz_engine_builder, + "assert_eq_bool_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_1_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertNone1, fuzz_engine_builder, "assert_none_1_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_1_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone1, + fuzz_engine_builder, + "assert_none_1_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_8_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(AssertNone8, fuzz_engine_builder, "assert_none_8_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_8_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone8, + fuzz_engine_builder, + "assert_none_8_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_16_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone16, + fuzz_engine_builder, + "assert_none_16_happy_path", + ) + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_16_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone16, + fuzz_engine_builder, + "assert_none_16_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_32_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone32, + fuzz_engine_builder, + "assert_none_32_happy_path", + ) + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_32_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone32, + fuzz_engine_builder, + "assert_none_32_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_64_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone64, + fuzz_engine_builder, + "assert_none_64_happy_path", + ) + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_64_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone64, + fuzz_engine_builder, + "assert_none_64_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_128_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone128, + fuzz_engine_builder, + "assert_none_128_happy_path", + ) + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_128_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone128, + fuzz_engine_builder, + "assert_none_128_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_256_happy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone256, + fuzz_engine_builder, + "assert_none_256_happy_path", + ) + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_256_unhappy_path( + fuzz_engine_builder: AssertsFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertNone256, + fuzz_engine_builder, + "assert_none_256_unhappy_path", + ) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/binary.rs b/tests/stdlib/binary.rs index ca00390..3457d69 100644 --- a/tests/stdlib/binary.rs +++ b/tests/stdlib/binary.rs @@ -7,6 +7,6 @@ use simplicityhl_std::artifacts::tests::binary::derived_binary::{ #[simplex::test] fn all_ops(context: simplex::TestContext) -> anyhow::Result<()> { - let program = BinaryTestProgram::new(&BinaryTestArguments {}); + let program = BinaryTestProgram::new(BinaryTestArguments {}); run(&context, program, BinaryTestWitness {}, Expect::Ok) } diff --git a/tests/stdlib/common/core.rs b/tests/stdlib/common/core.rs index ada5e42..ffaef9e 100644 --- a/tests/stdlib/common/core.rs +++ b/tests/stdlib/common/core.rs @@ -2,7 +2,8 @@ // part of it, so per-crate dead-code analysis would warn about the rest. #![allow(dead_code)] -use simplex::program::{Program, WitnessTrait}; +use simplex::program::Program; +use simplex::simplicityhl::WitnessValues; use simplex::simplicityhl::elements::Script; use simplex::transaction::{ FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, @@ -52,7 +53,7 @@ pub fn construct_final_tx( data: Option<&[u8]>, ) -> anyhow::Result where - W: WitnessTrait + 'static, + W: Into + 'static, { let utxos = context .get_default_provider() @@ -61,7 +62,7 @@ where let mut ft = FinalTransaction::new(); 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::None, ); @@ -81,7 +82,7 @@ pub fn spend( data: Option<&[u8]>, ) -> anyhow::Result where - W: WitnessTrait + 'static, + W: Into + 'static, { let ft = construct_final_tx(context, program, script, witness, data)?; @@ -116,7 +117,7 @@ pub fn run( expect: Expect, ) -> anyhow::Result<()> where - W: WitnessTrait + 'static, + W: Into + 'static, { let script = fund(context, &program)?; let result = spend(context, &program, &script, witness, None); @@ -134,10 +135,70 @@ pub fn run_with_op_return( data: &[u8], ) -> anyhow::Result<()> where - W: WitnessTrait + 'static, + W: Into + 'static, { let script = fund(context, &program)?; let result = spend(context, &program, &script, witness, Some(data)); assert_error_msg(result, expect) } + +use simplex::fuzz::core::FuzzContext; +use simplex::fuzz::{ProgramCheck, ProgramExecResult}; +use simplex::program::ProgramError; +use simplex::simplicityhl::elements::pset::PartiallySignedTransaction; +use simplex::simplicityhl::simplicity::bit_machine::ExecutionError; + +/// Checks that a fuzzed program produces the exact execution outcome expected +/// by the test case. +pub struct FuzzExecutionCheck { + test_name: &'static str, + expect: Expect, +} + +impl FuzzExecutionCheck { + pub const fn new(test_name: &'static str, expect: Expect) -> Self { + Self { test_name, expect } + } +} + +impl ProgramCheck for FuzzExecutionCheck { + fn call( + &self, + _context: &FuzzContext, + _transaction: &PartiallySignedTransaction, + _arguments: &simplex::simplicityhl::Arguments, + _witness: &WitnessValues, + _input_index: usize, + program_exec_result: ProgramExecResult, + ) -> Result<(), String> { + match (self.expect, program_exec_result) { + (Expect::Ok, Ok(_)) => Ok(()), + (Expect::AssertFailed, Err(ProgramError::Pruning(ExecutionError::JetFailed(_)))) => { + Ok(()) + } + ( + Expect::PrunedBranch, + Err(ProgramError::Pruning(ExecutionError::ReachedPrunedBranch(_))), + ) => Ok(()), + (expect, Ok(_)) => Err(format!( + "{} unexpectedly succeeded; expected {}", + self.test_name, + expected_outcome(expect) + )), + (expect, Err(error)) => Err(format!( + "{} failed with {error}; expected {}", + self.test_name, + expected_outcome(expect) + )), + } + } +} + +fn expected_outcome(expect: Expect) -> &'static str { + match expect { + Expect::Ok => "a successful execution", + Expect::AssertFailed => "a jet failure from assert!", + Expect::PrunedBranch => "a reached pruned branch", + } +} diff --git a/tests/stdlib/common/mod.rs b/tests/stdlib/common/mod.rs index e8669e2..959c62d 100644 --- a/tests/stdlib/common/mod.rs +++ b/tests/stdlib/common/mod.rs @@ -2,3 +2,4 @@ pub mod core; pub mod helper; pub mod u256_wrapper; pub mod uint; +pub mod uint_fuzz; diff --git a/tests/stdlib/common/uint.rs b/tests/stdlib/common/uint.rs index eeb5ff1..a16ddc8 100644 --- a/tests/stdlib/common/uint.rs +++ b/tests/stdlib/common/uint.rs @@ -7,9 +7,9 @@ use std::ops::{Add, Div, Mul, Sub}; use rand::Rng; use rand::distributions::uniform::SampleUniform; -use simplex::program::{Program, WitnessTrait}; - use super::core::{Expect, run}; +use simplex::program::Program; +use simplex::simplicityhl::WitnessValues; /// Dispatch indices for the operations that exist for every unsigned width. /// These map 1:1 onto the `is_selected(N, ..)` arms in each width's math test @@ -44,7 +44,7 @@ pub trait TestUint: + Div { type Program: AsRef; - type Witness: WitnessTrait + 'static; + type Witness: Into + 'static; const ZERO: Self; const ONE: Self; diff --git a/tests/stdlib/common/uint_fuzz.rs b/tests/stdlib/common/uint_fuzz.rs new file mode 100644 index 0000000..45f620d --- /dev/null +++ b/tests/stdlib/common/uint_fuzz.rs @@ -0,0 +1,364 @@ +// Each `tests/*.rs` is a separate crate that mounts this module but uses only +// part of it, so per-crate dead-code analysis would warn about the rest. +#![allow(dead_code)] + +use std::fmt::Debug; + +use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; +use simplex::fuzz::engine::FuzzStrategyBuilder; +use simplex::fuzz::proptest::prelude::Just; +use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; +use simplex::fuzz::{FuzzEngineBuilder, FuzzError, FuzzableProgram}; +use simplex::program::{ + ArgumentsTrait, ProgramFactory, RandomArguments, RandomWitness, WitnessTrait, +}; +use simplex::simplicityhl::{Arguments, WitnessValues}; +use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + +use super::core::{Expect, FuzzExecutionCheck}; +use super::uint::{CommonOp, TestUint}; + +const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + +/// Fuzz-specific program data and number generators for a common unsigned +/// integer width. +pub trait TestUintFuzz: TestUint + Debug + 'static { + /// Program arguments used for each generated test case. + type Arguments: ArgumentsTrait + RandomArguments + Debug + Clone + Into + 'static; + + /// Returns the fixed program arguments for this uint test program. + fn arguments() -> Self::Arguments; + + /// Generates any representable value of this uint type. + fn arb_any() -> BoxedStrategy; + + /// Generates any representable value except zero. + fn arb_non_zero() -> BoxedStrategy; + + /// Generates a value in the inclusive interval `low..=high`. + /// + /// Implementations should preserve both bounds exactly. This lets shared + /// scenarios construct ranges such as `0..=a` and `(a + 1)..=MAX` without + /// changing their input shape. + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy; + + /// Generates an addend in the fitting range used by `uint.rs`. + fn arb_fitting_addend() -> BoxedStrategy { + Self::arb_fitting(Self::ZERO, Self::HALF_MAX) + } + + /// Generates a multiplier in the fitting range used by `uint.rs`. + fn arb_fitting_multiplier() -> BoxedStrategy { + Self::arb_fitting(Self::ZERO, Self::MUL_BOUND - Self::ONE) + } +} + +fn initial_transaction() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx +} + +fn transaction_builder() -> Result { + FinalTransactionBuilder::new(initial_transaction(), [PROGRAM_TARGET]) +} + +fn uint_strategy( + operation: CommonOp, + inputs: BoxedStrategy<(T, T, Option)>, +) -> BoxedStrategy<(Arguments, WitnessValues)> { + let function_index = operation as u8; + + FuzzStrategyBuilder::::new() + .with_custom_strategy(inputs.prop_map(move |(a, b, expected)| { + let arguments: Arguments = T::arguments().into(); + let witness: WitnessValues = T::witness(function_index, a, b, expected).into(); + + (arguments, witness) + })) + .build() +} + +pub fn checked_add_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedAdd, + (T::arb_fitting_addend(), T::arb_fitting_addend()) + .prop_map(|(a, b)| (a, b, Some(a + b))) + .boxed(), + ) +} + +pub fn checked_add_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedAdd, + (Just(T::MAX), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn safe_add_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeAdd, + (T::arb_fitting_addend(), T::arb_fitting_addend()) + .prop_map(|(a, b)| (a, b, Some(a + b))) + .boxed(), + ) +} + +pub fn safe_add_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeAdd, + (Just(T::MAX), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn checked_sub_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedSub, + T::arb_fitting(T::ZERO, T::MAX) + .prop_flat_map(|a| T::arb_fitting(T::ZERO, a).prop_map(move |b| (a, b, Some(a - b)))) + .boxed(), + ) +} + +pub fn checked_sub_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedSub, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn safe_sub_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeSub, + T::arb_fitting(T::ZERO, T::MAX) + .prop_flat_map(|a| T::arb_fitting(T::ZERO, a).prop_map(move |b| (a, b, Some(a - b)))) + .boxed(), + ) +} + +pub fn safe_sub_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeSub, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn checked_mul_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedMul, + (T::arb_fitting_multiplier(), T::arb_fitting_multiplier()) + .prop_map(|(a, b)| (a, b, Some(a * b))) + .boxed(), + ) +} + +pub fn checked_mul_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedMul, + (Just(T::MAX), T::arb_fitting(T::ONE + T::ONE, T::MAX)) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn safe_mul_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeMul, + (T::arb_fitting_multiplier(), T::arb_fitting_multiplier()) + .prop_map(|(a, b)| (a, b, Some(a * b))) + .boxed(), + ) +} + +pub fn safe_mul_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeMul, + (Just(T::MAX), T::arb_fitting(T::ONE + T::ONE, T::MAX)) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn checked_div_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedDiv, + (T::arb_any(), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, Some(a / b))) + .boxed(), + ) +} + +pub fn checked_div_by_zero_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedDiv, + T::arb_any().prop_map(|a| (a, T::ZERO, None)).boxed(), + ) +} + +pub fn safe_div_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeDiv, + (T::arb_any(), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, Some(a / b))) + .boxed(), + ) +} + +pub fn safe_div_by_zero_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::SafeDiv, + T::arb_any().prop_map(|a| (a, T::ZERO, None)).boxed(), + ) +} + +pub fn gt_greater_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Gt, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|b| { + T::arb_fitting(b + T::ONE, T::MAX).prop_map(move |a| (a, b, Some(T::ZERO))) + }) + .boxed(), + ) +} + +pub fn gt_equal_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Gt, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_map(|a| (a, a, None)) + .boxed(), + ) +} + +pub fn gt_less_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Gt, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn ge_greater_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Ge, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|b| { + T::arb_fitting(b + T::ONE, T::MAX).prop_map(move |a| (a, b, Some(T::ZERO))) + }) + .boxed(), + ) +} + +pub fn ge_equal_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Ge, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_map(|a| (a, a, Some(T::ZERO))) + .boxed(), + ) +} + +pub fn ge_less_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Ge, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn run_uint_fuzz( + fuzz_engine_builder: FuzzEngineBuilder, + strategy: BoxedStrategy<(Arguments, WitnessValues)>, + expect: Expect, +) -> anyhow::Result<()> +where + T::Program: FuzzableProgram + ProgramFactory + Clone + 'static, + T::Witness: WitnessTrait + RandomWitness + Debug + Clone + 'static, +{ + let transaction_builder = transaction_builder()?; + + fuzz_engine_builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new("uint operation", expect)); + + Ok(()) +} + +/// Stamps the common `#[simplex::fuzz]` entry points for one unsigned width. +/// +/// Each expansion still emits one independently selectable fuzz test per +/// scenario. The macro only removes the identical runner wiring from the +/// width-specific test modules. +/// +/// # Usage +/// +/// ```ignore +/// uint_fuzz_tests!(u8, U8MathFuzzEngineBuilder); +/// ``` +/// +/// The type must implement [`TestUintFuzz`], including its bounded +/// number-generation strategy. +#[macro_export] +macro_rules! uint_fuzz_tests { + ($t:ty, $fuzz_engine_builder:ty) => { + use simplex::fuzz; + + $crate::uint_fuzz_tests!(@stub $t, $fuzz_engine_builder; + checked_add_fitting checked_add_fitting_strategy Ok + checked_add_overflow checked_add_overflow_strategy Ok + safe_add_fitting safe_add_fitting_strategy Ok + safe_add_overflow safe_add_overflow_strategy PrunedBranch + checked_sub_fitting checked_sub_fitting_strategy Ok + checked_sub_overflow checked_sub_overflow_strategy Ok + safe_sub_fitting safe_sub_fitting_strategy Ok + safe_sub_overflow safe_sub_overflow_strategy PrunedBranch + checked_mul_fitting checked_mul_fitting_strategy Ok + checked_mul_overflow checked_mul_overflow_strategy Ok + safe_mul_fitting safe_mul_fitting_strategy Ok + safe_mul_overflow safe_mul_overflow_strategy PrunedBranch + checked_div_fitting checked_div_fitting_strategy Ok + checked_div_by_zero checked_div_by_zero_strategy Ok + safe_div_fitting safe_div_fitting_strategy Ok + safe_div_by_zero safe_div_by_zero_strategy PrunedBranch + gt_greater gt_greater_strategy Ok + gt_equal gt_equal_strategy Ok + gt_less gt_less_strategy Ok + ge_greater ge_greater_strategy Ok + ge_equal ge_equal_strategy Ok + ge_less ge_less_strategy Ok + ); + }; + (@stub $t:ty, $fuzz_engine_builder:ty; $( + $name:ident $strategy:ident $expected_execution:ident + )+) => { + $( + #[simplex::fuzz] + fn $name(fuzz_engine_builder: $fuzz_engine_builder) -> anyhow::Result<()> { + $crate::common::uint_fuzz::run_uint_fuzz::<$t>( + fuzz_engine_builder, + $crate::common::uint_fuzz::$strategy::<$t>(), + $crate::common::core::Expect::$expected_execution, + ) + } + )+ + }; +} diff --git a/tests/stdlib/op_return.rs b/tests/stdlib/op_return.rs index 2b83226..f2cfd97 100644 --- a/tests/stdlib/op_return.rs +++ b/tests/stdlib/op_return.rs @@ -17,7 +17,7 @@ enum FunctionToTest { const DEFAULT_DATA: &[u8; 1] = &[1]; fn program() -> OpReturnTestProgram { - OpReturnTestProgram::new(&OpReturnTestArguments {}) + OpReturnTestProgram::new(OpReturnTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads and whether the @@ -129,3 +129,197 @@ fn assert_output_is_op_return_fail(context: simplex::TestContext) -> anyhow::Res .index(index) .expecting(&context, Expect::AssertFailed) } + +mod op_return_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, + }; + + const EXPECTED_IS_OP_RETURN: bool = true; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type OpReturnFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_non_zero_u32() -> impl Strategy { + any::().prop_filter("output index should not be zero", |index| *index != 0) + } + + struct CaseFuzz { + case: Case, + builder: OpReturnFuzzEngineBuilder, + indices: BoxedStrategy, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: OpReturnFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + indices: Just(0_u32).boxed(), + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, indices: impl Strategy + 'static) -> Self { + self.indices = indices.boxed(); + self + } + + fn flag(mut self, flag: bool) -> Self { + self.case = self.case.flag(flag); + self + } + + fn op_return(mut self, data: &'static [u8]) -> Self { + self.case = self.case.op_return(data); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx(data: Option<&[u8]>) -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + + if let Some(data) = data { + tx.add_output(PartialOutput::new_metadata(data)); + } + + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness, data } = self.case; + + let strategy = self + .indices + .prop_map(move |index| { + let arguments: Arguments = OpReturnTestArguments {}.into(); + let witness: WitnessValues = OpReturnTestWitness { + index, + ..witness.clone() + } + .into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(CaseFuzz::build_initial_tx(data), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn is_output_op_return_true( + fuzz_engine_builder: OpReturnFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + IsOpReturn, + fuzz_engine_builder, + "is_output_op_return with OP_RETURN", + ) + .flag(EXPECTED_IS_OP_RETURN) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn is_output_op_return_empty_index_false( + fuzz_engine_builder: OpReturnFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + IsOpReturn, + fuzz_engine_builder, + "is_output_op_return outside outputs", + ) + .strategy(arb_non_zero_u32()) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn is_output_op_return_false( + fuzz_engine_builder: OpReturnFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + IsOpReturn, + fuzz_engine_builder, + "is_output_op_return without outputs", + ) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_pass( + fuzz_engine_builder: OpReturnFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + fuzz_engine_builder, + "assert_output_is_op_return with OP_RETURN", + ) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_empty_index_fail( + fuzz_engine_builder: OpReturnFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + fuzz_engine_builder, + "assert_output_is_op_return outside outputs", + ) + .strategy(arb_non_zero_u32()) + .op_return(DEFAULT_DATA) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_fail( + fuzz_engine_builder: OpReturnFuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + fuzz_engine_builder, + "assert_output_is_op_return without outputs", + ) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/secp256k1/operations.rs b/tests/stdlib/secp256k1/operations.rs index 673dedf..55cc27b 100644 --- a/tests/stdlib/secp256k1/operations.rs +++ b/tests/stdlib/secp256k1/operations.rs @@ -39,10 +39,11 @@ const SECP_N: [u8; 32] = [ ]; fn program() -> Secp256k1OperationsTestProgram { - Secp256k1OperationsTestProgram::new(&Secp256k1OperationsTestArguments {}) + Secp256k1OperationsTestProgram::new(Secp256k1OperationsTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. +#[derive(Clone, Debug)] struct Case { witness: Secp256k1OperationsTestWitness, } @@ -409,3 +410,314 @@ fn safe_gej_normalize_roundtrip(context: simplex::TestContext) -> anyhow::Result .expect_ge(ge) .run(&context) } + +mod fuzz { + use std::fmt::Debug; + + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + + type Ge = ([u8; 32], [u8; 32]); + type Gej = (Ge, [u8; 32]); + + type Secp256k1FuzzEngineBuilder = FuzzEngineBuilder< + Secp256k1OperationsTestProgram, + Secp256k1OperationsTestArguments, + Secp256k1OperationsTestWitness, + >; + + fn arb_fe() -> impl Strategy { + any::<[u8; 32]>().prop_filter("valid secp256k1 field element", |value| *value < SECP_P) + } + + fn arb_non_zero_fe() -> impl Strategy { + arb_fe().prop_filter("non-zero secp256k1 field element", |value| { + *value != [0; 32] + }) + } + + fn arb_scalar() -> impl Strategy { + any::<[u8; 32]>().prop_filter("valid secp256k1 scalar", |value| *value < SECP_N) + } + + fn arb_secret_key() -> impl Strategy { + any::<[u8; 32]>().prop_filter_map("valid secp256k1 secret key", |bytes| { + SecretKey::from_slice(&bytes).ok() + }) + } + + fn ge_from_secret_key(secret_key: SecretKey) -> Ge { + let secp = Secp256k1::new(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let serialized = public_key.serialize_uncompressed(); + + let mut x = [0; 32]; + x.copy_from_slice(&serialized[1..33]); + + let mut y = [0; 32]; + y.copy_from_slice(&serialized[33..65]); + + (x, y) + } + + fn arb_ge() -> impl Strategy { + arb_secret_key().prop_map(ge_from_secret_key) + } + + /// Fuzz counterparts for every `#[simplex::test]` case with randomized inputs. + /// Deterministic cases stay as regular tests. + struct CaseFuzz { + cases: BoxedStrategy, + builder: Secp256k1FuzzEngineBuilder, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: Secp256k1FuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + cases: Just(case(function)).boxed(), + builder, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn inject( + mut self, + values: impl Strategy + 'static, + inject: impl Fn(Case, T) -> Case + 'static, + ) -> Self + where + T: Debug + 'static, + { + self.cases = (self.cases, values) + .prop_map(move |(case, values)| inject(case, values)) + .boxed(); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let strategy = self + .cases + .prop_map(|case| { + let arguments: Arguments = Secp256k1OperationsTestArguments {}.into(); + let witness: WitnessValues = case.witness.into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = FuzzStrategyBuilder::< + Secp256k1OperationsTestArguments, + Secp256k1OperationsTestWitness, + _, + >::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + // 0. ge_to_point + #[simplex::fuzz] + fn ge_to_point_matches_parity( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + // Sample one on-curve point; whatever parity it has, that's what we expect + // ge_to_point to produce. + case_fuzz(GeToPoint, fuzz_engine_builder, "ge_to_point") + .inject(arb_ge(), |case, ge| { + let expected_parity = ge.1[31] & 1; // 0 (even y) or 1 (odd y) + + case.ge(ge).expect_point((expected_parity, ge.0)) + }) + .run() + } + + // 1. point_to_gej + #[simplex::fuzz] + fn point_to_gej_roundtrip( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(PointToGej, fuzz_engine_builder, "point_to_gej") + .inject(arb_ge(), |case, ge| { + let point = compress(ge); + case.point(point).expect_point(point) + }) + .run() + } + + // 2. fe_sub + #[simplex::fuzz] + fn fe_sub_self_is_zero(fuzz_engine_builder: Secp256k1FuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(FeSub, fuzz_engine_builder, "fe_sub self is zero") + .inject(arb_fe(), |case, a| case.uints(a, a).expect_uint([0u8; 32])) + .run() + } + + #[simplex::fuzz] + fn fe_sub_matches_reference( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(FeSub, fuzz_engine_builder, "fe_sub") + .inject((arb_fe(), arb_fe()), |case, (a, b)| { + let exp = fe_sub_ref(a, b); + case.uints(a, b).expect_uint(exp) + }) + .run() + } + + // 3. scalar_sub + #[simplex::fuzz] + fn scalar_sub_matches_reference( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(ScalarSub, fuzz_engine_builder, "scalar_sub") + .inject((arb_scalar(), arb_scalar()), |case, (a, b)| { + let exp = scalar_sub_ref(a, b); + case.uints(a, b).expect_uint(exp) + }) + .run() + } + + // 4. gej_sub + #[simplex::fuzz] + fn gej_sub_matches_reference( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(GejSub, fuzz_engine_builder, "gej_sub") + .inject( + (arb_secret_key(), arb_secret_key()) + .prop_filter("distinct secp256k1 points", |(p, q)| p != q), + |case, (p_secret, q_secret)| { + let secp = Secp256k1::new(); + let p = PublicKey::from_secret_key(&secp, &p_secret); + let q = PublicKey::from_secret_key(&secp, &q_secret); + let diff = p.combine(&q.negate(&secp)).expect("p - q non-infinity"); + + case.gejs(pk_to_gej(&p), pk_to_gej(&q)) + .expect_gej(pk_to_gej(&diff)) + }, + ) + .run() + } + + // 5. fe_eq + #[simplex::fuzz] + fn fe_eq_reflexive(fuzz_engine_builder: Secp256k1FuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(FeEq, fuzz_engine_builder, "fe_eq_reflexive") + .inject(arb_fe(), |case, value| case.uints(value, value)) + .run() + } + + // 6. scalar_eq has only a deterministic case and remains a regular test. + + // 7. ge_eq + #[simplex::fuzz] + fn ge_eq_rejects_negation( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(GeEq, fuzz_engine_builder, "ge_eq negation") + .inject(arb_ge(), |case, ge| { + case.ges(ge, (ge.0, fe_negate_ref(ge.1))) + }) + .expect(Expect::AssertFailed) + .run() + } + + // 8. gej_point_eq + #[simplex::fuzz] + fn gej_point_eq_rescaled( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(GejPointEq, fuzz_engine_builder, "gej_point_eq_rescaled") + .inject((arb_ge(), arb_non_zero_fe()), |case, (ge, lambda)| { + let lambda_squared = fe_mul_ref(lambda, lambda); + let lambda_cubed = fe_mul_ref(lambda_squared, lambda); + let gej: Gej = ( + ( + fe_mul_ref(ge.0, lambda_squared), + fe_mul_ref(ge.1, lambda_cubed), + ), + lambda, + ); + + case.gej(gej).point(compress(ge)) + }) + .run() + } + + #[simplex::fuzz] + fn gej_point_eq_rejects_negation( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz(GejPointEq, fuzz_engine_builder, "gej_point_eq negation") + .inject(arb_ge(), |case, ge| { + let (parity, x) = compress(ge); + case.gej(ge_to_gej(ge)).point((parity ^ 1, x)) + }) + .expect(Expect::AssertFailed) + .run() + } + + // 9. safe_gej_normalize + #[simplex::fuzz] + fn safe_gej_normalize_roundtrip( + fuzz_engine_builder: Secp256k1FuzzEngineBuilder, + ) -> anyhow::Result<()> { + case_fuzz( + SafeGejNormalize, + fuzz_engine_builder, + "safe_gej_normalize_roundtrip", + ) + .inject((arb_ge(), arb_non_zero_fe()), |case, (ge, lambda)| { + let lambda_squared = fe_mul_ref(lambda, lambda); + let lambda_cubed = fe_mul_ref(lambda_squared, lambda); + let gej: Gej = ( + ( + fe_mul_ref(ge.0, lambda_squared), + fe_mul_ref(ge.1, lambda_cubed), + ), + lambda, + ); + + case.gej(gej).expect_ge(ge) + }) + .run() + } +} diff --git a/tests/stdlib/u1/convert.rs b/tests/stdlib/u1/convert.rs index 69e84e4..49c58f1 100644 --- a/tests/stdlib/u1/convert.rs +++ b/tests/stdlib/u1/convert.rs @@ -21,7 +21,7 @@ enum FunctionToTest { } fn program() -> U1ConvertTestProgram { - U1ConvertTestProgram::new(&U1ConvertTestArguments {}) + U1ConvertTestProgram::new(U1ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -132,3 +132,143 @@ fn split_u1_to_u1(context: simplex::TestContext) -> anyhow::Result<()> { .expect(U256::from(a).to_big_endian()) .run(&context) } + +mod convert_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U1ConvertFuzzEngineBuilder = + FuzzEngineBuilder; + + struct FuzzCaseBuilder { + case: Case, + builder: U1ConvertFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U1ConvertFuzzEngineBuilder, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + test_name, + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_bool_u8() -> impl Strategy { + arb_bool().prop_map(u8::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let strategy: BoxedStrategy<(Arguments, WitnessValues)> = inputs + .prop_map(move |a| { + let arguments: Arguments = U1ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u1_to_u8(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToU8, builder, "u1 to u8") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u16(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToU16, builder, "u1 to u16") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u32(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToU32, builder, "u1 to u32") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u64(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToU64, builder, "u1 to u64") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u128(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToU128, builder, "u1 to u128") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u256(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToU256, builder, "u1 to u256") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn split_u1_to_u1(builder: U1ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U1ToBool, builder, "split u1 to u1") + .strategy(arb_bool_u8()) + .run() + } +} diff --git a/tests/stdlib/u128/bit.rs b/tests/stdlib/u128/bit.rs index bba6b0b..b1d4aa1 100644 --- a/tests/stdlib/u128/bit.rs +++ b/tests/stdlib/u128/bit.rs @@ -17,8 +17,11 @@ enum FunctionToTest { RightShift128, } +const EXPECT_EQUAL: bool = true; +const EXPECT_NOT_EQUAL: bool = false; + fn program() -> U128TestBitsProgram { - U128TestBitsProgram::new(&U128TestBitsArguments {}) + U128TestBitsProgram::new(U128TestBitsArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -92,7 +95,11 @@ fn or_128(context: simplex::TestContext) -> anyhow::Result<()> { fn eq_128_true(context: simplex::TestContext) -> anyhow::Result<()> { let a = rand::thread_rng().gen_range(0..=u128::MAX); - case(Eq128).args(a, a).expect(0).flag(true).run(&context) + case(Eq128) + .args(a, a) + .expect(0) + .flag(EXPECT_EQUAL) + .run(&context) } #[simplex::test] @@ -100,7 +107,11 @@ fn eq_128_false(context: simplex::TestContext) -> anyhow::Result<()> { let a = rand::thread_rng().gen_range(1..=u128::MAX); let b = a - 1; - case(Eq128).args(a, b).expect(0).run(&context) + case(Eq128) + .args(a, b) + .expect(0) + .flag(EXPECT_NOT_EQUAL) + .run(&context) } #[simplex::test] @@ -166,3 +177,297 @@ fn right_shift_128_out_of_range(context: simplex::TestContext) -> anyhow::Result case(RightShift128).args(shift, val).expect(0).run(&context) } + +mod bit_tests_fuzz { + use super::*; + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + type Builder = + FuzzEngineBuilder; + + const EXPECTED_FALSE: bool = false; + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + /// Values for one bit operation before building the contract witness. + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + expected_bool: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u128) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: u128) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: u128) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn flag(mut self, expected_bool: bool) -> Self { + let _ = self.expected_bool.insert(expected_bool); + self + } + + fn into_witness(self, witness: U128TestBitsWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.expect("no first_arg in witness"), + self.second_arg.expect("no second_arg in witness"), + ) + .expect(self.expected.expect("no expected in witness")) + .flag(self.expected_bool.expect("no expected_bool in witness")) + .witness + .into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: Builder, + inputs: Option>, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: Builder, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let witness = self.case.witness; + let strategy = self + .inputs + .expect("a fuzz strategy must be specified") + .prop_map(move |case| { + let arguments: Arguments = U128TestBitsArguments {}.into(); + let witness = case.into_witness(witness.clone()); + (arguments, witness) + }); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + let tx_builder = FinalTransactionBuilder::new(tx, [ProgramTarget::Input(0)])?; + self.builder + .build(strategy, tx_builder) + .run_with_check(FuzzExecutionCheck::new(self.name, Expect::Ok)); + Ok(()) + } + } + + #[simplex::fuzz] + fn and(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expect(a & b) + .flag(false) + }) + }; + + case_fuzz(And128, builder, "u128 bitwise and") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn or(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expect(a | b) + .flag(false) + }) + }; + + case_fuzz(Or128, builder, "u128 bitwise or") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn eq(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).flag(a == b).expect(0)) + }; + + case_fuzz(Eq128, builder, "u128 equality") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn eq_same(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|a| { + FuzzCase::first_arg(a) + .second_arg(a) + .expect(0) + .flag(EXPECT_EQUAL) + }) + }; + + case_fuzz(Eq128, builder, "u128 equality with equal operands") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn eq_different(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|a| { + FuzzCase::first_arg(a) + .second_arg(a.wrapping_add(1)) + .expect(0) + .flag(EXPECT_NOT_EQUAL) + }) + }; + + case_fuzz(Eq128, builder, "u128 inequality") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn left_shift(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u8(), arb_u128()).prop_map(|(shift, value)| { + let expected = if shift >= 128 { 0 } else { value << shift }; + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(expected) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(LeftShift128, builder, "u128 left shift") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn right_shift(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u8(), arb_u128()).prop_map(|(shift, value)| { + let expected = if shift >= 128 { 0 } else { value >> shift }; + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(expected) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(RightShift128, builder, "u128 right shift") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn left_shift_by_zero(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|value| { + FuzzCase::first_arg(0) + .second_arg(value) + .expect(value) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(LeftShift128, builder, "u128 left shift by zero") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn right_shift_by_zero(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|value| { + FuzzCase::first_arg(0) + .second_arg(value) + .expect(value) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(RightShift128, builder, "u128 right shift by zero") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn left_shift_out_of_range(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (128u8..=u8::MAX, arb_u128()).prop_map(|(shift, value)| { + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(0) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(LeftShift128, builder, "u128 left shift out of range") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn right_shift_out_of_range(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (128u8..=u8::MAX, arb_u128()).prop_map(|(shift, value)| { + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(0) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(RightShift128, builder, "u128 right shift out of range") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u128/comparison.rs b/tests/stdlib/u128/comparison.rs index b97c596..fa63205 100644 --- a/tests/stdlib/u128/comparison.rs +++ b/tests/stdlib/u128/comparison.rs @@ -16,7 +16,7 @@ enum FunctionToTest { } fn program() -> U128TestCompareProgram { - U128TestCompareProgram::new(&U128TestCompareArguments {}) + U128TestCompareProgram::new(U128TestCompareArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -127,3 +127,221 @@ fn le_128_bigger(context: simplex::TestContext) -> anyhow::Result<()> { case(Le128).args(a, b).run(&context) } + +mod comparison_tests_fuzz { + use super::*; + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + type Builder = + FuzzEngineBuilder; + + const EXPECTED_TRUE: bool = true; + const EXPECTED_FALSE: bool = false; + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + any::().prop_filter("u128 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u128) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: u128) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: bool) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness(self, witness: U128TestCompareWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + ) + .flag(self.expected.expect("no expected arg in witness")) + .witness + .into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: Builder, + inputs: Option>, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: Builder, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let witness = self.case.witness; + let strategy = self + .inputs + .expect("a fuzz strategy must be specified") + .prop_map(move |fuzz_case| { + let arguments: Arguments = U128TestCompareArguments {}.into(); + let witness: WitnessValues = fuzz_case.into_witness(witness.clone()); + (arguments, witness) + }); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + let tx_builder = FinalTransactionBuilder::new(tx, [ProgramTarget::Input(0)])?; + self.builder + .build(strategy, tx_builder) + .run_with_check(FuzzExecutionCheck::new(self.name, Expect::Ok)); + Ok(()) + } + } + + #[simplex::fuzz] + fn is_zero(builder: Builder) -> anyhow::Result<()> { + case_fuzz(IsZero128, builder, "u128 is zero") + .strategy(arb_u128().prop_map(|a| FuzzCase::first_arg(a).second_arg(0).expect(a == 0))) + .run() + } + + #[simplex::fuzz] + fn lt(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Lt128, builder, "u128 less than") + .strategy( + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a < b)), + ) + .run() + } + + #[simplex::fuzz] + fn le(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Le128, builder, "u128 less than or equal") + .strategy( + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a <= b)), + ) + .run() + } + + #[simplex::fuzz] + fn equal_operands(builder: Builder) -> anyhow::Result<()> { + case_fuzz( + Le128, + builder, + "u128 less than or equal with equal operands", + ) + .strategy( + arb_u128().prop_map(|a| FuzzCase::first_arg(a).second_arg(a).expect(EXPECTED_TRUE)), + ) + .run() + } + + #[simplex::fuzz] + fn non_zero_operand(builder: Builder) -> anyhow::Result<()> { + case_fuzz(IsZero128, builder, "u128 nonzero operand") + .strategy(arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_arg(value) + .second_arg(0) + .expect(EXPECTED_FALSE) + })) + .run() + } + + #[simplex::fuzz] + fn lt_strict(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Lt128, builder, "u128 strict less than") + .strategy((0u128..u128::MAX).prop_flat_map(|a| { + (a + 1..=u128::MAX) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_TRUE)) + })) + .run() + } + + #[simplex::fuzz] + fn lt_equal(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Lt128, builder, "u128 less than equal operands") + .strategy( + arb_u128() + .prop_map(|a| FuzzCase::first_arg(a).second_arg(a).expect(EXPECTED_FALSE)), + ) + .run() + } + + #[simplex::fuzz] + fn lt_greater(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Lt128, builder, "u128 less than with greater first operand") + .strategy(arb_non_zero_u128().prop_flat_map(|a| { + (0u128..a) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_FALSE)) + })) + .run() + } + + #[simplex::fuzz] + fn le_strict_less(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Le128, builder, "u128 strict less than or equal") + .strategy((0u128..u128::MAX).prop_flat_map(|a| { + (a + 1..=u128::MAX) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_TRUE)) + })) + .run() + } + + #[simplex::fuzz] + fn le_strict_greater(builder: Builder) -> anyhow::Result<()> { + case_fuzz(Le128, builder, "u128 strict greater than") + .strategy((1u128..=u128::MAX).prop_flat_map(|a| { + (0u128..a) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_FALSE)) + })) + .run() + } +} diff --git a/tests/stdlib/u128/convert.rs b/tests/stdlib/u128/convert.rs index b1cf059..5d5b85d 100644 --- a/tests/stdlib/u128/convert.rs +++ b/tests/stdlib/u128/convert.rs @@ -24,7 +24,7 @@ enum FunctionToTest { } fn program() -> U128ConvertTestProgram { - U128ConvertTestProgram::new(&U128ConvertTestArguments {}) + U128ConvertTestProgram::new(U128ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -215,3 +215,236 @@ fn safe_u128_to_u64_overflow(context: simplex::TestContext) -> anyhow::Result<() .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + type Builder = + FuzzEngineBuilder; + + struct FuzzCaseBuilder { + case: Case, + builder: Builder, + inputs: Option>, + expect: Expect, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: Builder, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + expect: Expect::Ok, + name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let witness = self.case.witness; + let strategy = self + .inputs + .expect("a fuzz strategy must be specified") + .prop_map(move |a| { + let arguments: Arguments = U128ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + (arguments, witness) + }); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + let tx_builder = FinalTransactionBuilder::new(tx, [ProgramTarget::Input(0)])?; + self.builder + .build(strategy, tx_builder) + .run_with_check(FuzzExecutionCheck::new(self.name, self.expect)); + Ok(()) + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_bool_u128() -> impl Strategy { + arb_bool().prop_map(u128::from) + } + + fn arb_u8_u128() -> impl Strategy { + arb_u8().prop_map(u128::from) + } + + fn arb_u16_u128() -> impl Strategy { + arb_u16().prop_map(u128::from) + } + + fn arb_u32_u128() -> impl Strategy { + arb_u32().prop_map(u128::from) + } + + fn arb_u64_u128() -> impl Strategy { + arb_u64().prop_map(u128::from) + } + + #[simplex::fuzz] + fn u128_to_u256(builder: Builder) -> anyhow::Result<()> { + case_fuzz(U128ToU256, builder, "u128 to u256") + .strategy(arb_u128()) + .run() + } + + #[simplex::fuzz] + fn split_u128_into_u8(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU8, builder, "split u128 into u8") + .strategy(arb_u128()) + .run() + } + + #[simplex::fuzz] + fn split_u128_into_u16(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU16, builder, "split u128 into u16") + .strategy(arb_u128()) + .run() + } + + #[simplex::fuzz] + fn split_u128_into_u32(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU32, builder, "split u128 into u32") + .strategy(arb_u128()) + .run() + } + + #[simplex::fuzz] + fn split_u128_into_u64(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU64, builder, "split u128 into u64") + .strategy(arb_u128()) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u1(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU1, builder, "safe u128 to u1") + .strategy(arb_bool_u128()) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u1_overflow(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU1, builder, "safe u128 to u1 overflow") + .strategy(2u128..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u8(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU8, builder, "safe u128 to u8") + .strategy(arb_u8_u128()) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u8_overflow(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU8, builder, "safe u128 to u8 overflow") + .strategy((u128::from(u8::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u16(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU16, builder, "safe u128 to u16") + .strategy(arb_u16_u128()) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u16_overflow(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU16, builder, "safe u128 to u16 overflow") + .strategy((u128::from(u16::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u32(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU32, builder, "safe u128 to u32") + .strategy(arb_u32_u128()) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u32_overflow(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU32, builder, "safe u128 to u32 overflow") + .strategy((u128::from(u32::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u64(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU64, builder, "safe u128 to u64") + .strategy(arb_u64_u128()) + .run() + } + + #[simplex::fuzz] + fn safe_u128_to_u64_overflow(builder: Builder) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU64, builder, "safe u128 to u64 overflow") + .strategy((u128::from(u64::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u128/math/api.rs b/tests/stdlib/u128/math/api.rs index c624b3f..1b10b47 100644 --- a/tests/stdlib/u128/math/api.rs +++ b/tests/stdlib/u128/math/api.rs @@ -17,7 +17,7 @@ impl TestUint for u128 { const MUL_BOUND: u128 = 1 << 64; // 2^(128/2) fn program() -> U128MathTestProgram { - U128MathTestProgram::new(&U128MathTestArguments {}) + U128MathTestProgram::new(U128MathTestArguments {}) } fn witness(op: u8, a: u128, b: u128, expected: Option) -> U128MathTestWitness { @@ -32,3 +32,37 @@ impl TestUint for u128 { // Stamps the 22 `#[simplex::test]` entry points for u128. Logic lives in common::uint. crate::uint_tests!(u128); + +mod math_api_tests_fuzz { + use super::*; + use crate::common::uint_fuzz::TestUintFuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + type Builder = + FuzzEngineBuilder; + + impl TestUintFuzz for u128 { + type Arguments = U128MathTestArguments; + + fn arguments() -> Self::Arguments { + U128MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u128, Builder); +} diff --git a/tests/stdlib/u128/math/primitives.rs b/tests/stdlib/u128/math/primitives.rs index 367e41f..b9599da 100644 --- a/tests/stdlib/u128/math/primitives.rs +++ b/tests/stdlib/u128/math/primitives.rs @@ -27,7 +27,7 @@ enum FunctionToTest { } fn program() -> U128BasicMathTestProgram { - U128BasicMathTestProgram::new(&U128BasicMathTestArguments {}) + U128BasicMathTestProgram::new(U128BasicMathTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -643,3 +643,771 @@ fn div_128_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { case(Div128).args(a, b).expect(0).run(&context) } + +mod primitives_tests_fuzz { + use super::*; + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + type Builder = FuzzEngineBuilder< + U128BasicMathTestProgram, + U128BasicMathTestArguments, + U128BasicMathTestWitness, + >; + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + const EXPECT_CARRY: bool = true; + const EXPECT_BORROW: bool = true; + const NORMALIZER_U128_DIVISOR: bool = true; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + any::().prop_filter("u64 should not be zero", |index| *index != 0) + } + + fn arb_non_zero_u64_u128() -> impl Strategy { + any::() + .prop_filter("u64 should not be zero", |index| *index != 0) + .prop_map(u128::from) + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_filter("u128 should not be zero", |index| *index != 0) + } + + /// Values for one primitive operation before building the contract witness. + #[derive(Debug, Default, Clone)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + expected_bool: bool, + second_expected: u128, + } + + impl FuzzCase { + fn first_argument(first_arg: u128) -> Self { + Self { + first_arg: Some(first_arg), + ..Self::default() + } + } + + fn second_argument(mut self, second_arg: u128) -> Self { + self.second_arg = Some(second_arg); + self + } + + fn expect(mut self, expected: u128) -> Self { + self.expected = Some(expected); + self + } + + fn flag(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn module(mut self, second_expected: u128) -> Self { + self.second_expected = second_expected; + self + } + + fn into_witness(self, witness: U128BasicMathTestWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + ) + .expect(self.expected.expect("no expected result in witness")) + .flag(self.expected_bool) + .second(self.second_expected) + .witness + .into() + } + } + + fn div_mod_case(a: u128, b: u128) -> FuzzCase { + FuzzCase::first_argument(a) + .second_argument(b) + .expect(a / b) + .module(a % b) + } + + struct CaseFuzz { + case: Case, + builder: Builder, + inputs: Option>, + expect: Expect, + name: &'static str, + } + + fn case_fuzz(function: FunctionToTest, builder: Builder, name: &'static str) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + expect: Expect::Ok, + name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U128BasicMathTestArguments {}.into(); + let witness = fuzz_case.into_witness(witness.clone()); + (arguments, witness) + }) + .boxed(); + let strategy = FuzzStrategyBuilder::< + U128BasicMathTestArguments, + U128BasicMathTestWitness, + _, + >::new() + .with_custom_strategy(strategy) + .build(); + + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + let tx_builder = FinalTransactionBuilder::new(tx, [PROGRAM_TARGET])?; + + self.builder + .build(strategy, tx_builder) + .run_with_check(FuzzExecutionCheck::new(self.name, self.expect)); + Ok(()) + } + } + + #[simplex::fuzz] + fn add(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (sum, carry) = a.overflowing_add(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(sum) + .flag(carry) + }) + }; + + case_fuzz(Add128, builder, "add").strategy(strategy).run() + } + + #[simplex::fuzz] + fn add_128_64(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u64()).prop_map(|(a, b)| { + let b = b as u128; + let (sum, carry) = a.overflowing_add(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(sum) + .flag(carry) + }) + }; + + case_fuzz(Add128_64, builder, "add_128_64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_no_carry(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (sum, carry) = a.overflowing_add(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(sum) + .flag(carry) + }) + }; + + case_fuzz(FullAdd128, builder, "full_add_no_carry") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_with_carry(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (sum, carry1) = a.overflowing_add(b); + let (sum, carry2) = sum.overflowing_add(1); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(sum) + .flag(carry1 || carry2) + .module(1) + }) + }; + + case_fuzz(FullAdd128, builder, "full_add_with_carry") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (diff, borrow) = a.overflowing_sub(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .flag(borrow) + }) + }; + + case_fuzz(Sub128, builder, "sub").strategy(strategy).run() + } + + #[simplex::fuzz] + fn sub_equal(builder: Builder) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(a).expect(0)) }; + + case_fuzz(Sub128, builder, "sub_equal") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_equal_low_words(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u64()).prop_map(|(a, high)| { + let b = ((high as u128) << 64) | (a as u64 as u128); + let (diff, borrow) = a.overflowing_sub(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .flag(borrow) + }) + }; + + case_fuzz(Sub128, builder, "sub_equal_low_words") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_max_low_word(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u64(), arb_u64()).prop_map(|(a_high, b_high)| { + let a = ((a_high as u128) << 64) | (u64::MAX as u128); + let b = (b_high as u128) << 64; + let (diff, borrow) = a.overflowing_sub(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .flag(borrow) + }) + }; + + case_fuzz(Sub128, builder, "sub_max_low_word") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_sub_no_borrow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (diff, borrow) = a.overflowing_sub(b); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .flag(borrow) + }) + }; + + case_fuzz(FullSub128, builder, "full_sub_no_borrow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_sub_with_borrow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (diff, borrow1) = a.overflowing_sub(b); + let (diff, borrow2) = diff.overflowing_sub(1); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .flag(borrow1 || borrow2) + .module(1) + }) + }; + + case_fuzz(FullSub128, builder, "full_sub_with_borrow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (high, low) = split_helper(U256::from(a) * U256::from(b)); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(high) + .module(low) + }) + }; + + case_fuzz(Mul128, builder, "mul").strategy(strategy).run() + } + + #[simplex::fuzz] + fn mul_128_64(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u64()).prop_map(|(a, b)| { + let b = b as u128; + let (high, low) = split_helper(U256::from(a) * U256::from(b)); + FuzzCase::first_argument(a) + .second_argument(b) + .expect(high) + .module(low) + }) + }; + + case_fuzz(Mul128_64, builder, "mul_128_64") + .strategy(strategy) + .run() + } + + const THRESHOLD: u128 = 1u128 << 63; + + #[simplex::fuzz] + fn normalizer_u64_small(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (1u128..THRESHOLD).prop_map(|b| { + FuzzCase::first_argument(0) + .second_argument(b) + .expect(THRESHOLD.div_ceil(b)) + }) + }; + + case_fuzz(CalculateNormalizerBase64, builder, "normalizer_u64_small") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn normalizer_u64_large(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (THRESHOLD..=u64::MAX as u128) + .prop_map(|b| FuzzCase::first_argument(0).second_argument(b).expect(1)) + }; + + case_fuzz(CalculateNormalizerBase64, builder, "normalizer_u64_large") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn normalizer_u128(builder: Builder) -> anyhow::Result<()> { + let strategy = { + ((u64::MAX as u128 + 1)..=u128::MAX).prop_map(|b| { + FuzzCase::first_argument(0) + .second_argument(b) + .expect(THRESHOLD.div_ceil(b >> 64)) + .flag(NORMALIZER_U128_DIVISOR) + }) + }; + + case_fuzz(CalculateNormalizerBase64, builder, "normalizer_u128") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn normalizer_u64_wrong_width(builder: Builder) -> anyhow::Result<()> { + let strategy = { + ((u64::MAX as u128 + 1)..=u128::MAX) + .prop_map(|b| FuzzCase::first_argument(0).second_argument(b).expect(0)) + }; + + case_fuzz( + CalculateNormalizerBase64, + builder, + "normalizer_u64_wrong_width", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn normalizer_u128_wrong_width(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64_u128().prop_map(|value| { + FuzzCase::first_argument(0) + .second_argument(value) + .expect(0) + .flag(NORMALIZER_U128_DIVISOR) + }) + }; + + case_fuzz( + CalculateNormalizerBase64, + builder, + "normalizer_u128_wrong_width", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn normalizer_zero(builder: Builder) -> anyhow::Result<()> { + let strategy = { Just(FuzzCase::first_argument(0).second_argument(0).expect(0)) }; + + case_fuzz(CalculateNormalizerBase64, builder, "normalizer_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn quotient_digit(builder: Builder) -> anyhow::Result<()> { + let strategy = { + ((1u64 << 63)..=u64::MAX).prop_flat_map(|b_high| { + (Just(b_high), arb_u64(), 0..b_high, arb_u128()).prop_map( + |(b_high, b_low, a_high, a_low)| { + let a = (U256::from(a_high) << 128) | U256::from(a_low); + let b = ((b_high as u128) << 64) | (b_low as u128); + FuzzCase::first_argument(a_high as u128) + .second_argument(a_low) + .expect((a / U256::from(b)).as_u128()) + .module(b) + }, + ) + }) + }; + + case_fuzz(EstimateQuotientDigitBase64, builder, "quotient_digit") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn quotient_digit_too_large(builder: Builder) -> anyhow::Result<()> { + let strategy = { + ((1u64 << 63)..u64::MAX).prop_flat_map(|b_high| { + (Just(b_high), arb_u64(), (b_high + 1)..=u64::MAX, arb_u128()).prop_map( + |(b_high, b_low, a_high, a_low)| { + let a = (U256::from(a_high) << 128) | U256::from(a_low); + let b = ((b_high as u128) << 64) | (b_low as u128); + FuzzCase::first_argument(a_high as u128) + .second_argument(a_low) + .expect((a / U256::from(b)).as_u128()) + .module(b) + }, + ) + }) + }; + + case_fuzz( + EstimateQuotientDigitBase64, + builder, + "quotient_digit_too_large", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn div_mod_128_64(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_non_zero_u64().prop_map(u128::from)) + .prop_map(|(a, b)| div_mod_case(a, b)) + }; + + case_fuzz(DivMod128_64, builder, "div_mod_128_64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_128_64_zero(builder: Builder) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(0).expect(0)) }; + + case_fuzz(DivMod128_64, builder, "div_mod_128_64_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn div_mod_128(builder: Builder) -> anyhow::Result<()> { + let strategy = { (arb_u128(), arb_non_zero_u128()).prop_map(|(a, b)| div_mod_case(a, b)) }; + + case_fuzz(DivMod128, builder, "div_mod_128") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_a_less_than_b(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (0u128..u128::MAX) + .prop_flat_map(|a| (a + 1..=u128::MAX).prop_map(move |b| div_mod_case(a, b))) + }; + + case_fuzz(DivMod128, builder, "div_mod_a_less_than_b") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_equal(builder: Builder) -> anyhow::Result<()> { + let strategy = { arb_non_zero_u128().prop_map(|value| div_mod_case(value, value)) }; + + case_fuzz(DivMod128, builder, "div_mod_equal") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_same_high_words(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u64(), arb_u64(), arb_u64()).prop_map(|(high, a_low, b_low)| { + let a = ((high as u128) << 64) | (a_low as u128); + let b = ((high as u128) << 64) | (b_low as u128); + div_mod_case(a, b) + }) + }; + + case_fuzz(DivMod128, builder, "div_mod_same_high_words") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_non_zero_u128()) + .prop_map(|(a, b)| FuzzCase::first_argument(a).second_argument(b).expect(a / b)) + }; + + case_fuzz(Div128, builder, "div").strategy(strategy).run() + } + + #[simplex::fuzz] + fn div_by_zero(builder: Builder) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(0).expect(0)) }; + + case_fuzz(Div128, builder, "div_by_zero") + .strategy(strategy) + .run() + } + + // Explicit edge families complement the unrestricted arithmetic properties. + #[simplex::fuzz] + fn add_overflow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value - 1) + .flag(EXPECT_CARRY) + }) + }; + + case_fuzz(Add128, builder, "add_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn add_128_64_overflow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value - 1) + .flag(EXPECT_CARRY) + }) + }; + + case_fuzz(Add128_64, builder, "add_128_64_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_overflow_with_carry(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value) + .flag(EXPECT_CARRY) + .module(1) + }) + }; + + case_fuzz(FullAdd128, builder, "full_add_overflow_with_carry") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_overflow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (0u128..u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(u128::MAX) + .expect(a + 1) + .flag(EXPECT_BORROW) + }) + }; + + case_fuzz(Sub128, builder, "sub_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_sub_overflow_with_borrow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (0..u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(u128::MAX) + .expect(a) + .flag(EXPECT_BORROW) + .module(1) + }) + }; + + case_fuzz(FullSub128, builder, "full_sub_overflow_with_borrow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_128_div_64(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64().prop_flat_map(|b| { + (b..=u64::MAX).prop_map(move |a| div_mod_case(u128::from(a), u128::from(b))) + }) + }; + + case_fuzz(DivMod128, builder, "div_mod_128_div_64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_quotient_one(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u64(), arb_u64()).prop_flat_map(|(high, b_low)| { + (b_low..=u64::MAX).prop_map(move |a_low| { + let a = (u128::from(high) << 64) | u128::from(a_low); + let b = (u128::from(high) << 64) | u128::from(b_low); + div_mod_case(a, b) + }) + }) + }; + + case_fuzz(DivMod128, builder, "div_mod_quotient_one") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_b_is_u64(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u64(), (u128::from(u64::MAX) + 1)..=u128::MAX) + .prop_map(|(b, a)| div_mod_case(a, u128::from(b))) + }; + + case_fuzz(DivMod128, builder, "div_mod_b_is_u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_b_is_u128(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (1u64..u64::MAX).prop_flat_map(|b_high| { + ( + (b_high + 1)..=u64::MAX, + arb_non_zero_u64(), + arb_non_zero_u64(), + ) + .prop_map(move |(a_high, a_low, b_low)| { + let a = (u128::from(a_high) << 64) | u128::from(a_low); + let b = (u128::from(b_high) << 64) | u128::from(b_low); + div_mod_case(a, b) + }) + }) + }; + + case_fuzz(DivMod128, builder, "div_mod_b_is_u128") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_equal_high_words_max_low_diff(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_u64().prop_map(|value| { + let high = u128::from(value.max(1)) << 64; + div_mod_case(high | u128::from(u64::MAX), high) + }) + }; + + case_fuzz(DivMod128, builder, "div_mod_equal_high_words_max_low_diff") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_equal_high_words_less(builder: Builder) -> anyhow::Result<()> { + let strategy = { + arb_u64().prop_map(|value| { + let high = u128::from(value.max(1)) << 64; + div_mod_case(high, high | u128::from(u64::MAX)) + }) + }; + + case_fuzz(DivMod128, builder, "div_mod_equal_high_words_less") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u128/mul_div.rs b/tests/stdlib/u128/mul_div.rs index 90f4315..c7bd99f 100644 --- a/tests/stdlib/u128/mul_div.rs +++ b/tests/stdlib/u128/mul_div.rs @@ -15,7 +15,7 @@ enum FunctionToTest { } fn program() -> U128MulDivTestProgram { - U128MulDivTestProgram::new(&U128MulDivTestArguments {}) + U128MulDivTestProgram::new(U128MulDivTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -95,7 +95,6 @@ fn mul_div_128_result_overflow(context: simplex::TestContext) -> anyhow::Result< case(MulDiv) .args(a, b, c) - .expect(0) .expecting(&context, Expect::AssertFailed) } @@ -107,3 +106,207 @@ fn mul_div_128_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> case(MulDiv).args(a, b, c).expect(0).run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type Builder = + FuzzEngineBuilder; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + any::().prop_filter("u128 should not be zero", |value| *value != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + divisor: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u128) -> Self { + Self { + first_arg: Some(first_arg), + ..Self::default() + } + } + + fn second_arg(mut self, second_arg: u128) -> Self { + self.second_arg = Some(second_arg); + self + } + + fn divisor(mut self, divisor: u128) -> Self { + self.divisor = Some(divisor); + self + } + + fn expect(mut self, expected: u128) -> Self { + self.expected = Some(expected); + self + } + + fn into_witness( + self, + witness: U128MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + self.divisor.expect("no divisor in witness"), + ); + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected result in witness")) + }; + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: Builder, + inputs: Option>, + expect: Expect, + test_name: &'static str, + } + + fn case_fuzz(builder: Builder, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + builder, + inputs: None, + expect: Expect::Ok, + test_name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U128MulDivTestArguments {}.into(); + let witness = case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn product_fits_u128(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_u64(), arb_u64(), arb_non_zero_u128()).prop_map(|(a, b, c)| { + let (a, b) = (a as u128, b as u128); + FuzzCase::first_arg(a) + .second_arg(b) + .divisor(c) + .expect(a * b / c) + }) + }; + + case_fuzz(builder, "u128 mul div with fitting product") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn intermediate_overflow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (2u128..=u128::MAX).prop_flat_map(|a| { + (a..=u128::MAX).prop_map(move |c| { + let expected = + (U256::from(a) * U256::from(u128::MAX) / U256::from(c)).low_u128(); + FuzzCase::first_arg(a) + .second_arg(u128::MAX) + .divisor(c) + .expect(expected) + }) + }) + }; + + case_fuzz(builder, "u128 mul div with intermediate overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn result_overflow(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (2u128..=u128::MAX).prop_flat_map(|a| { + (1..a).prop_map(move |c| FuzzCase::first_arg(a).second_arg(u128::MAX).divisor(c)) + }) + }; + + case_fuzz(builder, "u128 mul div result overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn divide_by_zero(builder: Builder) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u128(), arb_non_zero_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).divisor(0).expect(0)) + }; + + case_fuzz(builder, "u128 mul div by zero") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u16/convert.rs b/tests/stdlib/u16/convert.rs index 036a8a4..d633279 100644 --- a/tests/stdlib/u16/convert.rs +++ b/tests/stdlib/u16/convert.rs @@ -21,7 +21,7 @@ enum FunctionToTest { } fn program() -> U16ConvertTestProgram { - U16ConvertTestProgram::new(&U16ConvertTestArguments {}) + U16ConvertTestProgram::new(U16ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -152,3 +152,180 @@ fn safe_u16_to_u8_overflow(context: simplex::TestContext) -> anyhow::Result<()> .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type ConvertFuzzEngineBuilder = + FuzzEngineBuilder; + + // (first_arg) + type ConvertInputs = u16; + + struct FuzzCaseBuilder { + case: Case, + builder: ConvertFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: ConvertFuzzEngineBuilder, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_bool_u16() -> impl Strategy { + arb_bool().prop_map(u16::from) + } + + fn arb_u8_u16() -> impl Strategy { + arb_u8().prop_map(u16::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U16ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u16_to_u32(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U16ToU32, builder, "u16_to_u32") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u64(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U16ToU64, builder, "u16_to_u64") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u128(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U16ToU128, builder, "u16_to_u128") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u256(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U16ToU256, builder, "u16_to_u256") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn split_u16_into_u8(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU16IntoU8, builder, "split_u16_into_u8") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u1(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU1, builder, "safe_u16_to_u1") + .strategy(arb_bool_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u1_overflow(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU1, builder, "safe_u16_to_u1_overflow") + .strategy(2u16..=u16::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u8(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU8, builder, "safe_u16_to_u8") + .strategy(arb_u8_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u8_overflow(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU8, builder, "safe_u16_to_u8_overflow") + .strategy(u8::MAX as u16 + 1..=u16::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u16/math.rs b/tests/stdlib/u16/math.rs index a343cfb..ed542ec 100644 --- a/tests/stdlib/u16/math.rs +++ b/tests/stdlib/u16/math.rs @@ -17,7 +17,7 @@ impl TestUint for u16 { const MUL_BOUND: u16 = 1 << 8; // 2^(16/2) fn program() -> U16MathTestProgram { - U16MathTestProgram::new(&U16MathTestArguments {}) + U16MathTestProgram::new(U16MathTestArguments {}) } fn witness(op: u8, a: u16, b: u16, expected: Option) -> U16MathTestWitness { @@ -32,3 +32,38 @@ impl TestUint for u16 { // Stamps the 22 `#[simplex::test]` entry points for u16. Logic lives in common::uint. crate::uint_tests!(u16); + +mod math_tests_fuzz { + use super::*; + + use crate::common::uint_fuzz::TestUintFuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + type U16MathFuzzEngineBuilder = + FuzzEngineBuilder; + + impl TestUintFuzz for u16 { + type Arguments = U16MathTestArguments; + + fn arguments() -> Self::Arguments { + U16MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u16, U16MathFuzzEngineBuilder); +} diff --git a/tests/stdlib/u16/mul_div.rs b/tests/stdlib/u16/mul_div.rs index 7fd260a..8cc5c3b 100644 --- a/tests/stdlib/u16/mul_div.rs +++ b/tests/stdlib/u16/mul_div.rs @@ -13,7 +13,7 @@ enum FunctionToTest { } fn program() -> U16MulDivTestProgram { - U16MulDivTestProgram::new(&U16MulDivTestArguments {}) + U16MulDivTestProgram::new(U16MulDivTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -102,3 +102,198 @@ fn mul_div_16_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { case(MulDiv).args(a, b, c).expect(0).run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type MulDivFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_non_zero_u16() -> impl Strategy { + any::().prop_filter("u16 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + third_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u16) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: u16) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn third_arg(mut self, denominator: u16) -> Self { + let _ = self.third_arg.insert(denominator); + self + } + + fn expect(mut self, expected: u16) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness( + self, + witness: U16MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + self.third_arg.expect("no third arg in witness"), + ); + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: MulDivFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(builder: MulDivFuzzEngineBuilder, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U16MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_16_product_is_u16(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_u8(), arb_u8(), arb_non_zero_u16()).prop_map(|(a, b, c)| { + let a = u16::from(a); + let b = u16::from(b); + + FuzzCase::first_arg(a) + .second_arg(b) + .third_arg(c) + .expect(a * b / c) + }); + + case_fuzz(builder, "mul_div_16_product_is_u16") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_intermediate_overflow(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2u16..=u16::MAX).prop_flat_map(|a| { + (a..=u16::MAX).prop_map(move |c| { + let b = u16::MAX; + let expected = ((u32::from(a) * u32::from(b)) / u32::from(c)) as u16; + FuzzCase::first_arg(a) + .second_arg(b) + .third_arg(c) + .expect(expected) + }) + }); + + case_fuzz(builder, "mul_div_16_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_result_overflow(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2u16..=u16::MAX).prop_flat_map(|a| { + (1u16..a).prop_map(move |c| FuzzCase::first_arg(a).second_arg(u16::MAX).third_arg(c)) + }); + + case_fuzz(builder, "mul_div_16_result_overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_div_by_zero(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u16(), arb_non_zero_u16()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).third_arg(0).expect(0)); + + case_fuzz(builder, "mul_div_16_div_by_zero") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/bit.rs b/tests/stdlib/u256/bit.rs index 0d0db36..f8e7560 100644 --- a/tests/stdlib/u256/bit.rs +++ b/tests/stdlib/u256/bit.rs @@ -19,7 +19,7 @@ enum FunctionToTest { } fn program() -> U256TestBitsProgram { - U256TestBitsProgram::new(&U256TestBitsArguments {}) + U256TestBitsProgram::new(U256TestBitsArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -64,6 +64,7 @@ impl Case { } } +// U256 stores its four u64 limbs from least to most significant. #[simplex::test] fn and_256(context: simplex::TestContext) -> anyhow::Result<()> { let a = generate_u256(U256::zero(), U256::MAX); @@ -159,3 +160,258 @@ fn right_shift_256_max(context: simplex::TestContext) -> anyhow::Result<()> { .expect(result) .run(&context) } + +mod bit_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256BitFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u8() -> impl Strategy { + any::().prop_filter("u8 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: U256) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: U256) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: U256) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness(self, witness: U256TestBitsWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg + .expect("no first arg in witness") + .to_big_endian(), + self.second_arg + .expect("no second arg in witness") + .to_big_endian(), + ) + .expect( + self.expected + .expect("no expected arg in witness") + .to_big_endian(), + ) + .witness + .into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: U256BitFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256BitFuzzEngineBuilder, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + test_name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestBitsArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn and_256(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + (arb_u256(), arb_u256()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a & b)) + }; + + case_fuzz(And256, fuzz_engine_builder, "u256 bitwise and") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn or_256(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + (arb_u256(), arb_u256()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a | b)) + }; + + case_fuzz(Or256, fuzz_engine_builder, "u256 bitwise or") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn left_shift_256(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u8(), arb_u256()).prop_map(|(shift, value)| { + FuzzCase::first_arg(U256::from(shift)) + .second_arg(value) + .expect(value << shift) + }) + }; + + case_fuzz(LeftShift256, fuzz_engine_builder, "u256 left shift") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn left_shift_256_by_zero(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::zero()) + .second_arg(value) + .expect(value) + }) + }; + + case_fuzz(LeftShift256, fuzz_engine_builder, "u256 left shift by zero") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn left_shift_256_max(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::from(u8::MAX)) + .second_arg(value) + .expect(value << u8::MAX) + }) + }; + + case_fuzz(LeftShift256, fuzz_engine_builder, "u256 left shift by max") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn right_shift_256(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u8(), arb_u256()).prop_map(|(shift, value)| { + FuzzCase::first_arg(U256::from(shift)) + .second_arg(value) + .expect(value >> shift) + }) + }; + + case_fuzz(RightShift256, fuzz_engine_builder, "u256 right shift") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn right_shift_256_by_zero( + fuzz_engine_builder: U256BitFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::zero()) + .second_arg(value) + .expect(value) + }) + }; + + case_fuzz( + RightShift256, + fuzz_engine_builder, + "u256 right shift by zero", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn right_shift_256_max(fuzz_engine_builder: U256BitFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::from(u8::MAX)) + .second_arg(value) + .expect(value >> u8::MAX) + }) + }; + + case_fuzz( + RightShift256, + fuzz_engine_builder, + "u256 right shift by max", + ) + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/comparison.rs b/tests/stdlib/u256/comparison.rs index 27bdbb5..a1ebdb2 100644 --- a/tests/stdlib/u256/comparison.rs +++ b/tests/stdlib/u256/comparison.rs @@ -17,7 +17,7 @@ enum FunctionToTest { } fn program() -> U256TestCompareProgram { - U256TestCompareProgram::new(&U256TestCompareArguments {}) + U256TestCompareProgram::new(U256TestCompareArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -136,3 +136,214 @@ fn le_256_bigger(context: simplex::TestContext) -> anyhow::Result<()> { .args(a.to_big_endian(), b.to_big_endian()) .run(&context) } + +mod comparison_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const EXPECTED_TRUE: bool = true; + const EXPECTED_FALSE: bool = false; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256ComparisonFuzzEngineBuilder = + FuzzEngineBuilder; + + // (A, B, Expected) + type ComparisonInputs = (U256, U256, bool); + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| value.max(U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + struct CaseFuzz { + case: Case, + builder: U256ComparisonFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256ComparisonFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + test_name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |(a, b, expected)| (a.to_big_endian(), b.to_big_endian(), expected)) + .prop_map(move |(a, b, expected)| { + let arguments: Arguments = U256TestCompareArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .args(a, b) + .flag(expected) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn is_zero_256_false( + fuzz_engine_builder: U256ComparisonFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = arb_non_zero_u256().prop_map(|a| (a, U256::zero(), EXPECTED_FALSE)); + + case_fuzz(IsZero256, fuzz_engine_builder, "u256 is non-zero") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn lt_256_less(fuzz_engine_builder: U256ComparisonFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::zero(), U256::MAX - 1); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(a + U256::one(), U256::MAX); + + b.prop_map(move |b| (a, b, EXPECTED_TRUE)) + }) + }; + + case_fuzz(Lt256, fuzz_engine_builder, "u256 less than") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn lt_256_eq(fuzz_engine_builder: U256ComparisonFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| (a, a, EXPECTED_FALSE)); + + case_fuzz(Lt256, fuzz_engine_builder, "u256 less than equal") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn lt_256_bigger(fuzz_engine_builder: U256ComparisonFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), a - U256::one()); + + b.prop_map(move |b| (a, b, EXPECTED_FALSE)) + }) + }; + + case_fuzz(Lt256, fuzz_engine_builder, "u256 greater than") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn le_256_less(fuzz_engine_builder: U256ComparisonFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::zero(), U256::MAX - 1); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(a + U256::one(), U256::MAX); + + b.prop_map(move |b| (a, b, EXPECTED_TRUE)) + }) + }; + + case_fuzz(Le256, fuzz_engine_builder, "u256 less than or equal") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn le_256_eq(fuzz_engine_builder: U256ComparisonFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| (a, a, EXPECTED_TRUE)); + + case_fuzz(Le256, fuzz_engine_builder, "u256 less than or equal equal") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn le_256_bigger(fuzz_engine_builder: U256ComparisonFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), a - U256::one()); + + b.prop_map(move |b| (a, b, EXPECTED_FALSE)) + }) + }; + + case_fuzz(Le256, fuzz_engine_builder, "u256 greater than") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/convert.rs b/tests/stdlib/u256/convert.rs index 2bd68cb..4a8cccc 100644 --- a/tests/stdlib/u256/convert.rs +++ b/tests/stdlib/u256/convert.rs @@ -24,7 +24,7 @@ enum FunctionToTest { } fn program() -> U256ConvertTestProgram { - U256ConvertTestProgram::new(&U256ConvertTestArguments {}) + U256ConvertTestProgram::new(U256ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -237,3 +237,353 @@ fn safe_u256_to_u128_overflow(context: simplex::TestContext) -> anyhow::Result<( .expect(a.to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256ConvertFuzzEngineBuilder = + FuzzEngineBuilder; + + // (A, Result) + type ConvertInputs = (U256, U256); + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[inline] + fn split_number(n: T) -> (T, T) { + (n.clone(), n) + } + + fn u256_duplicated_strategy() -> impl Strategy { + arb_u256().prop_map(split_number) + } + + fn safe_u256_to_u1_strategy() -> impl Strategy { + any::().prop_map(|x| U256::from(x as u8)) + } + + fn safe_u256_to_u8_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u16_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u32_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u64_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u128_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn overflow_strategy(max: U256) -> impl Strategy { + arb_u256_in_range(max + U256::one(), U256::MAX) + } + + struct CaseFuzz { + case: Case, + builder: U256ConvertFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256ConvertFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |(a, expected)| (a.to_big_endian(), expected.to_big_endian())) + .prop_map(move |(a, expected)| { + let arguments: Arguments = U256ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(expected) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u256_into_u8(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU8, fuzz_engine_builder, "u256 split into u8") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn u256_into_u16(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU16, fuzz_engine_builder, "u256 split into u16") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn u256_into_u32(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU32, fuzz_engine_builder, "u256 split into u32") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn u256_into_u64(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU64, fuzz_engine_builder, "u256 split into u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn u256_into_u128(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz( + SplitU256IntoU128, + fuzz_engine_builder, + "u256 split into u128", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u1(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = safe_u256_to_u1_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU1, fuzz_engine_builder, "safe u256 to u1") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u1_overflow( + fuzz_engine_builder: U256ConvertFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = overflow_strategy(U256::one()).prop_map(split_number); + + case_fuzz( + SafeU256ToU1, + fuzz_engine_builder, + "safe u256 to u1 overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u8(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = safe_u256_to_u8_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU8, fuzz_engine_builder, "safe u256 to u8") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u8_overflow( + fuzz_engine_builder: U256ConvertFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = overflow_strategy(u8::MAX.into()).prop_map(split_number); + + case_fuzz( + SafeU256ToU8, + fuzz_engine_builder, + "safe u256 to u8 overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u16(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = safe_u256_to_u16_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU16, fuzz_engine_builder, "safe u256 to u16") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u16_overflow( + fuzz_engine_builder: U256ConvertFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = overflow_strategy(u16::MAX.into()).prop_map(split_number); + + case_fuzz( + SafeU256ToU16, + fuzz_engine_builder, + "safe u256 to u16 overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u32(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = safe_u256_to_u32_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU32, fuzz_engine_builder, "safe u256 to u32") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u32_overflow( + fuzz_engine_builder: U256ConvertFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = overflow_strategy(u32::MAX.into()).prop_map(split_number); + + case_fuzz( + SafeU256ToU32, + fuzz_engine_builder, + "safe u256 to u32 overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u64(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = safe_u256_to_u64_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU64, fuzz_engine_builder, "safe u256 to u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u64_overflow( + fuzz_engine_builder: U256ConvertFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = overflow_strategy(u64::MAX.into()).prop_map(split_number); + + case_fuzz( + SafeU256ToU64, + fuzz_engine_builder, + "safe u256 to u64 overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u128(fuzz_engine_builder: U256ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = safe_u256_to_u128_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU128, fuzz_engine_builder, "safe u256 to u128") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_u256_to_u128_overflow( + fuzz_engine_builder: U256ConvertFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = overflow_strategy(u128::MAX.into()).prop_map(split_number); + + case_fuzz( + SafeU256ToU128, + fuzz_engine_builder, + "safe u256 to u128 overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u256/math/add.rs b/tests/stdlib/u256/math/add.rs index bbd813c..6595535 100644 --- a/tests/stdlib/u256/math/add.rs +++ b/tests/stdlib/u256/math/add.rs @@ -17,7 +17,7 @@ enum FunctionToTest { } fn program() -> U256TestAddProgram { - U256TestAddProgram::new(&U256TestAddArguments {}) + U256TestAddProgram::new(U256TestAddArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -193,3 +193,345 @@ fn full_add_256_overflow_carry_low_true(context: simplex::TestContext) -> anyhow .flag(result_carry) .run(&context) } + +mod add_tests_fuzz { + use super::*; + use std::cmp::max; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256AddFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[derive(Debug)] + struct FuzzCase { + first_arg: U256, + second_arg: U256, + carry_low: bool, + expected: U256, + expected_bool: bool, + } + + impl FuzzCase { + fn new(first_arg: U256, second_arg: U256) -> Self { + Self { + first_arg, + second_arg, + carry_low: false, + expected: U256::zero(), + expected_bool: false, + } + } + + fn carry_low(mut self) -> Self { + self.carry_low = true; + self + } + + fn expect(mut self, expected: U256) -> Self { + self.expected = expected; + self + } + + fn flag(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn into_witness(self, witness: U256TestAddWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.to_big_endian(), + self.second_arg.to_big_endian(), + ) + .third_arg(self.carry_low) + .expect(self.expected.to_big_endian()) + .flag(self.expected_bool) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + builder: U256AddFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256AddFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + test_name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestAddArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn add_256_not_overflow(fuzz_engine_builder: U256AddFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), U256::MAX - a); + + b.prop_map(move |b| FuzzCase::new(a, b).expect(a + b)) + }) + }; + + case_fuzz(Add256, fuzz_engine_builder, "u256 add without overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn add_256_overflow(fuzz_engine_builder: U256AddFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::MAX - a + U256::one(), U256::MAX); + + b.prop_map(move |b| { + let (expected, carry) = a.overflowing_add(b); + + FuzzCase::new(a, b).expect(expected).flag(carry) + }) + }) + }; + + case_fuzz(Add256, fuzz_engine_builder, "u256 add with overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn add_256_128_not_overflow( + fuzz_engine_builder: U256AddFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), (U256::MAX - a).min(u128::MAX.into())); + + b.prop_map(move |b| FuzzCase::new(a, b).expect(a + b)) + }) + }; + + case_fuzz( + Add256_128, + fuzz_engine_builder, + "u256 add u128 without overflow", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn add_256_128_overflow(fuzz_engine_builder: U256AddFuzzEngineBuilder) -> anyhow::Result<()> { + let max_u128 = U256::from(u128::MAX); + let strategy = { + let a = arb_u256_in_range(U256::MAX - max_u128 + U256::one(), U256::MAX); + + a.prop_flat_map(move |a| { + let b = arb_u256_in_range(U256::MAX - a + U256::one(), max_u128); + + b.prop_map(move |b| { + let (expected, carry) = a.overflowing_add(b); + + FuzzCase::new(a, b).expect(expected).flag(carry) + }) + }) + }; + + case_fuzz( + Add256_128, + fuzz_engine_builder, + "u256 add u128 with overflow", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_256_not_overflow_carry_low_false( + fuzz_engine_builder: U256AddFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), U256::MAX - a); + + b.prop_map(move |b| FuzzCase::new(a, b).expect(a + b)) + }) + }; + + case_fuzz( + FullAdd256, + fuzz_engine_builder, + "u256 full add without overflow and low carry false", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_256_overflow_carry_low_false( + fuzz_engine_builder: U256AddFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::MAX - a + U256::one(), U256::MAX); + + b.prop_map(move |b| { + let (expected, carry) = a.overflowing_add(b); + + FuzzCase::new(a, b).expect(expected).flag(carry) + }) + }) + }; + + case_fuzz( + FullAdd256, + fuzz_engine_builder, + "u256 full add with overflow and low carry false", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_256_not_overflow_carry_low_true( + fuzz_engine_builder: U256AddFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::zero(), U256::MAX - U256::one()); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), U256::MAX - U256::one() - a); + + b.prop_map(move |b| FuzzCase::new(a, b).carry_low().expect(a + b + U256::one())) + }) + }; + + case_fuzz( + FullAdd256, + fuzz_engine_builder, + "u256 full add without overflow and low carry true", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn full_add_256_overflow_carry_low_true( + fuzz_engine_builder: U256AddFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::MAX - a, U256::MAX); + + b.prop_map(move |b| { + let (result, carry) = a.overflowing_add(b); + let (expected, carry_low) = result.overflowing_add(U256::one()); + + FuzzCase::new(a, b) + .carry_low() + .expect(expected) + .flag(carry || carry_low) + }) + }) + }; + + case_fuzz( + FullAdd256, + fuzz_engine_builder, + "u256 full add with overflow and low carry true", + ) + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/math/api.rs b/tests/stdlib/u256/math/api.rs index 3964165..e145183 100644 --- a/tests/stdlib/u256/math/api.rs +++ b/tests/stdlib/u256/math/api.rs @@ -20,7 +20,7 @@ impl TestUint for U256Wrapper { const MUL_BOUND: U256Wrapper = U256Wrapper(U256([0, 0, 1, 0])); // 2^(256/2) fn program() -> U256MathTestProgram { - U256MathTestProgram::new(&U256MathTestArguments {}) + U256MathTestProgram::new(U256MathTestArguments {}) } fn witness( @@ -40,3 +40,53 @@ impl TestUint for U256Wrapper { // Stamps the 22 `#[simplex::test]` entry points for U256Wrapper. Logic lives in common::uint. crate::uint_tests!(U256Wrapper); + +mod u256_math_tests_fuzz { + use super::*; + + use crate::common::uint_fuzz::TestUintFuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + type U256MathFuzzEngineBuilder = + FuzzEngineBuilder; + + impl TestUintFuzz for U256Wrapper { + type Arguments = U256MathTestArguments; + + fn arguments() -> Self::Arguments { + U256MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::<[u8; 32]>() + .prop_map(|bytes| U256Wrapper(U256::from_big_endian(&bytes))) + .boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + Self::arb_any() + .prop_map(|value| U256Wrapper(value.0.max(U256::one()))) + .boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + + let range = high.0 - low.0; + + Self::arb_any() + .prop_map(move |value| { + if range == U256::MAX { + value + } else { + U256Wrapper(low.0 + value.0 % (range + U256::one())) + } + }) + .boxed() + } + } + + crate::uint_fuzz_tests!(U256Wrapper, U256MathFuzzEngineBuilder); +} diff --git a/tests/stdlib/u256/math/div.rs b/tests/stdlib/u256/math/div.rs index 8fb84db..518aecb 100644 --- a/tests/stdlib/u256/math/div.rs +++ b/tests/stdlib/u256/math/div.rs @@ -20,7 +20,7 @@ enum FunctionToTest { } fn program() -> U256TestDivProgram { - U256TestDivProgram::new(&U256TestDivArguments {}) + U256TestDivProgram::new(U256TestDivArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -451,3 +451,648 @@ fn div_256_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect([0; 32]) .run(&context) } + +mod div_tests_fuzz { + use super::*; + use std::cmp::max; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256DivFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + arb_u64().prop_map(|value| max(value, 1)) + } + + fn arb_non_zero_64bit_u256() -> impl Strategy { + arb_non_zero_u64().prop_map(U256::from) + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_map(|value| max(value, 1)) + } + + fn arb_128bit_u256() -> impl Strategy { + arb_u128().prop_map(U256::from) + } + + fn arb_non_zero_128bit_u256() -> impl Strategy { + arb_non_zero_u128().prop_map(U256::from) + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[derive(Debug)] + struct FuzzCase { + first_arg: U256, + second_arg: U256, + expected: U256, + second_expected: U256, + } + + impl FuzzCase { + fn arg(first_arg: U256) -> Self { + Self::new(first_arg, U256::zero()) + } + + fn new(first_arg: U256, second_arg: U256) -> Self { + Self { + first_arg, + second_arg, + expected: U256::zero(), + second_expected: U256::zero(), + } + } + + fn expect(mut self, expected: U256) -> Self { + self.expected = expected; + self + } + + fn second(mut self, second_expected: U256) -> Self { + self.second_expected = second_expected; + self + } + + fn into_witness(self, witness: U256TestDivWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.to_big_endian(), + self.second_arg.to_big_endian(), + ) + .expect(self.expected.to_big_endian()) + .second(self.second_expected.to_big_endian()) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + builder: U256DivFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256DivFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestDivArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn calculate_normalizer_base_128( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let threshold = U256::one() << 127; + let strategy = { + let a = arb_u256_in_range(U256::one() << 128, U256::MAX); + + a.prop_map(move |a| { + let high = (a >> 128).as_u128(); + let normalizer = threshold.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + fuzz_engine_builder, + "u256 calculate base 128 normalizer", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn calculate_normalizer_base_128_norm_is_1( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let threshold = U256::one() << 127; + let strategy = { + let a = arb_u256_in_range(U256::one() << 255, U256::MAX); + + a.prop_map(move |a| { + let high = (a >> 128).as_u128(); + let normalizer = threshold.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + fuzz_engine_builder, + "u256 calculate base 128 normalizer is one", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn calculate_normalizer_base_128_norm_greater_than_1( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let threshold = U256::one() << 127; + let strategy = { + let a = arb_u256_in_range(U256::one() << 128, (U256::one() << 255) - 1); + + a.prop_map(move |a| { + let high = (a >> 128).as_u128(); + let normalizer = threshold.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + fuzz_engine_builder, + "u256 calculate base 128 normalizer is greater than one", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn calculate_normalizer_base_128_a_is_u128_fail( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), (U256::one() << 128) - 1); + + a.prop_map(FuzzCase::arg) + }; + + case_fuzz( + CalculateNormalizerBase128, + fuzz_engine_builder, + "u256 calculate base 128 normalizer rejects u128", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn calculate_normalizer_base_128_b_is_zero_fail( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = Just(U256::zero()).prop_map(FuzzCase::arg); + + case_fuzz( + CalculateNormalizerBase128, + fuzz_engine_builder, + "u256 calculate base 128 normalizer rejects zero", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_64(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }; + + case_fuzz(DivMod256_64, fuzz_engine_builder, "u256 div mod by u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_64_overflow( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz( + DivMod256_64, + fuzz_engine_builder, + "u256 div mod by u64 zero", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn algorithm_d_256_128(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_u256_in_range(U256::from(u64::MAX) + 1, U256::from(u128::MAX)); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }; + + case_fuzz( + AlgorithmD256_128, + fuzz_engine_builder, + "u256 algorithm d by u128", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn algorithm_d_256_128_fail_b_fits_into_u64( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }; + + case_fuzz( + AlgorithmD256_128, + fuzz_engine_builder, + "u256 algorithm d rejects u64 divisor", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn algorithm_d_256_128_overflow( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz( + AlgorithmD256_128, + fuzz_engine_builder, + "u256 algorithm d rejects zero divisor", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn algorithm_d_256_128_a_eq_b( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_128bit_u256(); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz( + AlgorithmD256_128, + fuzz_engine_builder, + "u256 algorithm d equal arguments", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_128(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_u256_in_range(U256::from(u64::MAX) + 1, U256::from(u128::MAX)); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }; + + case_fuzz(DivMod256_128, fuzz_engine_builder, "u256 div mod by u128") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_128_b_fits_into_u64( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }; + + case_fuzz( + DivMod256_128, + fuzz_engine_builder, + "u256 div mod by u64 through u128", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_128_overflow( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz( + DivMod256_128, + fuzz_engine_builder, + "u256 div mod by u128 zero", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_128_a_eq_b(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_128bit_u256(); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz( + DivMod256_128, + fuzz_engine_builder, + "u256 div mod by u128 equal arguments", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_a_less_than_b( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), U256::MAX - 1); + + a.prop_flat_map(|a| { + arb_u256_in_range(a + 1, U256::MAX) + .prop_map(move |b| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }) + }; + + case_fuzz(DivMod256, fuzz_engine_builder, "u256 div mod a less than b") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_div_128(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let b = arb_non_zero_128bit_u256(); + + b.prop_flat_map(|b| { + arb_u256_in_range(b, U256::from(u128::MAX)) + .prop_map(move |a| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }) + }; + + case_fuzz(DivMod256, fuzz_engine_builder, "u256 div mod u128 dividend") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_q_is_1(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let b_low = arb_128bit_u256(); + let high = arb_non_zero_128bit_u256(); + + (b_low, high).prop_flat_map(|(b_low, high)| { + arb_u256_in_range(b_low, U256::from(u128::MAX)).prop_map(move |a_low| { + let a = (high << 128) | a_low; + let b = (high << 128) | b_low; + + FuzzCase::new(a, b).expect(a / b).second(a % b) + }) + }) + }; + + case_fuzz( + DivMod256, + fuzz_engine_builder, + "u256 div mod quotient is one", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_b_fits_into_u128( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(u128::MAX) + 1, U256::MAX); + let b = arb_non_zero_128bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }; + + case_fuzz(DivMod256, fuzz_engine_builder, "u256 div mod u128 divisor") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_b_is_u256(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let b = arb_u256_in_range(U256::one(), U256::MAX - 1); + + b.prop_flat_map(|b| { + arb_u256_in_range(b + 1, U256::MAX) + .prop_map(move |a| FuzzCase::new(a, b).expect(a / b).second(a % b)) + }) + }; + + case_fuzz(DivMod256, fuzz_engine_builder, "u256 div mod u256 divisor") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_a_equal_b(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz( + DivMod256, + fuzz_engine_builder, + "u256 div mod equal arguments", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_equal_high_words_max_low_diff( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let high = arb_non_zero_128bit_u256(); + + high.prop_map(|high| { + let a = (high << 128) | U256::from(u128::MAX); + let b = high << 128; + + FuzzCase::new(a, b) + .expect(U256::one()) + .second(U256::from(u128::MAX)) + }) + }; + + case_fuzz( + DivMod256, + fuzz_engine_builder, + "u256 div mod equal high words maximum low difference", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_mod_256_eq_high_words_a_less_than_b( + fuzz_engine_builder: U256DivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let high = arb_non_zero_128bit_u256(); + + high.prop_map(|high| { + let a = high << 128; + let b = (high << 128) | U256::from(u128::MAX); + + FuzzCase::new(a, b).second(a) + }) + }; + + case_fuzz( + DivMod256, + fuzz_engine_builder, + "u256 div mod equal high words a less than b", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_256(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b)) + }; + + case_fuzz(Div256, fuzz_engine_builder, "u256 division") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn div_256_div_by_zero(fuzz_engine_builder: U256DivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz(Div256, fuzz_engine_builder, "u256 division by zero") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/math/sub_mul.rs b/tests/stdlib/u256/math/sub_mul.rs index 2d5a64d..578735e 100644 --- a/tests/stdlib/u256/math/sub_mul.rs +++ b/tests/stdlib/u256/math/sub_mul.rs @@ -22,7 +22,7 @@ enum FunctionToTest { } fn program() -> U256TestSubMulProgram { - U256TestSubMulProgram::new(&U256TestSubMulArguments {}) + U256TestSubMulProgram::new(U256TestSubMulArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -274,3 +274,455 @@ fn safe_mul_256_128_overflow(context: simplex::TestContext) -> anyhow::Result<() .expect([0; 32]) .expecting(&context, Expect::AssertFailed) } + +mod sub_mul_tests_fuzz { + use super::*; + use std::cmp::max; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const CARRY_TRUE: bool = true; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256SubMulFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + arb_u64().prop_map(|value| max(value, 1)) + } + + fn arb_non_zero_64bit_u256() -> impl Strategy { + arb_non_zero_u64().prop_map(U256::from) + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_map(|value| max(value, 1)) + } + + fn arb_128bit_u256() -> impl Strategy { + arb_u128().prop_map(U256::from) + } + + fn arb_non_zero_128bit_u256() -> impl Strategy { + arb_non_zero_u128().prop_map(U256::from) + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[derive(Debug)] + struct FuzzCase { + first_arg: U256, + second_arg: U256, + third_arg: u128, + expected: U256, + expected_bool: bool, + second_expected: U256, + third_expected: U256, + } + + impl FuzzCase { + fn new(first_arg: U256, second_arg: U256) -> Self { + Self { + first_arg, + second_arg, + third_arg: 0, + expected: U256::zero(), + expected_bool: false, + second_expected: U256::zero(), + third_expected: U256::zero(), + } + } + + fn third_arg(mut self, third_arg: u128) -> Self { + self.third_arg = third_arg; + self + } + + fn expect(mut self, expected: U256) -> Self { + self.expected = expected; + self + } + + fn flag(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn second(mut self, second_expected: U256) -> Self { + self.second_expected = second_expected; + self + } + + fn third(mut self, third_expected: U256) -> Self { + self.third_expected = third_expected; + self + } + + fn into_witness(self, witness: U256TestSubMulWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.to_big_endian(), + self.second_arg.to_big_endian(), + ) + .third_arg(self.third_arg) + .expect(self.expected.to_big_endian()) + .flag(self.expected_bool) + .second(self.second_expected.to_big_endian()) + .third(self.third_expected.to_big_endian()) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + builder: U256SubMulFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256SubMulFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestSubMulArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn sub_256_not_overflow( + fuzz_engine_builder: U256SubMulFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + arb_u256_in_range(U256::zero(), a).prop_map(move |b| { + let result = a - b; + + FuzzCase::new(a, b).expect(result) + }) + }) + }; + + case_fuzz( + Sub256, + fuzz_engine_builder, + "u256 subtraction without overflow", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_256_a_eq_b(fuzz_engine_builder: U256SubMulFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| FuzzCase::new(a, a).expect(U256::zero())); + + case_fuzz( + Sub256, + fuzz_engine_builder, + "u256 subtraction equal operands", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_256_a_low_eq_b_low( + fuzz_engine_builder: U256SubMulFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b_high = arb_u128(); + + (a, b_high).prop_map(|(a, b_high)| { + let low = U256::from(a.low_u128()); + let b = (U256::from(b_high) << 128) | low; + let (result, carry) = a.overflowing_sub(b); + + FuzzCase::new(a, b).expect(result).flag(carry) + }) + }; + + case_fuzz( + Sub256, + fuzz_engine_builder, + "u256 subtraction equal low halves", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_256_diff_is_u128_max( + fuzz_engine_builder: U256SubMulFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a_high = arb_u128(); + let b_high = arb_u128(); + + (a_high, b_high).prop_map(|(a_high, b_high)| { + let a = (U256::from(a_high) << 128) | U256::from(u128::MAX); + let b = U256::from(b_high) << 128; + let (result, carry) = a.overflowing_sub(b); + + FuzzCase::new(a, b).expect(result).flag(carry) + }) + }; + + case_fuzz( + Sub256, + fuzz_engine_builder, + "u256 subtraction u128 max difference", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn sub_256_overflow(fuzz_engine_builder: U256SubMulFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), U256::MAX - 1); + let b = U256::MAX; + + a.prop_map(move |a| { + let result = a + U256::one(); + + FuzzCase::new(a, b).expect(result).flag(CARRY_TRUE) + }) + }; + + case_fuzz(Sub256, fuzz_engine_builder, "u256 subtraction overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_256(fuzz_engine_builder: U256SubMulFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_u256(); + + (a, b).prop_map(|(a, b)| { + let (high, low) = split_u512(a.full_mul(b).to_big_endian()); + let high = U256::from_big_endian(&high); + let low = U256::from_big_endian(&low); + + FuzzCase::new(a, b).expect(high).second(low) + }) + }; + + case_fuzz(Mul256, fuzz_engine_builder, "u256 multiplication") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_256_64(fuzz_engine_builder: U256SubMulFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| { + let (high, low) = split_u512(a.full_mul(b).to_big_endian()); + let high = U256::from_big_endian(&high); + let low = U256::from_big_endian(&low); + + FuzzCase::new(a, b).expect(high).second(low) + }) + }; + + case_fuzz(Mul256_64, fuzz_engine_builder, "u256 by u64 multiplication") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_256_128(fuzz_engine_builder: U256SubMulFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_128bit_u256(); + + (a, b).prop_map(|(a, b)| { + let (high, low) = split_u512(a.full_mul(b).to_big_endian()); + let high = U256::from_big_endian(&high); + let low = U256::from_big_endian(&low); + + FuzzCase::new(a, b).expect(high).second(low) + }) + }; + + case_fuzz( + Mul256_128, + fuzz_engine_builder, + "u256 by u128 multiplication", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_512_128(fuzz_engine_builder: U256SubMulFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = { + let a_1 = arb_non_zero_u256(); + let a_0 = arb_non_zero_u256(); + let b = 1..=u128::MAX; + + (a_1, a_0, b).prop_map(|(a_1, a_0, b)| { + let result_low = U512::from(a_0) * U512::from(b); + let result_high = U512::from(a_1) * U512::from(b); + + let (res_1, res_0) = split_u512(result_low.to_big_endian()); + let (res_3, res_2) = split_u512(result_high.to_big_endian()); + + let res_2_1 = U512::from_big_endian(&res_1) + U512::from_big_endian(&res_2); + let (res_3_1, res_2_1) = split_u512(res_2_1.to_big_endian()); + let res_3_final = U256::from_big_endian(&res_3_1) + U256::from_big_endian(&res_3); + let res_2_1 = U256::from_big_endian(&res_2_1); + let res_0 = U256::from_big_endian(&res_0); + + FuzzCase::new(a_1, a_0) + .third_arg(b) + .expect(res_3_final) + .second(res_2_1) + .third(res_0) + }) + }; + + case_fuzz( + Mul512_128, + fuzz_engine_builder, + "u512 by u128 multiplication", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_mul_256_128_fitting( + fuzz_engine_builder: U256SubMulFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_128bit_u256(); + let b = arb_128bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a * b)) + }; + + case_fuzz( + SafeMul256_128, + fuzz_engine_builder, + "safe u256 by u128 multiplication fitting", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn safe_mul_256_128_overflow( + fuzz_engine_builder: U256SubMulFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let b = U256::from(u128::MAX); + let first_overflowing_a = U256::MAX / b + U256::one(); + let strategy = arb_u256_in_range(first_overflowing_a, U256::MAX) + .prop_map(move |a| FuzzCase::new(a, b)); + + case_fuzz( + SafeMul256_128, + fuzz_engine_builder, + "safe u256 by u128 multiplication overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u256/mul_div.rs b/tests/stdlib/u256/mul_div.rs index afd5123..c0ef006 100644 --- a/tests/stdlib/u256/mul_div.rs +++ b/tests/stdlib/u256/mul_div.rs @@ -1,4 +1,4 @@ -use primitive_types::U256; +use primitive_types::{U256, U512}; use std::cmp::max; use std::ops::Div; @@ -17,7 +17,7 @@ enum FunctionToTest { } fn program() -> U256MulDivTestProgram { - U256MulDivTestProgram::new(&U256MulDivTestArguments {}) + U256MulDivTestProgram::new(U256MulDivTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -68,11 +68,21 @@ fn safe_u512_to_u256(a: [u8; 64]) -> [u8; 32] { let high = U256::from_big_endian(&a[0..32]); let low = U256::from_big_endian(&a[32..64]); - assert!(high == U256::zero()); + assert_eq!(high, U256::zero()); low.to_big_endian() } +fn safe_u512_to_u256_typed(a: U512) -> U256 { + let a = a.to_big_endian(); + let high = U256::from_big_endian(&a[0..32]); + let low = U256::from_big_endian(&a[32..64]); + + assert_eq!(high, U256::zero()); + + low +} + #[simplex::test] fn mul_div_256_product_fits_into_u256(context: simplex::TestContext) -> anyhow::Result<()> { let a = generate_u256(U256::zero(), U256::from(u128::MAX)); @@ -260,3 +270,418 @@ fn mul_div_256_normalize_to_threshold_512_127_norm_greater_than_1( .expect(res) .run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U256MulDivFuzzEngineBuilder = + FuzzEngineBuilder; + + // (A, B, C, Result) + type MulDivInputs = (U256, U256, U256, U256); + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_map(|value| max(value, 1)) + } + + fn arb_128bit_u256() -> impl Strategy { + arb_u128().prop_map(U256::from) + } + + fn arb_non_zero_128bit_u256() -> impl Strategy { + arb_non_zero_u128().prop_map(U256::from) + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[inline] + fn product_high(a: U256, b: U256) -> U256 { + safe_u512_to_u256_typed(a.full_mul(b) >> 256) + } + + struct CaseFuzz { + case: Case, + builder: U256MulDivFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U256MulDivFuzzEngineBuilder, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |(a, b, c, expected)| { + ( + a.to_big_endian(), + b.to_big_endian(), + c.to_big_endian(), + expected.to_big_endian(), + ) + }) + .prop_map(move |(a, b, c, expected)| { + let arguments: Arguments = U256MulDivTestArguments {}.into(); + let case = Case { + witness: witness.clone(), + } + .args(a, b, c); + let case = if expect_failure { + case + } else { + case.expect(expected) + }; + let witness: WitnessValues = case.witness.into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_256_product_fits_into_u256( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_128bit_u256(); + let b = arb_128bit_u256(); + let c = arb_non_zero_u256(); + + (a, b, c).prop_map(|(a, b, c)| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + (a, b, c, expected) + }) + }; + + case_fuzz( + MulDiv, + fuzz_engine_builder, + "mul_div_256 product fits into u256", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_intermediate_overflow( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(2), U256::MAX); + let b = Just(U256::MAX); + + (a, b).prop_flat_map(|(a, b)| { + arb_u256_in_range(a, U256::MAX).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + (a, b, c, expected) + }) + }) + }; + + case_fuzz( + MulDiv, + fuzz_engine_builder, + "mul_div_256 intermediate overflow", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_result_overflow( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(2), U256::MAX); + let b = Just(U256::MAX); + + (a, b).prop_flat_map(|(a, b)| { + arb_u256_in_range(U256::one(), a - U256::one()) + .prop_map(move |c| (a, b, c, U256::zero())) + }) + }; + + case_fuzz(MulDiv, fuzz_engine_builder, "mul_div_256 result overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_remainder_is_zero( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX); + + (a, b).prop_map(|(a, b)| (a, b, a, b)) + }; + + case_fuzz(MulDiv, fuzz_engine_builder, "mul_div_256 remainder is zero") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_denominator_is_u128( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_128bit_u256(); + + (a, b).prop_flat_map(|(a, b)| { + arb_u256_in_range(b, U256::from(u128::MAX)).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }) + }; + + case_fuzz( + MulDiv, + fuzz_engine_builder, + "mul_div_256 denominator is u128", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_min_denom_high( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let pow2_129 = U256::one() << 129; + let strategy = { + let a = arb_u256_in_range(pow2_128, pow2_129); + let b = arb_u256_in_range(pow2_128, pow2_129); + let c = arb_u256_in_range(pow2_128, pow2_129 - U256::one()); + + (a, b, c).prop_map(|(a, b, c)| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }; + + case_fuzz( + MulDiv, + fuzz_engine_builder, + "mul_div_256 minimal denominator high", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_div_by_zero( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_u256(); + + (a, b).prop_map(|(a, b)| (a, b, U256::zero(), U256::zero())) + }; + + case_fuzz(MulDiv, fuzz_engine_builder, "mul_div_256 division by zero") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_algorithm_d_512_256_check( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX - pow2_128); + + (a, b).prop_flat_map(move |(a, b)| { + let min_c = max(pow2_128, product_high(a, b) + U256::one()); + + arb_u256_in_range(min_c, U256::MAX).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }) + }; + + case_fuzz(MulDiv, fuzz_engine_builder, "mul_div_256 algorithm D") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_algorithm_d_512_256_c_is_res_high( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX - pow2_128); + + (a, b).prop_map(|(a, b)| { + let c = product_high(a, b) + U256::one(); + + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }; + + case_fuzz( + MulDiv, + fuzz_engine_builder, + "mul_div_256 denominator just above product high", + ) + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_normalize_to_threshold_512_127_norm_is_1( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let pow2_255 = U256::one() << 255; + + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX - pow2_128); + + (a, b).prop_flat_map(move |(a, b)| { + let min_c = max(pow2_255, product_high(a, b) + U256::one()); + + arb_u256_in_range(min_c, U256::MAX).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + (a, b, c, expected) + }) + }) + }; + + case_fuzz(MulDiv, fuzz_engine_builder, "mul_div_256 normalizer is one") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_256_normalize_to_threshold_512_127_norm_greater_than_1( + fuzz_engine_builder: U256MulDivFuzzEngineBuilder, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let pow2_192 = U256::one() << 192; + let pow2_255 = U256::one() << 255; + + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, pow2_192); + + (a, b).prop_flat_map(move |(a, b)| { + let min_c = max(pow2_128, product_high(a, b) + U256::one()); + + arb_u256_in_range(min_c, pow2_255 - U256::one()).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + (a, b, c, expected) + }) + }) + }; + + case_fuzz( + MulDiv, + fuzz_engine_builder, + "mul_div_256 normalizer is greater than one", + ) + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u32/convert.rs b/tests/stdlib/u32/convert.rs index ccf0cf9..a0ae90d 100644 --- a/tests/stdlib/u32/convert.rs +++ b/tests/stdlib/u32/convert.rs @@ -22,7 +22,7 @@ enum FunctionToTest { } fn program() -> U32ConvertTestProgram { - U32ConvertTestProgram::new(&U32ConvertTestArguments {}) + U32ConvertTestProgram::new(U32ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -173,3 +173,203 @@ fn safe_u32_to_u16_overflow(context: simplex::TestContext) -> anyhow::Result<()> .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type ConvertFuzzEngineBuilder = + FuzzEngineBuilder; + + // (first_arg) + type ConvertInputs = u32; + + struct FuzzCaseBuilder { + case: Case, + builder: ConvertFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: ConvertFuzzEngineBuilder, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_bool_u32() -> impl Strategy { + arb_bool().prop_map(u32::from) + } + + fn arb_u8_u32() -> impl Strategy { + arb_u8().prop_map(u32::from) + } + + fn arb_u16_u32() -> impl Strategy { + arb_u16().prop_map(u32::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U32ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u32_to_u64(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U32ToU64, builder, "u32_to_u64") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn u32_to_u128(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U32ToU128, builder, "u32_to_u128") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn u32_to_u256(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U32ToU256, builder, "u32_to_u256") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn split_u32_into_u8(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU32IntoU8, builder, "split_u32_into_u8") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn split_u32_into_u16(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU32IntoU16, builder, "split_u32_into_u16") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u1(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU1, builder, "safe_u32_to_u1") + .strategy(arb_bool_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u1_overflow(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU1, builder, "safe_u32_to_u1_overflow") + .strategy(2u32..=u32::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u8(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU8, builder, "safe_u32_to_u8") + .strategy(arb_u8_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u8_overflow(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU8, builder, "safe_u32_to_u8_overflow") + .strategy(u8::MAX as u32 + 1..=u32::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u16(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU16, builder, "safe_u32_to_u16") + .strategy(arb_u16_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u16_overflow(builder: ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU16, builder, "safe_u32_to_u16_overflow") + .strategy(u16::MAX as u32 + 1..=u32::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u32/math.rs b/tests/stdlib/u32/math.rs index c25a816..d056216 100644 --- a/tests/stdlib/u32/math.rs +++ b/tests/stdlib/u32/math.rs @@ -17,7 +17,7 @@ impl TestUint for u32 { const MUL_BOUND: u32 = 1 << 16; // 2^(32/2) fn program() -> U32MathTestProgram { - U32MathTestProgram::new(&U32MathTestArguments {}) + U32MathTestProgram::new(U32MathTestArguments {}) } fn witness(op: u8, a: u32, b: u32, expected: Option) -> U32MathTestWitness { @@ -32,3 +32,38 @@ impl TestUint for u32 { // Stamps the 22 `#[simplex::test]` entry points for u32. Logic lives in common::uint. crate::uint_tests!(u32); + +mod math_tests_fuzz { + use super::*; + + use crate::common::uint_fuzz::TestUintFuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + type U32MathFuzzEngineBuilder = + FuzzEngineBuilder; + + impl TestUintFuzz for u32 { + type Arguments = U32MathTestArguments; + + fn arguments() -> Self::Arguments { + U32MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u32, U32MathFuzzEngineBuilder); +} diff --git a/tests/stdlib/u32/mul_div.rs b/tests/stdlib/u32/mul_div.rs index 1a45dca..a5e8cdc 100644 --- a/tests/stdlib/u32/mul_div.rs +++ b/tests/stdlib/u32/mul_div.rs @@ -13,7 +13,7 @@ enum FunctionToTest { } fn program() -> U32MulDivTestProgram { - U32MulDivTestProgram::new(&U32MulDivTestArguments {}) + U32MulDivTestProgram::new(U32MulDivTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -105,3 +105,198 @@ fn mul_div_32_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { case(MulDiv).args(a, b, c).expect(0).run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type MulDivFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_non_zero_u32() -> impl Strategy { + any::().prop_filter("u32 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + divisor: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u32) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: u32) -> Self { + let _ = self.second_numerator.insert(second_arg); + self + } + + fn third_arg(mut self, denominator: u32) -> Self { + let _ = self.divisor.insert(denominator); + self + } + + fn expect(mut self, expected: u32) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness( + self, + witness: U32MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first arg in witness"), + self.second_numerator.expect("no second arg in witness"), + self.divisor.expect("no divisor in witness"), + ); + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: MulDivFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(builder: MulDivFuzzEngineBuilder, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U32MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_32_product_is_u32(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_u16(), arb_u16(), arb_non_zero_u32()).prop_map(|(a, b, c)| { + let a = u32::from(a); + let b = u32::from(b); + + FuzzCase::first_arg(a) + .second_arg(b) + .third_arg(c) + .expect(a * b / c) + }); + + case_fuzz(builder, "mul_div_32_product_is_u32") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_intermediate_overflow(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2u32..=u32::MAX).prop_flat_map(|a| { + (a..=u32::MAX).prop_map(move |c| { + let b = u32::MAX; + let expected = ((u64::from(a) * u64::from(b)) / u64::from(c)) as u32; + FuzzCase::first_arg(a) + .second_arg(b) + .third_arg(c) + .expect(expected) + }) + }); + + case_fuzz(builder, "mul_div_32_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_result_overflow(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2u32..=u32::MAX).prop_flat_map(|a| { + (1u32..a).prop_map(move |c| FuzzCase::first_arg(a).second_arg(u32::MAX).third_arg(c)) + }); + + case_fuzz(builder, "mul_div_32_result_overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_div_by_zero(builder: MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u32(), arb_non_zero_u32()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).third_arg(0).expect(0)); + + case_fuzz(builder, "mul_div_32_div_by_zero") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u64/convert.rs b/tests/stdlib/u64/convert.rs index 78a6289..57a86ba 100644 --- a/tests/stdlib/u64/convert.rs +++ b/tests/stdlib/u64/convert.rs @@ -23,7 +23,7 @@ enum FunctionToTest { } fn program() -> U64ConvertTestProgram { - U64ConvertTestProgram::new(&U64ConvertTestArguments {}) + U64ConvertTestProgram::new(U64ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -194,3 +194,221 @@ fn safe_u64_to_u32_overflow(context: simplex::TestContext) -> anyhow::Result<()> .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U64ConvertFuzzEngineBuilder = + FuzzEngineBuilder; + + struct FuzzCaseBuilder { + case: Case, + builder: U64ConvertFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U64ConvertFuzzEngineBuilder, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U64ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + let transaction_builder = FinalTransactionBuilder::new(tx, [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_bool_u64() -> impl Strategy { + arb_bool().prop_map(u64::from) + } + + fn arb_u8_u64() -> impl Strategy { + arb_u8().prop_map(u64::from) + } + + fn arb_u16_u64() -> impl Strategy { + arb_u16().prop_map(u64::from) + } + + fn arb_u32_u64() -> impl Strategy { + arb_u32().prop_map(u64::from) + } + + #[simplex::fuzz] + fn u64_to_u128(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U64ToU128, builder, "u64 to u128") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn u64_to_u256(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U64ToU256, builder, "u64 to u256") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u8(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU8, builder, "split u64 into u8") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u16(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU16, builder, "split u64 into u16") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u32(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU32, builder, "split u64 into u32") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u1(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU1, builder, "safe u64 to u1") + .strategy(arb_bool_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u1_overflow(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU1, builder, "safe u64 to u1 overflow") + .strategy(2u64..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u8(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU8, builder, "safe u64 to u8") + .strategy(arb_u8_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u8_overflow(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU8, builder, "safe u64 to u8 overflow") + .strategy((u64::from(u8::MAX) + 1)..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u16(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU16, builder, "safe u64 to u16") + .strategy(arb_u16_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u16_overflow(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU16, builder, "safe u64 to u16 overflow") + .strategy((u64::from(u16::MAX) + 1)..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u32(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU32, builder, "safe u64 to u32") + .strategy(arb_u32_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u32_overflow(builder: U64ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU32, builder, "safe u64 to u32 overflow") + .strategy((u64::from(u32::MAX) + 1)..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u64/math.rs b/tests/stdlib/u64/math.rs index 327894e..fef33d6 100644 --- a/tests/stdlib/u64/math.rs +++ b/tests/stdlib/u64/math.rs @@ -17,7 +17,7 @@ impl TestUint for u64 { const MUL_BOUND: u64 = 1 << 32; // 2^(64/2) fn program() -> U64MathTestProgram { - U64MathTestProgram::new(&U64MathTestArguments {}) + U64MathTestProgram::new(U64MathTestArguments {}) } fn witness(op: u8, a: u64, b: u64, expected: Option) -> U64MathTestWitness { @@ -32,3 +32,38 @@ impl TestUint for u64 { // Stamps the 22 `#[simplex::test]` entry points for u64. Logic lives in common::uint. crate::uint_tests!(u64); + +mod math_tests_fuzz { + use super::*; + + use crate::common::uint_fuzz::TestUintFuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + type U64MathFuzzEngineBuilder = + FuzzEngineBuilder; + + impl TestUintFuzz for u64 { + type Arguments = U64MathTestArguments; + + fn arguments() -> Self::Arguments { + U64MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u64, U64MathFuzzEngineBuilder); +} diff --git a/tests/stdlib/u64/mul_div.rs b/tests/stdlib/u64/mul_div.rs index d460e28..9fed466 100644 --- a/tests/stdlib/u64/mul_div.rs +++ b/tests/stdlib/u64/mul_div.rs @@ -13,7 +13,7 @@ enum FunctionToTest { } fn program() -> U64MulDivTestProgram { - U64MulDivTestProgram::new(&U64MulDivTestArguments {}) + U64MulDivTestProgram::new(U64MulDivTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -102,3 +102,203 @@ fn mul_div_64_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { case(MulDiv).args(a, b, c).expect(0).run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U64MulDivFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + any::().prop_filter("u64 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + divisor: Option, + expected: Option, + } + + impl FuzzCase { + fn first_numerator(first_arg: u64) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_arg); + x + } + + fn second_numerator(mut self, second_arg: u64) -> Self { + let _ = self.second_numerator.insert(second_arg); + self + } + + fn divisor(mut self, denominator: u64) -> Self { + let _ = self.divisor.insert(denominator); + self + } + + fn expect(mut self, expected: u64) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness( + self, + witness: U64MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first arg in witness"), + self.second_numerator.expect("no second arg in witness"), + self.divisor.expect("no divisor in witness"), + ); + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + case.witness.into() + } + } + + struct CaseFuzz { + case: Case, + builder: U64MulDivFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(builder: U64MulDivFuzzEngineBuilder, test_name: &'static str) -> CaseFuzz { + CaseFuzz { + case: case(MulDiv), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U64MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + let transaction_builder = FinalTransactionBuilder::new(tx, [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_64_product_is_u64(builder: U64MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_u32(), arb_u32(), arb_non_zero_u64()).prop_map(|(a, b, c)| { + let (a, b) = (u64::from(a), u64::from(b)); + + FuzzCase::first_numerator(a) + .second_numerator(b) + .divisor(c) + .expect(a * b / c) + }); + + case_fuzz(builder, "mul_div_64_product_is_u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_intermediate_overflow(builder: U64MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2..=u64::MAX).prop_flat_map(|a| { + (a..=u64::MAX).prop_map(move |c| { + let expected = (u128::from(a) * u128::from(u64::MAX) / u128::from(c)) as u64; + FuzzCase::first_numerator(a) + .second_numerator(u64::MAX) + .divisor(c) + .expect(expected) + }) + }); + + case_fuzz(builder, "mul_div_64_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_result_overflow(builder: U64MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2..=u64::MAX).prop_flat_map(|a| { + (1..a).prop_map(move |c| { + FuzzCase::first_numerator(a) + .second_numerator(u64::MAX) + .divisor(c) + }) + }); + + case_fuzz(builder, "mul_div_64_result_overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_div_by_zero(builder: U64MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = + (arb_non_zero_u64(), arb_non_zero_u64(), Just(0u64)).prop_map(|(a, b, c)| { + FuzzCase::first_numerator(a) + .second_numerator(b) + .divisor(c) + .expect(0) + }); + + case_fuzz(builder, "mul_div_64_div_by_zero") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u8/convert.rs b/tests/stdlib/u8/convert.rs index d536dd8..11164e8 100644 --- a/tests/stdlib/u8/convert.rs +++ b/tests/stdlib/u8/convert.rs @@ -21,7 +21,7 @@ enum FunctionToTest { } fn program() -> U8ConvertTestProgram { - U8ConvertTestProgram::new(&U8ConvertTestArguments {}) + U8ConvertTestProgram::new(U8ConvertTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -142,3 +142,162 @@ fn safe_u8_to_u1_overflow(context: simplex::TestContext) -> anyhow::Result<()> { .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U8ConvertFuzzEngineBuilder = + FuzzEngineBuilder; + + struct FuzzCaseBuilder { + case: Case, + builder: U8ConvertFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + builder: U8ConvertFuzzEngineBuilder, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_bool_u8() -> impl Strategy { + arb_bool().prop_map(u8::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U8ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u8_to_u16(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U8ToU16, builder, "u8 to u16") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u32(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U8ToU32, builder, "u8 to u32") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u64(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U8ToU64, builder, "u8 to u64") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u128(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U8ToU128, builder, "u8 to u128") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u256(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(U8ToU256, builder, "u8 to u256") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn split_u8_into_u1(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SplitU8IntoU1, builder, "split u8 into u1") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn safe_u8_to_u1(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU8ToU1, builder, "safe u8 to u1") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn safe_u8_to_u1_overflow(builder: U8ConvertFuzzEngineBuilder) -> anyhow::Result<()> { + case_fuzz(SafeU8ToU1, builder, "safe u8 to u1 overflow") + .strategy(2u8..=u8::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u8/math.rs b/tests/stdlib/u8/math.rs index cefd1b8..f8bcf9a 100644 --- a/tests/stdlib/u8/math.rs +++ b/tests/stdlib/u8/math.rs @@ -17,7 +17,7 @@ impl TestUint for u8 { const MUL_BOUND: u8 = 1 << 4; // 2^(8/2) fn program() -> U8MathTestProgram { - U8MathTestProgram::new(&U8MathTestArguments {}) + U8MathTestProgram::new(U8MathTestArguments {}) } fn witness(op: u8, a: u8, b: u8, expected: Option) -> U8MathTestWitness { @@ -32,3 +32,38 @@ impl TestUint for u8 { // Stamps the 22 `#[simplex::test]` entry points for u8. Logic lives in common::uint. crate::uint_tests!(u8); + +mod math_tests_fuzz { + use super::*; + + use crate::common::uint_fuzz::TestUintFuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + type U8MathFuzzEngineBuilder = + FuzzEngineBuilder; + + impl TestUintFuzz for u8 { + type Arguments = U8MathTestArguments; + + fn arguments() -> Self::Arguments { + U8MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u8, U8MathFuzzEngineBuilder); +} diff --git a/tests/stdlib/u8/mul_div.rs b/tests/stdlib/u8/mul_div.rs index eddc18b..8d65303 100644 --- a/tests/stdlib/u8/mul_div.rs +++ b/tests/stdlib/u8/mul_div.rs @@ -13,7 +13,7 @@ enum FunctionToTest { } fn program() -> U8MulDivTestProgram { - U8MulDivTestProgram::new(&U8MulDivTestArguments {}) + U8MulDivTestProgram::new(U8MulDivTestArguments {}) } /// One dispatch arm of the contract, plus the witness it reads. @@ -102,3 +102,197 @@ fn mul_div_8_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { case(MulDiv).args(a, b, c).expect(0).run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + use crate::common::core::FuzzExecutionCheck; + use simplex::fuzz; + use simplex::fuzz::FuzzEngineBuilder; + use simplex::fuzz::builders::{FinalTransactionBuilder, ProgramTarget}; + use simplex::fuzz::engine::FuzzStrategyBuilder; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::simplicityhl::{Arguments, WitnessValues}; + use simplex::transaction::{FinalTransaction, PartialInput, RequiredSignature, UTXO}; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + type U8MulDivFuzzEngineBuilder = + FuzzEngineBuilder; + + fn arb_non_zero_u8() -> impl Strategy { + any::().prop_filter("u8 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn first_numerator(first_arg: u8) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_numerator(mut self, second_arg: u8) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn denominator(mut self, denominator: u8) -> Self { + let _ = self.denominator.insert(denominator); + self + } + + fn expect(mut self, expected: u8) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness(self, witness: U8MulDivTestWitness, expect_failure: bool) -> WitnessValues { + let case = Case { witness }.args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + self.denominator.expect("no denominator in witness"), + ); + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + builder: U8MulDivFuzzEngineBuilder, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(builder: U8MulDivFuzzEngineBuilder, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + builder, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx() -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U8MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + let strategy = + FuzzStrategyBuilder::::new() + .with_custom_strategy(strategy) + .build(); + let transaction_builder = + FinalTransactionBuilder::new(Self::build_initial_tx(), [PROGRAM_TARGET])?; + + self.builder + .build(strategy, transaction_builder) + .run_with_check(FuzzExecutionCheck::new(self.test_name, self.expect)); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_8_product_is_u8(builder: U8MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u8(), arb_non_zero_u8()).prop_map(|(a, c)| { + let b = u8::MAX / a; + FuzzCase::first_numerator(a) + .second_numerator(b) + .denominator(c) + .expect(a * b / c) + }); + + case_fuzz(builder, "mul_div_8 product is u8") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_intermediate_overflow(builder: U8MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2u8..=u8::MAX).prop_flat_map(|a| { + (a..=u8::MAX).prop_map(move |c| { + let b = u8::MAX; + let expected = (u16::from(a) * u16::from(b) / u16::from(c)) as u8; + FuzzCase::first_numerator(a) + .second_numerator(b) + .denominator(c) + .expect(expected) + }) + }); + + case_fuzz(builder, "mul_div_8 intermediate overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_result_overflow(builder: U8MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (2u8..=u8::MAX).prop_flat_map(|a| { + (1u8..a).prop_map(move |c| { + FuzzCase::first_numerator(a) + .second_numerator(u8::MAX) + .denominator(c) + }) + }); + + case_fuzz(builder, "mul_div_8 result overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_div_by_zero(builder: U8MulDivFuzzEngineBuilder) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u8(), arb_non_zero_u8()).prop_map(|(a, b)| { + FuzzCase::first_numerator(a) + .second_numerator(b) + .denominator(0) + .expect(0) + }); + + case_fuzz(builder, "mul_div_8 div by zero") + .strategy(strategy) + .run() + } +}