diff --git a/fuzz_props/src/generators.rs b/fuzz_props/src/generators.rs index 3aec1ccd..ca79628c 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -74,6 +74,21 @@ pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result (u128, u128) { + let nonce = u128::from(nonce_byte) % 4; // 0..=3 + let amount = amount_raw % balance.saturating_add(1); // 0..=balance + (nonce, amount) +} + /// Generate a native-transfer [`LeeTransaction`] between two accounts chosen /// from `accounts`. /// @@ -102,9 +117,7 @@ pub fn arb_fuzz_native_transfer( let (nonce, amount) = if bool::arbitrary(u)? { // Biased valid: nonce near the genesis value, amount within balance. - let nonce = u128::from(u8::arbitrary(u)?) % 4; // 0..=3 - let amount = u128::arbitrary(u)? % from.balance.saturating_add(1); // 0..=balance - (nonce, amount) + biased_valid_nonce_amount(u8::arbitrary(u)?, u128::arbitrary(u)?, from.balance) } else { // Adversarial: full range drives the rejection paths. (u128::arbitrary(u)?, u128::arbitrary(u)?) diff --git a/fuzz_props/src/tests.rs b/fuzz_props/src/tests.rs index 7cecd841..38f98451 100644 --- a/fuzz_props/src/tests.rs +++ b/fuzz_props/src/tests.rs @@ -1,5 +1,5 @@ -mod arbitrary_types_test; -mod generators_test; +mod arbitrary_types; +mod generators; mod invariants; mod privacy; mod replay_proptest; diff --git a/fuzz_props/src/tests/arbitrary_types_test.rs b/fuzz_props/src/tests/arbitrary_types.rs similarity index 100% rename from fuzz_props/src/tests/arbitrary_types_test.rs rename to fuzz_props/src/tests/arbitrary_types.rs diff --git a/fuzz_props/src/tests/generators_test.rs b/fuzz_props/src/tests/generators.rs similarity index 75% rename from fuzz_props/src/tests/generators_test.rs rename to fuzz_props/src/tests/generators.rs index 44c7b139..ec5584b5 100644 --- a/fuzz_props/src/tests/generators_test.rs +++ b/fuzz_props/src/tests/generators.rs @@ -4,7 +4,8 @@ use arbitrary::Unstructured; use nssa::{AccountId, PrivateKey}; use crate::generators::{ - FuzzAccount, arb_fuzz_native_transfer, arbitrary_fuzz_state, signer_account_ids, test_accounts, + FuzzAccount, arb_fuzz_native_transfer, arbitrary_fuzz_state, biased_valid_nonce_amount, + signer_account_ids, test_accounts, }; /// Verifies that `signer_account_ids` returns a **non-empty** list for a properly signed @@ -133,3 +134,45 @@ fn native_transfer_index_uses_modulo_not_div_add() { mutation: `% accounts.len()` replaced by `/ accounts.len()` or `+ accounts.len()`" ); } + +#[test] +fn biased_nonce_is_always_in_genesis_range() { + // Every possible nonce byte must reduce into 0..=3. This rules out the + // `/` and `+` variants of the `% 4` reduction, which escape that range. + for byte in 0..=u8::MAX { + let (nonce, _) = biased_valid_nonce_amount(byte, 0, 0); + assert!( + nonce <= 3, + "byte {byte} produced out-of-range nonce {nonce}" + ); + } +} + +#[test] +fn biased_nonce_wraps_modulo_four() { + // Pin specific residues so `/ 4` (→1, →63) and `+ 4` (→8, →259) both fail. + assert_eq!(biased_valid_nonce_amount(4, 0, 0).0, 0); + assert_eq!(biased_valid_nonce_amount(255, 0, 0).0, 3); + assert_eq!(biased_valid_nonce_amount(7, 0, 0).0, 3); +} + +#[test] +fn biased_amount_never_exceeds_balance() { + for balance in [0_u128, 1, 100, u128::MAX] { + for amount_raw in [0_u128, 1, balance, balance.wrapping_add(1), u128::MAX] { + let (_, amount) = biased_valid_nonce_amount(0, amount_raw, balance); + assert!( + amount <= balance, + "amount {amount} exceeded balance {balance} (raw {amount_raw})" + ); + } + } +} + +#[test] +fn biased_amount_wraps_modulo_balance_plus_one() { + // `10 % 101 == 10` but `10 / 101 == 0`, so this kills the `/` variant. + assert_eq!(biased_valid_nonce_amount(0, 10, 100).1, 10); + // balance 0 → modulus 1 → amount always 0. + assert_eq!(biased_valid_nonce_amount(0, u128::MAX, 0).1, 0); +}