From 759d4a849eece33359f0b7a9a4747af1f369d953 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 11 Jun 2026 17:37:21 +0800 Subject: [PATCH] test: address mutants discovered during fuzzing --- .../privacy_preserving_transaction/circuit.rs | 11 ++++ lee/state_machine/src/program.rs | 14 +++++ .../program_deployment_transaction/message.rs | 15 +++++ lee/state_machine/src/state.rs | 56 +++++++++++++++++++ lee/state_machine/src/validated_state_diff.rs | 38 +++++++++++++ lez/common/src/config.rs | 30 ++++++++++ lez/common/src/lib.rs | 12 ++++ lez/common/src/transaction.rs | 44 +++++++++++++++ 8 files changed, 220 insertions(+) diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs index cebef4cf..6c986550 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -210,6 +210,17 @@ mod tests { kind } + #[test] + fn proof_inner_roundtrip() { + // `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches + // mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`, + // and of `from_inner` discarding its argument. + let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF]; + assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes); + assert!(Proof::from_inner(vec![]).into_inner().is_empty()); + assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]); + } + #[test] fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { let recipient_keys = test_private_account_keys_1(); diff --git a/lee/state_machine/src/program.rs b/lee/state_machine/src/program.rs index 9692a5ee..c4223810 100644 --- a/lee/state_machine/src/program.rs +++ b/lee/state_machine/src/program.rs @@ -498,6 +498,20 @@ mod tests { } } + #[test] + fn elf_returns_the_program_bytecode_constant() { + // `Program::elf` must return exactly the compile-time ELF, never an empty + // or placeholder slice. Catches mutations returning `Vec::leak(Vec::new())`, + // `Vec::leak(vec![0])`, or `Vec::leak(vec![1])`. + let at = Program::authenticated_transfer_program(); + assert!(!at.elf().is_empty()); + assert_eq!(at.elf(), AUTHENTICATED_TRANSFER_ELF); + + let token = Program::token(); + assert!(!token.elf().is_empty()); + assert_eq!(token.elf(), TOKEN_ELF); + } + #[test] fn program_execution() { let program = Program::simple_balance_transfer(); diff --git a/lee/state_machine/src/program_deployment_transaction/message.rs b/lee/state_machine/src/program_deployment_transaction/message.rs index a51e4149..866399e8 100644 --- a/lee/state_machine/src/program_deployment_transaction/message.rs +++ b/lee/state_machine/src/program_deployment_transaction/message.rs @@ -16,3 +16,18 @@ impl Message { self.bytecode } } + +#[cfg(test)] +mod tests { + use super::Message; + + #[test] + fn bytecode_roundtrip() { + // `Message::new(b).into_bytecode()` must return exactly `b`. Catches + // mutations of `into_bytecode` returning `vec![]`, `vec![0]`, or `vec![1]`. + let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic + assert_eq!(Message::new(bytecode.clone()).into_bytecode(), bytecode); + assert!(Message::new(vec![]).into_bytecode().is_empty()); + assert_eq!(Message::new(vec![0xAB]).into_bytecode(), vec![0xAB_u8]); + } +} diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index 4b74cf55..6034858c 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -613,6 +613,62 @@ pub mod tests { PublicTransaction::new(message, witness_set) } + #[test] + fn genesis_system_accounts_have_expected_contents() { + // System-account IDs must be distinct and non-default, and the genesis + // faucet/bridge accounts must carry their expected field values. Catches + // mutations that replace `system_faucet_account`/`system_bridge_account` + // with `Default::default()`, delete their `balance`/`program_owner` + // fields, or replace `system_bridge_account_id` with `Default::default()`. + let faucet_id = system_faucet_account_id(); + let bridge_id = system_bridge_account_id(); + assert_ne!(bridge_id, AccountId::default()); + assert_ne!(faucet_id, bridge_id); + + let state = V03State::new_with_genesis_accounts(&[], vec![], 0); + let default_owner = Account::default().program_owner; + + let faucet = state.get_account_by_id(faucet_id); + assert_eq!(faucet.balance, u128::MAX, "faucet must hold u128::MAX"); + assert_ne!( + faucet.program_owner, default_owner, + "faucet must have a non-default program_owner" + ); + + let bridge = state.get_account_by_id(bridge_id); + assert_ne!( + bridge.program_owner, default_owner, + "bridge must have a non-default program_owner" + ); + } + + #[test] + fn genesis_commitment_set_digest_differs_from_empty_state() { + // The genesis state inserts DUMMY_COMMITMENT, so its commitment-set digest + // must differ from a freshly-created empty state's all-zero root. Catches + // the mutation that replaces `commitment_set_digest` with `Default::default()`. + let genesis = V03State::new_with_genesis_accounts(&[], vec![], 0); + let empty = V03State::new(); + assert_ne!( + genesis.commitment_set_digest(), + empty.commitment_set_digest() + ); + } + + #[test] + fn add_pinata_token_program_sets_non_default_owner_and_data() { + // The account created by `add_pinata_token_program` must have a non-default + // `program_owner` and non-default `data`. Catches deletion of either field + // from the struct literal. + let id = AccountId::new([0xAB_u8; 32]); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + state.add_pinata_token_program(id); + let acc = state.get_account_by_id(id); + let default = Account::default(); + assert_ne!(acc.program_owner, default.program_owner); + assert_ne!(acc.data, default.data); + } + #[test] fn new_with_genesis() { let key1 = PrivateKey::try_new([1; 32]).unwrap(); diff --git a/lee/state_machine/src/validated_state_diff.rs b/lee/state_machine/src/validated_state_diff.rs index cfbd1703..3bd77cae 100644 --- a/lee/state_machine/src/validated_state_diff.rs +++ b/lee/state_machine/src/validated_state_diff.rs @@ -526,6 +526,44 @@ mod tests { validated_state_diff::ValidatedStateDiff, }; + #[test] + fn public_diff_reflects_a_successful_transfer() { + // A successful native transfer must record the debited sender in + // `public_diff()`. Catches the mutation that replaces `public_diff` with + // `HashMap::new()` (which would hide every account change). + use authenticated_transfer_core::Instruction as AtInstruction; + + let from_key = PrivateKey::try_new([1_u8; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); + let to_key = PrivateKey::try_new([2_u8; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + + let state = V03State::new_with_genesis_accounts(&[(from, 100)], vec![], 0); + let program_id = Program::authenticated_transfer_program().id(); + let message = Message::try_new( + program_id, + vec![from, to], + vec![Nonce(0), Nonce(0)], + AtInstruction::Transfer { amount: 5 }, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, &[&from_key, &to_key]); + let tx = crate::PublicTransaction::new(message, witness_set); + + let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) + .expect("a valid native transfer must validate"); + let public_diff = diff.public_diff(); + + assert!( + public_diff.contains_key(&from), + "public_diff must contain the debited sender", + ); + assert_eq!( + public_diff[&from].balance, 95, + "sender balance in the diff must reflect the debit", + ); + } + /// Privacy-path version of the authorization-injection attack. The test passes when the /// attack is rejected and the victim's balance is left untouched. /// diff --git a/lez/common/src/config.rs b/lez/common/src/config.rs index c076f699..b6ceeba4 100644 --- a/lez/common/src/config.rs +++ b/lez/common/src/config.rs @@ -53,3 +53,33 @@ impl From for BasicAuthCredentials { Self::new(value.username, value.password) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::BasicAuth; + + #[test] + fn parse_preserves_non_empty_password() { + let auth = BasicAuth::from_str("user:secret").expect("must parse"); + assert_eq!(auth.username, "user"); + assert_eq!(auth.password.as_deref(), Some("secret")); + } + + #[test] + fn parse_empty_password_is_none() { + // A trailing colon means an empty password, which must become `None`. + // Catches deletion of `!` in `.filter(|p| !p.is_empty())`, which would + // instead yield `Some("")`. + let auth = BasicAuth::from_str("user:").expect("must parse"); + assert_eq!(auth.password, None); + } + + #[test] + fn parse_username_only_has_no_password() { + let auth = BasicAuth::from_str("alice").expect("must parse"); + assert_eq!(auth.username, "alice"); + assert_eq!(auth.password, None); + } +} diff --git a/lez/common/src/lib.rs b/lez/common/src/lib.rs index a7744d63..cfbbbd9b 100644 --- a/lez/common/src/lib.rs +++ b/lez/common/src/lib.rs @@ -93,4 +93,16 @@ mod tests { let deserialized = HashType::from_str(&serialized).unwrap(); assert_eq!(original, deserialized); } + + #[test] + fn as_ref_returns_exact_inner_bytes() { + // `HashType::as_ref` must return exactly the inner `[u8; 32]` — not an + // empty slice or a placeholder. Catches mutations of `as_ref` that return + // `Vec::leak(Vec::new())`, `vec![0]`, or `vec![1]`. + let known = [0x42_u8; 32]; + let hash = HashType(known); + assert_eq!(hash.as_ref(), &known); + assert_eq!(hash.as_ref().len(), 32); + assert_eq!(HashType([0_u8; 32]).as_ref().len(), 32); + } } diff --git a/lez/common/src/transaction.rs b/lez/common/src/transaction.rs index 42a7b761..6f6a4425 100644 --- a/lez/common/src/transaction.rs +++ b/lez/common/src/transaction.rs @@ -188,3 +188,47 @@ fn validate_doesnt_modify_account( Ok(()) } } + +#[cfg(test)] +mod tests { + use lee::{ + AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PrivateKey, PublicKey, V03State, + system_bridge_account_id, system_faucet_account_id, + }; + + use crate::test_utils::create_transaction_native_token_transfer; + + #[test] + fn system_account_ids_are_distinct_and_non_default() { + let faucet = system_faucet_account_id(); + let bridge = system_bridge_account_id(); + assert_ne!(faucet, AccountId::default()); + assert_ne!(bridge, AccountId::default()); + assert_ne!(faucet, bridge); + } + + #[test] + fn validate_on_state_rejects_modifying_a_system_account() { + // A native transfer that credits a clock system account *changes* that + // account, so `validate_doesnt_modify_account` must reject it. Catches + // the `!=` → `==` inversion at `validate_doesnt_modify_account` (a changed + // account would no longer be flagged) and `public_diff → HashMap::new()` + // (an empty diff hides the modification). + let sender_key = PrivateKey::try_new([5_u8; 32]).expect("valid key"); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000)], vec![], 0); + + let tx = create_transaction_native_token_transfer( + sender_id, + 0, + CLOCK_01_PROGRAM_ACCOUNT_ID, + 100, + &sender_key, + ); + + assert!( + tx.validate_on_state(&state, 1, 0).is_err(), + "validate_on_state must reject a transfer that credits a clock system account", + ); + } +}