diff --git a/fuzz_props/src/generators.rs b/fuzz_props/src/generators.rs index ca79628c..a11488b8 100644 --- a/fuzz_props/src/generators.rs +++ b/fuzz_props/src/generators.rs @@ -1,8 +1,8 @@ use arbitrary::{Arbitrary, Unstructured}; use common::{block::HashableBlockData, transaction::LeeTransaction}; -use nssa::{AccountId, PrivateKey}; +use nssa::{AccountId, PrivateKey, PublicKey}; -use crate::arbitrary_types::{ArbAccountId, ArbLeeTransaction, ArbPrivateKey}; +use crate::arbitrary_types::{ArbLeeTransaction, ArbPrivateKey}; use proptest::prelude::*; use testnet_initial_state::initial_pub_accounts_private_keys; @@ -31,16 +31,29 @@ pub fn signer_account_ids(tx: &common::transaction::LeeTransaction) -> Vec AccountId { + AccountId::from(&PublicKey::new_from_private_key(key)) +} + // ── Fuzz-driven state generation ───────────────────────────────────────────── -/// An account with an arbitrary identifier, balance, and private key, -/// generated entirely from unstructured fuzzer bytes. +/// An account with a fuzz-driven balance and private key, plus the [`AccountId`] +/// **derived from that key**. /// -/// Using random account IDs (rather than the fixed `testnet_initial_state` set) -/// exposes state-dependent bugs that only manifest with specific account shapes — -/// for example: zero balance, [`u128::MAX`] balance, or a nonce at the -/// wrap-around boundary. The [`PrivateKey`] field lets downstream generators -/// produce correctly-signed transfers referencing accounts present in this state. +/// Deriving `account_id` from `private_key` (rather than drawing it independently) +/// is what makes the funded account and its signer the *same* account: a transfer +/// signed by `private_key` is then authorized to spend `account_id`, so downstream +/// generators like [`arb_fuzz_native_transfer`] can actually reach the **successful** +/// state-transition path instead of always being rejected as unauthorized. The key +/// is still fuzz-driven, so account shapes (zero balance, [`u128::MAX`] balance, +/// nonce wrap-around) remain controlled by the fuzzer. pub struct FuzzAccount { pub account_id: AccountId, pub balance: u128, @@ -62,12 +75,16 @@ pub struct FuzzAccount { pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result> { let n = ((u8::arbitrary(u)? as usize) % 8) + 1; // 1..=8 std::iter::repeat_with(|| { + let private_key = ArbPrivateKey::arbitrary(u)?.0; + // Derive the account id from the key so the funded account *is* the signer; + // otherwise every "biased-valid" transfer is unauthorized and rejected. + let account_id = account_id_for_key(&private_key); Ok(FuzzAccount { - account_id: ArbAccountId::arbitrary(u)?.0, + account_id, // Divide by 8 so the sum of 8 accounts is at most u128::MAX, preventing // false-positive checked_add panics that would mask real inflation bugs. balance: u128::arbitrary(u)? / 8, - private_key: ArbPrivateKey::arbitrary(u)?.0, + private_key, }) }) .take(n) diff --git a/fuzz_props/src/privacy.rs b/fuzz_props/src/privacy.rs index 180700d7..a45656f1 100644 --- a/fuzz_props/src/privacy.rs +++ b/fuzz_props/src/privacy.rs @@ -40,8 +40,7 @@ use arbitrary::{Arbitrary, Result as ArbResult, Unstructured}; use borsh::to_vec as borsh_to_vec; use nssa::{ - AccountId, PRIVACY_PRESERVING_CIRCUIT_ID, PrivacyPreservingTransaction, PrivateKey, PublicKey, - V03State, + AccountId, PRIVACY_PRESERVING_CIRCUIT_ID, PrivacyPreservingTransaction, PrivateKey, V03State, privacy_preserving_transaction::{ Message as PPMessage, WitnessSet as PPWitnessSet, circuit::Proof, }, @@ -54,7 +53,7 @@ use nssa_core::{ }; use risc0_zkvm::{FakeReceipt, InnerReceipt, ReceiptClaim}; -use crate::generators::FuzzAccount; +use crate::generators::{FuzzAccount, account_id_for_key}; /// Synthesise a [`Proof`] that **passes** `Proof::is_valid_for` for `message` against /// `state`, under `RISC0_DEV_MODE`. @@ -193,8 +192,9 @@ pub fn arb_privacy_preserving_tx( ) -> ArbResult { // ── Signers ────────────────────────────────────────────────────────────────────── // 0..=3 distinct signers drawn from the keyed fuzz accounts. A signer's public-account - // id is `AccountId::from(&its_public_key)` — exactly what the validator derives from the - // witness set — and is independent of `FuzzAccount.account_id`. + // id is `account_id_for_key(key)` — exactly what the validator derives from the witness + // set. Since `arbitrary_fuzz_state` now derives `FuzzAccount.account_id` the same way, + // this id also equals that account's `account_id`, so the funded account is the signer. let max_signers = accounts.len().min(3); let n_signers = if max_signers == 0 { 0 @@ -205,7 +205,7 @@ pub fn arb_privacy_preserving_tx( let mut signer_ids: Vec = Vec::with_capacity(n_signers); for _ in 0..n_signers { let key = &accounts[(u8::arbitrary(u)? as usize) % accounts.len()].private_key; - let id = AccountId::from(&PublicKey::new_from_private_key(key)); + let id = account_id_for_key(key); if signer_ids.contains(&id) { continue; // keep signer ids distinct so `nonces` stays 1:1 with `keys` }