mirror of
https://github.com/logos-blockchain/lez-fuzzing.git
synced 2026-06-14 06:59:29 +00:00
test: add merkle tree target to catch 33 mutants
This commit is contained in:
parent
1cfe58ebf9
commit
2974cd5e30
1
fuzz/Cargo.lock
generated
1
fuzz/Cargo.lock
generated
@ -2084,6 +2084,7 @@ dependencies = [
|
||||
"lee",
|
||||
"lee_core",
|
||||
"libfuzzer-sys",
|
||||
"sha2",
|
||||
"testnet_initial_state",
|
||||
]
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ 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" }
|
||||
@ -119,3 +120,9 @@ name = "fuzz_sequencer_vs_replayer"
|
||||
path = "fuzz_targets/fuzz_sequencer_vs_replayer.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_merkle_tree"
|
||||
path = "fuzz_targets/fuzz_merkle_tree.rs"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
130
fuzz/fuzz_targets/fuzz_merkle_tree.rs
Normal file
130
fuzz/fuzz_targets/fuzz_merkle_tree.rs
Normal file
@ -0,0 +1,130 @@
|
||||
#![cfg_attr(feature = "fuzzer-libfuzzer", no_main)]
|
||||
//! Fuzz target: `MerkleTree` structural invariants
|
||||
//!
|
||||
//! Covered code paths (all in `lee/state_machine/src/merkle_tree/mod.rs`):
|
||||
//!
|
||||
//! ```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
|
||||
//! ```
|
||||
//!
|
||||
//! # 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.
|
||||
//!
|
||||
//! # Invariants checked
|
||||
//!
|
||||
//! 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`.
|
||||
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
// ─── 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
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Nothing to test with an empty input.
|
||||
if values.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);
|
||||
|
||||
// ── INVARIANT [InsertionIndex] ────────────────────────────────────────────
|
||||
// insert() must return 0, 1, 2, … in order.
|
||||
for (expected_index, &value) in values.iter().enumerate() {
|
||||
let actual_index = tree.insert(value);
|
||||
assert_eq!(
|
||||
actual_index,
|
||||
expected_index,
|
||||
"INVARIANT VIOLATION [InsertionIndex]: \
|
||||
insert returned {actual_index} but expected {expected_index}",
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
assert!(
|
||||
verify_auth_path(value, index, &path, &root),
|
||||
"INVARIANT VIOLATION [AuthPathValid]: \
|
||||
authentication path for index {index} does not re-hash to root()",
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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(),
|
||||
);
|
||||
});
|
||||
@ -36,6 +36,7 @@ targets=(
|
||||
fuzz_apply_state_diff_split_path
|
||||
fuzz_multi_block_state_sequence
|
||||
fuzz_sequencer_vs_replayer
|
||||
fuzz_merkle_tree
|
||||
)
|
||||
|
||||
# cargo-fuzz requires the nightly toolchain (-Zsanitizer=address etc.).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user