lez-fuzzing/fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs

76 lines
3.5 KiB
Rust

#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
//! Fuzz target: `NullifierSet` Borsh serialisation.
//!
//! The `NullifierSet` has a hand-written `BorshDeserialize` (in
//! `lee/state_machine/src/state.rs`) that rejects duplicate nullifiers via
//! `if !set.insert(n)`. This target verifies that:
//!
//! 1. States containing distinct nullifiers survive a Borsh round-trip. The
//! `delete-!` mutation at `state.rs:104` flips the dedup check so that
//! `deserialize_reader` errors on the *first* (non-duplicate) element; a state
//! with two distinct nullifiers then fails to deserialise, tripping Part 1.
//! 2. Feeding arbitrary fuzz bytes to the `V03State` deserialiser never panics.
//!
//! # Corpus note
//!
//! 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_core::{Commitment, Nullifier};
fuzz_props::fuzz_entry!(|data: &[u8]| {
// ── Part 1: State with nullifiers — Borsh round-trip ─────────────────────
// Create a V03State that contains committed nullifiers via the
// `initial_private_accounts` constructor argument.
//
// With state.rs:105 mutation (delete `!`):
// - `BorshDeserialize for NullifierSet` returns `Err` on the FIRST element
// - `borsh::from_slice::<V03State>(&bytes)` returns Err
// - The assert_eq below fires → mutation CAUGHT
{
// Two deterministic nullifier values (use from_byte_array):
let null1 = Nullifier::from_byte_array([0xAA_u8; 32]);
let null2 = Nullifier::from_byte_array([0xBB_u8; 32]);
// Commitment::new takes (&AccountId, &Account):
let comm1 = Commitment::new(&AccountId::new([0x11_u8; 32]), &Account::default());
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)],
vec![(comm1, null1), (comm2, null2)],
0,
);
// Serialise the state:
let bytes = borsh::to_vec(&state)
.expect("BorshSerialize for V03State must not fail");
assert!(!bytes.is_empty());
// Deserialise: with the mutation, this returns Err for any state with
// nullifiers, triggering the assertion below.
let state2 = borsh::from_slice::<V03State>(&bytes)
.expect("INVARIANT VIOLATION [NullifierSetRoundtrip]: \
borsh::from_slice of a state with nullifiers must succeed \
(mutation delete-! in NullifierSet::deserialize_reader detected)");
// Re-encode and verify idempotence:
let bytes2 = borsh::to_vec(&state2)
.expect("second BorshSerialize must not fail");
assert_eq!(
bytes,
bytes2,
"INVARIANT VIOLATION [NullifierSetRoundtrip]: \
encode(decode(encode(state))) != encode(state) — \
NullifierSet round-trip is not idempotent",
);
}
// ── Part 2: Fuzz-driven raw bytes ─────────────────────────────────────────
// Feed raw fuzz bytes through V03State deserialiser — no panic allowed.
{
let _ = borsh::from_slice::<V03State>(data); // NoPanic: Ok or Err, no panic
}
});