mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-14 06:59:29 +00:00
chore: add new fuzz targets to cover 40 missed mutants
This commit is contained in:
parent
c9d37b88d1
commit
9cb0d43c40
1
fuzz/Cargo.lock
generated
1
fuzz/Cargo.lock
generated
@ -2084,7 +2084,6 @@ dependencies = [
|
||||
"lee",
|
||||
"lee_core",
|
||||
"libfuzzer-sys",
|
||||
"sha2",
|
||||
"testnet_initial_state",
|
||||
]
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ libfuzzer-sys = { version = "0.4", optional = true }
|
||||
afl = { version = "0.15", optional = true }
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
borsh = "1"
|
||||
sha2 = "0.10"
|
||||
nssa = { path = "../../logos-execution-zone/lee/state_machine", package = "lee" }
|
||||
nssa_core = { path = "../../logos-execution-zone/lee/state_machine/core", package = "lee_core" }
|
||||
common = { path = "../../logos-execution-zone/lez/common" }
|
||||
@ -132,3 +131,39 @@ name = "fuzz_genesis_invariants"
|
||||
path = "fuzz_targets/fuzz_genesis_invariants.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_common_invariants"
|
||||
path = "fuzz_targets/fuzz_common_invariants.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_transaction_properties"
|
||||
path = "fuzz_targets/fuzz_transaction_properties.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_privacy_preserving_witness"
|
||||
path = "fuzz_targets/fuzz_privacy_preserving_witness.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_encoding_privacy_preserving"
|
||||
path = "fuzz_targets/fuzz_encoding_privacy_preserving.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_nullifier_set_roundtrip"
|
||||
path = "fuzz_targets/fuzz_nullifier_set_roundtrip.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_system_account_protection"
|
||||
path = "fuzz_targets/fuzz_system_account_protection.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
171
fuzz/fuzz_targets/fuzz_common_invariants.rs
Normal file
171
fuzz/fuzz_targets/fuzz_common_invariants.rs
Normal file
@ -0,0 +1,171 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: common-crate and low-level type invariants.
|
||||
//!
|
||||
//! This target is **input-independent**: the fuzz input is always ignored.
|
||||
//! It asserts deterministic invariants about types in `lez/common` and
|
||||
//! low-level `lee` types that are not exercised by higher-level state-transition
|
||||
//! targets.
|
||||
//!
|
||||
//! # Corpus note
|
||||
//!
|
||||
//! A single `\x00` seed file is sufficient — the input bytes are never read.
|
||||
|
||||
use common::{HashType, config::BasicAuth};
|
||||
use nssa::{
|
||||
privacy_preserving_transaction::circuit::Proof,
|
||||
program::Program,
|
||||
program_deployment_transaction::Message as DeployMessage,
|
||||
program_methods::{
|
||||
AUTHENTICATED_TRANSFER_ELF, TOKEN_ELF,
|
||||
},
|
||||
};
|
||||
|
||||
fuzz_props::fuzz_entry!(|_data: &[u8]| {
|
||||
// ── INVARIANT [HashTypeAsRefLength] ────────────────────────────────────────
|
||||
// `HashType::as_ref()` must always return exactly 32 bytes.
|
||||
// Catches mutations that return an empty slice or a slice of the wrong size.
|
||||
let all_ones = HashType([1_u8; 32]);
|
||||
assert_eq!(
|
||||
all_ones.as_ref().len(),
|
||||
32,
|
||||
"INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref must return 32 bytes",
|
||||
);
|
||||
|
||||
let zero = HashType::default();
|
||||
assert_eq!(
|
||||
zero.as_ref().len(),
|
||||
32,
|
||||
"INVARIANT VIOLATION [HashTypeAsRefLength]: HashType::as_ref on default must return 32 bytes",
|
||||
);
|
||||
|
||||
// ── INVARIANT [HashTypeAsRefBytes] ────────────────────────────────────────
|
||||
// `HashType::as_ref()` must return the exact inner bytes.
|
||||
// Catches mutations that return `vec![0]` or `vec![1]` instead of `&self.0`.
|
||||
let known = [0x42_u8; 32];
|
||||
let hash = HashType(known);
|
||||
assert_eq!(
|
||||
hash.as_ref(),
|
||||
&known,
|
||||
"INVARIANT VIOLATION [HashTypeAsRefBytes]: HashType::as_ref must return the inner [u8;32]",
|
||||
);
|
||||
|
||||
// ── INVARIANT [BasicAuthPasswordPreserved] ───────────────────────────────
|
||||
// Parsing "user:password" must preserve the non-empty password as `Some`.
|
||||
// Catches the mutation that deletes `!` in the `.filter(|p| !p.is_empty())`
|
||||
// predicate, which would flip the logic and accept only empty passwords.
|
||||
let auth: BasicAuth = "user:secret"
|
||||
.parse()
|
||||
.expect("INVARIANT VIOLATION: 'user:secret' must parse as BasicAuth");
|
||||
assert_eq!(
|
||||
auth.password.as_deref(),
|
||||
Some("secret"),
|
||||
"INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \
|
||||
parsing 'user:secret' must give password = Some(\"secret\")",
|
||||
);
|
||||
|
||||
let auth2: BasicAuth = "alice:hunter2"
|
||||
.parse()
|
||||
.expect("INVARIANT VIOLATION: 'alice:hunter2' must parse");
|
||||
assert_eq!(
|
||||
auth2.password.as_deref(),
|
||||
Some("hunter2"),
|
||||
"INVARIANT VIOLATION [BasicAuthPasswordPreserved]: \
|
||||
password must match the part after the colon",
|
||||
);
|
||||
|
||||
// ── INVARIANT [BasicAuthEmptyPasswordIsNone] ─────────────────────────────
|
||||
// Parsing "user:" (empty password) must give `password = None`.
|
||||
// With the `!` deleted, this would become `Some("")` instead of `None`.
|
||||
let auth_empty: BasicAuth = "user:"
|
||||
.parse()
|
||||
.expect("INVARIANT VIOLATION: 'user:' must parse as BasicAuth");
|
||||
assert_eq!(
|
||||
auth_empty.password,
|
||||
None,
|
||||
"INVARIANT VIOLATION [BasicAuthEmptyPasswordIsNone]: \
|
||||
an empty password (trailing colon) must give password = None",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProgramElfNonEmpty] ───────────────────────────────────────
|
||||
// `Program::elf()` must return a non-empty byte slice.
|
||||
// Catches the mutation that returns `Vec::leak(Vec::new())`.
|
||||
let at_prog = Program::authenticated_transfer_program();
|
||||
assert!(
|
||||
!at_prog.elf().is_empty(),
|
||||
"INVARIANT VIOLATION [ProgramElfNonEmpty]: \
|
||||
Program::authenticated_transfer_program().elf() must not be empty",
|
||||
);
|
||||
|
||||
let token_prog = Program::token();
|
||||
assert!(
|
||||
!token_prog.elf().is_empty(),
|
||||
"INVARIANT VIOLATION [ProgramElfNonEmpty]: \
|
||||
Program::token().elf() must not be empty",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProgramElfCorrect] ────────────────────────────────────────
|
||||
// `Program::elf()` must return exactly the compile-time bytecode constant.
|
||||
// Catches the mutations that return `vec![0]` or `vec![1]`.
|
||||
assert_eq!(
|
||||
at_prog.elf(),
|
||||
AUTHENTICATED_TRANSFER_ELF,
|
||||
"INVARIANT VIOLATION [ProgramElfCorrect]: \
|
||||
Program::authenticated_transfer_program().elf() must equal AUTHENTICATED_TRANSFER_ELF",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
token_prog.elf(),
|
||||
TOKEN_ELF,
|
||||
"INVARIANT VIOLATION [ProgramElfCorrect]: \
|
||||
Program::token().elf() must equal TOKEN_ELF",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProofIntoInnerRoundtrip] ──────────────────────────────────
|
||||
// `Proof::from_inner(bytes).into_inner()` must return the original bytes.
|
||||
// Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`.
|
||||
let proof_bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF];
|
||||
let proof = Proof::from_inner(proof_bytes.clone());
|
||||
assert_eq!(
|
||||
proof.into_inner(),
|
||||
proof_bytes,
|
||||
"INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \
|
||||
Proof::from_inner(b).into_inner() must return b",
|
||||
);
|
||||
|
||||
// Also test with an empty proof (round-trip must preserve emptiness).
|
||||
let empty_proof = Proof::from_inner(vec![]);
|
||||
assert!(
|
||||
empty_proof.into_inner().is_empty(),
|
||||
"INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \
|
||||
empty Proof::from_inner(vec![]).into_inner() must be empty",
|
||||
);
|
||||
|
||||
// And with a single non-zero byte:
|
||||
let single = Proof::from_inner(vec![0xFF]);
|
||||
assert_eq!(
|
||||
single.into_inner(),
|
||||
vec![0xFF_u8],
|
||||
"INVARIANT VIOLATION [ProofIntoInnerRoundtrip]: \
|
||||
Proof from single byte must round-trip correctly",
|
||||
);
|
||||
|
||||
// ── INVARIANT [DeployMessageBytecodeRoundtrip] ────────────────────────────
|
||||
// `Message::new(bytecode).into_bytecode()` must return the original bytecode.
|
||||
// Catches the mutations that return `vec![]`, `vec![0]`, or `vec![1]`.
|
||||
let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic
|
||||
let msg = DeployMessage::new(bytecode.clone());
|
||||
assert_eq!(
|
||||
msg.into_bytecode(),
|
||||
bytecode,
|
||||
"INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \
|
||||
Message::new(b).into_bytecode() must return b",
|
||||
);
|
||||
|
||||
// Empty bytecode round-trip:
|
||||
let empty_msg = DeployMessage::new(vec![]);
|
||||
assert!(
|
||||
empty_msg.into_bytecode().is_empty(),
|
||||
"INVARIANT VIOLATION [DeployMessageBytecodeRoundtrip]: \
|
||||
empty bytecode must round-trip as empty",
|
||||
);
|
||||
});
|
||||
203
fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs
Normal file
203
fuzz/fuzz_targets/fuzz_encoding_privacy_preserving.rs
Normal file
@ -0,0 +1,203 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: privacy-preserving encoding invariants.
|
||||
//!
|
||||
//! Tests that `to_bytes` / `from_bytes` round-trips work correctly for the
|
||||
//! privacy-preserving `Message` type, and that `try_from_circuit_output`
|
||||
//! validates ciphertext-to-key length matching.
|
||||
//!
|
||||
//! `PrivacyPreservingTransaction` is also tested for serialisation stability
|
||||
//! (non-empty, deterministic bytes) without requiring a real ZK receipt.
|
||||
|
||||
use nssa::{
|
||||
AccountId, PrivateKey, PublicKey,
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
Message as PPMessage,
|
||||
WitnessSet as PPWitnessSet,
|
||||
circuit::Proof,
|
||||
},
|
||||
};
|
||||
use nssa_core::{
|
||||
PrivacyPreservingCircuitOutput,
|
||||
account::Nonce,
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
/// Build a minimal `Message` with no private state.
|
||||
fn minimal_message() -> PPMessage {
|
||||
let addr = AccountId::from(
|
||||
&PublicKey::new_from_private_key(
|
||||
&PrivateKey::try_new([1_u8; 32]).expect("known-good"),
|
||||
),
|
||||
);
|
||||
PPMessage {
|
||||
public_account_ids: vec![addr],
|
||||
nonces: vec![Nonce::from(0_u128)],
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
// ── INVARIANT [MessageEncodingRoundtrip] ──────────────────────────────────
|
||||
// `Message::to_bytes()` followed by `Message::from_bytes()` must reproduce
|
||||
// the original message. Catches mutations that return `vec![]`, `vec![0]`,
|
||||
// or `vec![1]` — these break round-trip identity.
|
||||
{
|
||||
let msg = minimal_message();
|
||||
let encoded = msg.to_bytes();
|
||||
|
||||
// Non-empty: catches `→ vec![]`
|
||||
assert!(
|
||||
!encoded.is_empty(),
|
||||
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
|
||||
Message::to_bytes must not return an empty vec",
|
||||
);
|
||||
|
||||
let decoded = PPMessage::from_bytes(&encoded)
|
||||
.expect("INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
|
||||
from_bytes(to_bytes(msg)) must succeed");
|
||||
|
||||
let re_encoded = decoded.to_bytes();
|
||||
assert_eq!(
|
||||
encoded,
|
||||
re_encoded,
|
||||
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
|
||||
encode(decode(encode(msg))) != encode(msg)",
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [TxEncodingNonEmpty] / [TxEncodingDeterministic] ────────────
|
||||
// `PrivacyPreservingTransaction::to_bytes()` must return a non-empty byte
|
||||
// slice and be deterministic. Catches mutations that return `vec![]` etc.
|
||||
{
|
||||
let key = PrivateKey::try_new([1_u8; 32]).expect("known-good");
|
||||
let msg = minimal_message();
|
||||
let proof = Proof::from_inner(vec![0xDE_u8, 0xAD, 0xBE, 0xEF]);
|
||||
let ws = PPWitnessSet::for_message(&msg, proof, &[&key]);
|
||||
let tx = PrivacyPreservingTransaction::new(msg, ws);
|
||||
|
||||
let bytes1 = tx.to_bytes();
|
||||
assert!(
|
||||
!bytes1.is_empty(),
|
||||
"INVARIANT VIOLATION [TxEncodingNonEmpty]: \
|
||||
PrivacyPreservingTransaction::to_bytes must not be empty",
|
||||
);
|
||||
|
||||
let bytes2 = tx.to_bytes();
|
||||
assert_eq!(
|
||||
bytes1,
|
||||
bytes2,
|
||||
"INVARIANT VIOLATION [TxEncodingDeterministic]: \
|
||||
to_bytes must be deterministic — called twice, got different results",
|
||||
);
|
||||
|
||||
// Verify round-trip for the full transaction:
|
||||
let decoded = PrivacyPreservingTransaction::from_bytes(&bytes1)
|
||||
.expect("INVARIANT VIOLATION: round-trip decode must succeed");
|
||||
assert_eq!(
|
||||
bytes1,
|
||||
decoded.to_bytes(),
|
||||
"INVARIANT VIOLATION [TxEncodingDeterministic]: \
|
||||
encode(decode(encode(tx))) != encode(tx)",
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [LengthMatchAccepted] ───────────────────────────────────────
|
||||
// When public_keys.len() == ciphertexts.len() == 0, `try_from_circuit_output`
|
||||
// must succeed.
|
||||
//
|
||||
// Original check: `if public_keys.len() != output.ciphertexts.len() { Err }`
|
||||
// With mutation `!=` → `==`: `if 0 == 0` → `true` → Err is returned.
|
||||
// Our assertion that the call SUCCEEDS catches the mutation.
|
||||
{
|
||||
let empty_output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: vec![],
|
||||
public_post_states: vec![],
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
ciphertexts: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
let result = PPMessage::try_from_circuit_output(
|
||||
vec![], // public_account_ids
|
||||
vec![], // nonces
|
||||
vec![], // public_keys (0 entries)
|
||||
empty_output,
|
||||
);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"INVARIANT VIOLATION [LengthMatchAccepted]: \
|
||||
try_from_circuit_output must accept when keys(0) == ciphertexts(0), \
|
||||
got: {:?} — \
|
||||
possible mutation: != changed to == in the length check",
|
||||
result.err(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Raw fuzz decode tests ─────────────────────────────────────────────────
|
||||
// Fuzz the Message decoder for no-panic and canonical round-trip.
|
||||
{
|
||||
// No-panic on arbitrary bytes:
|
||||
let _ = PPMessage::from_bytes(data);
|
||||
|
||||
// Canonical round-trip: if fuzz bytes decode, re-encoding must reproduce them.
|
||||
if let Ok(msg) = PPMessage::from_bytes(data) {
|
||||
let re_encoded = msg.to_bytes();
|
||||
assert_eq!(
|
||||
data,
|
||||
re_encoded.as_slice(),
|
||||
"INVARIANT VIOLATION: PP Message decoded from raw bytes but \
|
||||
re-encoding differs (non-canonical encoding accepted)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Varied-size message round-trips ──────────────────────────────────────
|
||||
// Verify round-trip for several multi-account messages.
|
||||
for n_accounts in [0, 1, 2, 3] {
|
||||
let mut account_ids = Vec::new();
|
||||
let mut nonces = Vec::new();
|
||||
for i in 0..n_accounts {
|
||||
let key_bytes = [i + 1_u8; 32];
|
||||
if let Ok(key) = PrivateKey::try_new(key_bytes) {
|
||||
let pk = PublicKey::new_from_private_key(&key);
|
||||
account_ids.push(AccountId::from(&pk));
|
||||
nonces.push(Nonce::from(i as u128));
|
||||
}
|
||||
}
|
||||
|
||||
let msg = PPMessage {
|
||||
public_account_ids: account_ids,
|
||||
nonces,
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
let encoded = msg.to_bytes();
|
||||
assert!(
|
||||
!encoded.is_empty(),
|
||||
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
|
||||
Message::to_bytes must not be empty for a {n_accounts}-account message",
|
||||
);
|
||||
|
||||
let decoded = PPMessage::from_bytes(&encoded)
|
||||
.expect("round-trip must succeed for well-formed message");
|
||||
assert_eq!(
|
||||
encoded,
|
||||
decoded.to_bytes(),
|
||||
"INVARIANT VIOLATION [MessageEncodingRoundtrip]: \
|
||||
round-trip failed for {n_accounts}-account message",
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -1,130 +1,136 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: `MerkleTree` structural invariants
|
||||
//! Fuzz target: Merkle-tree structural invariants, exercised through the
|
||||
//! **public** commitment-set API (no `pub mod merkle_tree` patch required).
|
||||
//!
|
||||
//! Covered code paths (all in `lee/state_machine/src/merkle_tree/mod.rs`):
|
||||
//! The commitment set in `V03State` is a thin wrapper around the internal
|
||||
//! `MerkleTree`:
|
||||
//!
|
||||
//! ```text
|
||||
//! MerkleTree::with_capacity(1) ← initial capacity forces reallocate_to_double_capacity
|
||||
//! MerkleTree::insert(value) ← per-value; also triggers reallocate_to_double_capacity
|
||||
//! MerkleTree::root() ← sampled once after all inserts
|
||||
//! MerkleTree::get_authentication_path_for(index) ← per-value
|
||||
//! prev_power_of_two ← exercised inside reallocate_to_double_capacity
|
||||
//! V03State::commitment_set_digest() → MerkleTree::root() (→ root_index)
|
||||
//! V03State::get_proof_for_commitment(c) → (index, MerkleTree::get_authentication_path_for(index))
|
||||
//! CommitmentSet::extend(commitments) → MerkleTree::insert(value) per commitment
|
||||
//! compute_digest_for_path(c, proof) → canonical leaf→root recomputation
|
||||
//! ```
|
||||
//!
|
||||
//! Inserting commitments via `V03State::new_with_genesis_accounts` 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`.
|
||||
//!
|
||||
//! Because the genesis commitment set has a fixed capacity of 32, a *small*
|
||||
//! number of commitments exercises the partial-fill regime (`depth <
|
||||
//! capacity_depth`, i.e. `root_index`'s else-branch), while a *large* number
|
||||
//! (> 31) forces one or more reallocations. A single target therefore covers
|
||||
//! both regimes — the committed corpus carries a small partial-fill seed
|
||||
//! (`seed_partial6`) and a large reallocation seed (`seed_realloc40`).
|
||||
//!
|
||||
//! # Input format
|
||||
//!
|
||||
//! The raw fuzz bytes are sliced into 32-byte chunks; each chunk becomes one
|
||||
//! value inserted into the tree. This makes the format trivial to reason about
|
||||
//! and lets us seed the corpus with well-known test vectors.
|
||||
//! Each 32-byte chunk of the fuzz input is reinterpreted as an `AccountId`, from
|
||||
//! which a distinct `Commitment` is derived (`Commitment::new`). Duplicate
|
||||
//! chunks are dropped so every inserted commitment is unique and lands at a
|
||||
//! distinct, sequential tree index. The number of distinct chunks selects the
|
||||
//! fill regime (partial-fill vs. reallocation).
|
||||
//!
|
||||
//! # Invariants checked
|
||||
//! # Invariants
|
||||
//!
|
||||
//! 1. **InsertionIndex** — `insert(value)` returns the sequential 0-based index.
|
||||
//! 2. **AuthPathSome** — `get_authentication_path_for(i)` is `Some` for every
|
||||
//! `i < length`.
|
||||
//! 3. **AuthPathValid** — every returned path re-hashes (SHA-256, same hash
|
||||
//! functions used by the production code) to the value reported by `root()`.
|
||||
//! 4. **OutOfBoundsNone** — `get_authentication_path_for(length)` returns `None`.
|
||||
//! 1. **ProofSome** — every inserted commitment has a membership proof.
|
||||
//! 2. **ProofValid** — `compute_digest_for_path(commitment, proof)` reproduces
|
||||
//! `commitment_set_digest()` for every inserted commitment. This is the core
|
||||
//! check: it independently recomputes the root from the leaf + authentication
|
||||
//! path and compares against the tree's reported root, catching arithmetic
|
||||
//! bugs in `root_index`, `insert`, and the path-walk.
|
||||
//! 3. **IndicesSequential** — the genesis dummy commitment occupies index 0, so
|
||||
//! `N` distinct user commitments must occupy exactly indices `1..=N`. Catches
|
||||
//! `insert -> 0` / `insert -> 1` return-value mutations.
|
||||
//! 4. **NonMembershipNone** — a commitment that was never inserted has no proof.
|
||||
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// ─── Reference hash helpers (mirrors the private functions in merkle_tree/mod.rs) ───
|
||||
|
||||
/// SHA-256 of a single 32-byte leaf value. Mirrors `hash_value`.
|
||||
fn sha256_one(v: &[u8; 32]) -> [u8; 32] {
|
||||
let mut h = Sha256::new();
|
||||
h.update(v);
|
||||
h.finalize().into()
|
||||
}
|
||||
|
||||
/// SHA-256 of two concatenated 32-byte nodes. Mirrors `hash_two`.
|
||||
fn sha256_two(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
|
||||
let mut h = Sha256::new();
|
||||
h.update(left);
|
||||
h.update(right);
|
||||
h.finalize().into()
|
||||
}
|
||||
|
||||
/// Reference implementation of authentication-path verification.
|
||||
///
|
||||
/// Mirrors `verify_authentication_path` from the test module inside
|
||||
/// `lee/state_machine/src/merkle_tree/mod.rs`.
|
||||
///
|
||||
/// Algorithm:
|
||||
/// result ← SHA-256(value)
|
||||
/// for each sibling in path:
|
||||
/// if level_index is even → result is the LEFT child → hash(result, sibling)
|
||||
/// if level_index is odd → result is the RIGHT child → hash(sibling, result)
|
||||
/// level_index >>= 1
|
||||
/// return result == root
|
||||
fn verify_auth_path(value: &[u8; 32], index: usize, path: &[[u8; 32]], root: &[u8; 32]) -> bool {
|
||||
let mut result = sha256_one(value);
|
||||
let mut level_index = index;
|
||||
for sibling in path {
|
||||
let is_left_child = level_index & 1 == 0;
|
||||
result = if is_left_child {
|
||||
sha256_two(&result, sibling)
|
||||
} else {
|
||||
sha256_two(sibling, &result)
|
||||
};
|
||||
level_index >>= 1;
|
||||
}
|
||||
&result == root
|
||||
}
|
||||
use nssa::V03State;
|
||||
use nssa_core::{
|
||||
Commitment, Nullifier,
|
||||
account::{Account, AccountId},
|
||||
compute_digest_for_path,
|
||||
};
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
// Treat each 32-byte chunk as one leaf value. Discard any trailing
|
||||
// incomplete chunk.
|
||||
let values: Vec<[u8; 32]> = data
|
||||
.chunks_exact(32)
|
||||
.map(|c| c.try_into().expect("chunks_exact(32) always yields [u8;32]"))
|
||||
.collect();
|
||||
// Reinterpret each 32-byte chunk as an AccountId; derive one commitment each.
|
||||
// Dedup chunks so commitments are distinct and indices are clean.
|
||||
let mut seen: HashSet<[u8; 32]> = HashSet::new();
|
||||
let mut pairs: Vec<(Commitment, Nullifier)> = Vec::new();
|
||||
for chunk in data.chunks_exact(32) {
|
||||
let bytes: [u8; 32] = chunk.try_into().expect("chunks_exact(32) yields [u8;32]");
|
||||
if !seen.insert(bytes) {
|
||||
continue; // skip duplicate account ids
|
||||
}
|
||||
let commitment = Commitment::new(&AccountId::new(bytes), &Account::default());
|
||||
// A distinct nullifier per pair (content is irrelevant to the merkle tree).
|
||||
let nullifier = Nullifier::from_byte_array(bytes);
|
||||
pairs.push((commitment, nullifier));
|
||||
}
|
||||
|
||||
// Nothing to test with an empty input.
|
||||
if values.is_empty() {
|
||||
if pairs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with capacity=1 so the very first pair of insertions triggers
|
||||
// `reallocate_to_double_capacity`, and each subsequent power-of-two boundary
|
||||
// triggers it again. This exercises `prev_power_of_two`, the copy loop,
|
||||
// and the capacity / length bookkeeping inside the reallocation path.
|
||||
let mut tree = nssa::merkle_tree::MerkleTree::with_capacity(1);
|
||||
// Keep the commitments so we can query their proofs after the state moves `pairs`.
|
||||
let commitments: Vec<Commitment> = pairs.iter().map(|(c, _)| c.clone()).collect();
|
||||
|
||||
// ── INVARIANT [InsertionIndex] ────────────────────────────────────────────
|
||||
// insert() must return 0, 1, 2, … in order.
|
||||
for (expected_index, &value) in values.iter().enumerate() {
|
||||
let actual_index = tree.insert(value);
|
||||
// Genesis inserts DUMMY_COMMITMENT at index 0, then our commitments at 1..=N.
|
||||
let state = V03State::new_with_genesis_accounts(&[], pairs, 0);
|
||||
let digest = state.commitment_set_digest();
|
||||
|
||||
let mut indices: Vec<usize> = Vec::with_capacity(commitments.len());
|
||||
for commitment in &commitments {
|
||||
// ── INVARIANT [ProofSome] ─────────────────────────────────────────────
|
||||
let proof = state.get_proof_for_commitment(commitment).expect(
|
||||
"INVARIANT VIOLATION [ProofSome]: \
|
||||
get_proof_for_commitment returned None for an inserted commitment",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ProofValid] ────────────────────────────────────────────
|
||||
// Recompute the root from the leaf + authentication path and compare to
|
||||
// the tree's reported digest. A bug in root_index / insert / the path
|
||||
// walk makes these disagree.
|
||||
assert_eq!(
|
||||
actual_index,
|
||||
expected_index,
|
||||
"INVARIANT VIOLATION [InsertionIndex]: \
|
||||
insert returned {actual_index} but expected {expected_index}",
|
||||
compute_digest_for_path(commitment, &proof),
|
||||
digest,
|
||||
"INVARIANT VIOLATION [ProofValid]: \
|
||||
membership proof for a commitment at index {} does not recompute to \
|
||||
commitment_set_digest()",
|
||||
proof.0,
|
||||
);
|
||||
|
||||
indices.push(proof.0);
|
||||
}
|
||||
|
||||
// ── INVARIANT [IndicesSequential] ─────────────────────────────────────────
|
||||
// The dummy commitment holds index 0; our N distinct commitments must hold
|
||||
// exactly indices 1..=N.
|
||||
indices.sort_unstable();
|
||||
for (k, &idx) in indices.iter().enumerate() {
|
||||
assert_eq!(
|
||||
idx,
|
||||
k + 1,
|
||||
"INVARIANT VIOLATION [IndicesSequential]: \
|
||||
inserted commitments must occupy sequential indices 1..=N (dummy at 0); \
|
||||
got index {idx} at sorted position {k}",
|
||||
);
|
||||
}
|
||||
|
||||
let root = tree.root();
|
||||
|
||||
// ── INVARIANTS [AuthPathSome] and [AuthPathValid] ─────────────────────────
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
let path = tree
|
||||
.get_authentication_path_for(index)
|
||||
.expect("INVARIANT VIOLATION [AuthPathSome]: \
|
||||
get_authentication_path_for returned None for a valid index");
|
||||
|
||||
// ── INVARIANT [NonMembershipNone] ─────────────────────────────────────────
|
||||
// A commitment derived from an account id that was NOT inserted must have no
|
||||
// proof. Use an all-0xFF sentinel id and only assert when it is genuinely
|
||||
// absent from the inserted set.
|
||||
let sentinel_bytes = [0xFF_u8; 32];
|
||||
if !seen.contains(&sentinel_bytes) {
|
||||
let absent =
|
||||
Commitment::new(&AccountId::new(sentinel_bytes), &Account::default());
|
||||
assert!(
|
||||
verify_auth_path(value, index, &path, &root),
|
||||
"INVARIANT VIOLATION [AuthPathValid]: \
|
||||
authentication path for index {index} does not re-hash to root()",
|
||||
state.get_proof_for_commitment(&absent).is_none(),
|
||||
"INVARIANT VIOLATION [NonMembershipNone]: \
|
||||
get_proof_for_commitment returned Some for a commitment never inserted",
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [OutOfBoundsNone] ───────────────────────────────────────────
|
||||
// The index one past the last inserted element must yield None.
|
||||
assert!(
|
||||
tree.get_authentication_path_for(values.len()).is_none(),
|
||||
"INVARIANT VIOLATION [OutOfBoundsNone]: \
|
||||
get_authentication_path_for({}) should return None but returned Some",
|
||||
values.len(),
|
||||
);
|
||||
});
|
||||
|
||||
75
fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs
Normal file
75
fuzz/fuzz_targets/fuzz_nullifier_set_roundtrip.rs
Normal file
@ -0,0 +1,75 @@
|
||||
#![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
|
||||
}
|
||||
});
|
||||
236
fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs
Normal file
236
fuzz/fuzz_targets/fuzz_privacy_preserving_witness.rs
Normal file
@ -0,0 +1,236 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: `privacy_preserving_transaction::WitnessSet` invariants.
|
||||
//!
|
||||
//! Mirrors `fuzz_witness_set_verification` but for the privacy-preserving
|
||||
//! witness set, which additionally holds a ZK `Proof` alongside the ECDSA
|
||||
//! signatures.
|
||||
//!
|
||||
//! # Invariants
|
||||
//!
|
||||
//! 1. **CorrectVerification** — a `WitnessSet` built for message A via
|
||||
//! `WitnessSet::for_message` must pass `signatures_are_valid_for(A)`.
|
||||
//!
|
||||
//! 2. **MessageIsolation** — the same `WitnessSet` must NOT pass
|
||||
//! `signatures_are_valid_for(B)` when B borsh-encodes differently from A.
|
||||
//!
|
||||
//! 3. **SignaturesAndPublicKeysNonEmpty** — after `for_message` with N keys,
|
||||
//! `signatures_and_public_keys()` must return N entries.
|
||||
//!
|
||||
//! 4. **SignerIdsMatchWitnessKeys** — `PrivacyPreservingTransaction::signer_account_ids`
|
||||
//! must equal `AccountId::from(pk)` for every key in the witness set.
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use fuzz_props::arbitrary_types::ArbPrivateKey;
|
||||
use nssa::{
|
||||
AccountId, PrivateKey, PublicKey,
|
||||
privacy_preserving_transaction::{
|
||||
Message as PPMessage,
|
||||
WitnessSet as PPWitnessSet,
|
||||
circuit::Proof,
|
||||
},
|
||||
PrivacyPreservingTransaction,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::Nonce,
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
/// Build a minimal `Message` for testing — no commitments, no nullifiers,
|
||||
/// no encrypted states. Sufficient to test signature binding.
|
||||
fn minimal_message(account_ids: Vec<AccountId>, nonces: Vec<Nonce>) -> PPMessage {
|
||||
PPMessage {
|
||||
public_account_ids: account_ids,
|
||||
nonces,
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a minimal (fake) `Proof` — bytes don't form a real ZK receipt but
|
||||
/// are valid for struct construction and serialisation.
|
||||
fn fake_proof() -> Proof {
|
||||
Proof::from_inner(vec![0xAB_u8; 32])
|
||||
}
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
// ── Fixed-key deterministic part ──────────────────────────────────────────
|
||||
// Always runs regardless of input length, ensuring the mutation is caught
|
||||
// even on an empty corpus.
|
||||
{
|
||||
let key1 = PrivateKey::try_new([1_u8; 32]).expect("known-good key");
|
||||
let key2 = PrivateKey::try_new([2_u8; 32]).expect("known-good key");
|
||||
let pub1 = PublicKey::new_from_private_key(&key1);
|
||||
let pub2 = PublicKey::new_from_private_key(&key2);
|
||||
let addr1 = AccountId::from(&pub1);
|
||||
let addr2 = AccountId::from(&pub2);
|
||||
|
||||
let msg = minimal_message(
|
||||
vec![addr1, addr2],
|
||||
vec![Nonce::from(0_u128), Nonce::from(1_u128)],
|
||||
);
|
||||
|
||||
let ws = PPWitnessSet::for_message(&msg, fake_proof(), &[&key1, &key2]);
|
||||
|
||||
// ── INVARIANT [SignaturesAndPublicKeysNonEmpty] ───────────────────────
|
||||
assert_eq!(
|
||||
ws.signatures_and_public_keys().len(),
|
||||
2,
|
||||
"INVARIANT VIOLATION [SignaturesAndPublicKeysNonEmpty]: \
|
||||
signatures_and_public_keys must return 2 entries for a 2-key witness set",
|
||||
);
|
||||
|
||||
// ── INVARIANT [CorrectVerification] ───────────────────────────────────
|
||||
assert!(
|
||||
ws.signatures_are_valid_for(&msg),
|
||||
"INVARIANT VIOLATION [CorrectVerification]: \
|
||||
WitnessSet::for_message produced a witness set that fails \
|
||||
signatures_are_valid_for on the same message",
|
||||
);
|
||||
|
||||
// ── INVARIANT [SignerIdsMatchWitnessKeys] ─────────────────────────────
|
||||
// signer_account_ids is pub(crate); derive from signatures_and_public_keys instead.
|
||||
let signers_from_ws: Vec<AccountId> = ws
|
||||
.signatures_and_public_keys()
|
||||
.iter()
|
||||
.map(|(_, pk)| AccountId::from(pk))
|
||||
.collect();
|
||||
assert_eq!(signers_from_ws.len(), 2);
|
||||
assert!(signers_from_ws.contains(&addr1));
|
||||
assert!(signers_from_ws.contains(&addr2));
|
||||
|
||||
// ── INVARIANT [SignerOnlyAccountInAffected] ───────────────────────────
|
||||
// `PrivacyPreservingTransaction::affected_public_account_ids` unions
|
||||
// `signer_account_ids()` with `message.public_account_ids`. To catch the
|
||||
// `signer_account_ids → vec![]` mutation, build a message whose
|
||||
// public_account_ids does NOT contain the signer, so the signer can only
|
||||
// reach `affected` via `signer_account_ids()`.
|
||||
let isolated_msg = minimal_message(
|
||||
vec![AccountId::new([0xB1_u8; 32]), AccountId::new([0xB2_u8; 32])],
|
||||
vec![Nonce::from(0_u128), Nonce::from(1_u128)],
|
||||
);
|
||||
// Sign with key1 — addr1 is (with overwhelming probability) not one of the
|
||||
// 0xB1/0xB2 placeholder accounts.
|
||||
if addr1 != AccountId::new([0xB1_u8; 32]) && addr1 != AccountId::new([0xB2_u8; 32]) {
|
||||
let isolated_ws = PPWitnessSet::for_message(&isolated_msg, fake_proof(), &[&key1]);
|
||||
let isolated_tx =
|
||||
PrivacyPreservingTransaction::new(isolated_msg, isolated_ws);
|
||||
let affected = isolated_tx.affected_public_account_ids();
|
||||
assert!(
|
||||
affected.contains(&addr1),
|
||||
"INVARIANT VIOLATION [SignerOnlyAccountInAffected]: \
|
||||
PP affected_public_account_ids must include the signer {:?} even when it \
|
||||
is absent from message.public_account_ids — signer_account_ids() must not \
|
||||
return an empty vec",
|
||||
addr1,
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [MessageIsolation] ──────────────────────────────────────
|
||||
// Build a different message (different nonces) — the witness set for msg
|
||||
// must NOT validate against msg_b.
|
||||
let msg_b = minimal_message(
|
||||
vec![addr1, addr2],
|
||||
vec![Nonce::from(999_u128), Nonce::from(1000_u128)],
|
||||
);
|
||||
let bytes_a = borsh::to_vec(&msg);
|
||||
let bytes_b = borsh::to_vec(&msg_b);
|
||||
if let (Ok(a), Ok(b)) = (bytes_a, bytes_b) {
|
||||
if a != b {
|
||||
assert!(
|
||||
!ws.signatures_are_valid_for(&msg_b),
|
||||
"INVARIANT VIOLATION [MessageIsolation]: \
|
||||
PP WitnessSet for msg accepted for a different msg_b — \
|
||||
possible signature-binding bypass",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Single-key variant:
|
||||
let ws_single = PPWitnessSet::for_message(&msg, fake_proof(), &[&key1]);
|
||||
assert_eq!(ws_single.signatures_and_public_keys().len(), 1);
|
||||
|
||||
let tx_single = PrivacyPreservingTransaction::new(msg.clone(), ws_single);
|
||||
// Use affected_public_account_ids (which calls signer_account_ids internally):
|
||||
let single_affected = tx_single.affected_public_account_ids();
|
||||
assert!(
|
||||
single_affected.contains(&addr1),
|
||||
"INVARIANT VIOLATION [SignerIdsMatchWitnessKeys]: 1-key tx must include addr1",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Fuzz-driven part ──────────────────────────────────────────────────────
|
||||
// Generate 0–3 random private keys, build a WitnessSet, verify correct validation.
|
||||
{
|
||||
let n_keys = (u8::arbitrary(&mut u).unwrap_or(0) % 4) as usize;
|
||||
let mut keys = Vec::with_capacity(n_keys);
|
||||
let mut addrs = Vec::with_capacity(n_keys);
|
||||
let mut nonces = Vec::with_capacity(n_keys);
|
||||
|
||||
for i in 0..n_keys {
|
||||
match ArbPrivateKey::arbitrary(&mut u) {
|
||||
Ok(k) => {
|
||||
let pk = PublicKey::new_from_private_key(&k.0);
|
||||
addrs.push(AccountId::from(&pk));
|
||||
nonces.push(Nonce::from(i as u128));
|
||||
keys.push(k.0);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if keys.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = minimal_message(addrs.clone(), nonces);
|
||||
let key_refs: Vec<&PrivateKey> = keys.iter().collect();
|
||||
let ws = PPWitnessSet::for_message(&msg, fake_proof(), &key_refs);
|
||||
|
||||
// INVARIANT [SignaturesAndPublicKeysNonEmpty]
|
||||
assert_eq!(
|
||||
ws.signatures_and_public_keys().len(),
|
||||
keys.len(),
|
||||
"INVARIANT VIOLATION [SignaturesAndPublicKeysNonEmpty]: \
|
||||
signatures_and_public_keys count must match number of keys",
|
||||
);
|
||||
|
||||
// INVARIANT [CorrectVerification]
|
||||
assert!(
|
||||
ws.signatures_are_valid_for(&msg),
|
||||
"INVARIANT VIOLATION [CorrectVerification]: \
|
||||
PP WitnessSet::for_message produced witnesses that fail validation",
|
||||
);
|
||||
|
||||
// INVARIANT [SignerIdsMatchWitnessKeys]
|
||||
// signer_account_ids is pub(crate); verify via affected_public_account_ids
|
||||
// (which internally calls signer_account_ids) and via signatures_and_public_keys.
|
||||
let tx = PrivacyPreservingTransaction::new(msg, ws.clone());
|
||||
let signer_ids_from_ws: Vec<AccountId> = ws
|
||||
.signatures_and_public_keys()
|
||||
.iter()
|
||||
.map(|(_, pk)| AccountId::from(pk))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
signer_ids_from_ws.len(),
|
||||
addrs.len(),
|
||||
"INVARIANT VIOLATION [SignerIdsMatchWitnessKeys]: \
|
||||
witness set key count must match number of keys provided",
|
||||
);
|
||||
// affected_public_account_ids includes signer IDs:
|
||||
let affected2 = tx.affected_public_account_ids();
|
||||
for addr in &addrs {
|
||||
assert!(
|
||||
affected2.contains(addr),
|
||||
"INVARIANT VIOLATION [SignerIdsMatchWitnessKeys]: \
|
||||
affected_public_account_ids must contain {:?}",
|
||||
addr,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
165
fuzz/fuzz_targets/fuzz_system_account_protection.rs
Normal file
165
fuzz/fuzz_targets/fuzz_system_account_protection.rs
Normal file
@ -0,0 +1,165 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: system-account modification protection.
|
||||
//!
|
||||
//! `LeeTransaction::validate_on_state` must reject any transaction that modifies
|
||||
//! a system account (faucet, bridge, or clock accounts). This is enforced by
|
||||
//! `validate_doesnt_modify_account` which inspects `ValidatedStateDiff::public_diff()`.
|
||||
//!
|
||||
//! # Corpus note
|
||||
//!
|
||||
//! This target is **input-independent**. A single `\x00` seed is sufficient.
|
||||
//!
|
||||
//! **Performance note**: the `[SystemAccountModificationRejected]` invariant
|
||||
//! executes a RISC0 program (a native transfer). This is inherently slow
|
||||
//! (~seconds). Only one corpus file is needed, so the corpus-regression oracle
|
||||
//! costs one program execution per mutant under test.
|
||||
|
||||
use common::transaction::LeeTransaction;
|
||||
use nssa::{
|
||||
AccountId, PrivateKey, PublicKey, V03State, ValidatedStateDiff,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, system_bridge_account_id, system_faucet_account_id,
|
||||
};
|
||||
|
||||
fuzz_props::fuzz_entry!(|_data: &[u8]| {
|
||||
// ── INVARIANT [SystemAccountIdsDistinct] ──────────────────────────────────
|
||||
let faucet_id = system_faucet_account_id();
|
||||
let bridge_id = system_bridge_account_id();
|
||||
|
||||
assert_ne!(
|
||||
faucet_id,
|
||||
AccountId::default(),
|
||||
"INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet account ID must be non-default",
|
||||
);
|
||||
assert_ne!(
|
||||
bridge_id,
|
||||
AccountId::default(),
|
||||
"INVARIANT VIOLATION [SystemAccountIdsDistinct]: bridge account ID must be non-default",
|
||||
);
|
||||
assert_ne!(
|
||||
faucet_id,
|
||||
bridge_id,
|
||||
"INVARIANT VIOLATION [SystemAccountIdsDistinct]: faucet and bridge must be distinct",
|
||||
);
|
||||
|
||||
// ── INVARIANT [ClockInvocationRejected] ──────────────────────────────────
|
||||
// A native transfer that CREDITS a clock system account modifies exactly one
|
||||
// system account — the clock account — and that account is *changed* (its
|
||||
// balance increases from 0). No other system account appears in the diff, so
|
||||
// this isolates the `validate_doesnt_modify_account` rejection cleanly.
|
||||
//
|
||||
// Why not a clock invocation? The clock program writes all three clock
|
||||
// accounts, but the 01/10/50 clocks tick at different rates, so its diff
|
||||
// contains BOTH changed and unchanged system accounts. The `!=`→`==`
|
||||
// mutation then still rejects (citing an unchanged account), so a clock
|
||||
// invocation cannot distinguish the mutant. Crediting a single clock account
|
||||
// gives a single, changed system account, which the mutant must accept.
|
||||
//
|
||||
// Why not credit the faucet? The faucet holds u128::MAX, so any credit
|
||||
// overflows and the program execution fails before the protection check is
|
||||
// reached. A clock account starts at balance 0, so a small credit succeeds.
|
||||
//
|
||||
// With mutation `!=` → `==` at transaction.rs:182:
|
||||
// The clock account is changed (post != pre), so the mutated `post == pre`
|
||||
// check is false → no error → validate_on_state returns Ok → our assert fires.
|
||||
//
|
||||
// With mutation `public_diff → HashMap::new()` at validated_state_diff.rs:479:
|
||||
// validate_doesnt_modify_account sees an empty map → can never find the
|
||||
// clock account → returns Ok for every transaction → our assert fires.
|
||||
{
|
||||
let sender_key = PrivateKey::try_new([5_u8; 32]).expect("known-good key");
|
||||
let sender_pub = PublicKey::new_from_private_key(&sender_key);
|
||||
let sender_id = AccountId::from(&sender_pub);
|
||||
|
||||
// Fund the sender; clock accounts already exist in genesis (balance 0).
|
||||
let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000_u128)], vec![], 0);
|
||||
|
||||
// Transfer tokens TO a clock account — credits (changes) that system account.
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
sender_id,
|
||||
0, // nonce
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
100, // amount credited to the clock account
|
||||
&sender_key,
|
||||
);
|
||||
|
||||
let result = tx.validate_on_state(&state, 1, 0);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"INVARIANT VIOLATION [SystemAccountModificationRejected]: \
|
||||
validate_on_state must reject a transfer that credits a clock system \
|
||||
account. If this fires, either validate_doesnt_modify_account has a logic \
|
||||
inversion (!=→==) or public_diff() returns an empty map",
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [PublicDiffNonEmptyOnSuccess] ────────────────────────────────
|
||||
// For a valid public transaction with signers, the signer accounts must appear
|
||||
// in public_diff after successful validation (nonces are updated in the diff).
|
||||
//
|
||||
// With mutation `public_diff → HashMap::new()`:
|
||||
// The map is empty → `contains_key(&signer)` returns false → assert fires.
|
||||
//
|
||||
// Uses `common::test_utils::create_transaction_native_token_transfer` to
|
||||
// construct a semantically valid transaction (correct instruction type).
|
||||
{
|
||||
let key = PrivateKey::try_new([7_u8; 32]).expect("known-good key");
|
||||
let pubkey = PublicKey::new_from_private_key(&key);
|
||||
let addr = AccountId::from(&pubkey);
|
||||
|
||||
let key2 = PrivateKey::try_new([8_u8; 32]).expect("known-good key");
|
||||
let pubkey2 = PublicKey::new_from_private_key(&key2);
|
||||
let addr2 = AccountId::from(&pubkey2);
|
||||
|
||||
let state = V03State::new_with_genesis_accounts(
|
||||
&[(addr, 10_000_u128), (addr2, 10_000_u128)],
|
||||
vec![],
|
||||
0,
|
||||
);
|
||||
|
||||
// Use the test utility to build a valid native token transfer.
|
||||
// This uses the correct authenticated_transfer_core::Instruction::Transfer,
|
||||
// which the program can actually execute without panicking.
|
||||
let lee_tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
addr,
|
||||
0, // nonce = 0 (matches initial state nonce)
|
||||
addr2,
|
||||
100, // amount
|
||||
&key,
|
||||
);
|
||||
|
||||
if let LeeTransaction::Public(pub_tx) = &lee_tx {
|
||||
if let Ok(diff) = ValidatedStateDiff::from_public_transaction(&pub_tx, &state, 1, 0) {
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
// The signer/sender (addr) must be in the diff: a native transfer
|
||||
// debits its balance, so it MUST appear in public_diff.
|
||||
// If public_diff() returns an empty HashMap, this assert fires.
|
||||
//
|
||||
// Note: nonce increments are applied separately during
|
||||
// `apply_state_diff` via `signer_account_ids` and are NOT recorded
|
||||
// in `public_diff`, so we do not assert on the nonce field here.
|
||||
assert!(
|
||||
public_diff.contains_key(&addr),
|
||||
"INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \
|
||||
public_diff must contain the sender {:?} (its balance is debited) \
|
||||
after a successful native transfer \
|
||||
(mutation public_diff→HashMap::new() detected)",
|
||||
addr,
|
||||
);
|
||||
|
||||
// The diff must reflect the balance debit on the sender — the
|
||||
// balance recorded in the diff must differ from the pre-state.
|
||||
let pre_balance = state.get_account_by_id(addr).balance;
|
||||
let post_balance = public_diff[&addr].balance;
|
||||
assert_ne!(
|
||||
post_balance,
|
||||
pre_balance,
|
||||
"INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \
|
||||
sender balance in the diff must differ from pre-state after a transfer \
|
||||
(pre={pre_balance}, post={post_balance})",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
271
fuzz/fuzz_targets/fuzz_transaction_properties.rs
Normal file
271
fuzz/fuzz_targets/fuzz_transaction_properties.rs
Normal file
@ -0,0 +1,271 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: transaction property invariants.
|
||||
//!
|
||||
//! Tests that key accessor methods on `LeeTransaction`, `PublicTransaction`, and
|
||||
//! `ValidatedStateDiff` return correct, non-stub values.
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
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,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
PublicTransaction,
|
||||
program::Program,
|
||||
};
|
||||
use nssa_core::account::Nonce;
|
||||
|
||||
fuzz_props::fuzz_entry!(|data: &[u8]| {
|
||||
let mut u = Unstructured::new(data);
|
||||
|
||||
// ── Part 1: Known-good witness set / transaction using fixed keys ──────────
|
||||
// Uses deterministic keys so we always have at least one valid transaction.
|
||||
// This ensures hash, signer_account_ids, into_raw_parts, and affected_accounts
|
||||
// are always tested, even when the fuzzer input is insufficient for arb generators.
|
||||
{
|
||||
let key1 = PrivateKey::try_new([1_u8; 32]).expect("known-good key");
|
||||
let key2 = PrivateKey::try_new([2_u8; 32]).expect("known-good key");
|
||||
let pub1 = PublicKey::new_from_private_key(&key1);
|
||||
let pub2 = PublicKey::new_from_private_key(&key2);
|
||||
let addr1 = AccountId::from(&pub1);
|
||||
let addr2 = AccountId::from(&pub2);
|
||||
|
||||
let nonces = vec![Nonce::from(0_u128), Nonce::from(0_u128)];
|
||||
let message = Message::try_new(
|
||||
Program::authenticated_transfer_program().id(),
|
||||
vec![addr1, addr2],
|
||||
nonces,
|
||||
1337_u64,
|
||||
)
|
||||
.expect("known-good message");
|
||||
|
||||
let ws = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let pub_tx = PublicTransaction::new(message, ws);
|
||||
|
||||
// ── INVARIANT [SignerIdsNonEmpty] ─────────────────────────────────────
|
||||
// A transaction signed by 2 keys must expose 2 signer (key, sig) pairs.
|
||||
// `signer_account_ids` is pub(crate); we verify via the public witness_set API.
|
||||
let ws_pairs = pub_tx.witness_set().signatures_and_public_keys();
|
||||
assert_eq!(
|
||||
ws_pairs.len(),
|
||||
2,
|
||||
"INVARIANT VIOLATION [SignerIdsNonEmpty]: \
|
||||
witness_set signatures_and_public_keys must have 2 entries",
|
||||
);
|
||||
|
||||
// ── INVARIANT [IntoRawPartsCount] ─────────────────────────────────────
|
||||
// `into_raw_parts` must return the same number of pairs as the witness set.
|
||||
// Catches the mutation that returns `vec![]`.
|
||||
let ws2 = WitnessSet::for_message(pub_tx.message(), &[&key1, &key2]);
|
||||
let parts = ws2.into_raw_parts();
|
||||
assert_eq!(
|
||||
parts.len(),
|
||||
2,
|
||||
"INVARIANT VIOLATION [IntoRawPartsCount]: \
|
||||
WitnessSet::into_raw_parts must return 2 pairs for a 2-key witness set",
|
||||
);
|
||||
|
||||
// ── INVARIANT [AffectedAccountsContainSigners] ───────────────────────
|
||||
// `affected_public_account_ids` must include the signer accounts.
|
||||
// Catches the mutation that returns `vec![]` or `vec![Default::default()]`.
|
||||
let affected = pub_tx.affected_public_account_ids();
|
||||
assert!(
|
||||
!affected.is_empty(),
|
||||
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
||||
affected_public_account_ids must be non-empty for a 2-signer tx",
|
||||
);
|
||||
assert!(
|
||||
affected.contains(&addr1),
|
||||
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
||||
affected_public_account_ids must include addr1 (signer)",
|
||||
);
|
||||
assert!(
|
||||
affected.contains(&addr2),
|
||||
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
||||
affected_public_account_ids must include addr2 (signer)",
|
||||
);
|
||||
|
||||
// ── INVARIANT [HashNonDefault] ────────────────────────────────────────
|
||||
// The transaction hash must not be the all-zero default.
|
||||
// Catches the mutation that returns `Default::default()`.
|
||||
let lee_tx = LeeTransaction::Public(pub_tx);
|
||||
let hash = lee_tx.hash();
|
||||
assert_ne!(
|
||||
hash.0,
|
||||
[0_u8; 32],
|
||||
"INVARIANT VIOLATION [HashNonDefault]: \
|
||||
LeeTransaction::hash must not return all-zero bytes",
|
||||
);
|
||||
|
||||
// Also verify it's deterministic (same tx → same hash):
|
||||
let hash2 = lee_tx.hash();
|
||||
assert_eq!(
|
||||
hash,
|
||||
hash2,
|
||||
"INVARIANT VIOLATION [HashDeterministic]: \
|
||||
LeeTransaction::hash must be deterministic",
|
||||
);
|
||||
|
||||
// LeeTransaction::affected_public_account_ids must also be non-empty:
|
||||
let lee_affected = lee_tx.affected_public_account_ids();
|
||||
assert!(
|
||||
lee_affected.contains(&addr1),
|
||||
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
||||
LeeTransaction::affected_public_account_ids must include addr1",
|
||||
);
|
||||
}
|
||||
|
||||
// ── INVARIANT [SignerOnlyAccountInAffected] ───────────────────────────────
|
||||
// Build a transaction signed by a key whose AccountId is NOT among
|
||||
// `message.account_ids`. Then `affected_public_account_ids` can only contain
|
||||
// the signer's AccountId via `signer_account_ids()` — it is absent from the
|
||||
// message's account list. This directly catches the `signer_account_ids`
|
||||
// mutations (`→ vec![]` / `→ vec![Default::default()]`) on PublicTransaction,
|
||||
// which the earlier checks miss because there the signer also appears in
|
||||
// `message.account_ids`.
|
||||
{
|
||||
// Signer key — its AccountId must NOT appear in the message account list.
|
||||
let signer_key = PrivateKey::try_new([9_u8; 32]).expect("known-good key");
|
||||
let signer_pub = PublicKey::new_from_private_key(&signer_key);
|
||||
let signer_addr = AccountId::from(&signer_pub);
|
||||
|
||||
// Two unrelated account IDs for the message (deterministic, not derived
|
||||
// from the signer key).
|
||||
let other1 = AccountId::new([0xA1_u8; 32]);
|
||||
let other2 = AccountId::new([0xA2_u8; 32]);
|
||||
|
||||
// Guard: ensure the signer is genuinely not one of the message accounts.
|
||||
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(),
|
||||
vec![other1, other2],
|
||||
nonces,
|
||||
7_u64,
|
||||
) {
|
||||
let ws = WitnessSet::for_message(&msg, &[&signer_key]);
|
||||
let pt = PublicTransaction::new(msg, ws);
|
||||
|
||||
let affected = pt.affected_public_account_ids();
|
||||
assert!(
|
||||
affected.contains(&signer_addr),
|
||||
"INVARIANT VIOLATION [SignerOnlyAccountInAffected]: \
|
||||
affected_public_account_ids must include the signer {:?} even when it \
|
||||
is absent from message.account_ids — signer_account_ids() must not \
|
||||
return an empty (or defaulted) vec",
|
||||
signer_addr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Part 2: Fuzz-driven state + valid native transfer ─────────────────────
|
||||
// Generates a random state and a correctly-signed transfer. When the transfer
|
||||
// succeeds, verifies that `public_diff` is non-empty and contains the
|
||||
// expected account changes.
|
||||
{
|
||||
let fuzz_accs = match arbitrary_fuzz_state(&mut u) {
|
||||
Ok(accs) => accs,
|
||||
Err(_) => return,
|
||||
};
|
||||
let init_accs: Vec<(AccountId, u128)> = fuzz_accs
|
||||
.iter()
|
||||
.map(|a| (a.account_id, a.balance))
|
||||
.collect();
|
||||
let state = V03State::new_with_genesis_accounts(&init_accs, vec![], 0);
|
||||
|
||||
let Ok(tx) = arb_fuzz_native_transfer(&mut u, &fuzz_accs) else {
|
||||
return;
|
||||
};
|
||||
let Ok(checked) = tx.transaction_stateless_check() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pub_tx = match &checked {
|
||||
LeeTransaction::Public(pt) => pt,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// For a public transaction with signers, affected_public_account_ids must
|
||||
// include all signer account IDs. Derive signers from the public witness API.
|
||||
let signers: Vec<AccountId> = pub_tx
|
||||
.witness_set()
|
||||
.signatures_and_public_keys()
|
||||
.iter()
|
||||
.map(|(_, pk)| AccountId::from(pk))
|
||||
.collect();
|
||||
let affected = pub_tx.affected_public_account_ids();
|
||||
for signer in &signers {
|
||||
assert!(
|
||||
affected.contains(signer),
|
||||
"INVARIANT VIOLATION [AffectedAccountsContainSigners]: \
|
||||
affected_public_account_ids must include signer {:?}",
|
||||
signer,
|
||||
);
|
||||
}
|
||||
|
||||
// When from_public_transaction succeeds, public_diff must be non-empty
|
||||
// (at least the signer nonces are updated).
|
||||
// Catches the mutation that returns `HashMap::new()`.
|
||||
if let Ok(diff) = ValidatedStateDiff::from_public_transaction(pub_tx, &state, 1, 0) {
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
// The diff must contain at least the signer accounts (nonce updates):
|
||||
for signer in &signers {
|
||||
// Signers appear in diff because their nonces are updated.
|
||||
// If public_diff() returns empty HashMap, this assert fires.
|
||||
assert!(
|
||||
public_diff.contains_key(signer),
|
||||
"INVARIANT VIOLATION [PublicDiffNonEmptyOnSuccess]: \
|
||||
public_diff must contain signer account {:?} after successful validation \
|
||||
(nonce must have been updated)",
|
||||
signer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Part 3: Fuzz-driven arbitrary keys for additional coverage ─────────────
|
||||
{
|
||||
if let Ok(key_wrap) = ArbPrivateKey::arbitrary(&mut u) {
|
||||
let key = key_wrap.0;
|
||||
let pubkey = PublicKey::new_from_private_key(&key);
|
||||
let addr = AccountId::from(&pubkey);
|
||||
|
||||
let nonces = vec![Nonce::from(0_u128)];
|
||||
if let Ok(msg) = Message::try_new(
|
||||
Program::authenticated_transfer_program().id(),
|
||||
vec![addr],
|
||||
nonces,
|
||||
42_u64,
|
||||
) {
|
||||
let ws = WitnessSet::for_message(&msg, &[&key]);
|
||||
let pt = PublicTransaction::new(msg, ws);
|
||||
|
||||
// Single-signer checks via witness_set (signer_account_ids is pub(crate)):
|
||||
let ws_pairs2 = pt.witness_set().signatures_and_public_keys();
|
||||
assert_eq!(
|
||||
ws_pairs2.len(),
|
||||
1,
|
||||
"INVARIANT VIOLATION [SignerIdsNonEmpty]: 1-key witness set must have 1 pair",
|
||||
);
|
||||
let derived_addr = AccountId::from(&ws_pairs2[0].1);
|
||||
assert_eq!(
|
||||
derived_addr,
|
||||
addr,
|
||||
"INVARIANT VIOLATION [SignerIdsDerivedFromKeys]: \
|
||||
derived signer address must match expected addr",
|
||||
);
|
||||
|
||||
let ws2 = WitnessSet::for_message(pt.message(), &[&key]);
|
||||
let parts = ws2.into_raw_parts();
|
||||
assert_eq!(
|
||||
parts.len(),
|
||||
1,
|
||||
"INVARIANT VIOLATION [IntoRawPartsCount]: 1-signer witness set → 1 raw part",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -38,6 +38,12 @@ targets=(
|
||||
fuzz_sequencer_vs_replayer
|
||||
fuzz_merkle_tree
|
||||
fuzz_genesis_invariants
|
||||
fuzz_common_invariants
|
||||
fuzz_transaction_properties
|
||||
fuzz_privacy_preserving_witness
|
||||
fuzz_encoding_privacy_preserving
|
||||
fuzz_nullifier_set_roundtrip
|
||||
fuzz_system_account_protection
|
||||
)
|
||||
|
||||
# cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user