diff --git a/fuzz/src/bech32_parse.rs b/fuzz/src/bech32_parse.rs new file mode 100644 index 00000000000..f3dd5ac9d1e --- /dev/null +++ b/fuzz/src/bech32_parse.rs @@ -0,0 +1,57 @@ +// 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 crate::utils::test_logger; +use core::convert::TryFrom; +use lightning::offers::parse::{Bech32Encode, ParseError}; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(bech32_encoded) = std::str::from_utf8(data) { + if let Ok(bytes) = Bytes::from_bech32_str(bech32_encoded) { + let bech32_encoded = bytes.to_string(); + assert_eq!(bytes, Bytes::from_bech32_str(&bech32_encoded).unwrap()); + } + } +} + +#[derive(Debug, PartialEq)] +struct Bytes(Vec); + +impl Bech32Encode for Bytes { + const BECH32_HRP: &'static str = "lno"; +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl TryFrom> for Bytes { + type Error = ParseError; + fn try_from(data: Vec) -> Result { + Ok(Bytes(data)) + } +} + +impl core::fmt::Display for Bytes { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + +pub fn bech32_parse_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn bech32_parse_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/bin/bech32_parse_target.rs b/fuzz/src/bin/bech32_parse_target.rs new file mode 100644 index 00000000000..629112f9fef --- /dev/null +++ b/fuzz/src/bin/bech32_parse_target.rs @@ -0,0 +1,113 @@ +// 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(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::bech32_parse::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + bech32_parse_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + bech32_parse_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + bech32_parse_run(data.as_ptr(), data.len()); +}); + +#[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(); + bech32_parse_run(data.as_ptr(), data.len()); +} + +#[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]; + bech32_parse_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/bech32_parse") { + 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 || { + bech32_parse_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/gen_target.sh b/fuzz/src/bin/gen_target.sh index fa29540f96b..d7928188d8c 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -6,12 +6,17 @@ GEN_TEST() { echo "void $1_run(const unsigned char* data, size_t data_len);" >> ../../targets.h } +GEN_TEST bech32_parse GEN_TEST chanmon_deser GEN_TEST chanmon_consistency GEN_TEST full_stack +GEN_TEST invoice_deser +GEN_TEST invoice_request_deser +GEN_TEST offer_deser GEN_TEST onion_message GEN_TEST peer_crypt GEN_TEST process_network_graph +GEN_TEST refund_deser GEN_TEST router GEN_TEST zbase32 GEN_TEST indexedmap diff --git a/fuzz/src/bin/invoice_deser_target.rs b/fuzz/src/bin/invoice_deser_target.rs new file mode 100644 index 00000000000..06dbbe3078c --- /dev/null +++ b/fuzz/src/bin/invoice_deser_target.rs @@ -0,0 +1,113 @@ +// 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(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::invoice_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + invoice_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + invoice_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + invoice_deser_run(data.as_ptr(), data.len()); +}); + +#[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(); + invoice_deser_run(data.as_ptr(), data.len()); +} + +#[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]; + invoice_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/invoice_deser") { + 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 || { + invoice_deser_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/invoice_request_deser_target.rs b/fuzz/src/bin/invoice_request_deser_target.rs new file mode 100644 index 00000000000..97741ff3ade --- /dev/null +++ b/fuzz/src/bin/invoice_request_deser_target.rs @@ -0,0 +1,113 @@ +// 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(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::invoice_request_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + invoice_request_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + invoice_request_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + invoice_request_deser_run(data.as_ptr(), data.len()); +}); + +#[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(); + invoice_request_deser_run(data.as_ptr(), data.len()); +} + +#[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]; + invoice_request_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/invoice_request_deser") { + 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 || { + invoice_request_deser_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/offer_deser_target.rs b/fuzz/src/bin/offer_deser_target.rs new file mode 100644 index 00000000000..49563b10291 --- /dev/null +++ b/fuzz/src/bin/offer_deser_target.rs @@ -0,0 +1,113 @@ +// 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(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::offer_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + offer_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + offer_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + offer_deser_run(data.as_ptr(), data.len()); +}); + +#[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(); + offer_deser_run(data.as_ptr(), data.len()); +} + +#[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]; + offer_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/offer_deser") { + 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 || { + offer_deser_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/refund_deser_target.rs b/fuzz/src/bin/refund_deser_target.rs new file mode 100644 index 00000000000..c9857783467 --- /dev/null +++ b/fuzz/src/bin/refund_deser_target.rs @@ -0,0 +1,113 @@ +// 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(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +extern crate lightning_fuzz; +use lightning_fuzz::refund_deser::*; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + refund_deser_run(data.as_ptr(), data.len()); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + refund_deser_run(data.as_ptr(), data.len()); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + refund_deser_run(data.as_ptr(), data.len()); +}); + +#[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(); + refund_deser_run(data.as_ptr(), data.len()); +} + +#[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]; + refund_deser_run(data.as_ptr(), data.len()); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("test_cases/refund_deser") { + 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 || { + refund_deser_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/invoice_deser.rs b/fuzz/src/invoice_deser.rs new file mode 100644 index 00000000000..7b93fd38db1 --- /dev/null +++ b/fuzz/src/invoice_deser.rs @@ -0,0 +1,31 @@ +// 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 crate::utils::test_logger; +use lightning::offers::invoice::Invoice; +use lightning::util::ser::Writeable; +use std::convert::TryFrom; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(invoice) = Invoice::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + invoice.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + } +} + +pub fn invoice_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn invoice_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs new file mode 100644 index 00000000000..aa3045ccb24 --- /dev/null +++ b/fuzz/src/invoice_request_deser.rs @@ -0,0 +1,112 @@ +// 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::secp256k1::{KeyPair, Parity, PublicKey, Secp256k1, SecretKey, self}; +use crate::utils::test_logger; +use core::convert::{Infallible, TryFrom}; +use lightning::chain::keysinterface::EntropySource; +use lightning::ln::PaymentHash; +use lightning::ln::features::BlindedHopFeatures; +use lightning::offers::invoice::{BlindedPayInfo, UnsignedInvoice}; +use lightning::offers::invoice_request::InvoiceRequest; +use lightning::offers::parse::SemanticError; +use lightning::onion_message::BlindedPath; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(invoice_request) = InvoiceRequest::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + invoice_request.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let mut buffer = Vec::new(); + + if let Ok(unsigned_invoice) = build_response(&invoice_request, &secp_ctx) { + let signing_pubkey = unsigned_invoice.signing_pubkey(); + let (x_only_pubkey, _) = keys.x_only_public_key(); + let odd_pubkey = x_only_pubkey.public_key(Parity::Odd); + let even_pubkey = x_only_pubkey.public_key(Parity::Even); + if signing_pubkey == odd_pubkey || signing_pubkey == even_pubkey { + unsigned_invoice + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap() + .write(&mut buffer) + .unwrap(); + } else { + unsigned_invoice + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap_err(); + } + } + } +} + +struct Randomness; + +impl EntropySource for Randomness { + fn get_secure_random_bytes(&self) -> [u8; 32] { [42; 32] } +} + +fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) +} + +fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} + +fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( + invoice_request: &'a InvoiceRequest, secp_ctx: &Secp256k1 +) -> Result, SemanticError> { + let entropy_source = Randomness {}; + let paths = vec![ + BlindedPath::new(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + BlindedPath::new(&[pubkey(45), pubkey(46), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + ]; + + let payinfo = vec![ + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ]; + + let payment_paths = paths.into_iter().zip(payinfo.into_iter()).collect(); + let payment_hash = PaymentHash([42; 32]); + invoice_request.respond_with(payment_paths, payment_hash)?.build() +} + +pub fn invoice_request_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn invoice_request_deser_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 462307d55b4..92142e56423 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -14,13 +14,18 @@ extern crate hex; pub mod utils; +pub mod bech32_parse; pub mod chanmon_deser; pub mod chanmon_consistency; pub mod full_stack; pub mod indexedmap; +pub mod invoice_deser; +pub mod invoice_request_deser; +pub mod offer_deser; pub mod onion_message; pub mod peer_crypt; pub mod process_network_graph; +pub mod refund_deser; pub mod router; pub mod zbase32; diff --git a/fuzz/src/offer_deser.rs b/fuzz/src/offer_deser.rs new file mode 100644 index 00000000000..213742d8c08 --- /dev/null +++ b/fuzz/src/offer_deser.rs @@ -0,0 +1,69 @@ +// 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::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +use crate::utils::test_logger; +use core::convert::{Infallible, TryFrom}; +use lightning::offers::invoice_request::UnsignedInvoiceRequest; +use lightning::offers::offer::{Amount, Offer, Quantity}; +use lightning::offers::parse::SemanticError; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(offer) = Offer::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + offer.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let pubkey = PublicKey::from(keys); + let mut buffer = Vec::new(); + + if let Ok(invoice_request) = build_response(&offer, pubkey) { + invoice_request + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap() + .write(&mut buffer) + .unwrap(); + } + } +} + +fn build_response<'a>( + offer: &'a Offer, pubkey: PublicKey +) -> Result, SemanticError> { + let mut builder = offer.request_invoice(vec![42; 64], pubkey)?; + + builder = match offer.amount() { + None => builder.amount_msats(1000).unwrap(), + Some(Amount::Bitcoin { amount_msats }) => builder.amount_msats(amount_msats + 1)?, + Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), + }; + + builder = match offer.supported_quantity() { + Quantity::Bounded(n) => builder.quantity(n.get()).unwrap(), + Quantity::Unbounded => builder.quantity(10).unwrap(), + Quantity::One => builder, + }; + + builder.build() +} + +pub fn offer_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn offer_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs new file mode 100644 index 00000000000..9adaa3e953c --- /dev/null +++ b/fuzz/src/refund_deser.rs @@ -0,0 +1,101 @@ +// 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::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey, self}; +use crate::utils::test_logger; +use core::convert::{Infallible, TryFrom}; +use lightning::chain::keysinterface::EntropySource; +use lightning::ln::PaymentHash; +use lightning::ln::features::BlindedHopFeatures; +use lightning::offers::invoice::{BlindedPayInfo, UnsignedInvoice}; +use lightning::offers::parse::SemanticError; +use lightning::offers::refund::Refund; +use lightning::onion_message::BlindedPath; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test(data: &[u8], _out: Out) { + if let Ok(refund) = Refund::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + refund.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let pubkey = PublicKey::from(keys); + let mut buffer = Vec::new(); + + if let Ok(invoice) = build_response(&refund, pubkey, &secp_ctx) { + invoice + .sign::<_, Infallible>( + |digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + ) + .unwrap() + .write(&mut buffer) + .unwrap(); + } + } +} + +struct Randomness; + +impl EntropySource for Randomness { + fn get_secure_random_bytes(&self) -> [u8; 32] { [42; 32] } +} + +fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) +} + +fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} + +fn build_response<'a, T: secp256k1::Signing + secp256k1::Verification>( + refund: &'a Refund, signing_pubkey: PublicKey, secp_ctx: &Secp256k1 +) -> Result, SemanticError> { + let entropy_source = Randomness {}; + let paths = vec![ + BlindedPath::new(&[pubkey(43), pubkey(44), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + BlindedPath::new(&[pubkey(45), pubkey(46), pubkey(42)], &entropy_source, secp_ctx).unwrap(), + ]; + + let payinfo = vec![ + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ]; + + let payment_paths = paths.into_iter().zip(payinfo.into_iter()).collect(); + let payment_hash = PaymentHash([42; 32]); + refund.respond_with(payment_paths, payment_hash, signing_pubkey)?.build() +} + +pub fn refund_deser_test(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn refund_deser_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 5bfee07dafb..8f846c5e037 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -1,10 +1,15 @@ #include +void bech32_parse_run(const unsigned char* data, size_t data_len); void chanmon_deser_run(const unsigned char* data, size_t data_len); void chanmon_consistency_run(const unsigned char* data, size_t data_len); void full_stack_run(const unsigned char* data, size_t data_len); +void invoice_deser_run(const unsigned char* data, size_t data_len); +void invoice_request_deser_run(const unsigned char* data, size_t data_len); +void offer_deser_run(const unsigned char* data, size_t data_len); void onion_message_run(const unsigned char* data, size_t data_len); void peer_crypt_run(const unsigned char* data, size_t data_len); void process_network_graph_run(const unsigned char* data, size_t data_len); +void refund_deser_run(const unsigned char* data, size_t data_len); void router_run(const unsigned char* data, size_t data_len); void zbase32_run(const unsigned char* data, size_t data_len); void indexedmap_run(const unsigned char* data, size_t data_len); diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index d4289d07d2e..71f82ed0134 100644 --- a/lightning/src/lib.rs +++ b/lightning/src/lib.rs @@ -78,8 +78,7 @@ extern crate core; pub mod util; pub mod chain; pub mod ln; -#[allow(unused)] -mod offers; +pub mod offers; pub mod routing; pub mod onion_message; diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index f423677fbc9..49c03a44347 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -16,7 +16,7 @@ //! The payment recipient must include a [`PaymentHash`], so as to reveal the preimage upon payment //! receipt, and one or more [`BlindedPath`]s for the payer to use when sending the payment. //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate lightning; //! @@ -45,7 +45,12 @@ //! //! // Invoice for the "offer to be paid" flow. //! InvoiceRequest::try_from(bytes)? -//! .respond_with(payment_paths, payment_hash)? +#![cfg_attr(feature = "std", doc = " + .respond_with(payment_paths, payment_hash)? +")] +#![cfg_attr(not(feature = "std"), doc = " + .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? +")] //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) @@ -69,7 +74,12 @@ //! // Invoice for the "offer for money" flow. //! "lnr1qcp4256ypq" //! .parse::()? -//! .respond_with(payment_paths, payment_hash, pubkey)? +#![cfg_attr(feature = "std", doc = " + .respond_with(payment_paths, payment_hash, pubkey)? +")] +#![cfg_attr(not(feature = "std"), doc = " + .respond_with_no_std(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? +")] //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) @@ -138,7 +148,8 @@ impl<'a> InvoiceBuilder<'a> { Some(amount_msats) => amount_msats, None => match invoice_request.contents.offer.amount() { Some(Amount::Bitcoin { amount_msats }) => { - amount_msats * invoice_request.quantity().unwrap_or(1) + amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) + .ok_or(SemanticError::InvalidAmount)? }, Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), None => return Err(SemanticError::MissingAmount), @@ -257,6 +268,11 @@ pub struct UnsignedInvoice<'a> { } impl<'a> UnsignedInvoice<'a> { + /// The public key corresponding to the key needed to sign the invoice. + pub fn signing_pubkey(&self) -> PublicKey { + self.invoice.fields().signing_pubkey + } + /// Signs the invoice using the given function. pub fn sign(self, sign: F) -> Result> where @@ -297,6 +313,7 @@ impl<'a> UnsignedInvoice<'a> { /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +#[derive(Clone, Debug, PartialEq)] pub struct Invoice { bytes: Vec, contents: InvoiceContents, @@ -307,6 +324,7 @@ pub struct Invoice { /// /// [`Offer`]: crate::offers::offer::Offer /// [`Refund`]: crate::offers::refund::Refund +#[derive(Clone, Debug, PartialEq)] enum InvoiceContents { /// Contents for an [`Invoice`] corresponding to an [`Offer`]. /// @@ -325,6 +343,7 @@ enum InvoiceContents { } /// Invoice-specific fields for an `invoice` message. +#[derive(Clone, Debug, PartialEq)] struct InvoiceFields { payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, @@ -440,12 +459,12 @@ impl Invoice { &self.contents.fields().features } - /// The public key used to sign invoices. + /// The public key corresponding to the key used to sign the invoice. pub fn signing_pubkey(&self) -> PublicKey { self.contents.fields().signing_pubkey } - /// Signature of the invoice using [`Invoice::signing_pubkey`]. + /// Signature of the invoice verified using [`Invoice::signing_pubkey`]. pub fn signature(&self) -> Signature { self.signature } @@ -571,12 +590,30 @@ type BlindedPayInfoIter<'a> = core::iter::Map< /// Information needed to route a payment across a [`BlindedPath`]. #[derive(Clone, Debug, PartialEq)] pub struct BlindedPayInfo { - fee_base_msat: u32, - fee_proportional_millionths: u32, - cltv_expiry_delta: u16, - htlc_minimum_msat: u64, - htlc_maximum_msat: u64, - features: BlindedHopFeatures, + /// Base fee charged (in millisatoshi) for the entire blinded path. + pub fee_base_msat: u32, + + /// Liquidity fee charged (in millionths of the amount transferred) for the entire blinded path + /// (i.e., 10,000 is 1%). + pub fee_proportional_millionths: u32, + + /// Number of blocks subtracted from an incoming HTLC's `cltv_expiry` for the entire blinded + /// path. + pub cltv_expiry_delta: u16, + + /// The minimum HTLC value (in millisatoshi) that is acceptable to all channel peers on the + /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as + /// seen by the recipient. + pub htlc_minimum_msat: u64, + + /// The maximum HTLC value (in millisatoshi) that is acceptable to all channel peers on the + /// blinded path from the introduction node to the recipient, accounting for any fees, i.e., as + /// seen by the recipient. + pub htlc_maximum_msat: u64, + + /// Features set in `encrypted_data_tlv` for the `encrypted_recipient_data` TLV record in an + /// onion payload. + pub features: BlindedHopFeatures, } impl_writeable!(BlindedPayInfo, { @@ -751,7 +788,7 @@ mod tests { use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures}; use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self}; - use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef}; + use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{ParseError, SemanticError}; use crate::offers::payer::PayerTlvStreamRef; use crate::offers::refund::RefundBuilder; @@ -876,7 +913,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths.clone(), payment_hash, now).unwrap() + .respond_with_no_std(payment_paths.clone(), payment_hash, now).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -952,7 +989,8 @@ mod tests { let now = now(); let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap() - .respond_with(payment_paths.clone(), payment_hash, recipient_pubkey(), now).unwrap() + .respond_with_no_std(payment_paths.clone(), payment_hash, recipient_pubkey(), now) + .unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1021,6 +1059,42 @@ mod tests { } } + #[cfg(feature = "std")] + #[test] + fn builds_invoice_from_offer_with_expiration() { + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(future_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash()) + .unwrap() + .build() + { + panic!("error building invoice: {:?}", e); + } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .absolute_expiry(past_expiry) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap() + .respond_with(payment_paths(), payment_hash()) + .unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::AlreadyExpired), + } + } + #[cfg(feature = "std")] #[test] fn builds_invoice_from_refund_with_expiration() { @@ -1030,7 +1104,8 @@ mod tests { if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .build().unwrap() - .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) + .unwrap() .build() { panic!("error building invoice: {:?}", e); @@ -1039,7 +1114,8 @@ mod tests { match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(past_expiry) .build().unwrap() - .respond_with(payment_paths(), payment_hash(), recipient_pubkey(), now()).unwrap() + .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) + .unwrap() .build() { Ok(_) => panic!("expected error"), @@ -1058,7 +1134,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now).unwrap() .relative_expiry(one_hour.as_secs() as u32) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1074,7 +1150,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now - one_hour).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now - one_hour).unwrap() .relative_expiry(one_hour.as_secs() as u32 - 1) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1094,7 +1170,7 @@ mod tests { .amount_msats(1001).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); @@ -1102,6 +1178,38 @@ mod tests { assert_eq!(tlv_stream.amount, Some(1001)); } + #[test] + fn builds_invoice_with_quantity_from_request() { + let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(2).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream(); + assert_eq!(invoice.amount_msats(), 2000); + assert_eq!(tlv_stream.amount, Some(2000)); + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), + } + } + #[test] fn builds_invoice_with_fallback_address() { let script = Script::new(); @@ -1115,7 +1223,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) .fallback_v1_p2tr_tweaked(&tweaked_pubkey) @@ -1160,7 +1268,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .allow_mpp() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1177,7 +1285,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(|_| Err(())) { @@ -1191,7 +1299,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(payer_sign) { @@ -1208,7 +1316,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1263,7 +1371,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1293,7 +1401,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .relative_expiry(3600) .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1315,7 +1423,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1345,7 +1453,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1373,7 +1481,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .allow_mpp() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1406,14 +1514,14 @@ mod tests { .build().unwrap() .sign(payer_sign).unwrap(); let mut unsigned_invoice = invoice_request - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) .fallback_v1_p2tr_tweaked(&tweaked_pubkey) .build().unwrap(); // Only standard addresses will be included. - let mut fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); + let fallbacks = unsigned_invoice.invoice.fields_mut().fallbacks.as_mut().unwrap(); // Non-standard addresses fallbacks.push(FallbackAddress { version: 1, program: vec![0u8; 41] }); fallbacks.push(FallbackAddress { version: 2, program: vec![0u8; 1] }); @@ -1463,7 +1571,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); @@ -1505,7 +1613,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .invoice .write(&mut buffer).unwrap(); @@ -1524,7 +1632,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); let last_signature_byte = invoice.bytes.last_mut().unwrap(); @@ -1549,7 +1657,7 @@ mod tests { .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap() - .respond_with(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() .sign(recipient_sign).unwrap(); diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 5b15704ca51..a1a0520c622 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -19,7 +19,7 @@ //! [`Invoice`]: crate::offers::invoice::Invoice //! [`Refund`]: crate::offers::refund::Refund //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate lightning; //! @@ -250,7 +250,7 @@ impl<'a> UnsignedInvoiceRequest<'a> { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct InvoiceRequest { pub(super) bytes: Vec, pub(super) contents: InvoiceRequestContents, @@ -260,7 +260,7 @@ pub struct InvoiceRequest { /// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`]. /// /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct InvoiceRequestContents { payer: PayerContents, pub(super) offer: OfferContents, @@ -322,12 +322,30 @@ impl InvoiceRequest { self.signature } + /// Creates an [`Invoice`] for the request with the given required fields and using the + /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time. + /// + /// See [`InvoiceRequest::respond_with_no_std`] for further details where the aforementioned + /// creation time is used for the `created_at` parameter. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + /// [`Duration`]: core::time::Duration + #[cfg(feature = "std")] + pub fn respond_with( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash + ) -> Result { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.respond_with_no_std(payment_paths, payment_hash, created_at) + } + /// Creates an [`Invoice`] for the request with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after - /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter - /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`] - /// is not available. + /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where + /// [`std::time::SystemTime`] is not available. /// /// The caller is expected to remember the preimage of `payment_hash` in order to claim a payment /// for the invoice. @@ -339,23 +357,16 @@ impl InvoiceRequest { /// /// Errors if the request contains unknown required features. /// - /// [`Duration`]: core::time::Duration /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at - pub fn respond_with( + pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, - #[cfg(any(test, not(feature = "std")))] created_at: core::time::Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } - #[cfg(all(not(test), feature = "std"))] - let created_at = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) } @@ -817,6 +828,18 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, SemanticError::MissingAmount), } + + match OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), + } } #[test] @@ -1112,6 +1135,23 @@ mod tests { assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnsupportedCurrency)); }, } + + let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Unbounded) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .quantity(u64::max_value()).unwrap() + .build_unchecked() + .sign(payer_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice_request.write(&mut buffer).unwrap(); + + match InvoiceRequest::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidAmount)), + } } #[test] diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index a2008b6a0b5..405e2e278d8 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -13,7 +13,7 @@ //! published as a QR code to be scanned by a customer. The customer uses the offer to request an //! invoice from the merchant to be paid. //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate core; //! extern crate lightning; @@ -242,7 +242,7 @@ impl OfferBuilder { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown // fields. @@ -254,7 +254,7 @@ pub struct Offer { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct OfferContents { chains: Option>, metadata: Option>, @@ -431,7 +431,8 @@ impl OfferContents { }; if !self.expects_quantity() || quantity.is_some() { - let expected_amount_msats = offer_amount_msats * quantity.unwrap_or(1); + let expected_amount_msats = offer_amount_msats.checked_mul(quantity.unwrap_or(1)) + .ok_or(SemanticError::InvalidAmount)?; let amount_msats = amount_msats.unwrap_or(expected_amount_msats); if amount_msats < expected_amount_msats { diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs index 35c1425acc1..6afd4d68fef 100644 --- a/lightning/src/offers/parse.rs +++ b/lightning/src/offers/parse.rs @@ -10,69 +10,83 @@ //! Parsing and formatting for bech32 message encoding. use bitcoin::bech32; -use bitcoin::bech32::{FromBase32, ToBase32}; use bitcoin::secp256k1; use core::convert::TryFrom; -use core::fmt; use crate::io; use crate::ln::msgs::DecodeError; use crate::util::ser::SeekReadable; use crate::prelude::*; -/// Indicates a message can be encoded using bech32. -pub(super) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { - /// Human readable part of the message's bech32 encoding. - const BECH32_HRP: &'static str; - - /// Parses a bech32-encoded message into a TLV stream. - fn from_bech32_str(s: &str) -> Result { - // Offer encoding may be split by '+' followed by optional whitespace. - let encoded = match s.split('+').skip(1).next() { - Some(_) => { - for chunk in s.split('+') { - let chunk = chunk.trim_start(); - if chunk.is_empty() || chunk.contains(char::is_whitespace) { - return Err(ParseError::InvalidContinuation); +#[cfg(not(fuzzing))] +pub(super) use sealed::Bech32Encode; + +#[cfg(fuzzing)] +pub use sealed::Bech32Encode; + +mod sealed { + use bitcoin::bech32; + use bitcoin::bech32::{FromBase32, ToBase32}; + use core::convert::TryFrom; + use core::fmt; + use super::ParseError; + + use crate::prelude::*; + + /// Indicates a message can be encoded using bech32. + pub trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { + /// Human readable part of the message's bech32 encoding. + const BECH32_HRP: &'static str; + + /// Parses a bech32-encoded message into a TLV stream. + fn from_bech32_str(s: &str) -> Result { + // Offer encoding may be split by '+' followed by optional whitespace. + let encoded = match s.split('+').skip(1).next() { + Some(_) => { + for chunk in s.split('+') { + let chunk = chunk.trim_start(); + if chunk.is_empty() || chunk.contains(char::is_whitespace) { + return Err(ParseError::InvalidContinuation); + } } - } - let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::(); - Bech32String::Owned(s) - }, - None => Bech32String::Borrowed(s), - }; + let s: String = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect(); + Bech32String::Owned(s) + }, + None => Bech32String::Borrowed(s), + }; - let (hrp, data) = bech32::decode_without_checksum(encoded.as_ref())?; + let (hrp, data) = bech32::decode_without_checksum(encoded.as_ref())?; - if hrp != Self::BECH32_HRP { - return Err(ParseError::InvalidBech32Hrp); - } + if hrp != Self::BECH32_HRP { + return Err(ParseError::InvalidBech32Hrp); + } - let data = Vec::::from_base32(&data)?; - Self::try_from(data) - } + let data = Vec::::from_base32(&data)?; + Self::try_from(data) + } - /// Formats the message using bech32-encoding. - fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32()) - .expect("HRP is invalid").unwrap(); + /// Formats the message using bech32-encoding. + fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32()) + .expect("HRP is invalid").unwrap(); - Ok(()) + Ok(()) + } } -} -// Used to avoid copying a bech32 string not containing the continuation character (+). -enum Bech32String<'a> { - Borrowed(&'a str), - Owned(String), -} + // Used to avoid copying a bech32 string not containing the continuation character (+). + enum Bech32String<'a> { + Borrowed(&'a str), + Owned(String), + } -impl<'a> AsRef for Bech32String<'a> { - fn as_ref(&self) -> &str { - match self { - Bech32String::Borrowed(s) => s, - Bech32String::Owned(s) => s, + impl<'a> AsRef for Bech32String<'a> { + fn as_ref(&self) -> &str { + match self { + Bech32String::Borrowed(s) => s, + Bech32String::Owned(s) => s, + } } } } diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs index e389a8f6d5d..7e1da769eda 100644 --- a/lightning/src/offers/payer.rs +++ b/lightning/src/offers/payer.rs @@ -17,7 +17,7 @@ use crate::prelude::*; /// [`InvoiceRequest::payer_id`]. /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct PayerContents(pub Vec); tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index fff33873954..cc0388c0241 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -18,7 +18,7 @@ //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest //! [`Offer`]: crate::offers::offer::Offer //! -//! ```ignore +//! ``` //! extern crate bitcoin; //! extern crate core; //! extern crate lightning; @@ -216,7 +216,7 @@ impl RefundBuilder { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Offer`]: crate::offers::offer::Offer -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Refund { pub(super) bytes: Vec, pub(super) contents: RefundContents, @@ -225,7 +225,7 @@ pub struct Refund { /// The contents of a [`Refund`], which may be shared with an [`Invoice`]. /// /// [`Invoice`]: crate::offers::invoice::Invoice -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(super) struct RefundContents { payer: PayerContents, // offer fields @@ -317,12 +317,31 @@ impl Refund { self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str())) } + /// Creates an [`Invoice`] for the refund with the given required fields and using the + /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time. + /// + /// See [`Refund::respond_with_no_std`] for further details where the aforementioned creation + /// time is used for the `created_at` parameter. + /// + /// [`Invoice`]: crate::offers::invoice::Invoice + /// [`Duration`]: core::time::Duration + #[cfg(feature = "std")] + pub fn respond_with( + &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, + signing_pubkey: PublicKey, + ) -> Result { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); + + self.respond_with_no_std(payment_paths, payment_hash, signing_pubkey, created_at) + } + /// Creates an [`Invoice`] for the refund with the given required fields. /// /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after - /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter - /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`] - /// is not available. + /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where + /// [`std::time::SystemTime`] is not available. /// /// The caller is expected to remember the preimage of `payment_hash` in order to /// claim a payment for the invoice. @@ -339,21 +358,14 @@ impl Refund { /// /// [`Invoice`]: crate::offers::invoice::Invoice /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at - pub fn respond_with( + pub fn respond_with_no_std( &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash, - signing_pubkey: PublicKey, - #[cfg(any(test, not(feature = "std")))] - created_at: Duration + signing_pubkey: PublicKey, created_at: Duration ) -> Result { if self.features().requires_unknown_bits() { return Err(SemanticError::UnknownRequiredFeatures); } - #[cfg(all(not(test), feature = "std"))] - let created_at = std::time::SystemTime::now() - .duration_since(std::time::SystemTime::UNIX_EPOCH) - .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey) } diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 363f067b1aa..d3e476fd5fc 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -971,7 +971,7 @@ impl EffectiveCapacity { /// Fees for routing via a given channel or a node #[derive(Eq, PartialEq, Copy, Clone, Debug, Hash)] pub struct RoutingFees { - /// Flat routing fee in satoshis + /// Flat routing fee in millisatoshis. pub base_msat: u32, /// Liquidity-based routing fee in millionths of a routed amount. /// In other words, 10000 is 1%. diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 373a64e3e0e..2dd764022cd 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -378,7 +378,6 @@ macro_rules! decode_tlv_stream_with_custom_tlv_decode { macro_rules! _decode_tlv_stream_range { ($stream: expr, $range: expr, $rewind: ident, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} $(, $decode_custom_tlv: expr)?) => { { - use core::ops::RangeBounds; use $crate::ln::msgs::DecodeError; let mut last_seen_type: Option = None; let mut stream_ref = $stream; @@ -401,7 +400,7 @@ macro_rules! _decode_tlv_stream_range { } }, Err(e) => return Err(e), - Ok(t) => if $range.contains(&t.0) { t } else { + Ok(t) => if core::ops::RangeBounds::contains(&$range, &t.0) { t } else { drop(tracking_reader); // Assumes the type id is minimally encoded, which is enforced on read.