Merge pull request #9 from logos-blockchain/chore-sync-with-lez

chore: Sync with LEZ - circuits installation
This commit is contained in:
Roman Zajic 2026-06-30 10:26:46 +02:00 committed by GitHub
commit ee5d5e4e68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 953 additions and 1343 deletions

View File

@ -14,7 +14,7 @@ inputs:
`just update-lez`, replace this SHA, and open a PR. The scheduled
lez-compat workflow overrides this with `main` to detect upstream drift.
required: false
default: dac429a94af932b0c827544fff8b9de85b83e6f3
default: e37876a64028a335eb693198a1ed6a0e875ec5b4
runs:
using: composite

View File

@ -80,10 +80,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/setup-libfuzzer
- name: Build fuzz target
@ -134,10 +130,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/setup-afl
- name: Build AFL++ target

View File

@ -75,11 +75,6 @@ jobs:
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up AFL++ toolchain
uses: ./.github/actions/setup-afl
@ -270,11 +265,6 @@ jobs:
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Rust nightly + llvm-tools-preview
uses: dtolnay/rust-toolchain@nightly
with:
@ -376,11 +366,6 @@ jobs:
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Rust nightly + llvm-tools-preview
uses: dtolnay/rust-toolchain@nightly
with:

View File

@ -64,11 +64,6 @@ jobs:
target
key: fuzz-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/setup-libfuzzer
- name: Build fuzz target
@ -236,10 +231,6 @@ jobs:
- uses: actions/checkout@v4
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/setup-libfuzzer
- name: Reproduce corpus
run: |
@ -256,10 +247,6 @@ jobs:
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- uses: dtolnay/rust-toolchain@stable
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: cargo test -p fuzz_props --release
- name: Run tests which require RISC0_DEV_MODE off.
run: env -u RISC0_DEV_MODE cargo test -p fuzz_props --release synthesized_proof_is_rejected_without_dev_mode
@ -273,10 +260,6 @@ jobs:
- uses: actions/checkout@v4
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/setup-libfuzzer
- name: Measure throughput (30 s per target)
run: |

View File

@ -8,6 +8,9 @@ on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
push:
branches:
- chore-sync-with-lez
env:
RISC0_DEV_MODE: "1"
@ -39,11 +42,6 @@ jobs:
target
key: lez-compat-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build shared fuzz harness against upstream main
run: cargo build -p fuzz_props --release

View File

@ -53,11 +53,6 @@ jobs:
- name: Checkout logos-execution-zone
uses: ./.github/actions/checkout-lez
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install nightly toolchain with clippy
uses: dtolnay/rust-toolchain@nightly
with:

View File

@ -36,11 +36,6 @@ jobs:
- name: Install stable Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Cache cargo registry
uses: actions/cache@v4
with:
@ -127,11 +122,6 @@ jobs:
with:
components: llvm-tools-preview
- name: Install logos-blockchain-circuits
uses: ./logos-execution-zone/.github/actions/install-logos-blockchain-circuits
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Cache cargo registry
uses: actions/cache@v4
with:

570
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -59,8 +59,10 @@ nssa_core = { path = "../logos-execution-zone/lee/state_machine/core
common = { path = "../logos-execution-zone/lez/common" }
key_protocol = { path = "../logos-execution-zone/lee/key_protocol" }
testnet_initial_state = { path = "../logos-execution-zone/lez/testnet_initial_state" }
token_core = { path = "../logos-execution-zone/programs/token/core" }
test_program_methods = { path = "../logos-execution-zone/test_program_methods" }
programs = { path = "../logos-execution-zone/lez/programs" }
system_accounts = { path = "../logos-execution-zone/lez/system_accounts" }
token_core = { path = "../logos-execution-zone/lez/programs/token/core" }
test_program_methods = { path = "../logos-execution-zone/test_programs", package = "test_programs" }
# ── Third-party dependencies (versions mirrored from logos-execution-zone) ────
anyhow = "1.0.98"

Binary file not shown.

Binary file not shown.

Binary file not shown.

1436
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,8 @@ nssa_core = { path = "../../logos-execution-zone/lee/state_machine/core", pa
common = { path = "../../logos-execution-zone/lez/common" }
fuzz_props = { path = "../fuzz_props" }
testnet_initial_state = { path = "../../logos-execution-zone/lez/testnet_initial_state" }
programs = { path = "../../logos-execution-zone/lez/programs" }
system_accounts = { path = "../../logos-execution-zone/lez/system_accounts" }
[profile.release]
debug = true

View File

@ -36,7 +36,6 @@ use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::arbitrary_types::ArbLeeTransaction;
use fuzz_props::generators::{arbitrary_fuzz_state, signer_account_ids};
use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness};
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -60,7 +59,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
return;
};
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// ── Split path: validate → apply ─────────────────────────────────────────
// `validate_on_state` borrows `tx`; the transaction is still usable after.

