From e011ad9b9eb1315270522c01d6200ede735def64 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 3 Nov 2026 16:09:09 +0200 Subject: [PATCH 1/3] add fuzz entry in CI --- .github/workflows/tests.yml | 6 ++++++ .gitignore | 1 + Simplex.toml | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 610a52e..5c875d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,6 +60,12 @@ jobs: run: | cargo build --workspace --all-features --verbose + - name: Fuzz + env: + SIMPLEX_TEST_VERBOSE: ${{ runner.debug == '1' && '-v' || '' }} + run: | + simplex test --fuzz --test-threads 4 "${SIMPLEX_TEST_VERBOSE}" -- --release + - name: Test env: SIMPLEX_TEST_VERBOSE: ${{ runner.debug == '1' && '-v' || '' }} diff --git a/.gitignore b/.gitignore index ddafd0a..ab9046a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ # Simplex output src/artifacts +/fuzz-failures # IDE/editors (optional but common minimal) **/.DS_Store diff --git a/Simplex.toml b/Simplex.toml index 555b27b..78db45e 100644 --- a/Simplex.toml +++ b/Simplex.toml @@ -25,8 +25,8 @@ # url = "" # network = "" -# [test.fuzz] -# cases = 10 +[test.fuzz] +cases = 100 # seed = 12345 # network = "" From f2a68adf8550053ed378c6f9d0a02b5d991f89c6 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 4 Nov 2026 15:40:42 +0200 Subject: [PATCH 2/3] add secp fuzz tests --- tests/stdlib/common/fuzz.rs | 36 +++ tests/stdlib/common/mod.rs | 1 + tests/stdlib/secp256k1/operations.rs | 392 ++++++++++++++++++++++++--- 3 files changed, 394 insertions(+), 35 deletions(-) create mode 100644 tests/stdlib/common/fuzz.rs diff --git a/tests/stdlib/common/fuzz.rs b/tests/stdlib/common/fuzz.rs new file mode 100644 index 0000000..529ee79 --- /dev/null +++ b/tests/stdlib/common/fuzz.rs @@ -0,0 +1,36 @@ +use simplex::simplicityhl::{Arguments, WitnessValues}; + +use simplex::fuzz::proptest::strategy::Strategy; +use simplex::fuzz::{Expect, FuzzProgramTrait, Scenario}; +use simplex::{FuzzMode, TestContext}; + +pub fn run( + context: &TestContext, + strategy: impl Strategy + 'static, + name: &'static str, + expect: Expect, + scenario: Option, +) { + const DEFAULT_SPEND_PATH: usize = 0; + + let mut scenario = scenario.unwrap_or_default(); + scenario + .add::

