fix: derive account_id from private_key

This commit is contained in:
Roman 2026-06-26 14:06:37 +08:00
parent e4c4d1eca7
commit e8cd1a767e
No known key found for this signature in database
GPG Key ID: 583BDF43C238B83E
2 changed files with 34 additions and 17 deletions

View File

@ -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<nssa:
}
}
/// The public-account [`AccountId`] that a transaction signed with `key` will have as its
/// signer — i.e. exactly what the validator derives from the witness set.
///
/// Centralises the `AccountId::from(&PublicKey::new_from_private_key(key))` derivation that
/// funded-account generation and the privacy synthesiser both depend on, so the funded
/// account and its signer never drift apart again.
#[must_use]
pub fn account_id_for_key(key: &PrivateKey) -> 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<Vec<FuzzAccount>> {
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)

View File

@ -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<PrivacyPreservingTransaction> {
// ── 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<AccountId> = 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`
}