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 = "" 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/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..66ff731 100644 --- a/tests/stdlib/common/mod.rs +++ b/tests/stdlib/common/mod.rs @@ -1,5 +1,7 @@ pub mod core; +pub mod fuzz; pub mod helper; pub mod u256_wrapper; pub mod uint; +pub mod uint_fuzz; pub mod utxo_helper; diff --git a/tests/stdlib/common/uint_fuzz.rs b/tests/stdlib/common/uint_fuzz.rs new file mode 100644 index 0000000..f78296a --- /dev/null +++ b/tests/stdlib/common/uint_fuzz.rs @@ -0,0 +1,360 @@ +#![allow(dead_code)] + +use std::fmt::Debug; + +use simplex::simplicityhl::{Arguments, WitnessValues}; + +use simplex::fuzz::proptest::prelude::Just; +use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; +use simplex::fuzz::{Expect, FuzzProgramTrait}; +use simplex::{FuzzMode, TestContext}; + +use super::uint::{CommonOp, TestUint}; + +/// 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: 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`. + 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 uint_strategy( + operation: CommonOp, + inputs: BoxedStrategy<(T, T, Option)>, +) -> BoxedStrategy<(Arguments, WitnessValues)> { + let function_index = operation as u8; + + 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) + }) + .boxed() +} + +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::Add, + (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::Add, + (Just(T::MAX), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, Some(T::ZERO))) + .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::Sub, + 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::Sub, + 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, Some(T::ZERO))) + }) + .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::Mul, + (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::Mul, + (Just(T::MAX), T::arb_fitting(T::ONE + T::ONE, T::MAX)) + .prop_map(|(a, b)| (a, b, Some(T::ZERO))) + .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::Div, + (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::Div, + T::arb_any() + .prop_map(|a| (a, T::ZERO, Some(T::ZERO))) + .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( + context: TestContext, + strategy: BoxedStrategy<(Arguments, WitnessValues)>, + expect: Expect, +) -> anyhow::Result<()> +where + T::Program: FuzzProgramTrait, + T::Witness: Debug + Clone + 'static, +{ + super::fuzz::run::(&context, strategy, "uint operation", expect, None); + + Ok(()) +} + +/// Stamps the common `#[simplex::fuzz]` fuzz tests 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); +/// ``` +/// +/// The unsigned type must implement [`TestUintFuzz`], including its bounded +/// number-generation strategy. +/// +/// # How to expand it +/// +/// **Add a new scenario** (e.g. `checked_add_max_plus_one`): +/// 1. Choose a descriptive, unique test name. +/// 2. Add a generic strategy function to this module with the signature +/// `pub fn checked_add_max_plus_one_strategy() +/// -> BoxedStrategy<(Arguments, WitnessValues)>`. +/// 3. Choose the expected execution outcome from `simplex::fuzz::Expect`. +/// 4. Add an entry to the `@stub` invocation below in the form +/// ` ;`. +/// This generates the test for every type passed to `uint_fuzz_tests!`. +/// +/// **Remove a scenario:** delete its generic function and its name from the list. +/// +/// The name in the `@stub` list must match the generic function name exactly; +/// the stub calls `crate::common::uint_fuzz::::<$t>`. +/// +/// Each strategy name must match a generic function in `common::uint_fuzz`. +/// The public rule delegates to the internal `@stub` rule, which generates one +/// `#[simplex::fuzz]` test per entry and passes its strategy and expected outcome +/// to `run_uint_fuzz`. +#[macro_export] +macro_rules! uint_fuzz_tests { + ($t:ty) => { + + $crate::uint_fuzz_tests!(@stub $t; + checked_add_fitting checked_add_fitting_strategy simplex::fuzz::Expect::Accept; + checked_add_overflow checked_add_overflow_strategy simplex::fuzz::Expect::Accept; + safe_add_fitting safe_add_fitting_strategy simplex::fuzz::Expect::Accept; + safe_add_overflow safe_add_overflow_strategy simplex::fuzz::Expect::Reject; + checked_sub_fitting checked_sub_fitting_strategy simplex::fuzz::Expect::Accept; + checked_sub_overflow checked_sub_overflow_strategy simplex::fuzz::Expect::Accept; + safe_sub_fitting safe_sub_fitting_strategy simplex::fuzz::Expect::Accept; + safe_sub_overflow safe_sub_overflow_strategy simplex::fuzz::Expect::Reject; + checked_mul_fitting checked_mul_fitting_strategy simplex::fuzz::Expect::Accept; + checked_mul_overflow checked_mul_overflow_strategy simplex::fuzz::Expect::Accept; + safe_mul_fitting safe_mul_fitting_strategy simplex::fuzz::Expect::Accept; + safe_mul_overflow safe_mul_overflow_strategy simplex::fuzz::Expect::Reject; + checked_div_fitting checked_div_fitting_strategy simplex::fuzz::Expect::Accept; + checked_div_by_zero checked_div_by_zero_strategy simplex::fuzz::Expect::Accept; + safe_div_fitting safe_div_fitting_strategy simplex::fuzz::Expect::Accept; + safe_div_by_zero safe_div_by_zero_strategy simplex::fuzz::Expect::Reject; + gt_greater gt_greater_strategy simplex::fuzz::Expect::Accept; + gt_equal gt_equal_strategy simplex::fuzz::Expect::Accept; + gt_less gt_less_strategy simplex::fuzz::Expect::Accept; + ge_greater ge_greater_strategy simplex::fuzz::Expect::Accept; + ge_equal ge_equal_strategy simplex::fuzz::Expect::Accept; + ge_less ge_less_strategy simplex::fuzz::Expect::Accept; + ); + }; + (@stub $t:ty; $( + $name:ident $strategy:ident $expected_execution:expr; + )+) => { + $( + #[simplex::fuzz] + fn $name(context: simplex::TestContext) -> anyhow::Result<()> { + $crate::common::uint_fuzz::run_uint_fuzz::<$t>( + context, + $crate::common::uint_fuzz::$strategy::<$t>(), + $expected_execution, + ) + } + )+ + }; +} 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() + } +} 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() + } +} diff --git a/tests/stdlib/u1/convert.rs b/tests/stdlib/u1/convert.rs index 69e84e4..f6feef9 100644 --- a/tests/stdlib/u1/convert.rs +++ b/tests/stdlib/u1/convert.rs @@ -132,3 +132,132 @@ 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 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}; + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_bool_u8() -> impl Strategy { + arb_bool().prop_map(u8::from) + } + + /// Alias for `first_arg` in [`Case`] entry. + type ConvertInputs = u8; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + 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(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + Expect::Accept, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u1_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU8, context, "u1 to u8") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU16, context, "u1 to u16") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU32, context, "u1 to u32") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU64, context, "u1 to u64") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU128, context, "u1 to u128") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU256, context, "u1 to u256") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn split_u1_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToBool, context, "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..1307728 100644 --- a/tests/stdlib/u128/bit.rs +++ b/tests/stdlib/u128/bit.rs @@ -166,3 +166,339 @@ 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 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_u8() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + const OPERANDS_EQUAL: bool = true; + const OPERANDS_DIFFER: bool = false; + const UNUSED_EXPECTED_BOOL: bool = false; + + /// Alias for `first_arg`, `second_arg`, `third_arg` in [`Case`] entry. + type ConvertInputs = u128; + + /// 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: ConvertInputs) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: ConvertInputs) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: ConvertInputs) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn expected_bool(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, + context: TestContext, + inputs: Option>, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + 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) + }); + + fuzz::run::( + &self.context, + strategy, + self.name, + Expect::Accept, + None, + ); + Ok(()) + } + } + + // Both operands are full-width u128; compares against the independent host bitwise result. + // + // Represents tests: [and_128] + #[simplex::fuzz] + fn and(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expect(a & b) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(And128, context, "u128 bitwise and") + .strategy(strategy) + .run() + } + + // Both operands are full-width u128; compares against the independent host bitwise result. + // + // Represents tests: [or_128] + #[simplex::fuzz] + fn or(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expect(a | b) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(Or128, context, "u128 bitwise or") + .strategy(strategy) + .run() + } + + // Both operands are full-width u128; compares the equality flag, with dedicated equal/different + // cases below. + // + // Represents tests: [eq_128_true, eq_128_false] + #[simplex::fuzz] + fn eq(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expected_bool(a == b) + .expect(0) + }) + }; + + case_fuzz(Eq128, context, "u128 equality") + .strategy(strategy) + .run() + } + + // a=b across the full width; expected equality=true. + // + // Represents tests: [eq_128_true] + #[simplex::fuzz] + fn eq_same(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|a| { + FuzzCase::first_arg(a) + .second_arg(a) + .expect(0) + .expected_bool(OPERANDS_EQUAL) + }) + }; + + case_fuzz(Eq128, context, "u128 equality with equal operands") + .strategy(strategy) + .run() + } + + // b=a.wrapping_add(1); operands are guaranteed different, including wraparound at MAX. + // + // Represents tests: [eq_128_false] + #[simplex::fuzz] + fn eq_different(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|a| { + FuzzCase::first_arg(a) + .second_arg(a.wrapping_add(1)) + .expect(0) + .expected_bool(OPERANDS_DIFFER) + }) + }; + + case_fuzz(Eq128, context, "u128 inequality") + .strategy(strategy) + .run() + } + + // 0<=shift<=255; shifts below 128 use the host result and larger shifts expect zero. + // + // Represents tests: [left_shift_128] + #[simplex::fuzz] + fn left_shift(context: TestContext) -> 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) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(LeftShift128, context, "u128 left shift") + .strategy(strategy) + .run() + } + + // 0<=shift<=255; shifts below 128 use the host result and larger shifts expect zero. + // + // Represents tests: [right_shift_128] + #[simplex::fuzz] + fn right_shift(context: TestContext) -> 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) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(RightShift128, context, "u128 right shift") + .strategy(strategy) + .run() + } + + // shift=0 and value is full-width u128; expected result=value. + // + // Represents tests: [left_shift_128_by_zero] + #[simplex::fuzz] + fn left_shift_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|value| { + FuzzCase::first_arg(0) + .second_arg(value) + .expect(value) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(LeftShift128, context, "u128 left shift by zero") + .strategy(strategy) + .run() + } + + // shift=0 and value is full-width u128; expected result=value. + // + // Represents tests: [right_shift_128_by_zero] + #[simplex::fuzz] + fn right_shift_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|value| { + FuzzCase::first_arg(0) + .second_arg(value) + .expect(value) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(RightShift128, context, "u128 right shift by zero") + .strategy(strategy) + .run() + } + + // 128<=shift<=255 and value is full-width u128; expected result=0. + // + // Represents tests: [left_shift_128_out_of_range] + #[simplex::fuzz] + fn left_shift_out_of_range(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (128u8..=u8::MAX, arb_u128()).prop_map(|(shift, value)| { + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(0) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(LeftShift128, context, "u128 left shift out of range") + .strategy(strategy) + .run() + } + + // 128<=shift<=255 and value is full-width u128; expected result=0. + // + // Represents tests: [right_shift_128_out_of_range] + #[simplex::fuzz] + fn right_shift_out_of_range(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (128u8..=u8::MAX, arb_u128()).prop_map(|(shift, value)| { + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(0) + .expected_bool(UNUSED_EXPECTED_BOOL) + }) + }; + + case_fuzz(RightShift128, context, "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..2640da3 100644 --- a/tests/stdlib/u128/comparison.rs +++ b/tests/stdlib/u128/comparison.rs @@ -127,3 +127,270 @@ fn le_128_bigger(context: simplex::TestContext) -> anyhow::Result<()> { case(Le128).args(a, b).run(&context) } + +mod comparison_tests_fuzz { + use super::*; + + 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}; + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + any::().prop_filter("u128 should not be zero", |index| *index != 0) + } + + const EXPECT_ZERO: bool = true; + const EXPECT_NONZERO: bool = false; + const EXPECT_LESS: bool = true; + const EXPECT_NOT_LESS: bool = false; + const EXPECT_LESS_OR_EQUAL: bool = true; + const EXPECT_NOT_LESS_OR_EQUAL: bool = false; + + /// Alias for `first_arg`, `second_arg` in [`Case`] entry. + type ConvertInputs = u128; + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: ConvertInputs) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: ConvertInputs) -> 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, + context: TestContext, + inputs: Option>, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + 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) + }); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.name, + Expect::Accept, + None, + ); + Ok(()) + } + } + + // a is full-width u128; checks a==0, with dedicated zero and nonzero cases below. + // + // Represents tests: [is_zero_128_true, is_zero_128_false] + #[simplex::fuzz] + fn is_zero(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsZero128, context, "u128 is zero") + .strategy(arb_u128().prop_map(|a| FuzzCase::first_arg(a).second_arg(0).expect(a == 0))) + .run() + } + + // Both operands are full-width u128; compares the host boolean across all orderings. + // + // Represents tests: [lt_128_less, lt_128_eq, lt_128_bigger] + #[simplex::fuzz] + fn lt(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "u128 less than") + .strategy( + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a < b)), + ) + .run() + } + + // Both operands are full-width u128; compares the host boolean across all orderings. + // + // Represents tests: [le_128_less, le_128_eq, le_128_bigger] + #[simplex::fuzz] + fn le(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Le128, context, "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() + } + + // a=b across the full width; expected le=true. + // + // Represents tests: [le_128_eq] + #[simplex::fuzz] + fn equal_operands(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + Le128, + context, + "u128 less than or equal with equal operands", + ) + .strategy(arb_u128().prop_map(|a| { + FuzzCase::first_arg(a) + .second_arg(a) + .expect(EXPECT_LESS_OR_EQUAL) + })) + .run() + } + + // 1<=a<=u128::MAX; expected is_zero=false. + // + // Represents tests: [is_zero_128_false] + #[simplex::fuzz] + fn non_zero_operand(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsZero128, context, "u128 nonzero operand") + .strategy(arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_arg(value) + .second_arg(0) + .expect(EXPECT_NONZERO) + })) + .run() + } + + // 0<=a) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "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(EXPECT_LESS)) + })) + .run() + } + + // a=b across the full width; expected lt=false. + // + // Represents tests: [lt_128_eq] + #[simplex::fuzz] + fn lt_equal(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "u128 less than equal operands") + .strategy( + arb_u128() + .prop_map(|a| FuzzCase::first_arg(a).second_arg(a).expect(EXPECT_NOT_LESS)), + ) + .run() + } + + // 1<=a<=MAX and 0<=b) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "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(EXPECT_NOT_LESS)) + })) + .run() + } + + // 0<=a) -> anyhow::Result<()> { + case_fuzz(Le128, context, "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(EXPECT_LESS_OR_EQUAL) + }) + })) + .run() + } + + // 1<=a<=MAX and 0<=b) -> anyhow::Result<()> { + case_fuzz(Le128, context, "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(EXPECT_NOT_LESS_OR_EQUAL) + }) + })) + .run() + } + + // a=0 is fixed with Just; expected is_zero=true. + // + // Represents tests: [is_zero_128_true] + #[simplex::fuzz] + fn is_zero_true(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsZero128, context, "u128 zero operand") + .strategy( + simplex::fuzz::proptest::prelude::Just(0u128) + .prop_map(|a| FuzzCase::first_arg(a).second_arg(0).expect(EXPECT_ZERO)), + ) + .run() + } +} diff --git a/tests/stdlib/u128/convert.rs b/tests/stdlib/u128/convert.rs index b1cf059..25eda60 100644 --- a/tests/stdlib/u128/convert.rs +++ b/tests/stdlib/u128/convert.rs @@ -215,3 +215,278 @@ 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 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}; + + 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) + } + + /// Alias for `first_arg` in [`Case`] entry. + type ConvertInputs = u128; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + expect: Expect, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + expect: Expect::Accept, + 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) + }); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.name, + self.expect, + None, + ); + Ok(()) + } + } + + // Custom full-width u128 input; checks exact zero extension to the larger type. + // + // Represents tests: [u128_to_u256] + #[simplex::fuzz] + fn u128_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U128ToU256, context, "u128 to u256") + .strategy(arb_u128()) + .run() + } + + // Custom full-width u128 input; checks every split component in the original order. + // + // Represents tests: [split_u128_into_u8] + #[simplex::fuzz] + fn split_u128_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU8, context, "split u128 into u8") + .strategy(arb_u128()) + .run() + } + + // Custom full-width u128 input; checks every split component in the original order. + // + // Represents tests: [split_u128_into_u16] + #[simplex::fuzz] + fn split_u128_into_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU16, context, "split u128 into u16") + .strategy(arb_u128()) + .run() + } + + // Custom full-width u128 input; checks every split component in the original order. + // + // Represents tests: [split_u128_into_u32] + #[simplex::fuzz] + fn split_u128_into_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU32, context, "split u128 into u32") + .strategy(arb_u128()) + .run() + } + + // Custom full-width u128 input; checks every split component in the original order. + // + // Represents tests: [split_u128_into_u64] + #[simplex::fuzz] + fn split_u128_into_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU64, context, "split u128 into u64") + .strategy(arb_u128()) + .run() + } + + // 0<=a<=1; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u128_to_u1] + #[simplex::fuzz] + fn safe_u128_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU1, context, "safe u128 to u1") + .strategy(arb_bool_u128()) + .run() + } + + // 1) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU1, context, "safe u128 to u1 overflow") + .strategy(2u128..=u128::MAX) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u8::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u128_to_u8] + #[simplex::fuzz] + fn safe_u128_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU8, context, "safe u128 to u8") + .strategy(arb_u8_u128()) + .run() + } + + // u8::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU8, context, "safe u128 to u8 overflow") + .strategy((u128::from(u8::MAX) + 1)..=u128::MAX) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u16::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u128_to_u16] + #[simplex::fuzz] + fn safe_u128_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU16, context, "safe u128 to u16") + .strategy(arb_u16_u128()) + .run() + } + + // u16::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU16, context, "safe u128 to u16 overflow") + .strategy((u128::from(u16::MAX) + 1)..=u128::MAX) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u32::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u128_to_u32] + #[simplex::fuzz] + fn safe_u128_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU32, context, "safe u128 to u32") + .strategy(arb_u32_u128()) + .run() + } + + // u32::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU32, context, "safe u128 to u32 overflow") + .strategy((u128::from(u32::MAX) + 1)..=u128::MAX) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u64::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u128_to_u64] + #[simplex::fuzz] + fn safe_u128_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU64, context, "safe u128 to u64") + .strategy(arb_u64_u128()) + .run() + } + + // u64::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU64, context, "safe u128 to u64 overflow") + .strategy((u128::from(u64::MAX) + 1)..=u128::MAX) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u128/math/api.rs b/tests/stdlib/u128/math/api.rs index c624b3f..5d8f9e3 100644 --- a/tests/stdlib/u128/math/api.rs +++ b/tests/stdlib/u128/math/api.rs @@ -32,3 +32,36 @@ 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 simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + use crate::common::uint_fuzz::TestUintFuzz; + + 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() + } + } + + // Stamps the 22 `#[simplex::fuzz]` entry points for u128. Logic lives in common::uint_fuzz. + crate::uint_fuzz_tests!(u128); +} diff --git a/tests/stdlib/u128/math/primitives.rs b/tests/stdlib/u128/math/primitives.rs index 3d39661..438c04f 100644 --- a/tests/stdlib/u128/math/primitives.rs +++ b/tests/stdlib/u128/math/primitives.rs @@ -636,3 +636,965 @@ fn div_mod_128_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod primitives_tests_fuzz { + use super::*; + + 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; + use crate::common::uint_fuzz::{ + run_uint_fuzz, safe_div_by_zero_strategy, safe_div_fitting_strategy, + }; + + 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) + } + + const EXPECT_CARRY: bool = true; + const EXPECT_BORROW: bool = true; + const NORMALIZER_U128_DIVISOR: bool = true; + const MIN_64_BIT_VALUE: u128 = 1u128 << 63; + + /// Alias for `first_arg`, `second_arg`, `expected` and `second_expected` in [`Case`] entry. + type ConvertInputs = u128; + + #[derive(Debug, Default, Clone)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + expected_bool: bool, + second_expected: ConvertInputs, + } + + impl FuzzCase { + fn first_argument(first_arg: ConvertInputs) -> Self { + Self { + first_arg: Some(first_arg), + ..Self::default() + } + } + + fn second_argument(mut self, second_arg: ConvertInputs) -> Self { + self.second_arg = Some(second_arg); + self + } + + fn expect(mut self, expected: ConvertInputs) -> Self { + self.expected = Some(expected); + self + } + + fn expect_bool(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn second_expected(mut self, second_expected: ConvertInputs) -> Self { + self.second_expected = second_expected; + self + } + + fn into_witness( + mut self, + witness: U128BasicMathTestWitness, + expect_failure: bool, + ) -> WitnessValues { + if expect_failure { + self.expected = Some(0); + self.second_expected = 0; + } + + 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: ConvertInputs, b: ConvertInputs) -> FuzzCase { + FuzzCase::first_argument(a) + .second_argument(b) + .expect(a / b) + .second_expected(a % b) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + expect: Expect, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + expect: Expect::Accept, + 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 expect_failure = self.expect == Expect::Reject; + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U128BasicMathTestArguments {}.into(); + let witness = fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + fuzz::run::( + &self.context, + strategy, + self.name, + self.expect, + None, + ); + Ok(()) + } + } + + // Full-width a and b; checks the wrapped sum and output carry for either outcome. + // + // Represents tests: [add_128_carry_not_overflow, add_128_carry_overflow] + #[simplex::fuzz] + fn add(context: TestContext) -> 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) + .expect_bool(carry) + }) + }; + + case_fuzz(Add128Carry, context, "add") + .strategy(strategy) + .run() + } + + // a is u128 and b is u64; checks the wrapped sum and output carry for either outcome. + // + // Represents tests: [add_128_64_carry_not_overflow, add_128_64_carry_overflow] + #[simplex::fuzz] + fn add_128_64(context: TestContext) -> 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) + .expect_bool(carry) + }) + }; + + case_fuzz(Add128_64Carry, context, "add_128_64") + .strategy(strategy) + .run() + } + + // Full-width a and b; carry_in=0; checks a+b and its output carry, including overflow. + // + // Represents tests: [full_add_128_not_overflow_carry_low_false, full_add_128_overflow_carry_low_false] + #[simplex::fuzz] + fn full_add_no_carry(context: TestContext) -> 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) + .second_expected(0) + .expect(sum) + .expect_bool(carry) + }) + }; + + case_fuzz(FullAdd128, context, "full_add_no_carry") + .strategy(strategy) + .run() + } + + // Full-width a and b; carry_in=1; checks wrapped a+b+1 and both carry steps. + // + // Represents tests: [full_add_128_not_overflow_carry_low_true, full_add_128_overflow_carry_low_true] + #[simplex::fuzz] + fn full_add_with_carry(context: TestContext) -> 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) + .second_expected(1) + .expect(sum) + .expect_bool(carry1 || carry2) + }) + }; + + case_fuzz(FullAdd128, context, "full_add_with_carry") + .strategy(strategy) + .run() + } + + // Full-width a and b; checks wrapped a-b and its output borrow for either outcome. + // + // Represents tests: [sub_128_borrow_not_overflow, sub_128_borrow_overflow] + #[simplex::fuzz] + fn sub(context: TestContext) -> 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) + .expect_bool(borrow) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub") + .strategy(strategy) + .run() + } + + // a=b across the full width; expected result=0 and output borrow=false. + // + // Represents tests: [sub_128_borrow_a_eq_b] + #[simplex::fuzz] + fn sub_equal(context: TestContext) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(a).expect(0)) }; + + case_fuzz(Sub128Borrow, context, "sub_equal") + .strategy(strategy) + .run() + } + + // a and b share the low u64 word; high words vary independently. + // + // Represents tests: [sub_128_borrow_a_low_eq_b_low] + #[simplex::fuzz] + fn sub_equal_low_words(context: TestContext) -> 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) + .expect_bool(borrow) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub_equal_low_words") + .strategy(strategy) + .run() + } + + // a_low=u64::MAX and b_low=0; high words vary independently. + // + // Represents tests: [sub_128_borrow_diff_is_u64_max] + #[simplex::fuzz] + fn sub_max_low_word(context: TestContext) -> 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) + .expect_bool(borrow) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub_max_low_word") + .strategy(strategy) + .run() + } + + // Full-width a and b; borrow_in=0; checks a-b and its output borrow, including underflow. + // + // Represents tests: [full_sub_128_borrow_low_false, full_sub_128_overflow_borrow_low_false] + #[simplex::fuzz] + fn full_sub_no_borrow(context: TestContext) -> 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) + .second_expected(0) + .expect(diff) + .expect_bool(borrow) + }) + }; + + case_fuzz(FullSub128, context, "full_sub_no_borrow") + .strategy(strategy) + .run() + } + + // Full-width a and b; borrow_in=1; checks wrapped a-b-1 and both borrow steps. + // + // Represents tests: [full_sub_128_borrow_low_true, full_sub_128_overflow_borrow_low_true] + #[simplex::fuzz] + fn full_sub_with_borrow(context: TestContext) -> 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) + .second_expected(1) + .expect(diff) + .expect_bool(borrow1 || borrow2) + }) + }; + + case_fuzz(FullSub128, context, "full_sub_with_borrow") + .strategy(strategy) + .run() + } + + // Full-width u128 operands; checks both product halves, including the high half. + // + // Represents tests: [mul_128_wide] + #[simplex::fuzz] + fn mul(context: TestContext) -> 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) + .second_expected(low) + }) + }; + + case_fuzz(Mul128Wide, context, "mul") + .strategy(strategy) + .run() + } + + // a is u128 and b is u64; checks both halves of the widened product. + // + // Represents tests: [mul_128_64_wide] + #[simplex::fuzz] + fn mul_128_64(context: TestContext) -> 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) + .second_expected(low) + }) + }; + + case_fuzz(Mul128_64Wide, context, "mul_128_64") + .strategy(strategy) + .run() + } + + // 1<=b<2^63; selects the low word; expected normalizer=ceil(2^63/b). + // + // Represents tests: [calculate_normalizer_base_64_b_is_u64] + #[simplex::fuzz] + fn normalizer_u64_small(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (1u128..MIN_64_BIT_VALUE).prop_map(|b| { + FuzzCase::first_argument(0) + .second_argument(b) + .expect(MIN_64_BIT_VALUE.div_ceil(b)) + }) + }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_u64_small") + .strategy(strategy) + .run() + } + + // 2^63<=b<=u64::MAX; selects the low word; expected normalizer=1. + // + // Represents tests: [calculate_normalizer_base_64_b_is_big_enough_not_normalize] + #[simplex::fuzz] + fn normalizer_u64_large(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (MIN_64_BIT_VALUE..=u64::MAX as u128) + .prop_map(|b| FuzzCase::first_argument(0).second_argument(b).expect(1)) + }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_u64_large") + .strategy(strategy) + .run() + } + + // u64::MAX) -> anyhow::Result<()> { + let strategy = { + ((u64::MAX as u128 + 1)..=u128::MAX).prop_map(|b| { + FuzzCase::first_argument(0) + .second_argument(b) + .expect(MIN_64_BIT_VALUE.div_ceil(b >> 64)) + .expect_bool(NORMALIZER_U128_DIVISOR) + }) + }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_u128") + .strategy(strategy) + .run() + } + + // b>u64::MAX with the low-word selector; expects an assertion failure. + // + // Represents tests: [calculate_normalizer_base_64_b_is_u64_fail] + #[simplex::fuzz] + fn normalizer_u64_wrong_width(context: TestContext) -> 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, + context, + "normalizer_u64_wrong_width", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 1<=b<=u64::MAX with the high-word selector; expects an assertion failure. + // + // Represents tests: [calculate_normalizer_base_64_b_is_u128_fail] + #[simplex::fuzz] + fn normalizer_u128_wrong_width(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64_u128().prop_map(|value| { + FuzzCase::first_argument(0) + .second_argument(value) + .expect(0) + .expect_bool(NORMALIZER_U128_DIVISOR) + }) + }; + + case_fuzz( + CalculateNormalizerBase64, + context, + "normalizer_u128_wrong_width", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // b=0 is fixed with Just; expects an assertion failure. + // + // Represents tests: [calculate_normalizer_base_64_b_is_zero_fail] + #[simplex::fuzz] + fn normalizer_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { Just(FuzzCase::first_argument(0).second_argument(0).expect(0)) }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 2^63<=b_high<=u64::MAX and a_high) -> 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()) + .second_expected(b) + }, + ) + }) + }; + + case_fuzz(EstimateQuotientDigitBase64, context, "quotient_digit") + .strategy(strategy) + .run() + } + + // 2^63<=b_highb_high; expects the original assertion-failure outcome. + // + // Represents tests: [estimate_quotient_digit_base_64_fail] + #[simplex::fuzz] + fn quotient_digit_too_large(context: TestContext) -> 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()) + .second_expected(b) + }, + ) + }) + }; + + case_fuzz( + EstimateQuotientDigitBase64, + context, + "quotient_digit_too_large", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a is full-width u128; 1<=b<=u64::MAX; checks quotient and remainder. + // + // Represents tests: [div_mod_128_64] + #[simplex::fuzz] + fn div_mod_128_64(context: TestContext) -> 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, context, "div_mod_128_64") + .strategy(strategy) + .run() + } + + // a is full-width u128 and b=0; expects an assertion failure. + // + // Represents tests: [div_mod_128_64_div_by_zero] + #[simplex::fuzz] + fn div_mod_128_64_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(0).expect(0)) }; + + case_fuzz(DivMod128_64, context, "div_mod_128_64_zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a is full-width u128 and b is nonzero u128; checks quotient and remainder across branches. + // + // Represents tests: [div_mod_128_a_less_than_b, div_mod_128_b_is_u64, div_mod_128_b_is_u128, div_mod_128_a_equal_b] + #[simplex::fuzz] + fn div_mod_128(context: TestContext) -> anyhow::Result<()> { + let strategy = { (arb_u128(), arb_non_zero_u128()).prop_map(|(a, b)| div_mod_case(a, b)) }; + + case_fuzz(DivMod128, context, "div_mod_128") + .strategy(strategy) + .run() + } + + // 0<=a) -> 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, context, "div_mod_a_less_than_b") + .strategy(strategy) + .run() + } + + // a=b>0; expects q=1 and r=0. + // + // Represents tests: [div_mod_128_a_equal_b] + #[simplex::fuzz] + fn div_mod_equal(context: TestContext) -> anyhow::Result<()> { + let strategy = { arb_non_zero_u128().prop_map(|value| div_mod_case(value, value)) }; + + case_fuzz(DivMod128, context, "div_mod_equal") + .strategy(strategy) + .run() + } + + // a_high=b_high>0; low words are independent, covering q=0 or q=1. + // + // Represents tests: [div_mod_128_q_is_1, div_mod_128_eq_high_words_a_less_than_b] + #[simplex::fuzz] + fn div_mod_same_high_words(context: TestContext) -> 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, context, "div_mod_same_high_words") + .strategy(strategy) + .run() + } + + // Additional API division property; a is u128 and b>0; checks floor(a/b). + // + // Represents tests: [api::div_fitting] + #[simplex::fuzz] + fn div(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::(context, safe_div_fitting_strategy::(), Expect::Accept) + } + + // Uses API div_128, which calls div_mod_128 indirectly; a is u128, b=0, and assertion failure + // is expected. + // + // Represents tests: [div_mod_128_div_by_zero] + #[simplex::fuzz] + fn div_by_zero(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::(context, safe_div_by_zero_strategy::(), Expect::Reject) + } + + // Explicit edge families complement the unrestricted arithmetic properties. + // a=u128::MAX and b>0; wrapped result=b-1 and output carry=true. + // + // Represents tests: [add_128_carry_overflow] + #[simplex::fuzz] + fn add_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value - 1) + .expect_bool(EXPECT_CARRY) + }) + }; + + case_fuzz(Add128Carry, context, "add_overflow") + .strategy(strategy) + .run() + } + + // a=u128::MAX and 1<=b<=u64::MAX; result=b-1 and output carry=true. + // + // Represents tests: [add_128_64_carry_overflow] + #[simplex::fuzz] + fn add_128_64_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value - 1) + .expect_bool(EXPECT_CARRY) + }) + }; + + case_fuzz(Add128_64Carry, context, "add_128_64_overflow") + .strategy(strategy) + .run() + } + + // a=u128::MAX, b>0, carry_in=1; wrapped result=b and output carry=true. + // + // Represents tests: [full_add_128_overflow_carry_low_true] + #[simplex::fuzz] + fn full_add_overflow_with_carry(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .second_expected(1) + .expect(value) + .expect_bool(EXPECT_CARRY) + }) + }; + + case_fuzz(FullAdd128, context, "full_add_overflow_with_carry") + .strategy(strategy) + .run() + } + + // 0<=a) -> anyhow::Result<()> { + let strategy = { + (0u128..u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(u128::MAX) + .expect(a + 1) + .expect_bool(EXPECT_BORROW) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub_overflow") + .strategy(strategy) + .run() + } + + // 0<=a) -> anyhow::Result<()> { + let strategy = { + (0..u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(u128::MAX) + .second_expected(1) + .expect(a) + .expect_bool(EXPECT_BORROW) + }) + }; + + case_fuzz(FullSub128, context, "full_sub_overflow_with_borrow") + .strategy(strategy) + .run() + } + + // a=u128::MAX, b=0, carry_in=1; only the input carry causes result=0 and carry_out=true. + // Additional input-carry boundary with no direct simplex::test counterpart. + // + // Represents none of the tests. + #[simplex::fuzz] + fn full_add_carry_in_causes_overflow(context: TestContext) -> anyhow::Result<()> { + // MAX + 0 fits; only the input carry causes overflow. + let strategy = Just(u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(0) + .second_expected(1) + .expect(0) + .expect_bool(EXPECT_CARRY) + }); + + case_fuzz(FullAdd128, context, "full_add_carry_in_causes_overflow") + .strategy(strategy) + .run() + } + + // a=b across the full width, borrow_in=1; result=u128::MAX and borrow_out=true. + // Additional input-borrow boundary with no direct simplex::test counterpart. + // + // Represents none of the tests. + #[simplex::fuzz] + fn full_sub_borrow_in_causes_underflow(context: TestContext) -> anyhow::Result<()> { + // a - a is zero; only the input borrow causes underflow. + let strategy = arb_u128().prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(a) + .second_expected(1) + .expect(u128::MAX) + .expect_bool(EXPECT_BORROW) + }); + + case_fuzz(FullSub128, context, "full_sub_borrow_in_causes_underflow") + .strategy(strategy) + .run() + } + + // 1<=b<=a<=u64::MAX; checks the small-operands branch of div_mod_128. + // + // Represents tests: [div_mod_128_div_64] + #[simplex::fuzz] + fn div_mod_128_div_64(context: TestContext) -> 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, context, "div_mod_128_div_64") + .strategy(strategy) + .run() + } + + // a_high=b_high>0 and a_low>=b_low; expects q=1 and the exact remainder. + // + // Represents tests: [div_mod_128_q_is_1] + #[simplex::fuzz] + fn div_mod_quotient_one(context: TestContext) -> 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, context, "div_mod_quotient_one") + .strategy(strategy) + .run() + } + + // a>u64::MAX and 1<=b<=u64::MAX; checks the small-divisor branch. + // + // Represents tests: [div_mod_128_b_is_u64] + #[simplex::fuzz] + fn div_mod_b_is_u64(context: TestContext) -> 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, context, "div_mod_b_is_u64") + .strategy(strategy) + .run() + } + + // a_high>b_high>0 and both low words are nonzero; checks the wide-divisor branch. + // + // Represents tests: [div_mod_128_b_is_u128] + #[simplex::fuzz] + fn div_mod_b_is_u128(context: TestContext) -> 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, context, "div_mod_b_is_u128") + .strategy(strategy) + .run() + } + + // a_high=b_high>0, a_low=u64::MAX, b_low=0; expects q=1 and r=u64::MAX. + // + // Represents tests: [div_mod_128_equal_high_words_max_low_diff] + #[simplex::fuzz] + fn div_mod_equal_high_words_max_low_diff(context: TestContext) -> 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, context, "div_mod_equal_high_words_max_low_diff") + .strategy(strategy) + .run() + } + + // a_high=b_high>0, a_low=0, b_low=u64::MAX; expects q=0 and r=a. + // + // Represents tests: [div_mod_128_eq_high_words_a_less_than_b] + #[simplex::fuzz] + fn div_mod_equal_high_words_less(context: TestContext) -> 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, context, "div_mod_equal_high_words_less") + .strategy(strategy) + .run() + } + + // a=u128::MAX and b=0; expected result=u128::MAX and output borrow=false. + // + // Represents tests: [sub_128_borrow_diff_is_u128_max] + #[simplex::fuzz] + fn sub_diff_is_u128_max(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Sub128Borrow, context, "u128 subtraction maximum difference") + .strategy(Just(u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(0) + .expect(u128::MAX) + })) + .run() + } +} diff --git a/tests/stdlib/u128/mul_div.rs b/tests/stdlib/u128/mul_div.rs index 19ed754..4336abf 100644 --- a/tests/stdlib/u128/mul_div.rs +++ b/tests/stdlib/u128/mul_div.rs @@ -110,3 +110,208 @@ fn mul_div_128_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + 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}; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + any::().prop_filter("u128 should not be zero", |value| *value != 0) + } + + /// Alias for `first_arg`, `second_arg`, `third_arg` and `expected` in [`Case`] entry. + type ConvertInputs = u128; + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: ConvertInputs) -> Self { + Self { + first_numerator: Some(first_numerator), + ..Self::default() + } + } + + fn mul(mut self, second_numerator: ConvertInputs) -> Self { + self.second_numerator = Some(second_numerator); + self + } + + fn div(mut self, denominator: ConvertInputs) -> Self { + self.denominator = Some(denominator); + self + } + + fn expect(mut self, expected: ConvertInputs) -> Self { + self.expected = Some(expected); + self + } + + fn into_witness( + self, + witness: U128MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case.expect(0) + } else { + case.expect(self.expected.expect("no expected result in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + expect: Expect, + test_name: &'static str, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + expect: Expect::Accept, + 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 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::Reject); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U128MulDivTestArguments {}.into(); + let witness = case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + // 0<=a,b<=u64::MAX and c>0; a*b fits u128; checks floor(a*b/c). + // + // Represents tests: [mul_div_128_product_is_u128] + #[simplex::fuzz] + fn product_fits_u128(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(a * b / c) + }) + }; + + case_fuzz(context, "u128 mul div with fitting product") + .strategy(strategy) + .run() + } + + // 2<=a<=MAX, b=MAX, a<=c<=MAX; the widened product overflows u128 but the quotient fits. + // + // Represents tests: [mul_div_128_intermediate_overflow] + #[simplex::fuzz] + fn intermediate_overflow(context: TestContext) -> 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::from_numerator(a) + .mul(u128::MAX) + .div(c) + .expect(expected) + }) + }) + }; + + case_fuzz(context, "u128 mul div with intermediate overflow") + .strategy(strategy) + .run() + } + + // 2<=a<=MAX, b=MAX, 1<=c) -> anyhow::Result<()> { + let strategy = { + (2u128..=u128::MAX).prop_flat_map(|a| { + (1..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u128::MAX).div(c)) + }) + }; + + case_fuzz(context, "u128 mul div result overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a,b>0 and c=0; expects an assertion failure; the failure witness sets expected=0. + // + // Represents tests: [mul_div_128_div_by_zero] + #[simplex::fuzz] + fn divide_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u128(), arb_non_zero_u128()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0)) + }; + + case_fuzz(context, "u128 mul div by zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u16/convert.rs b/tests/stdlib/u16/convert.rs index 036a8a4..68cd61e 100644 --- a/tests/stdlib/u16/convert.rs +++ b/tests/stdlib/u16/convert.rs @@ -152,3 +152,166 @@ 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 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}; + + 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) + } + + /// Alias for `first_arg` in [`Case`] entry. + type ConvertInputs = u16; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 = U16ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u16_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU32, context, "u16_to_u32") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU64, context, "u16_to_u64") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU128, context, "u16_to_u128") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU256, context, "u16_to_u256") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn split_u16_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU16IntoU8, context, "split_u16_into_u8") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU1, context, "safe_u16_to_u1") + .strategy(arb_bool_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU1, context, "safe_u16_to_u1_overflow") + .strategy(2u16..=u16::MAX) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU8, context, "safe_u16_to_u8") + .strategy(arb_u8_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u8_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU8, context, "safe_u16_to_u8_overflow") + .strategy(u8::MAX as u16 + 1..=u16::MAX) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u16/math.rs b/tests/stdlib/u16/math.rs index a343cfb..355664d 100644 --- a/tests/stdlib/u16/math.rs +++ b/tests/stdlib/u16/math.rs @@ -32,3 +32,36 @@ 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 simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + use crate::common::uint_fuzz::TestUintFuzz; + + 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() + } + } + + // Stamps the 22 `#[simplex::fuzz]` entry points for u16. Logic lives in common::uint_fuzz. + crate::uint_fuzz_tests!(u16); +} diff --git a/tests/stdlib/u16/mul_div.rs b/tests/stdlib/u16/mul_div.rs index dbdbcab..0ae1874 100644 --- a/tests/stdlib/u16/mul_div.rs +++ b/tests/stdlib/u16/mul_div.rs @@ -1,6 +1,7 @@ -use crate::common::core::{Expect, run}; use rand::Rng; +use crate::common::core::{Expect, run}; + use simplicityhl_std::artifacts::tests::u16::mul_div::MulDivProgram as U16MulDivTestProgram; use simplicityhl_std::artifacts::tests::u16::mul_div::derived_mul_div::{ MulDivArguments as U16MulDivTestArguments, MulDivWitness as U16MulDivTestWitness, @@ -105,3 +106,183 @@ fn mul_div_16_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + 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}; + + 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_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u16) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u16) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u16) -> Self { + let _ = self.denominator.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_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case.expect(0) + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 expect_failure = matches!(self.expect, Expect::Reject); + + 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(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_16_product_is_u16(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_16_product_is_u16") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_intermediate_overflow(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(expected) + }) + }); + + case_fuzz(context, "mul_div_16_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u16..=u16::MAX).prop_flat_map(|a| { + (1u16..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u16::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_16_result_overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u16(), arb_non_zero_u16()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)); + + case_fuzz(context, "mul_div_16_div_by_zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u256/bit.rs b/tests/stdlib/u256/bit.rs index 0d0db36..bac232f 100644 --- a/tests/stdlib/u256/bit.rs +++ b/tests/stdlib/u256/bit.rs @@ -159,3 +159,260 @@ fn right_shift_256_max(context: simplex::TestContext) -> anyhow::Result<()> { .expect(result) .run(&context) } + +mod bit_tests_fuzz { + use super::*; + + 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_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) + } + + /// Alias for `first_arg`, `second_arg` and `expected` in [`Case`] entry. + type ConvertInputs = U256; + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: ConvertInputs) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: ConvertInputs) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: ConvertInputs) -> 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, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + 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 |case| { + let arguments: Arguments = U256TestBitsArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + Expect::Accept, + None, + ); + + Ok(()) + } + } + + // Both operands are full-width u256; compares against the independent host bitwise result. + // + // Represents tests: [and_256] + #[simplex::fuzz] + fn and_256(context: TestContext) -> 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, context, "u256 bitwise and") + .strategy(strategy) + .run() + } + + // Both operands are full-width u256; compares against the independent host bitwise result. + // + // Represents tests: [or_256] + #[simplex::fuzz] + fn or_256(context: TestContext) -> 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, context, "u256 bitwise or") + .strategy(strategy) + .run() + } + + // 1<=shift<=255 and value is full-width u256; checks the host shift result. + // + // Represents tests: [left_shift_256] + #[simplex::fuzz] + fn left_shift_256(context: TestContext) -> 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, context, "u256 left shift") + .strategy(strategy) + .run() + } + + // shift=0 and value is full-width u256; expected result=value. + // + // Represents tests: [left_shift_256_by_zero] + #[simplex::fuzz] + fn left_shift_256_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::zero()) + .second_arg(value) + .expect(value) + }) + }; + + case_fuzz(LeftShift256, context, "u256 left shift by zero") + .strategy(strategy) + .run() + } + + // shift=255 and value is full-width u256; checks the maximum accepted u8 shift. + // + // Represents tests: [left_shift_256_max] + #[simplex::fuzz] + fn left_shift_256_max(context: TestContext) -> 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, context, "u256 left shift by max") + .strategy(strategy) + .run() + } + + // 1<=shift<=255 and value is full-width u256; checks the host shift result. + // + // Represents tests: [right_shift_256] + #[simplex::fuzz] + fn right_shift_256(context: TestContext) -> 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, context, "u256 right shift") + .strategy(strategy) + .run() + } + + // shift=0 and value is full-width u256; expected result=value. + // + // Represents tests: [right_shift_256_by_zero] + #[simplex::fuzz] + fn right_shift_256_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::zero()) + .second_arg(value) + .expect(value) + }) + }; + + case_fuzz(RightShift256, context, "u256 right shift by zero") + .strategy(strategy) + .run() + } + + // shift=255 and value is full-width u256; checks the maximum accepted u8 shift. + // + // Represents tests: [right_shift_256_max] + #[simplex::fuzz] + fn right_shift_256_max(context: TestContext) -> 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, context, "u256 right shift by max") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/comparison.rs b/tests/stdlib/u256/comparison.rs index 27bdbb5..c1113c2 100644 --- a/tests/stdlib/u256/comparison.rs +++ b/tests/stdlib/u256/comparison.rs @@ -136,3 +136,232 @@ 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 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; + + const EXPECT_ZERO: bool = true; + const EXPECT_NONZERO: bool = false; + const EXPECT_LESS: bool = true; + const EXPECT_NOT_LESS: bool = false; + const EXPECT_LESS_OR_EQUAL: bool = true; + const EXPECT_NOT_LESS_OR_EQUAL: bool = false; + + /// Alias for `first_arg`, `second_arg` and `expected` in [`Case`] entry. + 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, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + 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, 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(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + Expect::Accept, + None, + ); + + Ok(()) + } + } + + // 1<=a<=u256::MAX; expected is_zero=false. + // + // Represents tests: [is_zero_256_false] + #[simplex::fuzz] + fn is_zero_256_false(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_non_zero_u256().prop_map(|a| (a, U256::zero(), EXPECT_NONZERO)); + + case_fuzz(IsZero256, context, "u256 is non-zero") + .strategy(strategy) + .run() + } + + // 0<=a) -> 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, EXPECT_LESS)) + }) + }; + + case_fuzz(Lt256, context, "u256 less than") + .strategy(strategy) + .run() + } + + // a=b across the full width; expected lt=false. + // + // Represents tests: [lt_256_eq] + #[simplex::fuzz] + fn lt_256_eq(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| (a, a, EXPECT_NOT_LESS)); + + case_fuzz(Lt256, context, "u256 less than equal") + .strategy(strategy) + .run() + } + + // 1<=a<=MAX and 0<=b) -> 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, EXPECT_NOT_LESS)) + }) + }; + + case_fuzz(Lt256, context, "u256 greater than") + .strategy(strategy) + .run() + } + + // 0<=a) -> 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, EXPECT_LESS_OR_EQUAL)) + }) + }; + + case_fuzz(Le256, context, "u256 less than or equal") + .strategy(strategy) + .run() + } + + // a=b across the full width; expected le=true. + // + // Represents tests: [le_256_eq] + #[simplex::fuzz] + fn le_256_eq(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| (a, a, EXPECT_LESS_OR_EQUAL)); + + case_fuzz(Le256, context, "u256 less than or equal equal") + .strategy(strategy) + .run() + } + + // 1<=a<=MAX and 0<=b) -> 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, EXPECT_NOT_LESS_OR_EQUAL)) + }) + }; + + case_fuzz(Le256, context, "u256 greater than") + .strategy(strategy) + .run() + } + + // a=0 is fixed; expects is_zero=true. + // + // Represents tests: [is_zero_256_true] + #[simplex::fuzz] + fn is_zero_256_true(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsZero256, context, "u256 zero operand") + .strategy(Just((U256::zero(), U256::zero(), EXPECT_ZERO))) + .run() + } +} diff --git a/tests/stdlib/u256/convert.rs b/tests/stdlib/u256/convert.rs index 2bd68cb..80fd080 100644 --- a/tests/stdlib/u256/convert.rs +++ b/tests/stdlib/u256/convert.rs @@ -1,6 +1,7 @@ use primitive_types::U256; use crate::common::core::{Expect, run}; +use crate::common::helper::generate_u256; use simplicityhl_std::artifacts::tests::u256::convert::ConvertProgram as U256ConvertTestProgram; use simplicityhl_std::artifacts::tests::u256::convert::derived_convert::{ @@ -66,8 +67,6 @@ impl Case { } } -use crate::common::helper::generate_u256; - #[simplex::test] fn u256_into_u8(context: simplex::TestContext) -> anyhow::Result<()> { let a = generate_u256(U256::zero(), U256::MAX); @@ -237,3 +236,349 @@ 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 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; + + // (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, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 |(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(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + // Custom full-width u256 input; checks every split component in the original order. + // + // Represents tests: [u256_into_u8] + #[simplex::fuzz] + fn u256_into_u8(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU8, context, "u256 split into u8") + .strategy(strategy) + .run() + } + + // Custom full-width u256 input; checks every split component in the original order. + // + // Represents tests: [u256_into_u16] + #[simplex::fuzz] + fn u256_into_u16(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU16, context, "u256 split into u16") + .strategy(strategy) + .run() + } + + // Custom full-width u256 input; checks every split component in the original order. + // + // Represents tests: [u256_into_u32] + #[simplex::fuzz] + fn u256_into_u32(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU32, context, "u256 split into u32") + .strategy(strategy) + .run() + } + + // Custom full-width u256 input; checks every split component in the original order. + // + // Represents tests: [u256_into_u64] + #[simplex::fuzz] + fn u256_into_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU64, context, "u256 split into u64") + .strategy(strategy) + .run() + } + + // Custom full-width u256 input; checks every split component in the original order. + // + // Represents tests: [u256_into_u128] + #[simplex::fuzz] + fn u256_into_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU128, context, "u256 split into u128") + .strategy(strategy) + .run() + } + + // 0<=a<=1; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u256_to_u1] + #[simplex::fuzz] + fn safe_u256_to_u1(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u1_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU1, context, "safe u256 to u1") + .strategy(strategy) + .run() + } + + // 1) -> anyhow::Result<()> { + let strategy = overflow_strategy(U256::one()).prop_map(split_number); + + case_fuzz(SafeU256ToU1, context, "safe u256 to u1 overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u8::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u256_to_u8] + #[simplex::fuzz] + fn safe_u256_to_u8(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u8_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU8, context, "safe u256 to u8") + .strategy(strategy) + .run() + } + + // u8::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u8::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU8, context, "safe u256 to u8 overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u16::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u256_to_u16] + #[simplex::fuzz] + fn safe_u256_to_u16(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u16_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU16, context, "safe u256 to u16") + .strategy(strategy) + .run() + } + + // u16::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u16::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU16, context, "safe u256 to u16 overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u32::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u256_to_u32] + #[simplex::fuzz] + fn safe_u256_to_u32(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u32_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU32, context, "safe u256 to u32") + .strategy(strategy) + .run() + } + + // u32::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u32::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU32, context, "safe u256 to u32 overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u64::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u256_to_u64] + #[simplex::fuzz] + fn safe_u256_to_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u64_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU64, context, "safe u256 to u64") + .strategy(strategy) + .run() + } + + // u64::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u64::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU64, context, "safe u256 to u64 overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 0<=a<=u128::MAX; checks a fitting narrowing conversion with exact output. + // + // Represents tests: [safe_u256_to_u128] + #[simplex::fuzz] + fn safe_u256_to_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u128_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU128, context, "safe u256 to u128") + .strategy(strategy) + .run() + } + + // u128::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u128::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU128, context, "safe u256 to u128 overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u256/math/add.rs b/tests/stdlib/u256/math/add.rs index 5af5660..e44378d 100644 --- a/tests/stdlib/u256/math/add.rs +++ b/tests/stdlib/u256/math/add.rs @@ -193,3 +193,381 @@ 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 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_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()) + } + }) + } + + const EXPECT_CARRY: bool = true; + const EXPECT_NO_CARRY: bool = false; + + /// Alias for `first_arg`, `second_arg` and `expected` in [`Case`] entry. + type ConvertInputs = U256; + + #[derive(Debug)] + struct FuzzCase { + first_arg: ConvertInputs, + second_arg: ConvertInputs, + carry_in: bool, + expected: ConvertInputs, + expected_bool: bool, + } + + impl FuzzCase { + fn new(first_arg: ConvertInputs, second_arg: ConvertInputs) -> Self { + Self { + first_arg, + second_arg, + carry_in: false, + expected: U256::zero(), + expected_bool: false, + } + } + + fn carry_in(mut self, carry_in: bool) -> Self { + self.carry_in = carry_in; + self + } + + fn expect(mut self, expected: ConvertInputs) -> Self { + self.expected = expected; + self + } + + fn expect_bool(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_in) + .expect(self.expected.to_big_endian()) + .flag(self.expected_bool) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + 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 |case| { + let arguments: Arguments = U256TestAddArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + Expect::Accept, + None, + ); + + Ok(()) + } + } + + // Custom full-width a; 0<=b<=MAX-a guarantees no output carry. + // + // Represents tests: [add_256_carry_not_overflow] + #[simplex::fuzz] + fn add_256_not_overflow(context: TestContext) -> 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(Add256Carry, context, "u256 add without overflow") + .strategy(strategy) + .run() + } + + // Custom a>0; MAX-a+1<=b<=MAX guarantees overflow; checks wrapped result and carry. + // + // Represents tests: [add_256_carry_overflow] + #[simplex::fuzz] + fn add_256_overflow(context: TestContext) -> 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).expect_bool(carry) + }) + }) + }; + + case_fuzz(Add256Carry, context, "u256 add with overflow") + .strategy(strategy) + .run() + } + + // Custom full-width a; 0<=b<=min(MAX-a,u128::MAX) guarantees a fitting sum. + // + // Represents tests: [add_256_128_carry_not_overflow] + #[simplex::fuzz] + fn add_256_128_not_overflow(context: TestContext) -> 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_128Carry, context, "u256 add u128 without overflow") + .strategy(strategy) + .run() + } + + // MAX-u128::MAX+1<=a<=MAX; MAX-a+1<=b<=u128::MAX guarantees overflow. + // + // Represents tests: [add_256_128_carry_overflow] + #[simplex::fuzz] + fn add_256_128_overflow(context: TestContext) -> 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).expect_bool(carry) + }) + }) + }; + + case_fuzz(Add256_128Carry, context, "u256 add u128 with overflow") + .strategy(strategy) + .run() + } + + // carry_in=false; 0<=a<=MAX and 0<=b<=MAX-a; expects sum=a+b and carry_out=false. + // + // Represents tests: [full_add_256_not_overflow_carry_low_false] + #[simplex::fuzz] + fn full_add_256_not_overflow_carry_in_false( + context: TestContext, + ) -> 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) + .carry_in(false) + .expect(a + b) + .expect_bool(EXPECT_NO_CARRY) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add without overflow and input carry false", + ) + .strategy(strategy) + .run() + } + + // carry_in=false; a>0 and MAX-a+1<=b<=MAX; checks wrapped sum and carry_out=true. + // + // Represents tests: [full_add_256_overflow_carry_low_false] + #[simplex::fuzz] + fn full_add_256_overflow_carry_in_false(context: TestContext) -> 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) + .carry_in(false) + .expect(expected) + .expect_bool(carry) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add with overflow and input carry false", + ) + .strategy(strategy) + .run() + } + + // carry_in=true; 0<=a, + ) -> 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_in(true) + .expect(a + b + U256::one()) + .expect_bool(EXPECT_NO_CARRY) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add without overflow and input carry true", + ) + .strategy(strategy) + .run() + } + + // carry_in=true; 0<=a<=MAX and MAX-a<=b<=MAX; checks wrapped a+b+1 and carry_out=true. + // + // Represents tests: [full_add_256_overflow_carry_low_true] + #[simplex::fuzz] + fn full_add_256_overflow_carry_in_true(context: TestContext) -> 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_from_input) = result.overflowing_add(U256::one()); + + FuzzCase::new(a, b) + .carry_in(true) + .expect(expected) + .expect_bool(carry || carry_from_input) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add with overflow and input carry true", + ) + .strategy(strategy) + .run() + } + + // a=MAX, b=0, carry_in=true; the input carry causes result=0 and carry_out=true. + // + // Represents none of the tests. + #[simplex::fuzz] + fn full_add_256_carry_in_causes_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = Just(U256::MAX).prop_map(|a| { + FuzzCase::new(a, U256::zero()) + .carry_in(true) + .expect(U256::zero()) + .expect_bool(EXPECT_CARRY) + }); + + case_fuzz( + FullAdd256, + context, + "u256 input carry alone causes overflow", + ) + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/math/api.rs b/tests/stdlib/u256/math/api.rs index 3964165..f4cc60c 100644 --- a/tests/stdlib/u256/math/api.rs +++ b/tests/stdlib/u256/math/api.rs @@ -40,3 +40,51 @@ 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 simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + use crate::common::uint_fuzz::TestUintFuzz; + + 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() + } + } + + // Stamps the 22 `#[simplex::fuzz]` entry points for u128. Logic lives in common::uint_fuzz. + crate::uint_fuzz_tests!(U256Wrapper); +} diff --git a/tests/stdlib/u256/math/div.rs b/tests/stdlib/u256/math/div.rs index 7b40598..90112b9 100644 --- a/tests/stdlib/u256/math/div.rs +++ b/tests/stdlib/u256/math/div.rs @@ -438,3 +438,699 @@ fn div_mod_256_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> .expect([0; 32]) .expecting(&context, Expect::AssertFailed) } + +mod div_tests_fuzz { + use super::*; + + use std::cmp::max; + + 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; + use crate::common::u256_wrapper::U256Wrapper; + use crate::common::uint_fuzz::{ + run_uint_fuzz, safe_div_by_zero_strategy, safe_div_fitting_strategy, + }; + + 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()) + } + }) + } + + const FAILURE_EXPECTED_PLACEHOLDER: U256 = U256::zero(); + + /// Alias for `first_arg`, `second_arg`, `expected` and `second_expected` in [`Case`] entry. + type ConvertInputs = U256; + + #[derive(Debug)] + struct FuzzCase { + first_arg: ConvertInputs, + second_arg: ConvertInputs, + expected: ConvertInputs, + second_expected: ConvertInputs, + } + + impl FuzzCase { + fn arg(first_arg: ConvertInputs) -> Self { + Self::new(first_arg, U256::zero()) + } + + fn new(first_arg: ConvertInputs, second_arg: ConvertInputs) -> Self { + Self { + first_arg, + second_arg, + expected: U256::zero(), + second_expected: U256::zero(), + } + } + + fn expect(mut self, expected: ConvertInputs) -> Self { + self.expected = expected; + self + } + + fn second_expected(mut self, second_expected: ConvertInputs) -> Self { + self.second_expected = second_expected; + self + } + + fn into_witness( + mut self, + witness: U256TestDivWitness, + expect_failure: bool, + ) -> WitnessValues { + if expect_failure { + self.expected = FAILURE_EXPECTED_PLACEHOLDER; + self.second_expected = FAILURE_EXPECTED_PLACEHOLDER; + } + + 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, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 = self.expect == Expect::Reject; + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestDivArguments {}.into(); + let witness = case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + // 2^128<=a<=MAX; expects ceil(2^127/a_high). + // + // Represents tests: [calculate_normalizer_base_128] + #[simplex::fuzz] + fn calculate_normalizer_base_128(context: TestContext) -> anyhow::Result<()> { + let min_u128_bit_number = 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 = min_u128_bit_number.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer", + ) + .strategy(strategy) + .run() + } + + // 2^255<=a<=MAX; expects normalizer=1. + // + // Represents tests: [calculate_normalizer_base_128_norm_is_1] + #[simplex::fuzz] + fn calculate_normalizer_base_128_norm_is_1( + context: TestContext, + ) -> anyhow::Result<()> { + let min_u128_bit_number = 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 = min_u128_bit_number.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer is one", + ) + .strategy(strategy) + .run() + } + + // 2^128<=a<2^255; expects normalizer>1. + // + // Represents tests: [calculate_normalizer_base_128_norm_greater_than_1] + #[simplex::fuzz] + fn calculate_normalizer_base_128_norm_greater_than_1( + context: TestContext, + ) -> anyhow::Result<()> { + let min_u128_bit_number = 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 = min_u128_bit_number.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer is greater than one", + ) + .strategy(strategy) + .run() + } + + // 1<=a<=u128::MAX has a zero high half; expects assertion failure. + // + // Represents tests: [calculate_normalizer_base_128_a_is_u128_fail] + #[simplex::fuzz] + fn calculate_normalizer_base_128_a_is_u128_fail( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), (U256::one() << 128) - 1); + + a.prop_map(FuzzCase::arg) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer rejects u128", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a=0 is fixed with Just; expects assertion failure. + // + // Represents tests: [calculate_normalizer_base_128_b_is_zero_fail] + #[simplex::fuzz] + fn calculate_normalizer_base_128_b_is_zero_fail( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = Just(U256::zero()).prop_map(FuzzCase::arg); + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer rejects zero", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a is full-width u256; 1<=b<=u64::MAX; checks quotient and remainder. + // + // Represents tests: [div_mod_256_64] + #[simplex::fuzz] + fn div_mod_256_64(context: TestContext) -> 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_expected(a % b)) + }; + + case_fuzz(DivMod256_64, context, "u256 div mod by u64") + .strategy(strategy) + .run() + } + + // a is full-width u256 and b=0; this is division by zero, not arithmetic overflow. + // + // Represents tests: [div_mod_256_64_div_by_zero] + #[simplex::fuzz] + fn div_mod_256_64_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz(DivMod256_64, context, "u256 div mod by u64 zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a is full-width u256; u64::MAX) -> 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_expected(a % b)) + }; + + case_fuzz(AlgorithmD256_128, context, "u256 algorithm d by u128") + .strategy(strategy) + .run() + } + + // a is u256; 1<=b<=u64::MAX violates the Algorithm D divisor-width precondition. + // + // Represents tests: [algorithm_d_256_128_fail_b_fits_into_u64] + #[simplex::fuzz] + fn algorithm_d_256_128_fail_b_fits_into_u64( + context: TestContext, + ) -> 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_expected(a % b)) + }; + + case_fuzz( + AlgorithmD256_128, + context, + "u256 algorithm d rejects u64 divisor", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a is full-width u256 and b=0; this is division by zero, not arithmetic overflow. + // + // Represents tests: [algorithm_d_256_128_div_by_zero] + #[simplex::fuzz] + fn algorithm_d_256_128_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz( + AlgorithmD256_128, + context, + "u256 algorithm d rejects zero divisor", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // u64::MAX) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(u64::MAX) + 1, U256::from(u128::MAX)); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz( + AlgorithmD256_128, + context, + "u256 algorithm d equal arguments", + ) + .strategy(strategy) + .run() + } + + // a is u256; u64::MAX) -> 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_expected(a % b)) + }; + + case_fuzz(DivMod256_128, context, "u256 div mod by u128") + .strategy(strategy) + .run() + } + + // a is u256; 1<=b<=u64::MAX; exercises the fallback through the u128-divisor entry point. + // + // Represents tests: [div_mod_256_128_b_fits_into_u64] + #[simplex::fuzz] + fn div_mod_256_128_b_fits_into_u64(context: TestContext) -> 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_expected(a % b)) + }; + + case_fuzz(DivMod256_128, context, "u256 div mod by u64 through u128") + .strategy(strategy) + .run() + } + + // a is full-width u256 and b=0; this is division by zero, not arithmetic overflow. + // + // Represents tests: [div_mod_256_128_div_by_zero] + #[simplex::fuzz] + fn div_mod_256_128_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz(DivMod256_128, context, "u256 div mod by u128 zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 1<=a=b<=u128::MAX; expects q=1 and r=0. + // + // Represents tests: [div_mod_256_128_a_eq_b] + #[simplex::fuzz] + fn div_mod_256_128_a_eq_b(context: TestContext) -> 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, + context, + "u256 div mod by u128 equal arguments", + ) + .strategy(strategy) + .run() + } + + // 1<=a) -> 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_expected(a % b)) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod a less than b") + .strategy(strategy) + .run() + } + + // 1<=b<=a<=u128::MAX; checks the small-operands branch. + // + // Represents tests: [div_mod_256_div_128] + #[simplex::fuzz] + fn div_mod_256_div_128(context: TestContext) -> 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_expected(a % b)) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod u128 dividend") + .strategy(strategy) + .run() + } + + // a_high=b_high>0 and a_low>=b_low; expects q=1 and the exact remainder. + // + // Represents tests: [div_mod_256_q_is_1] + #[simplex::fuzz] + fn div_mod_256_q_is_1(context: TestContext) -> 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_expected(a % b) + }) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod quotient is one") + .strategy(strategy) + .run() + } + + // a>u128::MAX and 1<=b<=u128::MAX; checks the small-divisor branch. + // + // Represents tests: [div_mod_256_b_fits_into_u128] + #[simplex::fuzz] + fn div_mod_256_b_fits_into_u128(context: TestContext) -> 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_expected(a % b)) + }; + + case_fuzz(DivMod256, context, "u256 div mod u128 divisor") + .strategy(strategy) + .run() + } + + // 1<=b) -> 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_expected(a % b)) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod u256 divisor") + .strategy(strategy) + .run() + } + + // a=b>0 across the full width; expects q=1 and r=0. + // + // Represents tests: [div_mod_256_a_equal_b] + #[simplex::fuzz] + fn div_mod_256_a_equal_b(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz(DivMod256, context, "u256 div mod equal arguments") + .strategy(strategy) + .run() + } + + // a_high=b_high>0, a_low=u128::MAX, b_low=0; expects q=1 and r=u128::MAX. + // + // Represents tests: [div_mod_256_equal_high_words_max_low_diff] + #[simplex::fuzz] + fn div_mod_256_equal_high_words_max_low_diff( + context: TestContext, + ) -> 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_expected(U256::from(u128::MAX)) + }) + }; + + case_fuzz( + DivMod256, + context, + "u256 div mod equal high words maximum low difference", + ) + .strategy(strategy) + .run() + } + + // a_high=b_high>0, a_low=0, b_low=u128::MAX; expects q=0 and r=a. + // + // Represents tests: [div_mod_256_eq_high_words_a_less_than_b] + #[simplex::fuzz] + fn div_mod_256_eq_high_words_a_less_than_b( + context: TestContext, + ) -> 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_expected(a) + }) + }; + + case_fuzz( + DivMod256, + context, + "u256 div mod equal high words a less than b", + ) + .strategy(strategy) + .run() + } + + // a is full-width u256 and b>0; checks the general division API. + // + // Represents none of the tests. + #[simplex::fuzz] + fn div_256(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::( + context, + safe_div_fitting_strategy::(), + Expect::Accept, + ) + } + + // Uses API div_256, which calls div_mod_256 indirectly; a is u256, b=0, and assertion failure + // is expected. + // + // Represents tests: [div_mod_256_div_by_zero] + #[simplex::fuzz] + fn div_256_div_by_zero(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::( + context, + safe_div_by_zero_strategy::(), + Expect::Reject, + ) + } + + // Fixed operands exercise the division edge case; checks quotient and remainder. + // + // Represents tests: [div_mod_256_edge_case] + #[simplex::fuzz] + fn div_mod_256_edge_case(context: TestContext) -> anyhow::Result<()> { + let a = U256::one() << 255; + let b = (U256::one() << 127) + (U256::one() << 64) - U256::one(); + case_fuzz(DivMod256, context, "u256 fixed division edge case") + .strategy( + simplex::fuzz::proptest::prelude::Just((a, b)) + .prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)), + ) + .run() + } +} diff --git a/tests/stdlib/u256/math/sub_mul.rs b/tests/stdlib/u256/math/sub_mul.rs index 3773232..52e4acd 100644 --- a/tests/stdlib/u256/math/sub_mul.rs +++ b/tests/stdlib/u256/math/sub_mul.rs @@ -274,3 +274,469 @@ fn 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 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; + + const EXPECT_BORROW: bool = true; + + 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 expect_bool(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( + mut self, + witness: U256TestSubMulWitness, + expect_failure: bool, + ) -> WitnessValues { + if expect_failure { + self.expected = U256::zero(); + self.second_expected = U256::zero(); + self.third_expected = U256::zero(); + } + 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, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 = self.expect == Expect::Reject; + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestSubMulArguments {}.into(); + let witness = case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + // 0<=b<=a<=MAX; expects a-b and borrow_out=false. + // + // Represents tests: [sub_256_borrow_not_overflow] + #[simplex::fuzz] + fn sub_256_not_overflow(context: TestContext) -> 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(Sub256Borrow, context, "u256 subtraction without overflow") + .strategy(strategy) + .run() + } + + // a=b across the full width; expects result=0 and borrow_out=false. + // + // Represents tests: [sub_256_borrow_a_eq_b] + #[simplex::fuzz] + fn sub_256_a_eq_b(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| FuzzCase::new(a, a).expect(U256::zero())); + + case_fuzz(Sub256Borrow, context, "u256 subtraction equal operands") + .strategy(strategy) + .run() + } + + // a and b share their low u128 half; high halves vary independently. + // + // Represents tests: [sub_256_borrow_a_low_eq_b_low] + #[simplex::fuzz] + fn sub_256_a_low_eq_b_low(context: TestContext) -> 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, borrow) = a.overflowing_sub(b); + + FuzzCase::new(a, b).expect(result).expect_bool(borrow) + }) + }; + + case_fuzz(Sub256Borrow, context, "u256 subtraction equal low halves") + .strategy(strategy) + .run() + } + + // a_low=u128::MAX and b_low=0; high halves vary independently. + // + // Represents tests: [sub_256_borrow_diff_is_u128_max] + #[simplex::fuzz] + fn sub_256_diff_is_u128_max(context: TestContext) -> 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, borrow) = a.overflowing_sub(b); + + FuzzCase::new(a, b).expect(result).expect_bool(borrow) + }) + }; + + case_fuzz( + Sub256Borrow, + context, + "u256 subtraction u128 max difference", + ) + .strategy(strategy) + .run() + } + + // 1<=a) -> 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) + .expect_bool(EXPECT_BORROW) + }) + }; + + case_fuzz(Sub256Borrow, context, "u256 subtraction overflow") + .strategy(strategy) + .run() + } + + // Both u256 operands are nonzero; checks high and low product halves against U512. + // + // Represents tests: [mul_256_wide] + #[simplex::fuzz] + fn mul_256(context: TestContext) -> 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(Mul256Wide, context, "u256 multiplication") + .strategy(strategy) + .run() + } + + // a is nonzero u256 and 1<=b<=u64::MAX; checks the widened product. + // + // Represents tests: [mul_256_64_wide] + #[simplex::fuzz] + fn mul_256_64(context: TestContext) -> 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_64Wide, context, "u256 by u64 multiplication") + .strategy(strategy) + .run() + } + + // a is nonzero u256 and 1<=b<=u128::MAX; checks the widened product. + // + // Represents tests: [mul_256_128_wide] + #[simplex::fuzz] + fn mul_256_128(context: TestContext) -> 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_128Wide, context, "u256 by u128 multiplication") + .strategy(strategy) + .run() + } + + // Both u256 multiplicand halves and the u128 multiplier are nonzero; checks all three result words. + // + // Represents tests: [mul_512_128_wide] + #[simplex::fuzz] + fn mul_512_128(context: TestContext) -> 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_128Wide, context, "u512 by u128 multiplication") + .strategy(strategy) + .run() + } + + // 0<=a,b<=u128::MAX ensures the product fits u256. + // + // Represents tests: [mul_256_128_fitting] + #[simplex::fuzz] + fn safe_mul_256_128_fitting(context: TestContext) -> 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( + Mul256_128, + context, + "safe u256 by u128 multiplication fitting", + ) + .strategy(strategy) + .run() + } + + // b=u128::MAX and a>=MAX/b+1; tighter than the original lower bound, guaranteeing overflow. + // + // Represents tests: [mul_256_128_overflow] + #[simplex::fuzz] + fn safe_mul_256_128_overflow(context: TestContext) -> 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( + Mul256_128, + context, + "safe u256 by u128 multiplication overflow", + ) + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a=MAX and b=0; expects result=MAX and no borrow. + // + // Represents tests: [sub_256_borrow_diff_is_u256_max] + #[simplex::fuzz] + fn sub_256_diff_is_u256_max(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Sub256Borrow, context, "u256 subtraction maximum difference") + .strategy( + simplex::fuzz::proptest::prelude::Just(U256::MAX) + .prop_map(|a| FuzzCase::new(a, U256::zero()).expect(a)), + ) + .run() + } +} diff --git a/tests/stdlib/u256/mul_div.rs b/tests/stdlib/u256/mul_div.rs index c4ffd11..f5d2f87 100644 --- a/tests/stdlib/u256/mul_div.rs +++ b/tests/stdlib/u256/mul_div.rs @@ -1,7 +1,8 @@ -use primitive_types::U256; use std::cmp::max; use std::ops::Div; +use primitive_types::U256; + use crate::common::core::{Expect, run}; use crate::common::helper::generate_u256; @@ -278,3 +279,450 @@ fn mul_div_256_div_step_320_256_add_back(context: simplex::TestContext) -> anyho .expect(res) .run(&context) } + +mod mul_div_tests_fuzz { + use super::*; + + 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 primitive_types::U512; + + use crate::common::fuzz; + + // Failure cases need an expected witness even though the operation fails first. + const FAILURE_EXPECTED_PLACEHOLDER: [u8; 32] = [0; 32]; + + // (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 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 + } + + fn product_high(a: U256, b: U256) -> U256 { + safe_u512_to_u256_typed(a.full_mul(b) >> 256) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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::Reject); + + 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.expect(FAILURE_EXPECTED_PLACEHOLDER) + } else { + case.expect(expected) + }; + + let witness: WitnessValues = case.witness.into(); + + (arguments, witness) + }) + .boxed(); + + fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + // 0<=a,b<=u128::MAX and c>0; checks floor(a*b/c) with a U512 reference. + // + // Represents tests: [mul_div_256_product_fits_into_u256] + #[simplex::fuzz] + fn mul_div_256_product_fits_into_u256(context: TestContext) -> 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, context, "mul_div_256 product fits into u256") + .strategy(strategy) + .run() + } + + // 2<=a<=MAX, b=MAX, a<=c<=MAX; product exceeds MAX but quotient fits. + // + // Represents tests: [mul_div_256_intermediate_overflow] + #[simplex::fuzz] + fn mul_div_256_intermediate_overflow(context: TestContext) -> 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, context, "mul_div_256 intermediate overflow") + .strategy(strategy) + .run() + } + + // 2<=a<=MAX, b=MAX, 1<=c) -> 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, context, "mul_div_256 result overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // 2^128<=a,b<=MAX and c=a; division is exact and expected result=b. + // + // Represents tests: [mul_div_256_remainder_is_zero] + #[simplex::fuzz] + fn mul_div_256_remainder_is_zero(context: TestContext) -> 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, context, "mul_div_256 remainder is zero") + .strategy(strategy) + .run() + } + + // a>0 and 1<=b<=c<=u128::MAX; quotient is guaranteed to fit u256. + // + // Represents tests: [mul_div_256_denominator_is_u128] + #[simplex::fuzz] + fn mul_div_256_denominator_is_u128(context: TestContext) -> 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, context, "mul_div_256 denominator is u128") + .strategy(strategy) + .run() + } + + // 2^128<=a,b<=2^129 and 2^128<=c<2^129; checks a divisor with the smallest nonzero high half. + // + // Represents tests: [mul_div_256_min_denom_high] + #[simplex::fuzz] + fn mul_div_256_min_denom_high(context: TestContext) -> 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, context, "mul_div_256 minimal denominator high") + .strategy(strategy) + .run() + } + + // a,b>0 and c=0; expects an assertion failure with a placeholder expected result. + // + // Represents tests: [mul_div_256_div_by_zero] + #[simplex::fuzz] + fn mul_div_256_div_by_zero(context: TestContext) -> 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, context, "mul_div_256 division by zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + // a,b>=2^128, b<=MAX-2^128, c>=max(2^128,product_high+1); quotient fits. + // + // Represents tests: [mul_div_256_algorithm_d_512_256_check] + #[simplex::fuzz] + fn mul_div_256_algorithm_d_512_256_check(context: TestContext) -> 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, context, "mul_div_256 algorithm D") + .strategy(strategy) + .run() + } + + // a,b>=2^128, b<=MAX-2^128, c=product_high+1; tests the tight fitting-divisor boundary. + // + // Represents tests: [mul_div_256_algorithm_d_512_256_c_is_res_high] + #[simplex::fuzz] + fn mul_div_256_algorithm_d_512_256_c_is_res_high( + context: TestContext, + ) -> 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, + context, + "mul_div_256 denominator just above product high", + ) + .strategy(strategy) + .run() + } + + // a,b>=2^128, b<=MAX-2^128, c>=max(2^255,product_high+1); normalization factor is one. + // + // Represents tests: [mul_div_256_normalize_to_threshold_512_256_63_norm_is_1] + #[simplex::fuzz] + fn mul_div_256_normalize_to_threshold_512_127_norm_is_1( + context: TestContext, + ) -> 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, context, "mul_div_256 normalizer is one") + .strategy(strategy) + .run() + } + + // a>=2^128, 2^128<=b<=2^192, max(2^128,product_high+1)<=c<2^255; normalization factor exceeds + // one. + // + // Represents tests: [mul_div_256_normalize_to_threshold_512_256_63_norm_greater_than_1] + #[simplex::fuzz] + fn mul_div_256_normalize_to_threshold_512_127_norm_greater_than_1( + context: TestContext, + ) -> 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, + context, + "mul_div_256 normalizer is greater than one", + ) + .strategy(strategy) + .run() + } + + // Correlated operands exercise the division add-back case; checks the quotient against U512. + // + // Represents tests: [mul_div_256_div_step_320_256_add_back] + #[simplex::fuzz] + fn mul_div_256_div_step_320_256_add_back(context: TestContext) -> anyhow::Result<()> { + let strategy = ( + arb_u256_in_range(U256::one() << 255, U256::MAX), + 1u64..=u64::MAX, + ) + .prop_map(|(c, q)| { + let c = c | U256::one(); + let a = (c >> 128) << 128; + let b = U256::from(q) << 192; + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }); + case_fuzz(MulDiv, context, "u256 correlated division add back") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u32/convert.rs b/tests/stdlib/u32/convert.rs index ccf0cf9..cd66f84 100644 --- a/tests/stdlib/u32/convert.rs +++ b/tests/stdlib/u32/convert.rs @@ -173,3 +173,190 @@ 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 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}; + + 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) + } + + /// Alias for `first_arg` in [`Case`] entry. + type ConvertInputs = u32; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 = U32ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u32_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U32ToU64, context, "u32_to_u64") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn u32_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U32ToU128, context, "u32_to_u128") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn u32_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U32ToU256, context, "u32_to_u256") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn split_u32_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU32IntoU8, context, "split_u32_into_u8") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn split_u32_into_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU32IntoU16, context, "split_u32_into_u16") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU1, context, "safe_u32_to_u1") + .strategy(arb_bool_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU1, context, "safe_u32_to_u1_overflow") + .strategy(2u32..=u32::MAX) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU8, context, "safe_u32_to_u8") + .strategy(arb_u8_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u8_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU8, context, "safe_u32_to_u8_overflow") + .strategy(u8::MAX as u32 + 1..=u32::MAX) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU16, context, "safe_u32_to_u16") + .strategy(arb_u16_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u16_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU16, context, "safe_u32_to_u16_overflow") + .strategy(u16::MAX as u32 + 1..=u32::MAX) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u32/math.rs b/tests/stdlib/u32/math.rs index c25a816..1bca84c 100644 --- a/tests/stdlib/u32/math.rs +++ b/tests/stdlib/u32/math.rs @@ -32,3 +32,36 @@ 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 simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + use crate::common::uint_fuzz::TestUintFuzz; + + 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() + } + } + + // Stamps the 22 `#[simplex::fuzz]` entry points for u32. Logic lives in common::uint_fuzz. + crate::uint_fuzz_tests!(u32); +} diff --git a/tests/stdlib/u32/mul_div.rs b/tests/stdlib/u32/mul_div.rs index ccec474..d9bf50f 100644 --- a/tests/stdlib/u32/mul_div.rs +++ b/tests/stdlib/u32/mul_div.rs @@ -1,6 +1,7 @@ -use crate::common::core::{Expect, run}; use rand::Rng; +use crate::common::core::{Expect, run}; + use simplicityhl_std::artifacts::tests::u32::mul_div::MulDivProgram as U32MulDivTestProgram; use simplicityhl_std::artifacts::tests::u32::mul_div::derived_mul_div::{ MulDivArguments as U32MulDivTestArguments, MulDivWitness as U32MulDivTestWitness, @@ -108,3 +109,183 @@ fn mul_div_32_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + 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}; + + 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, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u32) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u32) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u32) -> Self { + let _ = self.denominator.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 numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case.expect(0) + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 expect_failure = matches!(self.expect, Expect::Reject); + + 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(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_32_product_is_u32(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_32_product_is_u32") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_intermediate_overflow(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(expected) + }) + }); + + case_fuzz(context, "mul_div_32_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u32..=u32::MAX).prop_flat_map(|a| { + (1u32..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u32::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_32_result_overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u32(), arb_non_zero_u32()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)); + + case_fuzz(context, "mul_div_32_div_by_zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u64/convert.rs b/tests/stdlib/u64/convert.rs index 78a6289..c72f1fc 100644 --- a/tests/stdlib/u64/convert.rs +++ b/tests/stdlib/u64/convert.rs @@ -194,3 +194,213 @@ 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 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}; + + 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) + } + + /// Alias for `first_arg` in [`Case`] entry. + type ConvertInputs = u64; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u64_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U64ToU128, context, "u64 to u128") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn u64_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U64ToU256, context, "u64 to u256") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU8, context, "split u64 into u8") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU16, context, "split u64 into u16") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU32, context, "split u64 into u32") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU1, context, "safe u64 to u1") + .strategy(arb_bool_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU1, context, "safe u64 to u1 overflow") + .strategy(2u64..=u64::MAX) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU8, context, "safe u64 to u8") + .strategy(arb_u8_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u8_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU8, context, "safe u64 to u8 overflow") + .strategy((u64::from(u8::MAX) + 1)..=u64::MAX) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU16, context, "safe u64 to u16") + .strategy(arb_u16_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u16_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU16, context, "safe u64 to u16 overflow") + .strategy((u64::from(u16::MAX) + 1)..=u64::MAX) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU32, context, "safe u64 to u32") + .strategy(arb_u32_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u32_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU32, context, "safe u64 to u32 overflow") + .strategy((u64::from(u32::MAX) + 1)..=u64::MAX) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u64/math.rs b/tests/stdlib/u64/math.rs index 327894e..0daff49 100644 --- a/tests/stdlib/u64/math.rs +++ b/tests/stdlib/u64/math.rs @@ -32,3 +32,36 @@ 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 simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + use crate::common::uint_fuzz::TestUintFuzz; + + 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() + } + } + + // Stamps the 22 `#[simplex::fuzz]` entry points for u64. Logic lives in common::uint_fuzz. + crate::uint_fuzz_tests!(u64); +} diff --git a/tests/stdlib/u64/mul_div.rs b/tests/stdlib/u64/mul_div.rs index 918661b..82c7e6c 100644 --- a/tests/stdlib/u64/mul_div.rs +++ b/tests/stdlib/u64/mul_div.rs @@ -1,6 +1,7 @@ -use crate::common::core::{Expect, run}; use rand::Rng; +use crate::common::core::{Expect, run}; + use simplicityhl_std::artifacts::tests::u64::mul_div::MulDivProgram as U64MulDivTestProgram; use simplicityhl_std::artifacts::tests::u64::mul_div::derived_mul_div::{ MulDivArguments as U64MulDivTestArguments, MulDivWitness as U64MulDivTestWitness, @@ -105,3 +106,184 @@ fn mul_div_64_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + 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}; + + 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, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u64) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u64) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u64) -> Self { + let _ = self.denominator.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 numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case.expect(0) + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> CaseFuzz { + CaseFuzz { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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::Reject); + + 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(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_64_product_is_u64(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_64_product_is_u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_intermediate_overflow(context: TestContext) -> 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::from_numerator(a) + .mul(u64::MAX) + .div(c) + .expect(expected) + }) + }); + + case_fuzz(context, "mul_div_64_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2..=u64::MAX).prop_flat_map(|a| { + (1..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u64::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_64_result_overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u64(), arb_non_zero_u64(), Just(0u64)) + .prop_map(|(a, b, c)| FuzzCase::from_numerator(a).mul(b).div(c).expect(0)); + + case_fuzz(context, "mul_div_64_div_by_zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u8/convert.rs b/tests/stdlib/u8/convert.rs index d536dd8..b8c1225 100644 --- a/tests/stdlib/u8/convert.rs +++ b/tests/stdlib/u8/convert.rs @@ -142,3 +142,151 @@ 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 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}; + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_bool_u8() -> impl Strategy { + arb_bool().prop_map(u8::from) + } + + /// Alias for `first_arg` in [`Case`] entry. + type ConvertInputs = u8; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 = U8ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u8_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU16, context, "u8 to u16") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU32, context, "u8 to u32") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU64, context, "u8 to u64") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU128, context, "u8 to u128") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU256, context, "u8 to u256") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn split_u8_into_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU8IntoU1, context, "split u8 into u1") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn safe_u8_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU8ToU1, context, "safe u8 to u1") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn safe_u8_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU8ToU1, context, "safe u8 to u1 overflow") + .strategy(2u8..=u8::MAX) + .expect(Expect::Reject) + .run() + } +} diff --git a/tests/stdlib/u8/math.rs b/tests/stdlib/u8/math.rs index cefd1b8..912d897 100644 --- a/tests/stdlib/u8/math.rs +++ b/tests/stdlib/u8/math.rs @@ -32,3 +32,36 @@ 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 simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + use crate::common::uint_fuzz::TestUintFuzz; + + 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() + } + } + + // Stamps the 22 `#[simplex::fuzz]` entry points for u8. Logic lives in common::uint_fuzz. + crate::uint_fuzz_tests!(u8); +} diff --git a/tests/stdlib/u8/mul_div.rs b/tests/stdlib/u8/mul_div.rs index c01383b..a8cb010 100644 --- a/tests/stdlib/u8/mul_div.rs +++ b/tests/stdlib/u8/mul_div.rs @@ -1,6 +1,7 @@ -use crate::common::core::{Expect, run}; use rand::Rng; +use crate::common::core::{Expect, run}; + use simplicityhl_std::artifacts::tests::u8::mul_div::MulDivProgram as U8MulDivTestProgram; use simplicityhl_std::artifacts::tests::u8::mul_div::derived_mul_div::{ MulDivArguments as U8MulDivTestArguments, MulDivWitness as U8MulDivTestWitness, @@ -105,3 +106,174 @@ fn mul_div_8_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + 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}; + + fn arb_non_zero_u8() -> impl Strategy { + any::().prop_filter("u8 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u8) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u8) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(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_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case.expect(0) + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Accept, + } + } + + 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 expect_failure = matches!(self.expect, Expect::Reject); + + 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(); + + crate::common::fuzz::run::( + &self.context, + strategy, + self.test_name, + self.expect, + None, + ); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_8_product_is_u8(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u8(), arb_non_zero_u8()).prop_map(|(a, c)| { + let b = u8::MAX / a; + + FuzzCase::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_8 product is u8") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_intermediate_overflow(context: TestContext) -> 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::from_numerator(a).mul(b).div(c).expect(expected) + }) + }); + + case_fuzz(context, "mul_div_8 intermediate overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u8..=u8::MAX).prop_flat_map(|a| { + (1u8..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u8::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_8 result overflow") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u8(), arb_non_zero_u8()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)); + + case_fuzz(context, "mul_div_8 div by zero") + .strategy(strategy) + .expect(Expect::Reject) + .run() + } +}