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 b4f0c7a12b9..15286deb82a 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -28,6 +28,8 @@ 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 GEN_TEST msg_accept_channel msg_targets:: 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/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/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 582fa346c54..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; @@ -44,6 +46,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..91922521c2e 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -21,6 +21,8 @@ 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); void msg_announcement_signatures_run(const unsigned char* data, size_t data_len);