From 321f1c77e5f1c5d9c93083ece083638c602a4df8 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 24 Feb 2026 10:45:51 +0100 Subject: [PATCH 1/2] Add fuzz target for UTXO validation logic Add a dedicated fuzz target exercising the UTXO verification logic in `routing::utxo`, covering the `PendingChecks` state machine through the `NetworkGraph` and `P2PGossipSync` public APIs. The `FuzzChainSource` returns fuzz-driven `UtxoResult` variants including sync success/failure, async with immediate resolution, and async with deferred resolution. The fuzz loop drives channel announcements (with and without UTXO lookup), node announcements, channel updates, deferred future resolution (success and failure), completed-check processing, and future drops without resolution. Co-Authored-By: HAL 9000 --- fuzz/src/bin/gen_target.sh | 1 + fuzz/src/bin/utxo_validation_target.rs | 121 ++++++++++++ fuzz/src/lib.rs | 1 + fuzz/src/utxo_validation.rs | 251 +++++++++++++++++++++++++ fuzz/targets.h | 1 + 5 files changed, 375 insertions(+) create mode 100644 fuzz/src/bin/utxo_validation_target.rs create mode 100644 fuzz/src/utxo_validation.rs diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index b4f0c7a12b9..df9042687cd 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -28,6 +28,7 @@ GEN_TEST base32 GEN_TEST fromstr_to_netaddress GEN_TEST feature_flags GEN_TEST lsps_message +GEN_TEST utxo_validation GEN_TEST fs_store GEN_TEST msg_accept_channel msg_targets:: diff --git a/fuzz/src/bin/utxo_validation_target.rs b/fuzz/src/bin/utxo_validation_target.rs new file mode 100644 index 00000000000..eed425eac31 --- /dev/null +++ b/fuzz/src/bin/utxo_validation_target.rs @@ -0,0 +1,121 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] +#![cfg_attr(rustfmt, rustfmt_skip)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +#[cfg(not(hashes_fuzz))] +compile_error!("Fuzz targets need cfg=hashes_fuzz"); + +#[cfg(not(secp256k1_fuzz))] +compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); + +extern crate lightning_fuzz; +use lightning_fuzz::utxo_validation::*; +use lightning_fuzz::utils::test_logger; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + utxo_validation_test(&data, test_logger::DevNull {}); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + utxo_validation_test(&data, test_logger::DevNull {}); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + utxo_validation_test(data, test_logger::DevNull {}); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + utxo_validation_test(&data, lightning_fuzz::utils::test_logger::Stdout {}); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + utxo_validation_test(&data, test_logger::DevNull {}); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/utxo_validation") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + utxo_validation_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 582fa346c54..d34f2515bac 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -44,6 +44,7 @@ pub mod process_onion_failure; pub mod refund_deser; pub mod router; pub mod static_invoice_deser; +pub mod utxo_validation; pub mod zbase32; pub mod fs_store; diff --git a/fuzz/src/utxo_validation.rs b/fuzz/src/utxo_validation.rs new file mode 100644 index 00000000000..894cb474fce --- /dev/null +++ b/fuzz/src/utxo_validation.rs @@ -0,0 +1,251 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bitcoin::amount::Amount; +use bitcoin::constants::ChainHash; +use bitcoin::network::Network; +use bitcoin::script::Builder; +use bitcoin::transaction::TxOut; + +use lightning::ln::msgs; +use lightning::ln::msgs::BaseMessageHandler; +use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; +use lightning::routing::utxo::{UtxoFuture, UtxoLookup, UtxoLookupError, UtxoResult}; +use lightning::util::ser::LengthReadable; +use lightning::util::wakers::Notifier; + +use crate::utils::test_logger; + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +#[inline] +pub fn slice_to_be16(v: &[u8]) -> u16 { + ((v[0] as u16) << 8) | (v[1] as u16) +} + +struct InputData { + data: Vec, + read_pos: AtomicUsize, +} +impl InputData { + fn get_slice(&self, len: usize) -> Option<&[u8]> { + let old_pos = self.read_pos.fetch_add(len, Ordering::AcqRel); + if self.data.len() < old_pos + len { + return None; + } + Some(&self.data[old_pos..old_pos + len]) + } + fn get_slice_nonadvancing(&self, len: usize) -> Option<&[u8]> { + let old_pos = self.read_pos.load(Ordering::Acquire); + if self.data.len() < old_pos + len { + return None; + } + Some(&self.data[old_pos..old_pos + len]) + } +} + +struct FuzzChainSource { + input: Arc, + pending_futures: Arc>>, +} + +impl UtxoLookup for FuzzChainSource { + fn get_utxo( + &self, _chain_hash: &ChainHash, _scid: u64, notifier: Arc, + ) -> UtxoResult { + let input_slice = match self.input.get_slice(2) { + Some(s) => s, + None => return UtxoResult::Sync(Err(UtxoLookupError::UnknownTx)), + }; + let txo_res = TxOut { + value: Amount::from_sat(1_000_000), + script_pubkey: Builder::new() + .push_int(input_slice[1] as i64) + .into_script() + .to_p2wsh(), + }; + match input_slice[0] % 6 { + 0 => UtxoResult::Sync(Err(UtxoLookupError::UnknownChain)), + 1 => UtxoResult::Sync(Err(UtxoLookupError::UnknownTx)), + 2 => UtxoResult::Sync(Ok(txo_res)), + 3 => { + // Async, resolve immediately with success + let future = UtxoFuture::new(notifier); + future.resolve(Ok(txo_res)); + UtxoResult::Async(future) + }, + 4 => { + // Async, resolve immediately with error + let future = UtxoFuture::new(notifier); + future.resolve(Err(UtxoLookupError::UnknownTx)); + UtxoResult::Async(future) + }, + 5 | _ => { + // Async, deferred resolution - store for later + let future = UtxoFuture::new(notifier); + self.pending_futures.lock().unwrap().push(future.clone()); + UtxoResult::Async(future) + }, + } + } +} + +#[inline] +pub fn do_test(data: &[u8], out: Out) { + let input = Arc::new(InputData { data: data.to_vec(), read_pos: AtomicUsize::new(0) }); + + macro_rules! get_slice_nonadvancing { + ($len: expr) => { + match input.get_slice_nonadvancing($len as usize) { + Some(slice) => slice, + None => return, + } + }; + } + macro_rules! get_slice { + ($len: expr) => { + match input.get_slice($len as usize) { + Some(slice) => slice, + None => return, + } + }; + } + + macro_rules! decode_msg { + ($MsgType: path, $len: expr) => {{ + let data = get_slice!($len); + let mut reader = &data[..]; + match <$MsgType>::read_from_fixed_length_buffer(&mut reader) { + Ok(msg) => { + assert!(reader.is_empty()); + msg + }, + Err(e) => match e { + msgs::DecodeError::UnknownVersion => return, + msgs::DecodeError::UnknownRequiredFeature => return, + msgs::DecodeError::InvalidValue => return, + msgs::DecodeError::BadLengthDescriptor => return, + msgs::DecodeError::ShortRead => panic!("We picked the length..."), + msgs::DecodeError::Io(e) => panic!("{:?}", e), + msgs::DecodeError::UnsupportedCompression => return, + msgs::DecodeError::DangerousValue => return, + }, + } + }}; + } + + macro_rules! decode_msg_with_len16 { + ($MsgType: path, $excess: expr) => {{ + let extra_len = slice_to_be16(get_slice_nonadvancing!(2)); + decode_msg!($MsgType, 2 + (extra_len as usize) + $excess) + }}; + } + + let logger = test_logger::TestLogger::new("".to_owned(), out); + let net_graph = NetworkGraph::new(Network::Bitcoin, &logger); + let pending_futures: Arc>> = Arc::new(Mutex::new(Vec::new())); + let chain_source = + FuzzChainSource { input: Arc::clone(&input), pending_futures: Arc::clone(&pending_futures) }; + // Create a P2PGossipSync so we can call get_and_clear_pending_msg_events to trigger + // check_resolved_futures processing. + let gossip_sync = + P2PGossipSync::new(&net_graph, None::<&FuzzChainSource>, &logger); + + loop { + match get_slice!(1)[0] % 8 { + // Channel announcement with UTXO lookup + 0 => { + let msg = decode_msg_with_len16!( + msgs::UnsignedChannelAnnouncement, + 32 + 8 + 33 * 4 + ); + let _ = net_graph + .update_channel_from_unsigned_announcement(&msg, &Some(&chain_source)); + }, + // Channel announcement without UTXO lookup + 1 => { + let msg = decode_msg_with_len16!( + msgs::UnsignedChannelAnnouncement, + 32 + 8 + 33 * 4 + ); + let _ = net_graph + .update_channel_from_unsigned_announcement::<&FuzzChainSource>(&msg, &None); + }, + // Node announcement + 2 => { + let start_len = slice_to_be16(&get_slice_nonadvancing!(2)[0..2]) as usize; + let addr_len = slice_to_be16( + &get_slice_nonadvancing!(start_len + 2 + 74) + [start_len + 2 + 72..start_len + 2 + 74], + ); + if addr_len > (37 + 1) * 4 { + return; + } + let msg = decode_msg_with_len16!(msgs::UnsignedNodeAnnouncement, 288); + let _ = net_graph.update_node_from_unsigned_announcement(&msg); + }, + // Channel update + 3 => { + let msg = decode_msg!(msgs::UnsignedChannelUpdate, 72); + let _ = net_graph.update_channel_unsigned(&msg); + }, + // Resolve a pending future with success + 4 => { + let mut futures = pending_futures.lock().unwrap(); + if !futures.is_empty() { + let idx_byte = get_slice!(1)[0] as usize; + let idx = idx_byte % futures.len(); + let future = futures.remove(idx); + let script_byte = get_slice!(1)[0]; + let txo = TxOut { + value: Amount::from_sat(1_000_000), + script_pubkey: Builder::new() + .push_int(script_byte as i64) + .into_script() + .to_p2wsh(), + }; + future.resolve(Ok(txo)); + } + }, + // Resolve a pending future with error + 5 => { + let mut futures = pending_futures.lock().unwrap(); + if !futures.is_empty() { + let idx_byte = get_slice!(1)[0] as usize; + let idx = idx_byte % futures.len(); + let future = futures.remove(idx); + future.resolve(Err(UtxoLookupError::UnknownTx)); + } + }, + // Process completed checks (triggers check_resolved_futures) + 6 => { + gossip_sync.get_and_clear_pending_msg_events(); + }, + // Drop a pending future without resolving + 7 | _ => { + let mut futures = pending_futures.lock().unwrap(); + if !futures.is_empty() { + let idx_byte = get_slice!(1)[0] as usize; + let idx = idx_byte % futures.len(); + futures.remove(idx); + } + }, + } + } +} + +pub fn utxo_validation_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn utxo_validation_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/targets.h b/fuzz/targets.h index 921439836af..e24144e85a9 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -21,6 +21,7 @@ void base32_run(const unsigned char* data, size_t data_len); void fromstr_to_netaddress_run(const unsigned char* data, size_t data_len); void feature_flags_run(const unsigned char* data, size_t data_len); void lsps_message_run(const unsigned char* data, size_t data_len); +void utxo_validation_run(const unsigned char* data, size_t data_len); void fs_store_run(const unsigned char* data, size_t data_len); void msg_accept_channel_run(const unsigned char* data, size_t data_len); void msg_announcement_signatures_run(const unsigned char* data, size_t data_len); From 7bd37056038659e3fb8a32a2720d543b962d2b33 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 24 Feb 2026 11:10:22 +0100 Subject: [PATCH 2/2] Add fuzz target for `GossipVerifier` block-sync flow Add a dedicated fuzz target exercising the `GossipVerifier` from `lightning-block-sync`, which implements `UtxoLookup` by fetching blocks and checking UTXOs via the `BlockSource`/`UtxoSource` traits. The `FuzzBlockSource` drives all async methods from fuzz input, covering `BlockSourceError` variants (persistent and transient), `HeaderOnly` vs `FullBlock` responses, variable block shapes (transaction and output counts), the 5-confirmation depth check, and the `is_output_unspent` spent/unspent paths. The fuzz loop feeds channel announcements through the `GossipVerifier`, interleaved with node announcements, channel updates, and `check_resolved_futures` processing via `P2PGossipSync`. Co-Authored-By: HAL 9000 --- fuzz/Cargo.toml | 1 + fuzz/src/bin/gen_target.sh | 1 + fuzz/src/bin/gossip_verifier_target.rs | 121 +++++++++++ fuzz/src/gossip_verifier.rs | 288 +++++++++++++++++++++++++ fuzz/src/lib.rs | 2 + fuzz/targets.h | 1 + 6 files changed, 414 insertions(+) create mode 100644 fuzz/src/bin/gossip_verifier_target.rs create mode 100644 fuzz/src/gossip_verifier.rs diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 5bf899f34b1..615fdf4af9a 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -19,6 +19,7 @@ stdin_fuzz = [] [dependencies] lightning = { path = "../lightning", features = ["regex", "_test_utils"] } +lightning-block-sync = { path = "../lightning-block-sync", features = ["tokio"] } lightning-invoice = { path = "../lightning-invoice" } lightning-liquidity = { path = "../lightning-liquidity" } lightning-rapid-gossip-sync = { path = "../lightning-rapid-gossip-sync" } diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index df9042687cd..15286deb82a 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -28,6 +28,7 @@ GEN_TEST base32 GEN_TEST fromstr_to_netaddress GEN_TEST feature_flags GEN_TEST lsps_message +GEN_TEST gossip_verifier GEN_TEST utxo_validation GEN_TEST fs_store diff --git a/fuzz/src/bin/gossip_verifier_target.rs b/fuzz/src/bin/gossip_verifier_target.rs new file mode 100644 index 00000000000..3655c58a40b --- /dev/null +++ b/fuzz/src/bin/gossip_verifier_target.rs @@ -0,0 +1,121 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] +#![cfg_attr(rustfmt, rustfmt_skip)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +#[cfg(not(hashes_fuzz))] +compile_error!("Fuzz targets need cfg=hashes_fuzz"); + +#[cfg(not(secp256k1_fuzz))] +compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); + +extern crate lightning_fuzz; +use lightning_fuzz::gossip_verifier::*; +use lightning_fuzz::utils::test_logger; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + gossip_verifier_test(&data, test_logger::DevNull {}); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + gossip_verifier_test(&data, test_logger::DevNull {}); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + gossip_verifier_test(data, test_logger::DevNull {}); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + gossip_verifier_test(&data, lightning_fuzz::utils::test_logger::Stdout {}); +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec = vec![0]; + gossip_verifier_test(&data, test_logger::DevNull {}); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/gossip_verifier") { + for test in tests { + let mut data: Vec = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + gossip_verifier_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/gossip_verifier.rs b/fuzz/src/gossip_verifier.rs new file mode 100644 index 00000000000..4c629597e03 --- /dev/null +++ b/fuzz/src/gossip_verifier.rs @@ -0,0 +1,288 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use bitcoin::block::{Block, Header, Version}; +use bitcoin::hash_types::BlockHash; +use bitcoin::hashes::Hash; +use bitcoin::network::Network; +use bitcoin::transaction::{OutPoint, Transaction, TxOut}; +use bitcoin::{Amount, CompactTarget, TxMerkleNode}; + +use lightning::ln::msgs; +use lightning::ln::msgs::BaseMessageHandler; +use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; +use lightning::util::ser::LengthReadable; + +use lightning_block_sync::gossip::{GossipVerifier, TokioSpawner, UtxoSource}; +use lightning_block_sync::{BlockData, BlockHeaderData, BlockSource, BlockSourceError}; + +use crate::utils::test_logger; + +use std::future::Future; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use tokio::runtime::Runtime; + +#[inline] +pub fn slice_to_be16(v: &[u8]) -> u16 { + ((v[0] as u16) << 8) | (v[1] as u16) +} + +struct InputData { + data: Vec, + read_pos: AtomicUsize, +} +impl InputData { + fn get_slice(&self, len: usize) -> Option<&[u8]> { + let old_pos = self.read_pos.fetch_add(len, Ordering::AcqRel); + if self.data.len() < old_pos + len { + return None; + } + Some(&self.data[old_pos..old_pos + len]) + } + fn get_slice_nonadvancing(&self, len: usize) -> Option<&[u8]> { + let old_pos = self.read_pos.load(Ordering::Acquire); + if self.data.len() < old_pos + len { + return None; + } + Some(&self.data[old_pos..old_pos + len]) + } +} + +fn dummy_header() -> Header { + Header { + version: Version::ONE, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 0, + bits: CompactTarget::from_consensus(0), + nonce: 0, + } +} + +/// A `BlockSource` + `UtxoSource` driven by fuzz input. Each async call consumes one byte from +/// the input to decide its behavior (success with various shapes, or persistent/transient error). +struct FuzzBlockSource { + input: Arc, +} + +impl BlockSource for FuzzBlockSource { + fn get_header<'a>( + &'a self, _header_hash: &'a BlockHash, _height_hint: Option, + ) -> impl Future> + Send + 'a { + async move { + // Not called by retrieve_utxo, but required by the trait. + Err(BlockSourceError::transient("not implemented")) + } + } + + fn get_block<'a>( + &'a self, _header_hash: &'a BlockHash, + ) -> impl Future> + Send + 'a { + let action = self.input.get_slice(1).map(|s| s[0]); + async move { + match action { + None | Some(0) => Err(BlockSourceError::persistent("eof/persistent")), + Some(1) => Err(BlockSourceError::transient("transient")), + Some(2) => Ok(BlockData::HeaderOnly(dummy_header())), + Some(b) => { + // Build a block with a configurable number of transactions (1..=4), + // each with a configurable number of outputs (1..=4). + let num_txs = ((b >> 2) % 4) as usize + 1; + let num_outputs = ((b >> 4) % 4) as usize + 1; + let txdata: Vec = (0..num_txs) + .map(|_| Transaction { + version: bitcoin::transaction::Version::ONE, + lock_time: bitcoin::blockdata::locktime::absolute::LockTime::ZERO, + input: vec![], + output: (0..num_outputs) + .map(|_| TxOut { + value: Amount::from_sat(1_000_000), + script_pubkey: bitcoin::ScriptBuf::new(), + }) + .collect(), + }) + .collect(); + + Ok(BlockData::FullBlock(Block { header: dummy_header(), txdata })) + }, + } + } + } + + fn get_best_block<'a>( + &'a self, + ) -> impl Future), BlockSourceError>> + Send + 'a { + let action = self.input.get_slice(1).map(|s| s[0]); + async move { + match action { + None | Some(0) => Err(BlockSourceError::persistent("eof/persistent")), + Some(1) => Err(BlockSourceError::transient("transient")), + // Return no height (skips the confirmation check) + Some(2) => Ok((BlockHash::all_zeros(), None)), + // Return a very high tip so confirmation check passes + Some(3) => Ok((BlockHash::all_zeros(), Some(1_000_000))), + // Return a low tip so confirmation check fails for most SCIDs + Some(_) => Ok((BlockHash::all_zeros(), Some(5))), + } + } + } +} + +impl UtxoSource for FuzzBlockSource { + fn get_block_hash_by_height<'a>( + &'a self, _block_height: u32, + ) -> impl Future> + Send + 'a { + let action = self.input.get_slice(1).map(|s| s[0]); + async move { + match action { + None | Some(0) => Err(BlockSourceError::persistent("eof/persistent")), + Some(1) => Err(BlockSourceError::transient("transient")), + Some(_) => Ok(BlockHash::all_zeros()), + } + } + } + + fn is_output_unspent<'a>( + &'a self, _outpoint: OutPoint, + ) -> impl Future> + Send + 'a { + let action = self.input.get_slice(1).map(|s| s[0]); + async move { + match action { + None | Some(0) => Err(BlockSourceError::persistent("eof/persistent")), + Some(1) => Err(BlockSourceError::transient("transient")), + Some(2) => Ok(false), // spent + Some(_) => Ok(true), // unspent + } + } + } +} + +#[inline] +pub fn do_test(data: &[u8], out: Out) { + let rt = Runtime::new().unwrap(); + rt.block_on(do_test_async(data, out)); +} + +async fn do_test_async(data: &[u8], out: Out) { + let input = Arc::new(InputData { data: data.to_vec(), read_pos: AtomicUsize::new(0) }); + + macro_rules! get_slice_nonadvancing { + ($len: expr) => { + match input.get_slice_nonadvancing($len as usize) { + Some(slice) => slice, + None => return, + } + }; + } + macro_rules! get_slice { + ($len: expr) => { + match input.get_slice($len as usize) { + Some(slice) => slice, + None => return, + } + }; + } + + macro_rules! decode_msg { + ($MsgType: path, $len: expr) => {{ + let data = get_slice!($len); + let mut reader = &data[..]; + match <$MsgType>::read_from_fixed_length_buffer(&mut reader) { + Ok(msg) => { + assert!(reader.is_empty()); + msg + }, + Err(e) => match e { + msgs::DecodeError::UnknownVersion => return, + msgs::DecodeError::UnknownRequiredFeature => return, + msgs::DecodeError::InvalidValue => return, + msgs::DecodeError::BadLengthDescriptor => return, + msgs::DecodeError::ShortRead => panic!("We picked the length..."), + msgs::DecodeError::Io(e) => panic!("{:?}", e), + msgs::DecodeError::UnsupportedCompression => return, + msgs::DecodeError::DangerousValue => return, + }, + } + }}; + } + + macro_rules! decode_msg_with_len16 { + ($MsgType: path, $excess: expr) => {{ + let extra_len = slice_to_be16(get_slice_nonadvancing!(2)); + decode_msg!($MsgType, 2 + (extra_len as usize) + $excess) + }}; + } + + let logger = test_logger::TestLogger::new("".to_owned(), out); + let net_graph = NetworkGraph::new(Network::Bitcoin, &logger); + let block_source = Arc::new(FuzzBlockSource { input: Arc::clone(&input) }); + let gossip_verifier = GossipVerifier::new(Arc::clone(&block_source), TokioSpawner); + let gossip_sync = P2PGossipSync::new(&net_graph, Some(&gossip_verifier), &logger); + + loop { + match get_slice!(1)[0] % 5 { + // Channel announcement via GossipVerifier (exercises retrieve_utxo) + 0 => { + let msg = decode_msg_with_len16!( + msgs::UnsignedChannelAnnouncement, + 32 + 8 + 33 * 4 + ); + let _ = net_graph + .update_channel_from_unsigned_announcement(&msg, &Some(&gossip_verifier)); + // Yield to let spawned tokio tasks complete. + tokio::task::yield_now().await; + }, + // Node announcement + 1 => { + let start_len = slice_to_be16(&get_slice_nonadvancing!(2)[0..2]) as usize; + let addr_len = slice_to_be16( + &get_slice_nonadvancing!(start_len + 2 + 74) + [start_len + 2 + 72..start_len + 2 + 74], + ); + if addr_len > (37 + 1) * 4 { + return; + } + let msg = decode_msg_with_len16!(msgs::UnsignedNodeAnnouncement, 288); + let _ = net_graph.update_node_from_unsigned_announcement(&msg); + }, + // Channel update + 2 => { + let msg = decode_msg!(msgs::UnsignedChannelUpdate, 72); + let _ = net_graph.update_channel_unsigned(&msg); + }, + // Process completed checks (triggers check_resolved_futures) + 3 => { + // Yield first so any in-flight tokio tasks can resolve their futures. + tokio::task::yield_now().await; + gossip_sync.get_and_clear_pending_msg_events(); + }, + // Channel announcement without UTXO lookup + 4 | _ => { + let msg = decode_msg_with_len16!( + msgs::UnsignedChannelAnnouncement, + 32 + 8 + 33 * 4 + ); + let _ = net_graph.update_channel_from_unsigned_announcement::< + &GossipVerifier>, + >(&msg, &None); + }, + } + } +} + +pub fn gossip_verifier_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn gossip_verifier_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index d34f2515bac..c1766239fe3 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -9,6 +9,7 @@ extern crate bitcoin; extern crate lightning; +extern crate lightning_block_sync; extern crate lightning_persister; extern crate lightning_rapid_gossip_sync; @@ -31,6 +32,7 @@ pub mod chanmon_deser; pub mod feature_flags; pub mod fromstr_to_netaddress; pub mod full_stack; +pub mod gossip_verifier; pub mod indexedmap; pub mod invoice_deser; pub mod invoice_request_deser; diff --git a/fuzz/targets.h b/fuzz/targets.h index e24144e85a9..91922521c2e 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -21,6 +21,7 @@ void base32_run(const unsigned char* data, size_t data_len); void fromstr_to_netaddress_run(const unsigned char* data, size_t data_len); void feature_flags_run(const unsigned char* data, size_t data_len); void lsps_message_run(const unsigned char* data, size_t data_len); +void gossip_verifier_run(const unsigned char* data, size_t data_len); void utxo_validation_run(const unsigned char* data, size_t data_len); void fs_store_run(const unsigned char* data, size_t data_len); void msg_accept_channel_run(const unsigned char* data, size_t data_len);