View File

@ -12,7 +12,7 @@
//! compute_digest_for_path(c, proof) → canonical leaf→root recomputation
//! ```
//!
//! Inserting commitments via `V03State::new_with_genesis_accounts` therefore
//! Inserting commitments via `fuzz_props::genesis::genesis_state` therefore
//! drives `insert`, `root`/`root_index`, `get_authentication_path_for`, `depth`,
//! `get_node`/`set_node`, and — once the count exceeds the genesis capacity (32)
//! — `reallocate_to_double_capacity` and `prev_power_of_two`.
@ -47,7 +47,6 @@
use std::collections::HashSet;
use nssa::V03State;
use nssa_core::{
Commitment, Nullifier,
account::{Account, AccountId},
@ -78,7 +77,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
let commitments: Vec<Commitment> = pairs.iter().map(|(c, _)| c.clone()).collect();
// Genesis inserts DUMMY_COMMITMENT at index 0, then our commitments at 1..=N.
let state = V03State::new_with_genesis_accounts(&[], pairs, 0);
let state = fuzz_props::genesis::genesis_state(&[], pairs);
let digest = state.commitment_set_digest();
let mut indices: Vec<usize> = Vec::with_capacity(commitments.len());

View File

@ -36,7 +36,6 @@
use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
use fuzz_props::invariants::{BalanceSnapshot, NonceSnapshot, assert_tx_execution_invariants};
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -51,7 +50,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
.map(|a| (a.account_id, a.balance))
.collect();
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let mut state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Record starting balances for the long-range conservation check.
let starting_total: u128 = init_accs

View File

@ -16,8 +16,9 @@
//! A single `\x00` seed is sufficient — Part 1 uses fixed inputs and catches the
//! `delete-!` mutation without fuzz-driven state.
use nssa::{Account, AccountId, V03State, system_faucet_account_id};
use nssa::{Account, AccountId, V03State};
use nssa_core::{Commitment, Nullifier};
use system_accounts::faucet_account_id;
fuzz_props::fuzz_entry!(|data: &[u8]| {
// ── Part 1: State with nullifiers — Borsh round-trip ─────────────────────
@ -37,10 +38,9 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
let comm2 = Commitment::new(&AccountId::new([0x22_u8; 32]), &Account::default());
// Build a state that holds two nullifiers in its private state.
let state = V03State::new_with_genesis_accounts(
&[(system_faucet_account_id(), 0)],
let state = fuzz_props::genesis::genesis_state(
&[(faucet_account_id(), 0)],
vec![(comm1, null1), (comm2, null2)],
0,
);
// Serialise the state:

View File

@ -36,7 +36,7 @@ use fuzz_props::invariants::{
StateIsolationOnFailure, assert_nonce_increment_correctness, assert_replay_rejection,
};
use fuzz_props::privacy::arb_privacy_preserving_tx;
use nssa::{AccountId, V03State};
use nssa::AccountId;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -50,7 +50,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
.iter()
.map(|a| (a.account_id, a.balance))
.collect();
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let mut state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Apply a short sequence so multi-transaction state evolution (commitment growth,
// signer-nonce advance) is exercised. Each transaction's proof is synthesised against

View File

@ -24,7 +24,6 @@
use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::arbitrary_types::ArbProgramDeploymentTransaction;
use fuzz_props::generators::arbitrary_fuzz_state;
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -46,7 +45,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
};
let tx = tx_wrap.0;
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let mut state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Capture per-account state snapshots before the deployment attempt.
let balances_before: Vec<u128> = init_accs

View File

@ -25,7 +25,6 @@
use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
use fuzz_props::invariants::{BalanceSnapshot, NonceSnapshot, assert_tx_execution_invariants};
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -39,7 +38,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
.iter()
.map(|a| (a.account_id, a.balance))
.collect();
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let mut state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Mix correlated transactions (correctly signed, referencing a fuzz account)
// with random ones. Correlated transactions have a higher chance of being

View File

@ -40,7 +40,6 @@ use std::collections::HashSet;
use arbitrary::{Arbitrary, Unstructured};
use common::transaction::{LeeTransaction, clock_invocation};
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -67,7 +66,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
let timestamp: u64 = u64::arbitrary(&mut u).unwrap_or(1_000);
// Shared base state — cloned once for each pipeline.
let base_state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let base_state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Track all account IDs touched by accepted transactions so we can compare
// them across both pipelines after the full block is applied.

View File

@ -22,7 +22,7 @@ use arbitrary::{Arbitrary, Unstructured};
use common::transaction::LeeTransaction;
use fuzz_props::arbitrary_types::ArbPublicTransaction;
use fuzz_props::generators::arbitrary_fuzz_state;
use nssa::{V03State, ValidatedStateDiff};
use nssa::ValidatedStateDiff;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -36,7 +36,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
.iter()
.map(|a| (a.account_id, a.balance))
.collect();
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Generate the public transaction from remaining fuzz bytes.
let pub_tx = match ArbPublicTransaction::arbitrary(&mut u) {

View File

@ -3,7 +3,6 @@
use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state, arbitrary_transaction};
use fuzz_props::invariants::{BalanceSnapshot, NonceSnapshot, assert_tx_execution_invariants};
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -22,7 +21,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
.collect();
// Construct the initial state
let mut state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let mut state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Generate up to 8 transactions and apply them
let n_txs: u8 = u8::arbitrary(&mut u).unwrap_or(0) % 8;

View File

@ -9,10 +9,9 @@ use common::transaction::LeeTransaction;
use fuzz_props::arbitrary_types::ArbPrivateKey;
use fuzz_props::generators::{arb_fuzz_native_transfer, arbitrary_fuzz_state};
use nssa::{
AccountId, PrivateKey, PublicKey, ValidatedStateDiff, V03State,
AccountId, PrivateKey, PublicKey, ValidatedStateDiff,
public_transaction::{Message, WitnessSet},
PublicTransaction,
program::Program,
};
use nssa_core::account::Nonce;
@ -33,7 +32,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
let nonces = vec![Nonce::from(0_u128), Nonce::from(0_u128)];
let message = Message::try_new(
Program::authenticated_transfer_program().id(),
programs::authenticated_transfer().id(),
vec![addr1, addr2],
nonces,
1337_u64,
@ -139,7 +138,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
if signer_addr != other1 && signer_addr != other2 {
let nonces = vec![Nonce::from(0_u128)];
if let Ok(msg) = Message::try_new(
Program::authenticated_transfer_program().id(),
programs::authenticated_transfer().id(),
vec![other1, other2],
nonces,
7_u64,
@ -173,7 +172,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
.iter()
.map(|a| (a.account_id, a.balance))
.collect();
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
let Ok(tx) = arb_fuzz_native_transfer(&mut u, &fuzz_accs) else {
return;
@ -235,7 +234,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
let nonces = vec![Nonce::from(0_u128)];
if let Ok(msg) = Message::try_new(
Program::authenticated_transfer_program().id(),
programs::authenticated_transfer().id(),
vec![addr],
nonces,
42_u64,

View File

@ -28,7 +28,6 @@ use arbitrary::{Arbitrary, Unstructured};
use fuzz_props::arbitrary_types::ArbLeeTransaction;
use fuzz_props::generators::{arbitrary_fuzz_state, signer_account_ids};
use fuzz_props::invariants::{NonceSnapshot, assert_nonce_increment_correctness};
use nssa::V03State;
fuzz_props::fuzz_entry!(|data: &[u8]| {
let mut u = Unstructured::new(data);
@ -55,7 +54,7 @@ fuzz_props::fuzz_entry!(|data: &[u8]| {
// Stateless gate — skip structurally malformed transactions.
let Ok(tx) = tx.transaction_stateless_check() else { return; };
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
let state = fuzz_props::genesis::genesis_state(&init_accs, vec![]);
// Capture nonces of all known accounts before execution so that
// assert_nonce_increment_correctness can verify the +1 step on success.

View File

@ -21,6 +21,10 @@ risc0-zkvm = { workspace = true }
proptest = "1.4"
arbitrary = { version = "1", features = ["derive"] }
testnet_initial_state = { workspace = true }
# Reproduce LEZ genesis (builtin programs + system accounts) in `genesis_state`,
# which LEZ moved out of the state machine into these crates.
programs = { workspace = true }
system_accounts = { workspace = true }
[dev-dependencies]
proptest = "1.4"

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,
@ -65,9 +78,9 @@ pub struct FuzzAccount {
/// The cap above is only sound if every generated balance survives genesis construction
/// unchanged. Two failure modes break that:
///
/// * **Reserved system accounts.** [`nssa::V03State::new_with_genesis_accounts`] inserts
/// the faucet account (`balance = u128::MAX`) and bridge account *after* the supplied
/// genesis accounts, overwriting any generated account whose ID collides. A fuzzer that
/// * **Reserved system accounts.** [`crate::genesis::genesis_state`] inserts the faucet
/// account (`balance = u128::MAX`) and bridge account *after* the supplied genesis
/// accounts, overwriting any generated account whose ID collides. A fuzzer that
/// lands on the faucet ID would make a caller read back `u128::MAX` instead of the capped
/// balance it generated, overflowing the conservation sum — a harness false positive, not
/// a protocol bug.
@ -81,19 +94,21 @@ pub struct FuzzAccount {
/// accounts are returned (an empty state is a valid degenerate case).
pub fn arbitrary_fuzz_state(u: &mut Unstructured<'_>) -> arbitrary::Result<Vec<FuzzAccount>> {
let reserved = [
nssa::system_faucet_account_id(),
nssa::system_bridge_account_id(),
system_accounts::faucet_account_id(),
system_accounts::bridge_account_id(),
];
let n = ((u8::arbitrary(u)? as usize) % 8) + 1; // 1..=8
let mut seen = std::collections::HashSet::with_capacity(n);
let mut accounts = Vec::with_capacity(n);
for _ in 0..n {
let account_id = ArbAccountId::arbitrary(u)?.0;
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);
// 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.
let balance = u128::arbitrary(u)? / 8;
let private_key = ArbPrivateKey::arbitrary(u)?.0;
// Skip IDs that genesis would overwrite (reserved system accounts) or that would
// collapse on insertion (duplicates); see the doc comment above.

64
fuzz_props/src/genesis.rs Normal file
View File

@ -0,0 +1,64 @@
//! Genesis state construction for fuzz targets and tests.
//!
//! LEZ moved builtin-program and system-account assembly out of the state machine
//! (the former `V03State::new_with_genesis_accounts`) into the `programs` /
//! `system_accounts` crates. [`genesis_state`] reproduces that genesis setup so fuzz
//! targets and tests can build a realistic starting state from arbitrary account data.
use nssa::{Account, AccountId, V03State};
use nssa_core::{Commitment, Nullifier};
/// Build a genesis [`V03State`] from the given public account balances and private accounts.
///
/// Mirrors the former `V03State::new_with_genesis_accounts(balances, private_accounts, 0)`:
/// every public account is owned by the authenticated-transfer program, the faucet/bridge/clock
/// system accounts are present, and the eight builtin programs are registered. The genesis
/// timestamp is fixed at 0, matching `system_accounts::clock_account()`'s default (every former
/// caller passed `0`).
#[must_use]
pub fn genesis_state(
balances: &[(AccountId, u128)],
private_accounts: Vec<(Commitment, Nullifier)>,
) -> V03State {
let public_accounts = balances
.iter()
.map(|&(account_id, balance)| {
(
account_id,
Account {
program_owner: programs::authenticated_transfer().id(),
balance,
..Account::default()
},
)
})
.chain([
(
system_accounts::faucet_account_id(),
system_accounts::faucet_account(),
),
(
system_accounts::bridge_account_id(),
system_accounts::bridge_account(),
),
])
.chain(
system_accounts::clock_account_ids()
.into_iter()
.map(|clock_id| (clock_id, system_accounts::clock_account())),
);
V03State::new()
.with_public_accounts(public_accounts)
.with_private_accounts(private_accounts)
.with_programs([
programs::authenticated_transfer(),
programs::token(),
programs::amm(),
programs::clock(),
programs::ata(),
programs::vault(),
programs::faucet(),
programs::bridge(),
])
}

View File

@ -68,6 +68,7 @@
pub mod arbitrary_types;
pub mod generators;
pub mod genesis;
pub mod invariants;
pub mod privacy;

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`
}

View File

@ -98,8 +98,8 @@ fn fuzz_state_excludes_reserved_system_ids() {
// would read back a balance the cap never produced, overflowing conservation sums.
// The generator must therefore never emit a reserved system ID.
let reserved = [
nssa::system_faucet_account_id(),
nssa::system_bridge_account_id(),
system_accounts::faucet_account_id(),
system_accounts::bridge_account_id(),
];
let buf = distinct_byte_buffer(10_000);
let mut u = Unstructured::new(&buf);

View File

@ -9,7 +9,7 @@ use nssa::V03State;
use nssa_core::account::Nonce;
fn make_empty_state() -> V03State {
V03State::new_with_genesis_accounts(&[], vec![], 0)
crate::genesis::genesis_state(&[], vec![])
}
fn make_empty_snapshot() -> BalanceSnapshot {
@ -49,8 +49,8 @@ fn assert_invariants_does_not_panic_on_success_with_empty_state() {
#[test]
fn balance_conservation_catches_inflation_on_success() {
let acc_id = nssa::AccountId::new([1_u8; 32]);
let state_before = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let state_after = V03State::new_with_genesis_accounts(&[(acc_id, 200)], vec![], 0);
let state_before = crate::genesis::genesis_state(&[(acc_id, 100)], vec![]);
let state_after = crate::genesis::genesis_state(&[(acc_id, 200)], vec![]);
let mut balances = std::collections::HashMap::new();
balances.insert(acc_id, 100_u128);
@ -83,7 +83,7 @@ fn nonce_increment_correctness_passes_when_signer_not_in_snapshot() {
#[test]
fn nonce_increment_correctness_catches_unchanged_nonce() {
let acc_id = nssa::AccountId::new([3_u8; 32]);
let state = V03State::new_with_genesis_accounts(&[], vec![], 0);
let state = crate::genesis::genesis_state(&[], vec![]);
let mut nonces = std::collections::HashMap::new();
nonces.insert(acc_id, Nonce(5));
@ -97,8 +97,8 @@ fn nonce_increment_correctness_catches_unchanged_nonce() {
#[test]
fn failed_tx_nonce_stability_catches_nonce_mutation() {
let acc_id = nssa::AccountId::new([2_u8; 32]);
let state_before = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let state_after = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let state_before = crate::genesis::genesis_state(&[(acc_id, 100)], vec![]);
let state_after = crate::genesis::genesis_state(&[(acc_id, 100)], vec![]);
let mut nonces = std::collections::HashMap::new();
nonces.insert(acc_id, Nonce(1));
@ -208,7 +208,7 @@ fn failed_tx_nonce_stability_name_is_nonempty_and_not_placeholder() {
fn state_isolation_check_detects_balance_change_on_failure() {
let acc_id = nssa::AccountId::new([1_u8; 32]);
// State has balance 100 for acc_id.
let state = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let state = crate::genesis::genesis_state(&[(acc_id, 100)], vec![]);
// balances_before claims balance was 50, but state_after (== state) has 100.
let mut balances = std::collections::HashMap::new();
@ -251,7 +251,7 @@ fn assert_replay_rejection_panics_when_replay_not_rejected() {
let validated = tx
.transaction_stateless_check()
.expect("test setup: transaction must pass stateless validation");
let mut scratch_state = V03State::new_with_genesis_accounts(&genesis, vec![], 0);
let mut scratch_state = crate::genesis::genesis_state(&genesis, vec![]);
let applied_tx = validated
.execute_check_on_state(&mut scratch_state, 1, 1)
.expect("test setup: first execution must succeed (block_id=1, timestamp=1)");
@ -259,7 +259,7 @@ fn assert_replay_rejection_panics_when_replay_not_rejected() {
// Replay `applied_tx` (nonce 0) against a FRESH state still at nonce 0.
// The nonce matches → execute_check_on_state ACCEPTS the replay — a protocol
// violation that assert_replay_rejection must detect and panic on.
let mut fresh_state = V03State::new_with_genesis_accounts(&genesis, vec![], 0);
let mut fresh_state = crate::genesis::genesis_state(&genesis, vec![]);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_replay_rejection(applied_tx, &mut fresh_state, 1, 1);
}));
@ -277,8 +277,8 @@ fn assert_replay_rejection_panics_when_replay_not_rejected() {
fn assert_tx_execution_invariants_is_not_noop() {
let acc_id = nssa::AccountId::new([5_u8; 32]);
// Both state_before and state_after have the account at balance 100.
let state_before = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let mut state_after = V03State::new_with_genesis_accounts(&[(acc_id, 100)], vec![], 0);
let state_before = crate::genesis::genesis_state(&[(acc_id, 100)], vec![]);
let mut state_after = crate::genesis::genesis_state(&[(acc_id, 100)], vec![]);
// Lie: claim balance was 50 before. State_after shows 100.
// With execution_succeeded=false, StateIsolationOnFailure detects the discrepancy.

View File

@ -5,7 +5,7 @@ use crate::privacy::{
arb_account, arb_privacy_preserving_tx, arb_validity_window, synthesize_passing_proof,
};
use nssa::privacy_preserving_transaction::{Message as PPMessage, WitnessSet as PPWitnessSet};
use nssa::{AccountId, PrivacyPreservingTransaction, PrivateKey, V03State};
use nssa::{AccountId, PrivacyPreservingTransaction, PrivateKey};
use nssa_core::Commitment;
use nssa_core::account::Account;
use nssa_core::program::{BlockValidityWindow, TimestampValidityWindow};
@ -25,7 +25,7 @@ fn synthesized_proof_reaches_checks_5_6_and_applies() {
return;
}
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
let mut state = crate::genesis::genesis_state(&[], vec![]);
// No signers and a single fresh commitment: checks 13 are vacuous/trivially met, so
// the only way to reach checks 56 is for the synthesised proof to pass check 4.
@ -84,7 +84,7 @@ fn synthesized_proof_is_rejected_without_dev_mode() {
return;
}
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0);
let mut state = crate::genesis::genesis_state(&[], vec![]);
// Same well-formed message as the positive test: checks 13 are vacuous/trivially met, so a
// rejection can only come from check 4 (proof verification) failing on the fake receipt.
@ -303,7 +303,7 @@ fn arb_privacy_preserving_tx_generator_invariants() {
.collect();
let genesis: Vec<(AccountId, u128)> =
accounts.iter().map(|a| (a.account_id, a.balance)).collect();
let state = V03State::new_with_genesis_accounts(&genesis, vec![], 0);
let state = crate::genesis::genesis_state(&genesis, vec![]);
let known_ids: std::collections::HashSet<AccountId> =
accounts.iter().map(|a| a.account_id).collect();

View File

@ -9,7 +9,7 @@ fn make_test_state() -> V03State {
.iter()
.map(|(id, _)| (*id, 1_000_000_u128))
.collect();
V03State::new_with_genesis_accounts(&init_accs, vec![], 0)
crate::genesis::genesis_state(&init_accs, vec![])
}
proptest! {