From a3d0b30234592463a366ca0052745982abd50073 Mon Sep 17 00:00:00 2001 From: Davidson Souza Date: Tue, 3 Feb 2026 16:51:08 -0300 Subject: [PATCH 1/3] feat: init fuzz integration --- fuzz/.gitignore | 4 ++++ fuzz/Cargo.toml | 28 ++++++++++++++++++++++++++++ src/accumulator/pollard.rs | 8 ++++---- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.toml diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..b817023 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rustreexo-fuzz" +version = "0.0.0" +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.rustreexo] +path = ".." + +[[bin]] +name = "pollard_operations" +path = "fuzz_targets/pollard_operations.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "pollard_serialization" +path = "fuzz_targets/pollard_ser.rs" +test = false +doc = false +bench = false diff --git a/src/accumulator/pollard.rs b/src/accumulator/pollard.rs index a494a06..0f3d84d 100644 --- a/src/accumulator/pollard.rs +++ b/src/accumulator/pollard.rs @@ -399,11 +399,11 @@ impl PollardNode { /// The deletion algorithm for utreexo works like this: let's say we have the following tree: /// /// ```! - /// 06 - /// |---------\ - /// 04 05 + /// 06 + /// |---------\ + /// 04 05 /// |-----\ |-----\ - /// 00 01 02 03 + /// 00 01 02 03 /// ``` /// /// to delete `03`, we simply move `02` up to `09`'s position, so now we have: From d2730593dcf45c4bc00c60dc200f3a10430939f7 Mon Sep 17 00:00:00 2001 From: Davidson Souza Date: Tue, 3 Feb 2026 16:51:37 -0300 Subject: [PATCH 2/3] fuzz: add a fuzz target for pollard serialization --- fuzz/fuzz_targets/pollard_ser.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 fuzz/fuzz_targets/pollard_ser.rs diff --git a/fuzz/fuzz_targets/pollard_ser.rs b/fuzz/fuzz_targets/pollard_ser.rs new file mode 100644 index 0000000..e6d955d --- /dev/null +++ b/fuzz/fuzz_targets/pollard_ser.rs @@ -0,0 +1,18 @@ +#![no_main] + +use std::io::Cursor; + +use libfuzzer_sys::fuzz_target; +use rustreexo::accumulator::{node_hash::BitcoinNodeHash, pollard::Pollard}; + +fuzz_target!(|data: &[u8]| { + let mut cursor = Cursor::new(data); + let Ok(pollard) = Pollard::::deserialize(&mut cursor) else { + return; + }; + + let mut serialized_pollard = Vec::new(); + pollard.serialize(&mut serialized_pollard).unwrap(); + + assert_eq!(data, serialized_pollard.as_slice()); +}); From 137fdf316052bceb0a35b4b73d710fcb51f0a42f Mon Sep 17 00:00:00 2001 From: Davidson Souza Date: Tue, 3 Feb 2026 16:52:09 -0300 Subject: [PATCH 3/3] fuzz: add a pollard target that tests basic operations This commit adds a fuzz target that generates mock blocks with adds and dels, then applies those blocks to a new full pollad, and makes sure that all operations works just as expected. Blocks are generated from fuzzer's random data, building fake values. We don't need them to be consensus-valid. --- fuzz/fuzz_targets/pollard_operations.rs | 20 +++++ fuzz/fuzz_targets/util.rs | 112 ++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 fuzz/fuzz_targets/pollard_operations.rs create mode 100644 fuzz/fuzz_targets/util.rs diff --git a/fuzz/fuzz_targets/pollard_operations.rs b/fuzz/fuzz_targets/pollard_operations.rs new file mode 100644 index 0000000..7cf85e5 --- /dev/null +++ b/fuzz/fuzz_targets/pollard_operations.rs @@ -0,0 +1,20 @@ +#![no_main] + +mod util; + +use util::*; + +use libfuzzer_sys::fuzz_target; +use rustreexo::accumulator::node_hash::BitcoinNodeHash; +use rustreexo::accumulator::pollard::Pollard; +use rustreexo::accumulator::proof::Proof; + +fuzz_target!(|data: FuzzInput| { + let mut pollard = Pollard::::new(); + + for block in data.blocks.iter() { + pollard + .modify(&block.additions, &block.removals, Proof::default()) + .unwrap(); + } +}); diff --git a/fuzz/fuzz_targets/util.rs b/fuzz/fuzz_targets/util.rs new file mode 100644 index 0000000..d66b367 --- /dev/null +++ b/fuzz/fuzz_targets/util.rs @@ -0,0 +1,112 @@ +use std::collections::HashSet; +use std::fmt::Debug; +use std::ops::ControlFlow; + +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; +use libfuzzer_sys::arbitrary::Unstructured; +use rustreexo::accumulator::node_hash::BitcoinNodeHash; +use rustreexo::accumulator::pollard::PollardAddition; + +/// A mock block containing additions and removals to an accumulator. +pub struct Block { + /// Things being added to the accumulator. + pub additions: Vec>, + + /// Things being removed from the accumulator. + /// + /// These must already be present in the accumulator. + pub removals: Vec, +} + +impl Debug for Block { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Block") + .field( + "additions", + &self + .additions + .iter() + .map(|a| &a.hash) + .collect::>(), + ) + .field("removals", &self.removals) + .finish() + } +} + +#[derive(Debug)] +/// The input for the fuzzer: a sequence of blocks. +pub struct FuzzInput { + pub blocks: Vec, +} + +/// Build a BitcoinNodeHash from a u64 by placing the u64 in the first 8 bytes and zeroing the +/// rest. +pub fn build_hash(hash: u64) -> BitcoinNodeHash { + let mut hash_arr = [0u8; 32]; + hash_arr[..8].copy_from_slice(&hash.to_le_bytes()); + BitcoinNodeHash::from(hash_arr) +} + +/// Build a mock block with additions and removals. +pub fn build_block( + u: &mut Unstructured<'_>, + txos: &mut HashSet, + utxos: &mut Vec>, +) -> arbitrary::Result { + let additions_count = u.arbitrary_len::<[u8; 32]>()? % 20; + let mut additions = Vec::with_capacity(additions_count); + for _ in 0..additions_count { + let new_hash_element: u64 = u.arbitrary()?; + let hash = build_hash(new_hash_element as u64); + if txos.contains(&hash) { + continue; + } + + let addition = PollardAddition { + hash, + remember: true, + }; + additions.push(addition.clone()); + } + + let mut removals = Vec::new(); + u.arbitrary_loop(None, None, |u| { + if utxos.is_empty() { + return Ok(std::ops::ControlFlow::Break(())); + } + + let remove_index = u.arbitrary::()? % utxos.len(); + let removed = utxos.swap_remove(remove_index); + removals.push(removed.hash); + Ok(ControlFlow::Continue(())) + })?; + + Ok(Block { + additions, + removals, + }) +} + +impl Arbitrary<'_> for FuzzInput { + fn arbitrary( + u: &mut Unstructured<'_>, + ) -> arbitrary::Result { + let n_blocks = u.int_in_range(1..=100)?; + let mut blocks = Vec::with_capacity(n_blocks); + let mut utxos = Vec::new(); + let mut txos = HashSet::new(); + + for _ in 0..n_blocks { + let block = build_block(u, &mut txos, &mut utxos)?; + for addition in &block.additions { + utxos.push(addition.clone()); + txos.insert(addition.hash); + } + blocks.push(block); + } + + Ok(FuzzInput { blocks }) + } +}