(name) + .spends(DEFAULT_SPEND_PATH) + .custom_strategy(strategy.prop_map(Into::into)); + + run_scenario(context, scenario, expect); +} + +pub fn run_scenario(context: &TestContext, scenario: Scenario, expect: Expect) { + let engine = context.engine(scenario).spec(move |_| expect); + + // Negative scenarios deliberately generate no accepting witness. + let engine = if expect == Expect::Reject { + engine.allow_unspendable() + } else { + engine + }; + + engine.run(); +} diff --git a/tests/stdlib/common/mod.rs b/tests/stdlib/common/mod.rs index 16f0294..a25bd30 100644 --- a/tests/stdlib/common/mod.rs +++ b/tests/stdlib/common/mod.rs @@ -1,4 +1,5 @@ pub mod core; +pub mod fuzz; pub mod helper; pub mod u256_wrapper; pub mod uint; diff --git a/tests/stdlib/secp256k1/operations.rs b/tests/stdlib/secp256k1/operations.rs index 3bc6798..d8cf173 100644 --- a/tests/stdlib/secp256k1/operations.rs +++ b/tests/stdlib/secp256k1/operations.rs @@ -13,6 +13,18 @@ use simplicityhl_std::artifacts::tests::secp256k1::operations::derived_operation use FunctionToTest::*; +type U256Alias = [u8; 32]; +/// A **G**roup **E**lement, represented as an affine curve point (u256, u256) +type Ge = (U256Alias, U256Alias); +/// A **G**roup **E**lement, represented using **J**acobian coordinates ((X, Y), Z) +type Gej = (Ge, U256Alias); +/// A compressed curve point (y_parity, x) +type Point = (u8, U256Alias); +// **F**ield **e**lement +type Fe = U256Alias; +// A multiplier for curve points, with arithmetic modulo the group order **n** +type Scalar = U256Alias; + enum FunctionToTest { GeToPoint, PointToGej, @@ -43,6 +55,7 @@ fn program() -> Secp256k1OperationsTestProgram { } /// One dispatch arm of the contract, plus the witness it reads. +#[derive(Clone, Debug)] struct Case { witness: Secp256k1OperationsTestWitness, } @@ -51,85 +64,81 @@ fn case(function: FunctionToTest) -> Case { Case { witness: Secp256k1OperationsTestWitness { function_index: function as u8, - first_uint: [0; 32], - second_uint: [0; 32], - first_ge: ([0; 32], [0; 32]), - second_ge: ([0; 32], [0; 32]), - first_gej: (([0; 32], [0; 32]), [0; 32]), - second_gej: (([0; 32], [0; 32]), [0; 32]), - first_point: (0, [0; 32]), - expected_uint: [0; 32], - expected_ge: ([0; 32], [0; 32]), - expected_gej: (([0; 32], [0; 32]), [0; 32]), - expected_point: (0, [0; 32]), + first_uint: U256Alias::default(), + second_uint: U256Alias::default(), + first_ge: Ge::default(), + second_ge: Ge::default(), + first_gej: Gej::default(), + second_gej: Gej::default(), + first_point: Point::default(), + expected_uint: U256Alias::default(), + expected_ge: Ge::default(), + expected_gej: Gej::default(), + expected_point: Point::default(), }, } } impl Case { /// The two field or scalar operands, `first_uint` and `second_uint`. - fn uints(mut self, first_uint: [u8; 32], second_uint: [u8; 32]) -> Self { + fn uints(mut self, first_uint: U256Alias, second_uint: U256Alias) -> Self { self.witness.first_uint = first_uint; self.witness.second_uint = second_uint; self } /// Only `first_ge`. - fn ge(mut self, first_ge: ([u8; 32], [u8; 32])) -> Self { + fn ge(mut self, first_ge: Ge) -> Self { self.witness.first_ge = first_ge; self } /// The two affine points, `first_ge` and `second_ge`. - fn ges(mut self, first_ge: ([u8; 32], [u8; 32]), second_ge: ([u8; 32], [u8; 32])) -> Self { + fn ges(mut self, first_ge: Ge, second_ge: Ge) -> Self { self.witness.first_ge = first_ge; self.witness.second_ge = second_ge; self } /// Only `first_gej`. - fn gej(mut self, first_gej: (([u8; 32], [u8; 32]), [u8; 32])) -> Self { + fn gej(mut self, first_gej: Gej) -> Self { self.witness.first_gej = first_gej; self } /// The two Jacobian points, `first_gej` and `second_gej`. - fn gejs( - mut self, - first_gej: (([u8; 32], [u8; 32]), [u8; 32]), - second_gej: (([u8; 32], [u8; 32]), [u8; 32]), - ) -> Self { + fn gejs(mut self, first_gej: Gej, second_gej: Gej) -> Self { self.witness.first_gej = first_gej; self.witness.second_gej = second_gej; self } /// `first_point`: a compressed point, parity byte and x coordinate. - fn point(mut self, first_point: (u8, [u8; 32])) -> Self { + fn point(mut self, first_point: Point) -> Self { self.witness.first_point = first_point; self } /// `expected_uint`: the field or scalar the arm should produce. - fn expect_uint(mut self, expected_uint: [u8; 32]) -> Self { + fn expect_uint(mut self, expected_uint: U256Alias) -> Self { self.witness.expected_uint = expected_uint; self } /// `expected_ge`: the affine point the arm should produce. - fn expect_ge(mut self, expected_ge: ([u8; 32], [u8; 32])) -> Self { + fn expect_ge(mut self, expected_ge: Ge) -> Self { self.witness.expected_ge = expected_ge; self } /// `expected_gej`: the Jacobian point the arm should produce. - fn expect_gej(mut self, expected_gej: (([u8; 32], [u8; 32]), [u8; 32])) -> Self { + fn expect_gej(mut self, expected_gej: Gej) -> Self { self.witness.expected_gej = expected_gej; self } /// `expected_point`: the compressed point the arm should produce. - fn expect_point(mut self, expected_point: (u8, [u8; 32])) -> Self { + fn expect_point(mut self, expected_point: Point) -> Self { self.witness.expected_point = expected_point; self } @@ -168,7 +177,7 @@ fn mod_n(x: &BigUint) -> BigUint { x % secp_n() } -fn fe_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { +fn fe_sub_ref(a: Fe, b: Fe) -> Fe { let p = secp_p(); let a = BigUint::from_bytes_be(&a) % &p; let b = BigUint::from_bytes_be(&b) % &p; @@ -176,7 +185,7 @@ fn fe_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { to_32_be(&((a + &p - b) % &p)) } -fn scalar_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { +fn scalar_sub_ref(a: Scalar, b: Scalar) -> Scalar { let n = secp_n(); let a = BigUint::from_bytes_be(&a) % &n; let b = BigUint::from_bytes_be(&b) % &n; @@ -184,7 +193,7 @@ fn scalar_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { to_32_be(&((a + &n - b) % &n)) } -fn fe_negate_ref(a: [u8; 32]) -> [u8; 32] { +fn fe_negate_ref(a: Fe) -> Fe { let p = secp_p(); let a = BigUint::from_bytes_be(&a) % &p; @@ -195,7 +204,7 @@ fn fe_negate_ref(a: [u8; 32]) -> [u8; 32] { } } -fn fe_mul_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { +fn fe_mul_ref(a: Fe, b: Fe) -> Fe { let p = secp_p(); let a = BigUint::from_bytes_be(&a); let b = BigUint::from_bytes_be(&b); @@ -211,15 +220,15 @@ fn random_uint_bytes() -> [u8; 32] { b } -fn random_fe_bytes() -> [u8; 32] { +fn random_fe_bytes() -> Fe { to_32_be(&mod_p(&BigUint::from_bytes_be(&random_uint_bytes()))) } -fn random_scalar_bytes() -> [u8; 32] { +fn random_scalar_bytes() -> Scalar { to_32_be(&mod_n(&BigUint::from_bytes_be(&random_uint_bytes()))) } -fn random_ge_bytes() -> ([u8; 32], [u8; 32]) { +fn random_ge_bytes() -> Ge { let secp = Secp256k1::new(); let sk = SecretKey::new(&mut SecpOsRng); let pk = PublicKey::from_secret_key(&secp, &sk); @@ -234,18 +243,18 @@ fn random_ge_bytes() -> ([u8; 32], [u8; 32]) { (x, y) } -fn ge_to_gej(ge: ([u8; 32], [u8; 32])) -> (([u8; 32], [u8; 32]), [u8; 32]) { +fn ge_to_gej(ge: Ge) -> Gej { let mut one = [0u8; 32]; one[31] = 1; (ge, one) } -fn compress(ge: ([u8; 32], [u8; 32])) -> (u8, [u8; 32]) { +fn compress(ge: Ge) -> Point { (ge.1[31] & 1, ge.0) } -fn pk_to_gej(pk: &PublicKey) -> (([u8; 32], [u8; 32]), [u8; 32]) { +fn pk_to_gej(pk: &PublicKey) -> Gej { let ser = pk.serialize_uncompressed(); let mut x = [0u8; 32]; @@ -432,3 +441,316 @@ fn safe_gej_normalize_roundtrip(context: simplex::TestContext) -> anyhow::Result .expect_ge(ge) .run(&context) } + +mod fuzz { + use super::*; + + use std::fmt::Debug; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::Expect; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + use crate::common::fuzz; + + 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, + context: TestContext, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + cases: Just(case(function)).boxed(), + context, + test_name, + expect: Expect::Accept, + } + } + + 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 run(self) -> anyhow::Result<()> { + let strategy = self.cases.prop_map(|case| { + let arguments: Arguments = Secp256k1OperationsTestArguments {}.into(); + let witness: WitnessValues = case.witness.into(); + + (arguments, witness) + }); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + // 0. ge_to_point + #[simplex::fuzz] + fn ge_to_point_matches_parity(context: TestContext) -> anyhow::Result<()> { + // Sample one on-curve point; whatever parity it has, that's what we expect + // ge_to_point to produce. + case_fuzz(GeToPoint, context, "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(context: TestContext) -> anyhow::Result<()> { + case_fuzz(PointToGej, context, "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_is_zero(context: TestContext) -> anyhow::Result<()> { + case_fuzz(FeSub, context, "fe_sub self is zero") + .inject(arb_fe(), |case, a| case.uints(a, a).expect_uint([0u8; 32])) + .run() + } + + #[simplex::fuzz] + fn fe_sub(context: TestContext) -> anyhow::Result<()> { + case_fuzz(FeSub, context, "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(context: TestContext) -> anyhow::Result<()> { + case_fuzz(ScalarSub, context, "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(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejSub, context, "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() + } + + // 4.1 gej_sub_gives_inf and fails + #[simplex::fuzz] + fn gej_sub_gives_inf(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejSub, context, "gej_sub") + .inject(arb_secret_key(), |case, p_secret| { + let secp = Secp256k1::new(); + let p = PublicKey::from_secret_key(&secp, &p_secret); + + // we expect zeroed arrays + case.gejs(pk_to_gej(&p), pk_to_gej(&p)) + .expect_gej(Default::default()) + }) + .expect(Expect::Accept) + .run() + } + + // 5. fe_eq + #[simplex::fuzz] + fn fe_eq_reflexive(context: TestContext) -> anyhow::Result<()> { + case_fuzz(FeEq, context, "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(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GeEq, context, "ge_eq negation") + .inject(arb_ge(), |case, ge| { + case.ges(ge, (ge.0, fe_negate_ref(ge.1))) + }) + .expect(Expect::Reject) + .run() + } + + // 8. gej_point_eq + #[simplex::fuzz] + fn gej_point_eq_rescaled(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejPointEq, context, "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(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejPointEq, context, "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::Reject) + .run() + } + + // 9. safe_gej_normalize + #[simplex::fuzz] + fn safe_gej_normalize_roundtrip(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeGejNormalize, context, "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() + } + + #[simplex::fuzz] + fn gej_point_eq_off_curve_is_false(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + GejPointEq, + context, + "gej_point_eq off-curve compressed point", + ) + .inject(arb_ge(), |case, ge| { + case.gej(ge_to_gej(ge)).point((0, [0; 32])) + }) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn gej_point_eq_rejects_off_curve_gej(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejPointEq, context, "gej_point_eq off-curve Jacobian point") + .inject(arb_ge(), |case, ge| { + // Obtain (x, 0, 1) point. + let gej = ge_to_gej((ge.0, [0; 32])); + + // Obtain (0, x) point with order 2 (secp256k1 has odd prime order). + let compressed_point = (0, ge.0); + + case.gej(gej).point(compressed_point) + }) + .expect(Expect::Reject) + .run() + } +} From 46aee944efb562d458f6b9fbba965e2da528f431 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Thu, 5 Nov 2026 14:34:45 +0200 Subject: [PATCH 3/3] add asserts and op_return fuzz tests --- tests/stdlib/asserts.rs | 539 +++++++++++++++++++++++++++++++++++++- tests/stdlib/op_return.rs | 163 ++++++++++++ 2 files changed, 696 insertions(+), 6 deletions(-) diff --git a/tests/stdlib/asserts.rs b/tests/stdlib/asserts.rs index b3ca487..63eef70 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, @@ -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(0); match function { FunctionToTest::AssertEq1 => { @@ -350,3 +355,525 @@ 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 simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::Expect; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + use crate::common::fuzz; + + 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, + context: TestContext, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + test_name, + expect: Expect::Accept, + } + } + + 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 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(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn assert_eq_1_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq1, context, "assert_eq_1_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_1_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq1, context, "assert_eq_1_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_8_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq8, context, "assert_eq_8_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_8_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq8, context, "assert_eq_8_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_16_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq16, context, "assert_eq_16_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_16_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq16, context, "assert_eq_16_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_32_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq32, context, "assert_eq_32_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_32_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq32, context, "assert_eq_32_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_64_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq64, context, "assert_eq_64_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_64_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq64, context, "assert_eq_64_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_128_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq128, context, "assert_eq_128_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_128_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq128, context, "assert_eq_128_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_256_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq256, context, "assert_eq_256_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_256_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq256, context, "assert_eq_256_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_eq_bool_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEqBool, context, "assert_eq_bool_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_bool_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEqBool, context, "assert_eq_bool_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_1_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone1, context, "assert_none_1_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_1_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone1, context, "assert_none_1_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_8_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone8, context, "assert_none_8_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_8_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone8, context, "assert_none_8_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_16_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone16, context, "assert_none_16_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_16_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone16, context, "assert_none_16_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_32_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone32, context, "assert_none_32_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_32_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone32, context, "assert_none_32_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_64_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone64, context, "assert_none_64_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_64_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone64, context, "assert_none_64_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_128_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone128, context, "assert_none_128_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_128_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone128, context, "assert_none_128_unhappy_path") + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_none_256_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone256, context, "assert_none_256_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_256_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone256, context, "assert_none_256_unhappy_path") + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/op_return.rs b/tests/stdlib/op_return.rs index 2b83226..2baed6e 100644 --- a/tests/stdlib/op_return.rs +++ b/tests/stdlib/op_return.rs @@ -129,3 +129,166 @@ 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 simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::fuzz::{Expect, Scenario}; + use simplex::transaction::{FinalTransaction, PartialInput, PartialOutput, UTXO}; + use simplex::{FuzzMode, TestContext}; + + use crate::common::fuzz; + + const EXPECTED_IS_OP_RETURN: bool = true; + + fn arb_non_zero_u32() -> impl Strategy { + any::().prop_filter("output index should not be zero", |index| *index != 0) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + indices: BoxedStrategy, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + indices: Just(0_u32).boxed(), + test_name, + expect: Expect::Accept, + } + } + + 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()), None); + + if let Some(data) = data { + tx.add_output(PartialOutput::new_metadata(data)); + } else { + tx.add_output(PartialOutput::default()); + } + + 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(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + Some(Scenario::new(Self::build_initial_tx(data))), + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn is_output_op_return_true(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsOpReturn, context, "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(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsOpReturn, context, "is_output_op_return outside outputs") + .strategy(arb_non_zero_u32()) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn is_output_op_return_false(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsOpReturn, context, "is_output_op_return ordinary output").run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_pass(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + context, + "assert_output_is_op_return with OP_RETURN", + ) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_empty_index_fail( + context: TestContext, + ) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + context, + "assert_output_is_op_return outside outputs", + ) + .strategy(arb_non_zero_u32()) + .op_return(DEFAULT_DATA) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_fail(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + context, + "assert_output_is_op_return ordinary output", + ) + .expect(Expect::Reject) + .run() + } +}