diff --git a/fuzz/fuzz_targets/fuzz_block_verification.rs b/fuzz/fuzz_targets/fuzz_block_verification.rs index cfc4c2d..639dfb4 100644 --- a/fuzz/fuzz_targets/fuzz_block_verification.rs +++ b/fuzz/fuzz_targets/fuzz_block_verification.rs @@ -18,12 +18,10 @@ fuzz_target!(|data: &[u8]| { let recomputed2 = hashable.block_hash(); assert_eq!(recomputed, recomputed2, "block_hash() is not deterministic"); - // Log divergence between stored and recomputed hash for coverage guidance. - // We do NOT assert equality because adversarially-crafted fuzz inputs can - // store an arbitrary hash field without matching the body content. - let stored_hash = block.header.hash; - if stored_hash == recomputed { - // Hashes match — this is the expected case for a valid sequencer-produced block - let _ = stored_hash; - } + // We intentionally do NOT assert that the stored header hash equals the + // recomputed one: adversarially-crafted fuzz inputs can store an arbitrary + // hash field that does not match the body content, and that is a valid input + // for the purpose of this target (which only tests hash stability, not + // block validity). + let _ = (block.header.hash, recomputed); }); diff --git a/fuzz/fuzz_targets/fuzz_state_transition.rs b/fuzz/fuzz_targets/fuzz_state_transition.rs index b394d45..1ba2a22 100644 --- a/fuzz/fuzz_targets/fuzz_state_transition.rs +++ b/fuzz/fuzz_targets/fuzz_state_transition.rs @@ -21,7 +21,7 @@ fuzz_target!(|data: &[u8]| { // Generate up to 8 transactions and apply them let n_txs: u8 = u8::arbitrary(&mut u).unwrap_or(0) % 8; - for _ in 0..n_txs { + for i in 0..n_txs { let Ok(tx) = arbitrary_transaction(&mut u) else { break; }; @@ -34,8 +34,12 @@ fuzz_target!(|data: &[u8]| { // Clone state before to detect state leakage on failure let state_snapshot = state.clone(); - let block_id: u64 = 1; - let timestamp: u64 = 0; + // Advance block_id and timestamp each iteration so the state machine + // sees a realistic monotonically-increasing context. Using the same + // block_id=1 / timestamp=0 for every tx hides bugs that only manifest + // when the block context changes across a multi-transaction sequence. + let block_id: u64 = 1 + u64::from(i); + let timestamp: u64 = u64::from(i); let result = tx.execute_check_on_state(&mut state, block_id, timestamp); if result.is_err() { diff --git a/fuzz/fuzz_targets/fuzz_transaction_decoding.rs b/fuzz/fuzz_targets/fuzz_transaction_decoding.rs index 8c1eff8..fae71e5 100644 --- a/fuzz/fuzz_targets/fuzz_transaction_decoding.rs +++ b/fuzz/fuzz_targets/fuzz_transaction_decoding.rs @@ -13,7 +13,7 @@ fuzz_target!(|data: &[u8]| { let tx2 = borsh::from_slice::(&re_encoded) .expect("second decode of re-encoded tx must succeed"); assert_eq!( - borsh::to_vec(&tx).unwrap(), + re_encoded, borsh::to_vec(&tx2).unwrap(), "NSSATransaction roundtrip encoding divergence" ); diff --git a/fuzz_props/src/generators.rs b/fuzz_props/src/generators.rs index 84f8015..11eb3e9 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -1,6 +1,8 @@ use arbitrary::{Arbitrary, Unstructured}; use common::{block::HashableBlockData, transaction::NSSATransaction}; use nssa::{AccountId, PrivateKey}; + +use crate::arbitrary_types::ArbNSSATransaction; use proptest::prelude::*; use testnet_initial_state::initial_pub_accounts_private_keys; @@ -9,26 +11,15 @@ use testnet_initial_state::initial_pub_accounts_private_keys; /// A best-effort attempt to create a structurally plausible `NSSATransaction` /// from unstructured bytes. Falls back to raw borsh decoding. pub fn arbitrary_transaction(u: &mut Unstructured<'_>) -> arbitrary::Result { - // Prefer structured generation; raw decode as fallback + // Prefer structured generation (via Arbitrary impls); raw borsh decode as fallback. if bool::arbitrary(u)? { let raw = Vec::::arbitrary(u)?; borsh::from_slice::(&raw).map_err(|_| arbitrary::Error::IncorrectFormat) } else { - // Generate a minimal empty public tx using known test keys - let signing_key = PrivateKey::try_new([u8::arbitrary(u)?; 32]) - .map_err(|_| arbitrary::Error::IncorrectFormat)?; - let program_id = nssa::program::Program::authenticated_transfer_program().id(); - let message = nssa::public_transaction::Message::try_new( - program_id, - vec![], - vec![], - u128::arbitrary(u)?, - ) - .map_err(|_| arbitrary::Error::IncorrectFormat)?; - let witness = nssa::public_transaction::WitnessSet::for_message(&message, &[&signing_key]); - Ok(NSSATransaction::Public(nssa::PublicTransaction::new( - message, witness, - ))) + // Use the full ArbNSSATransaction generator, which produces both Public and + // ProgramDeployment variants with realistic account IDs, nonces, and witness sets — + // far richer than the previous degenerate single-byte key / empty-message path. + ArbNSSATransaction::arbitrary(u).map(|w| w.0) } } diff --git a/fuzz_props/src/invariants.rs b/fuzz_props/src/invariants.rs index c2ae85c..7ea34e9 100644 --- a/fuzz_props/src/invariants.rs +++ b/fuzz_props/src/invariants.rs @@ -44,11 +44,18 @@ impl ProtocolInvariant for StateIsolationOnFailure { fn check(&self, ctx: &InvariantCtx<'_>) -> Option { if ctx.result.is_err() { - // Capture snapshot totals for comparison - let _before_total = ctx.balances_before.total(); - let _state_after = ctx.state_after; - // TODO: implement actual balance extraction from V03State once API is confirmed - // (use state_after.get_account_by_id per known account and compare with before) + for (acc_id, &expected_balance) in &ctx.balances_before.0 { + let actual_balance = ctx.state_after.get_account_by_id(*acc_id).balance; + if actual_balance != expected_balance { + return Some(InvariantViolation { + invariant: self.name(), + message: format!( + "balance changed despite tx rejection: account {:?} had {expected_balance} before, {actual_balance} after", + acc_id, + ), + }); + } + } } None }