From 50aeee5afb57644432254122a921e438f10e92d5 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 24 Jun 2022 16:40:49 -0500 Subject: [PATCH 1/6] Offer features for BOLT 12 The offer message in BOLT 12 contains a features TLV record. Add a corresponding OfferFeatures type where the length is not included in the serialization as it would be redundant with the record length. Otherwise, define the features to be the same as InvoiceFeatures. --- lightning/src/ln/features.rs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index e8a3e6d26fa..f630a86f99e 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -157,6 +157,7 @@ mod sealed { // Byte 2 BasicMPP, ]); + define_context!(OfferContext, []); // This isn't a "real" feature context, and is only used in the channel_type field in an // `OpenChannel` message. define_context!(ChannelTypeContext, [ @@ -366,7 +367,7 @@ mod sealed { supports_keysend, requires_keysend); #[cfg(test)] - define_feature!(123456789, UnknownFeature, [NodeContext, ChannelContext, InvoiceContext], + define_feature!(123456789, UnknownFeature, [NodeContext, ChannelContext, InvoiceContext, OfferContext], "Feature flags for an unknown feature used in testing.", set_unknown_feature_optional, set_unknown_feature_required, supports_unknown_test_feature, requires_unknown_test_feature); } @@ -425,6 +426,8 @@ pub type NodeFeatures = Features; pub type ChannelFeatures = Features; /// Features used within an invoice. pub type InvoiceFeatures = Features; +/// Features used within an offer. +pub type OfferFeatures = Features; /// Features used within the channel_type field in an OpenChannel message. /// @@ -704,21 +707,26 @@ impl_feature_len_prefixed_write!(ChannelFeatures); impl_feature_len_prefixed_write!(NodeFeatures); impl_feature_len_prefixed_write!(InvoiceFeatures); -// Because ChannelTypeFeatures only appears inside of TLVs, it doesn't have a length prefix when -// serialized. Thus, we can't use `impl_feature_len_prefixed_write`, above, and have to write our -// own serialization. -impl Writeable for ChannelTypeFeatures { - fn write(&self, w: &mut W) -> Result<(), io::Error> { - self.write_be(w) - } -} -impl Readable for ChannelTypeFeatures { - fn read(r: &mut R) -> Result { - let v = io_extras::read_to_end(r)?; - Ok(Self::from_be_bytes(v)) +// Some features only appear inside of TLVs, so they don't have a length prefix when serialized. +macro_rules! impl_feature_tlv_write { + ($features: ident) => { + impl Writeable for $features { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.write_be(w) + } + } + impl Readable for $features { + fn read(r: &mut R) -> Result { + let v = io_extras::read_to_end(r)?; + Ok(Self::from_be_bytes(v)) + } + } } } +impl_feature_tlv_write!(ChannelTypeFeatures); +impl_feature_tlv_write!(OfferFeatures); + #[cfg(test)] mod tests { use super::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, InvoiceFeatures, NodeFeatures, sealed}; From 48bb9edba1cd4f6c876bb35f82b374f20502abfb Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 6 Oct 2022 13:44:35 -0500 Subject: [PATCH 2/6] Add PrintableString utility Strings defined by third parties may contain control characters. Provide a wrapper such that these are replaced when displayed. Useful in node aliases and offer fields. --- lightning/src/routing/gossip.rs | 15 ++++-------- lightning/src/util/mod.rs | 1 + lightning/src/util/string.rs | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 lightning/src/util/string.rs diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 6b0f88d09a2..00846a7ba79 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -31,6 +31,7 @@ use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer, MaybeReadable} use crate::util::logger::{Logger, Level}; use crate::util::events::{Event, EventHandler, MessageSendEvent, MessageSendEventsProvider}; use crate::util::scid_utils::{block_from_scid, scid_from_parts, MAX_SCID_BLOCK}; +use crate::util::string::PrintableString; use crate::io; use crate::io_extras::{copy, sink}; @@ -1022,23 +1023,17 @@ pub struct NodeAlias(pub [u8; 32]); impl fmt::Display for NodeAlias { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let control_symbol = core::char::REPLACEMENT_CHARACTER; let first_null = self.0.iter().position(|b| *b == 0).unwrap_or(self.0.len()); let bytes = self.0.split_at(first_null).0; match core::str::from_utf8(bytes) { - Ok(alias) => { - for c in alias.chars() { - let mut bytes = [0u8; 4]; - let c = if !c.is_control() { c } else { control_symbol }; - f.write_str(c.encode_utf8(&mut bytes))?; - } - }, + Ok(alias) => PrintableString(alias).fmt(f)?, Err(_) => { + use core::fmt::Write; for c in bytes.iter().map(|b| *b as char) { // Display printable ASCII characters - let mut bytes = [0u8; 4]; + let control_symbol = core::char::REPLACEMENT_CHARACTER; let c = if c >= '\x20' && c <= '\x7e' { c } else { control_symbol }; - f.write_str(c.encode_utf8(&mut bytes))?; + f.write_char(c)?; } }, }; diff --git a/lightning/src/util/mod.rs b/lightning/src/util/mod.rs index 9ffe1b7494f..730131f2241 100644 --- a/lightning/src/util/mod.rs +++ b/lightning/src/util/mod.rs @@ -21,6 +21,7 @@ pub mod ser; pub mod message_signing; pub mod invoice; pub mod persist; +pub mod string; pub mod wakers; pub(crate) mod atomic_counter; diff --git a/lightning/src/util/string.rs b/lightning/src/util/string.rs new file mode 100644 index 00000000000..1af04e085fd --- /dev/null +++ b/lightning/src/util/string.rs @@ -0,0 +1,41 @@ +// 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. + +//! Utilities for strings. + +use core::fmt; + +/// A string that displays only printable characters, replacing control characters with +/// [`core::char::REPLACEMENT_CHARACTER`]. +pub struct PrintableString<'a>(pub &'a str); + +impl<'a> fmt::Display for PrintableString<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use core::fmt::Write; + for c in self.0.chars() { + let c = if c.is_control() { core::char::REPLACEMENT_CHARACTER } else { c }; + f.write_char(c)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::PrintableString; + + #[test] + fn displays_printable_string() { + assert_eq!( + format!("{}", PrintableString("I \u{1F496} LDK!\t\u{26A1}")), + "I \u{1F496} LDK!\u{FFFD}\u{26A1}", + ); + } +} From 24b63de10c6b951263e7d262e739c55e01e650b3 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 9 Aug 2022 17:24:10 -0500 Subject: [PATCH 3/6] Offer message interface and data format Define an interface for BOLT 12 `offer` messages. The underlying format consists of the original bytes and the parsed contents. The bytes are later needed when constructing an `invoice_request` message. This is because it must mirror all the `offer` TLV records, including unknown ones, which aren't represented in the contents. The contents will be used in `invoice_request` messages to avoid duplication. Some fields while required in a typical user-pays-merchant flow may not be necessary in the merchant-pays-user flow (i.e., refund). --- lightning/src/lib.rs | 2 + lightning/src/offers/mod.rs | 15 ++ lightning/src/offers/offer.rs | 169 +++++++++++++++++++ lightning/src/onion_message/blinded_route.rs | 6 +- lightning/src/onion_message/mod.rs | 2 +- lightning/src/util/ser.rs | 3 +- 6 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 lightning/src/offers/mod.rs create mode 100644 lightning/src/offers/offer.rs diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index 25eba1d74f8..1f3ab47b1ae 100644 --- a/lightning/src/lib.rs +++ b/lightning/src/lib.rs @@ -78,6 +78,8 @@ extern crate core; pub mod util; pub mod chain; pub mod ln; +#[allow(unused)] +mod offers; pub mod routing; pub mod onion_message; diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs new file mode 100644 index 00000000000..2f961a0bb6e --- /dev/null +++ b/lightning/src/offers/mod.rs @@ -0,0 +1,15 @@ +// 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. + +//! Implementation of Lightning Offers +//! ([BOLT 12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md)). +//! +//! Offers are a flexible protocol for Lightning payments. + +pub mod offer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs new file mode 100644 index 00000000000..d64091155d7 --- /dev/null +++ b/lightning/src/offers/offer.rs @@ -0,0 +1,169 @@ +// 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. + +//! Data structures and encoding for `offer` messages. + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::PublicKey; +use core::num::NonZeroU64; +use core::time::Duration; +use crate::ln::features::OfferFeatures; +use crate::onion_message::BlindedPath; +use crate::util::string::PrintableString; + +use crate::prelude::*; + +#[cfg(feature = "std")] +use std::time::SystemTime; + +/// An `Offer` is a potentially long-lived proposal for payment of a good or service. +/// +/// An offer is a precursor to an `InvoiceRequest`. A merchant publishes an offer from which a +/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to +/// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`]. +/// +/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the +/// latter. +/// +/// Through the use of [`BlindedPath`]s, offers provide recipient privacy. +#[derive(Clone, Debug)] +pub struct Offer { + // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown + // fields. + bytes: Vec, + contents: OfferContents, +} + +/// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`. +#[derive(Clone, Debug)] +pub(crate) struct OfferContents { + chains: Option>, + metadata: Option>, + amount: Option, + description: String, + features: OfferFeatures, + absolute_expiry: Option, + issuer: Option, + paths: Option>, + quantity_max: Option, + signing_pubkey: Option, +} + +impl Offer { + // TODO: Return a slice once ChainHash has constants. + // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1283 + // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1286 + /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet). + /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) + /// for the selected chain. + pub fn chains(&self) -> Vec { + self.contents.chains + .as_ref() + .cloned() + .unwrap_or_else(|| vec![ChainHash::using_genesis_block(Network::Bitcoin)]) + } + + // TODO: Link to corresponding method in `InvoiceRequest`. + /// Opaque bytes set by the originator. Useful for authentication and validating fields since it + /// is reflected in `invoice_request` messages along with all the other fields from the `offer`. + pub fn metadata(&self) -> Option<&Vec> { + self.contents.metadata.as_ref() + } + + /// The minimum amount required for a successful payment of a single item. + pub fn amount(&self) -> Option<&Amount> { + self.contents.amount.as_ref() + } + + /// A complete description of the purpose of the payment. Intended to be displayed to the user + /// but with the caveat that it has not been verified in any way. + pub fn description(&self) -> PrintableString { + PrintableString(&self.contents.description) + } + + /// Features pertaining to the offer. + pub fn features(&self) -> &OfferFeatures { + &self.contents.features + } + + /// Duration since the Unix epoch when an invoice should no longer be requested. + /// + /// If `None`, the offer does not expire. + pub fn absolute_expiry(&self) -> Option { + self.contents.absolute_expiry + } + + /// Whether the offer has expired. + #[cfg(feature = "std")] + pub fn is_expired(&self) -> bool { + match self.absolute_expiry() { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } + } + + /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be + /// displayed to the user but with the caveat that it has not been verified in any way. + pub fn issuer(&self) -> Option { + self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str())) + } + + /// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide + /// recipient privacy by obfuscating its node id. + pub fn paths(&self) -> &[BlindedPath] { + self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[]) + } + + /// The quantity of items supported. + pub fn supported_quantity(&self) -> Quantity { + match self.contents.quantity_max { + Some(0) => Quantity::Unbounded, + Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), + None => Quantity::Bounded(NonZeroU64::new(1).unwrap()), + } + } + + /// The public key used by the recipient to sign invoices. + pub fn signing_pubkey(&self) -> PublicKey { + self.contents.signing_pubkey.unwrap() + } +} + +/// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or +/// another currency. +#[derive(Clone, Debug)] +pub enum Amount { + /// An amount of bitcoin. + Bitcoin { + /// The amount in millisatoshi. + amount_msats: u64, + }, + /// An amount of currency specified using ISO 4712. + Currency { + /// The currency that the amount is denominated in. + iso4217_code: CurrencyCode, + /// The amount in the currency unit adjusted by the ISO 4712 exponent (e.g., USD cents). + amount: u64, + }, +} + +/// An ISO 4712 three-letter currency code (e.g., USD). +pub type CurrencyCode = [u8; 3]; + +/// Quantity of items supported by an [`Offer`]. +pub enum Quantity { + /// Up to a specific number of items (inclusive). + Bounded(NonZeroU64), + /// One or more items. + Unbounded, +} diff --git a/lightning/src/onion_message/blinded_route.rs b/lightning/src/onion_message/blinded_route.rs index 82a325a9e04..29d78d6ab58 100644 --- a/lightning/src/onion_message/blinded_route.rs +++ b/lightning/src/onion_message/blinded_route.rs @@ -28,6 +28,7 @@ use crate::prelude::*; /// Onion messages can be sent and received to blinded routes, which serve to hide the identity of /// the recipient. +#[derive(Clone, Debug)] pub struct BlindedRoute { /// To send to a blinded route, the sender first finds a route to the unblinded /// `introduction_node_id`, which can unblind its [`encrypted_payload`] to find out the onion @@ -41,14 +42,15 @@ pub struct BlindedRoute { /// [`encrypted_payload`]: BlindedHop::encrypted_payload pub(super) blinding_point: PublicKey, /// The hops composing the blinded route. - pub(super) blinded_hops: Vec, + pub(crate) blinded_hops: Vec, } /// Used to construct the blinded hops portion of a blinded route. These hops cannot be identified /// by outside observers and thus can be used to hide the identity of the recipient. +#[derive(Clone, Debug)] pub struct BlindedHop { /// The blinded node id of this hop in a blinded route. - pub(super) blinded_node_id: PublicKey, + pub(crate) blinded_node_id: PublicKey, /// The encrypted payload intended for this hop in a blinded route. // The node sending to this blinded route will later encode this payload into the onion packet for // this hop. diff --git a/lightning/src/onion_message/mod.rs b/lightning/src/onion_message/mod.rs index 3c522e30c51..89c1545a7df 100644 --- a/lightning/src/onion_message/mod.rs +++ b/lightning/src/onion_message/mod.rs @@ -28,6 +28,6 @@ mod utils; mod functional_tests; // Re-export structs so they can be imported with just the `onion_message::` module prefix. -pub use self::blinded_route::{BlindedRoute, BlindedHop}; +pub use self::blinded_route::{BlindedRoute, BlindedRoute as BlindedPath, BlindedHop}; pub use self::messenger::{CustomOnionMessageContents, CustomOnionMessageHandler, Destination, OnionMessageContents, OnionMessenger, SendError, SimpleArcOnionMessenger, SimpleRefOnionMessenger}; pub(crate) use self::packet::Packet; diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 5ff6dc86a0b..c5ca3524079 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -399,7 +399,8 @@ impl Readable for BigSize { /// In TLV we occasionally send fields which only consist of, or potentially end with, a /// variable-length integer which is simply truncated by skipping high zero bytes. This type /// encapsulates such integers implementing Readable/Writeable for them. -#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[derive(Clone, Debug)] pub(crate) struct HighZeroBytesDroppedBigSize(pub T); macro_rules! impl_writeable_primitive { From 227fd51cb49adaca903972464166b73468d3a257 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 6 Oct 2022 23:12:48 -0500 Subject: [PATCH 4/6] Add WithoutLength wrapper When serializing variable-length types as part of a TLV stream, the length does not need to be serialized as it is already encoded in TLV records. Add a WithoutLength wrapper for this encoding. Replace VecReadWrapper and VecWriteWrapper with this single type to avoid redundant encoders. --- lightning/src/ln/channelmanager.rs | 2 +- lightning/src/onion_message/packet.rs | 4 +- lightning/src/util/events.rs | 14 +++--- lightning/src/util/ser.rs | 69 ++++++++++++++------------- lightning/src/util/ser_macros.rs | 8 ++-- 5 files changed, 50 insertions(+), 47 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 63e0bb5ea79..2f0b0e1564a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6620,7 +6620,7 @@ impl Writeable for HTLCSource { (1, payment_id_opt, option), (2, first_hop_htlc_msat, required), (3, payment_secret, option), - (4, path, vec_type), + (4, *path, vec_type), (5, payment_params, option), }); } diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 73f58036c86..8fff53642e1 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -164,7 +164,7 @@ impl Writeable for (Payload, [u8; 32]) { match &self.0 { Payload::Forward(ForwardControlTlvs::Blinded(encrypted_bytes)) => { encode_varint_length_prefixed_tlv!(w, { - (4, encrypted_bytes, vec_type) + (4, *encrypted_bytes, vec_type) }) }, Payload::Receive { @@ -172,7 +172,7 @@ impl Writeable for (Payload, [u8; 32]) { } => { encode_varint_length_prefixed_tlv!(w, { (2, reply_path, option), - (4, encrypted_bytes, vec_type), + (4, *encrypted_bytes, vec_type), (message.tlv_type(), message, required) }) }, diff --git a/lightning/src/util/events.rs b/lightning/src/util/events.rs index 5a0a9d0cc0a..a9687d28802 100644 --- a/lightning/src/util/events.rs +++ b/lightning/src/util/events.rs @@ -24,7 +24,7 @@ use crate::ln::msgs; use crate::ln::msgs::DecodeError; use crate::ln::{PaymentPreimage, PaymentHash, PaymentSecret}; use crate::routing::gossip::NetworkUpdate; -use crate::util::ser::{BigSize, FixedLengthReader, Writeable, Writer, MaybeReadable, Readable, VecReadWrapper, VecWriteWrapper, OptionDeserWrapper}; +use crate::util::ser::{BigSize, FixedLengthReader, Writeable, Writer, MaybeReadable, Readable, WithoutLength, OptionDeserWrapper}; use crate::routing::router::{RouteHop, RouteParameters}; use bitcoin::{PackedLockTime, Transaction}; @@ -785,7 +785,7 @@ impl Writeable for Event { (1, network_update, option), (2, payment_failed_permanently, required), (3, all_paths_failed, required), - (5, path, vec_type), + (5, *path, vec_type), (7, short_channel_id, option), (9, retry, option), (11, payment_id, option), @@ -799,7 +799,7 @@ impl Writeable for Event { &Event::SpendableOutputs { ref outputs } => { 5u8.write(writer)?; write_tlv_fields!(writer, { - (0, VecWriteWrapper(outputs), required), + (0, WithoutLength(outputs), required), }); }, &Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id } => { @@ -831,7 +831,7 @@ impl Writeable for Event { write_tlv_fields!(writer, { (0, payment_id, required), (2, payment_hash, option), - (4, path, vec_type) + (4, *path, vec_type) }) }, &Event::PaymentFailed { ref payment_id, ref payment_hash } => { @@ -859,7 +859,7 @@ impl Writeable for Event { write_tlv_fields!(writer, { (0, payment_id, required), (2, payment_hash, required), - (4, path, vec_type) + (4, *path, vec_type) }) }, &Event::ProbeFailed { ref payment_id, ref payment_hash, ref path, ref short_channel_id } => { @@ -867,7 +867,7 @@ impl Writeable for Event { write_tlv_fields!(writer, { (0, payment_id, required), (2, payment_hash, required), - (4, path, vec_type), + (4, *path, vec_type), (6, short_channel_id, option), }) }, @@ -1007,7 +1007,7 @@ impl MaybeReadable for Event { 4u8 => Ok(None), 5u8 => { let f = || { - let mut outputs = VecReadWrapper(Vec::new()); + let mut outputs = WithoutLength(Vec::new()); read_tlv_fields!(reader, { (0, outputs, required), }); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index c5ca3524079..b9722325eed 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -283,39 +283,6 @@ impl From for OptionDeserWrapper { fn from(t: T) -> OptionDeserWrapper { OptionDeserWrapper(Some(t)) } } -/// Wrapper to write each element of a Vec with no length prefix -pub(crate) struct VecWriteWrapper<'a, T: Writeable>(pub &'a Vec); -impl<'a, T: Writeable> Writeable for VecWriteWrapper<'a, T> { - #[inline] - fn write(&self, writer: &mut W) -> Result<(), io::Error> { - for ref v in self.0.iter() { - v.write(writer)?; - } - Ok(()) - } -} - -/// Wrapper to read elements from a given stream until it reaches the end of the stream. -pub(crate) struct VecReadWrapper(pub Vec); -impl Readable for VecReadWrapper { - #[inline] - fn read(mut reader: &mut R) -> Result { - let mut values = Vec::new(); - loop { - let mut track_read = ReadTrackingReader::new(&mut reader); - match MaybeReadable::read(&mut track_read) { - Ok(Some(v)) => { values.push(v); }, - Ok(None) => { }, - // If we failed to read any bytes at all, we reached the end of our TLV - // stream and have simply exhausted all entries. - Err(ref e) if e == &DecodeError::ShortRead && !track_read.have_read => break, - Err(e) => return Err(e), - } - } - Ok(Self(values)) - } -} - pub(crate) struct U48(pub u64); impl Writeable for U48 { #[inline] @@ -547,6 +514,42 @@ impl Readable for [u16; 8] { } } +/// For variable-length values within TLV record where the length is encoded as part of the record. +/// Used to prevent encoding the length twice. +pub(crate) struct WithoutLength(pub T); + +impl<'a, T: Writeable> Writeable for WithoutLength<&'a Vec> { + #[inline] + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + for ref v in self.0.iter() { + v.write(writer)?; + } + Ok(()) + } +} + +impl Readable for WithoutLength> { + #[inline] + fn read(mut reader: &mut R) -> Result { + let mut values = Vec::new(); + loop { + let mut track_read = ReadTrackingReader::new(&mut reader); + match MaybeReadable::read(&mut track_read) { + Ok(Some(v)) => { values.push(v); }, + Ok(None) => { }, + // If we failed to read any bytes at all, we reached the end of our TLV + // stream and have simply exhausted all entries. + Err(ref e) if e == &DecodeError::ShortRead && !track_read.have_read => break, + Err(e) => return Err(e), + } + } + Ok(Self(values)) + } +} +impl<'a, T> From<&'a Vec> for WithoutLength<&'a Vec> { + fn from(v: &'a Vec) -> Self { Self(v) } +} + // HashMap impl Writeable for HashMap where K: Writeable + Eq + Hash, diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 74b206d3a57..019bea3ee93 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -17,7 +17,7 @@ macro_rules! encode_tlv { $field.write($stream)?; }; ($stream: expr, $type: expr, $field: expr, vec_type) => { - encode_tlv!($stream, $type, $crate::util::ser::VecWriteWrapper(&$field), required); + encode_tlv!($stream, $type, $crate::util::ser::WithoutLength(&$field), required); }; ($stream: expr, $optional_type: expr, $optional_field: expr, option) => { if let Some(ref field) = $optional_field { @@ -66,7 +66,7 @@ macro_rules! get_varint_length_prefixed_tlv_length { $len.0 += field_len; }; ($len: expr, $type: expr, $field: expr, vec_type) => { - get_varint_length_prefixed_tlv_length!($len, $type, $crate::util::ser::VecWriteWrapper(&$field), required); + get_varint_length_prefixed_tlv_length!($len, $type, $crate::util::ser::WithoutLength(&$field), required); }; ($len: expr, $optional_type: expr, $optional_field: expr, option) => { if let Some(ref field) = $optional_field { @@ -160,7 +160,7 @@ macro_rules! decode_tlv { $field = $crate::util::ser::Readable::read(&mut $reader)?; }}; ($reader: expr, $field: ident, vec_type) => {{ - let f: $crate::util::ser::VecReadWrapper<_> = $crate::util::ser::Readable::read(&mut $reader)?; + let f: $crate::util::ser::WithoutLength> = $crate::util::ser::Readable::read(&mut $reader)?; $field = Some(f.0); }}; ($reader: expr, $field: ident, option) => {{ @@ -453,7 +453,7 @@ macro_rules! _impl_writeable_tlv_based_enum_common { let id: u8 = $variant_id; id.write(writer)?; write_tlv_fields!(writer, { - $(($type, $field, $fieldty)),* + $(($type, *$field, $fieldty)),* }); }),* $($st::$tuple_variant_name (ref field) => { From 904d3229235f3a7bc5b11d95bb0e53fbb51f0da4 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 24 Jun 2022 16:27:42 -0500 Subject: [PATCH 5/6] Serialization macro for TLV streams BOLT 12's offer message is encoded as a TLV stream (i.e., a sequence of TLV records). impl_writeable_tlv_based can't be used because it writes the overall length of the struct, whereas TLV streams only include the length of each TLV record. Add a `tlv_stream` macro for defining structs used in encoding. TLV records containing a single variable-length type should not encode the types length in the value since it is redundant. Add a wrapper type that can be used within a TLV stream to support the correct behavior during serialization and de-serialization. --- lightning/src/util/ser.rs | 20 +++++++ lightning/src/util/ser_macros.rs | 90 ++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index b9722325eed..37fade88835 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -419,6 +419,9 @@ macro_rules! impl_writeable_primitive { } } } + impl From<$val_type> for HighZeroBytesDroppedBigSize<$val_type> { + fn from(val: $val_type) -> Self { Self(val) } + } } } @@ -518,6 +521,23 @@ impl Readable for [u16; 8] { /// Used to prevent encoding the length twice. pub(crate) struct WithoutLength(pub T); +impl Writeable for WithoutLength<&String> { + #[inline] + fn write(&self, w: &mut W) -> Result<(), io::Error> { + w.write_all(self.0.as_bytes()) + } +} +impl Readable for WithoutLength { + #[inline] + fn read(r: &mut R) -> Result { + let v: WithoutLength> = Readable::read(r)?; + Ok(Self(String::from_utf8(v.0).map_err(|_| DecodeError::InvalidValue)?)) + } +} +impl<'a> From<&'a String> for WithoutLength<&'a String> { + fn from(s: &'a String) -> Self { Self(s) } +} + impl<'a, T: Writeable> Writeable for WithoutLength<&'a Vec> { #[inline] fn write(&self, writer: &mut W) -> Result<(), io::Error> { diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 019bea3ee93..3ec0848680f 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -26,6 +26,12 @@ macro_rules! encode_tlv { field.write($stream)?; } }; + ($stream: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident))) => { + encode_tlv!($stream, $type, $field.map(|f| $encoding(f)), option); + }; + ($stream: expr, $type: expr, $field: expr, (option, encoding: $fieldty: ty)) => { + encode_tlv!($stream, $type, $field, option); + }; } macro_rules! encode_tlv_stream { @@ -121,6 +127,9 @@ macro_rules! check_tlv_order { ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{ // no-op }}; + ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (option, encoding: $encoding: tt)) => {{ + // no-op + }}; } macro_rules! check_missing_tlv { @@ -150,6 +159,9 @@ macro_rules! check_missing_tlv { ($last_seen_type: expr, $type: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{ // no-op }}; + ($last_seen_type: expr, $type: expr, $field: ident, (option, encoding: $encoding: tt)) => {{ + // no-op + }}; } macro_rules! decode_tlv { @@ -172,6 +184,15 @@ macro_rules! decode_tlv { ($reader: expr, $field: ident, (option: $trait: ident $(, $read_arg: expr)?)) => {{ $field = Some($trait::read(&mut $reader $(, $read_arg)*)?); }}; + ($reader: expr, $field: ident, (option, encoding: ($fieldty: ty, $encoding: ident))) => {{ + $field = { + let field: $encoding<$fieldty> = ser::Readable::read(&mut $reader)?; + Some(field.0) + }; + }}; + ($reader: expr, $field: ident, (option, encoding: $fieldty: ty)) => {{ + decode_tlv!($reader, $field, option); + }}; } // `$decode_custom_tlv` is a closure that may be optionally provided to handle custom message types. @@ -441,6 +462,75 @@ macro_rules! impl_writeable_tlv_based { } } +/// Defines a struct for a TLV stream and a similar struct using references for non-primitive types, +/// implementing [`Readable`] for the former and [`Writeable`] for the latter. Useful as an +/// intermediary format when reading or writing a type encoded as a TLV stream. Note that each field +/// representing a TLV record has its type wrapped with an [`Option`]. A tuple consisting of a type +/// and a serialization wrapper may be given in place of a type when custom serialization is +/// required. +/// +/// [`Readable`]: crate::util::ser::Readable +/// [`Writeable`]: crate::util::ser::Writeable +macro_rules! tlv_stream { + ($name:ident, $nameref:ident, { + $(($type:expr, $field:ident : $fieldty:tt)),* $(,)* + }) => { + #[derive(Debug)] + struct $name { + $( + $field: Option, + )* + } + + pub(crate) struct $nameref<'a> { + $( + pub(crate) $field: Option, + )* + } + + impl<'a> $crate::util::ser::Writeable for $nameref<'a> { + fn write(&self, writer: &mut W) -> Result<(), $crate::io::Error> { + encode_tlv_stream!(writer, { + $(($type, self.$field, (option, encoding: $fieldty))),* + }); + Ok(()) + } + } + + impl $crate::util::ser::Readable for $name { + fn read(reader: &mut R) -> Result { + $( + init_tlv_field_var!($field, option); + )* + decode_tlv_stream!(reader, { + $(($type, $field, (option, encoding: $fieldty))),* + }); + + Ok(Self { + $( + $field: $field + ),* + }) + } + } + } +} + +macro_rules! tlv_record_type { + (($type:ty, $wrapper:ident)) => { $type }; + ($type:ty) => { $type }; +} + +macro_rules! tlv_record_ref_type { + (char) => { char }; + (u8) => { u8 }; + ((u16, $wrapper: ident)) => { u16 }; + ((u32, $wrapper: ident)) => { u32 }; + ((u64, $wrapper: ident)) => { u64 }; + (($type:ty, $wrapper:ident)) => { &'a $type }; + ($type:ty) => { &'a $type }; +} + macro_rules! _impl_writeable_tlv_based_enum_common { ($st: ident, $(($variant_id: expr, $variant_name: ident) => {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} From 8ba09e068b8810fe10978a3a8be116f031468440 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 9 Aug 2022 17:40:26 -0500 Subject: [PATCH 6/6] Builder for creating offers Add a builder for creating offers given a required description and node_id. Other settings are optional and duplicative settings will override previous settings for non-Vec fields. --- lightning/src/ln/features.rs | 9 + lightning/src/offers/offer.rs | 556 ++++++++++++++++++- lightning/src/onion_message/blinded_route.rs | 10 +- lightning/src/util/ser.rs | 19 +- lightning/src/util/string.rs | 1 + 5 files changed, 579 insertions(+), 16 deletions(-) diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index f630a86f99e..77d0fa4529f 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -687,6 +687,15 @@ impl Features { } } +#[cfg(test)] +impl Features { + pub(crate) fn unknown() -> Self { + let mut features = Self::empty(); + features.set_unknown_feature_required(); + features + } +} + macro_rules! impl_feature_len_prefixed_write { ($features: ident) => { impl Writeable for $features { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index d64091155d7..bf569fbc022 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -8,14 +8,60 @@ // licenses. //! Data structures and encoding for `offer` messages. +//! +//! An [`Offer`] represents an "offer to be paid." It is typically constructed by a merchant and +//! 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; +//! +//! use core::num::NonZeroU64; +//! use core::time::Duration; +//! +//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +//! use lightning::offers::offer::{OfferBuilder, Quantity}; +//! +//! # use bitcoin::secp256k1; +//! # use lightning::onion_message::BlindedPath; +//! # #[cfg(feature = "std")] +//! # use std::time::SystemTime; +//! # +//! # fn create_blinded_path() -> BlindedPath { unimplemented!() } +//! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() } +//! # +//! # #[cfg(feature = "std")] +//! # fn build() -> Result<(), secp256k1::Error> { +//! let secp_ctx = Secp256k1::new(); +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let pubkey = PublicKey::from(keys); +//! +//! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); +//! let offer = OfferBuilder::new("coffee, large".to_string(), pubkey) +//! .amount_msats(20_000) +//! .supported_quantity(Quantity::Unbounded) +//! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap()) +//! .issuer("Foo Bar".to_string()) +//! .path(create_blinded_path()) +//! .path(create_another_blinded_path()) +//! .build() +//! .unwrap(); +//! # Ok(()) +//! # } +//! ``` use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::PublicKey; use core::num::NonZeroU64; use core::time::Duration; +use crate::io; use crate::ln::features::OfferFeatures; +use crate::ln::msgs::MAX_VALUE_MSAT; use crate::onion_message::BlindedPath; +use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; use crate::prelude::*; @@ -23,6 +69,141 @@ use crate::prelude::*; #[cfg(feature = "std")] use std::time::SystemTime; +/// Builds an [`Offer`] for the "offer to be paid" flow. +/// +/// See [module-level documentation] for usage. +/// +/// [module-level documentation]: self +pub struct OfferBuilder { + offer: OfferContents, +} + +impl OfferBuilder { + /// Creates a new builder for an offer setting the [`Offer::description`] and using the + /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered + /// while the offer is valid. + /// + /// Use a different pubkey per offer to avoid correlating offers. + pub fn new(description: String, signing_pubkey: PublicKey) -> Self { + let offer = OfferContents { + chains: None, metadata: None, amount: None, description, + features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, + supported_quantity: Quantity::one(), signing_pubkey: Some(signing_pubkey), + }; + OfferBuilder { offer } + } + + /// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called, + /// the chain hash of [`Network::Bitcoin`] is assumed to be the only one supported. + /// + /// See [`Offer::chains`] on how this relates to the payment currency. + /// + /// Successive calls to this method will add another chain hash. + pub fn chain(mut self, network: Network) -> Self { + let chains = self.offer.chains.get_or_insert_with(Vec::new); + let chain = ChainHash::using_genesis_block(network); + if !chains.contains(&chain) { + chains.push(chain); + } + + self + } + + /// Sets the [`Offer::metadata`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn metadata(mut self, metadata: Vec) -> Self { + self.offer.metadata = Some(metadata); + self + } + + /// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn amount_msats(mut self, amount_msats: u64) -> Self { + self.amount(Amount::Bitcoin { amount_msats }) + } + + /// Sets the [`Offer::amount`]. + /// + /// Successive calls to this method will override the previous setting. + fn amount(mut self, amount: Amount) -> Self { + self.offer.amount = Some(amount); + self + } + + /// Sets the [`Offer::features`]. + /// + /// Successive calls to this method will override the previous setting. + #[cfg(test)] + pub fn features(mut self, features: OfferFeatures) -> Self { + self.offer.features = features; + self + } + + /// Sets the [`Offer::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has + /// already passed is valid and can be checked for using [`Offer::is_expired`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn absolute_expiry(mut self, absolute_expiry: Duration) -> Self { + self.offer.absolute_expiry = Some(absolute_expiry); + self + } + + /// Sets the [`Offer::issuer`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn issuer(mut self, issuer: String) -> Self { + self.offer.issuer = Some(issuer); + self + } + + /// Adds a blinded path to [`Offer::paths`]. Must include at least one path if only connected by + /// private channels or if [`Offer::signing_pubkey`] is not a public node id. + /// + /// Successive calls to this method will add another blinded path. Caller is responsible for not + /// adding duplicate paths. + pub fn path(mut self, path: BlindedPath) -> Self { + self.offer.paths.get_or_insert_with(Vec::new).push(path); + self + } + + /// Sets the quantity of items for [`Offer::supported_quantity`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn supported_quantity(mut self, quantity: Quantity) -> Self { + self.offer.supported_quantity = quantity; + self + } + + /// Builds an [`Offer`] from the builder's settings. + pub fn build(mut self) -> Result { + match self.offer.amount { + Some(Amount::Bitcoin { amount_msats }) => { + if amount_msats > MAX_VALUE_MSAT { + return Err(()); + } + }, + Some(Amount::Currency { .. }) => unreachable!(), + None => {}, + } + + if let Some(chains) = &self.offer.chains { + if chains.len() == 1 && chains[0] == self.offer.implied_chain() { + self.offer.chains = None; + } + } + + let mut bytes = Vec::new(); + self.offer.write(&mut bytes).unwrap(); + + Ok(Offer { + bytes, + contents: self.offer, + }) + } +} + /// An `Offer` is a potentially long-lived proposal for payment of a good or service. /// /// An offer is a precursor to an `InvoiceRequest`. A merchant publishes an offer from which a @@ -52,7 +233,7 @@ pub(crate) struct OfferContents { absolute_expiry: Option, issuer: Option, paths: Option>, - quantity_max: Option, + supported_quantity: Quantity, signing_pubkey: Option, } @@ -67,7 +248,7 @@ impl Offer { self.contents.chains .as_ref() .cloned() - .unwrap_or_else(|| vec![ChainHash::using_genesis_block(Network::Bitcoin)]) + .unwrap_or_else(|| vec![self.contents.implied_chain()]) } // TODO: Link to corresponding method in `InvoiceRequest`. @@ -126,22 +307,67 @@ impl Offer { /// The quantity of items supported. pub fn supported_quantity(&self) -> Quantity { - match self.contents.quantity_max { - Some(0) => Quantity::Unbounded, - Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), - None => Quantity::Bounded(NonZeroU64::new(1).unwrap()), - } + self.contents.supported_quantity() } /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { self.contents.signing_pubkey.unwrap() } + + #[cfg(test)] + fn as_tlv_stream(&self) -> OfferTlvStreamRef { + self.contents.as_tlv_stream() + } +} + +impl OfferContents { + pub fn implied_chain(&self) -> ChainHash { + ChainHash::using_genesis_block(Network::Bitcoin) + } + + pub fn supported_quantity(&self) -> Quantity { + self.supported_quantity + } + + fn as_tlv_stream(&self) -> OfferTlvStreamRef { + let (currency, amount) = match &self.amount { + None => (None, None), + Some(Amount::Bitcoin { amount_msats }) => (None, Some(*amount_msats)), + Some(Amount::Currency { iso4217_code, amount }) => ( + Some(iso4217_code), Some(*amount) + ), + }; + + let features = { + if self.features == OfferFeatures::empty() { None } else { Some(&self.features) } + }; + + OfferTlvStreamRef { + chains: self.chains.as_ref(), + metadata: self.metadata.as_ref(), + currency, + amount, + description: Some(&self.description), + features, + absolute_expiry: self.absolute_expiry.map(|duration| duration.as_secs()), + paths: self.paths.as_ref(), + issuer: self.issuer.as_ref(), + quantity_max: self.supported_quantity.to_tlv_record(), + node_id: self.signing_pubkey.as_ref(), + } + } +} + +impl Writeable for OfferContents { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.as_tlv_stream().write(writer) + } } /// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or /// another currency. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Amount { /// An amount of bitcoin. Bitcoin { @@ -161,9 +387,323 @@ pub enum Amount { pub type CurrencyCode = [u8; 3]; /// Quantity of items supported by an [`Offer`]. +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Quantity { /// Up to a specific number of items (inclusive). Bounded(NonZeroU64), /// One or more items. Unbounded, } + +impl Quantity { + fn one() -> Self { + Quantity::Bounded(NonZeroU64::new(1).unwrap()) + } + + fn to_tlv_record(&self) -> Option { + match self { + Quantity::Bounded(n) => { + let n = n.get(); + if n == 1 { None } else { Some(n) } + }, + Quantity::Unbounded => Some(0), + } + } +} + +tlv_stream!(OfferTlvStream, OfferTlvStreamRef, { + (2, chains: (Vec, WithoutLength)), + (4, metadata: (Vec, WithoutLength)), + (6, currency: CurrencyCode), + (8, amount: (u64, HighZeroBytesDroppedBigSize)), + (10, description: (String, WithoutLength)), + (12, features: OfferFeatures), + (14, absolute_expiry: (u64, HighZeroBytesDroppedBigSize)), + (16, paths: (Vec, WithoutLength)), + (18, issuer: (String, WithoutLength)), + (20, quantity_max: (u64, HighZeroBytesDroppedBigSize)), + (22, node_id: PublicKey), +}); + +#[cfg(test)] +mod tests { + use super::{Amount, OfferBuilder, Quantity}; + + use bitcoin::blockdata::constants::ChainHash; + use bitcoin::network::constants::Network; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::num::NonZeroU64; + use core::time::Duration; + use crate::ln::features::OfferFeatures; + use crate::ln::msgs::MAX_VALUE_MSAT; + use crate::onion_message::{BlindedHop, BlindedPath}; + use crate::util::ser::Writeable; + use crate::util::string::PrintableString; + + 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() + } + + #[test] + fn builds_offer_with_defaults() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let tlv_stream = offer.as_tlv_stream(); + let mut buffer = Vec::new(); + offer.contents.write(&mut buffer).unwrap(); + + assert_eq!(offer.bytes, buffer.as_slice()); + assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); + assert_eq!(offer.metadata(), None); + assert_eq!(offer.amount(), None); + assert_eq!(offer.description(), PrintableString("foo")); + assert_eq!(offer.features(), &OfferFeatures::empty()); + assert_eq!(offer.absolute_expiry(), None); + #[cfg(feature = "std")] + assert!(!offer.is_expired()); + assert_eq!(offer.paths(), &[]); + assert_eq!(offer.issuer(), None); + assert_eq!(offer.supported_quantity(), Quantity::one()); + assert_eq!(offer.signing_pubkey(), pubkey(42)); + + assert_eq!(tlv_stream.chains, None); + assert_eq!(tlv_stream.metadata, None); + assert_eq!(tlv_stream.currency, None); + assert_eq!(tlv_stream.amount, None); + assert_eq!(tlv_stream.description, Some(&String::from("foo"))); + assert_eq!(tlv_stream.features, None); + assert_eq!(tlv_stream.absolute_expiry, None); + assert_eq!(tlv_stream.paths, None); + assert_eq!(tlv_stream.issuer, None); + assert_eq!(tlv_stream.quantity_max, None); + assert_eq!(tlv_stream.node_id, Some(&pubkey(42))); + } + + #[test] + fn builds_offer_with_chains() { + let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); + let testnet = ChainHash::using_genesis_block(Network::Testnet); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .chain(Network::Bitcoin) + .build() + .unwrap(); + assert_eq!(offer.chains(), vec![mainnet]); + assert_eq!(offer.as_tlv_stream().chains, None); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .chain(Network::Testnet) + .build() + .unwrap(); + assert_eq!(offer.chains(), vec![testnet]); + assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .chain(Network::Testnet) + .chain(Network::Testnet) + .build() + .unwrap(); + assert_eq!(offer.chains(), vec![testnet]); + assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .chain(Network::Bitcoin) + .chain(Network::Testnet) + .build() + .unwrap(); + assert_eq!(offer.chains(), vec![mainnet, testnet]); + assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet])); + } + + #[test] + fn builds_offer_with_metadata() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .metadata(vec![42; 32]) + .build() + .unwrap(); + assert_eq!(offer.metadata(), Some(&vec![42; 32])); + assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32])); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .metadata(vec![42; 32]) + .metadata(vec![43; 32]) + .build() + .unwrap(); + assert_eq!(offer.metadata(), Some(&vec![43; 32])); + assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32])); + } + + #[test] + fn builds_offer_with_amount() { + let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 }; + let currency_amount = Amount::Currency { iso4217_code: *b"USD", amount: 10 }; + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .amount_msats(1000) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(offer.amount(), Some(&bitcoin_amount)); + assert_eq!(tlv_stream.amount, Some(1000)); + assert_eq!(tlv_stream.currency, None); + + let builder = OfferBuilder::new("foo".into(), pubkey(42)) + .amount(currency_amount.clone()); + let tlv_stream = builder.offer.as_tlv_stream(); + assert_eq!(builder.offer.amount, Some(currency_amount.clone())); + assert_eq!(tlv_stream.amount, Some(10)); + assert_eq!(tlv_stream.currency, Some(b"USD")); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .amount(currency_amount.clone()) + .amount(bitcoin_amount.clone()) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(tlv_stream.amount, Some(1000)); + assert_eq!(tlv_stream.currency, None); + + let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 }; + match OfferBuilder::new("foo".into(), pubkey(42)).amount(invalid_amount).build() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ()), + } + } + + #[test] + fn builds_offer_with_features() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .features(OfferFeatures::unknown()) + .build() + .unwrap(); + assert_eq!(offer.features(), &OfferFeatures::unknown()); + assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown())); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .features(OfferFeatures::unknown()) + .features(OfferFeatures::empty()) + .build() + .unwrap(); + assert_eq!(offer.features(), &OfferFeatures::empty()); + assert_eq!(offer.as_tlv_stream().features, None); + } + + #[test] + fn builds_offer_with_absolute_expiry() { + let future_expiry = Duration::from_secs(u64::max_value()); + let past_expiry = Duration::from_secs(0); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .absolute_expiry(future_expiry) + .build() + .unwrap(); + #[cfg(feature = "std")] + assert!(!offer.is_expired()); + assert_eq!(offer.absolute_expiry(), Some(future_expiry)); + assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs())); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .absolute_expiry(future_expiry) + .absolute_expiry(past_expiry) + .build() + .unwrap(); + #[cfg(feature = "std")] + assert!(offer.is_expired()); + assert_eq!(offer.absolute_expiry(), Some(past_expiry)); + assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(past_expiry.as_secs())); + } + + #[test] + fn builds_offer_with_paths() { + let paths = vec![ + BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }, + BlindedPath { + introduction_node_id: pubkey(40), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + }, + ]; + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .path(paths[0].clone()) + .path(paths[1].clone()) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(offer.paths(), paths.as_slice()); + assert_eq!(offer.signing_pubkey(), pubkey(42)); + assert_ne!(pubkey(42), pubkey(44)); + assert_eq!(tlv_stream.paths, Some(&paths)); + assert_eq!(tlv_stream.node_id, Some(&pubkey(42))); + } + + #[test] + fn builds_offer_with_issuer() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .issuer("bar".into()) + .build() + .unwrap(); + assert_eq!(offer.issuer(), Some(PrintableString("bar"))); + assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar"))); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .issuer("bar".into()) + .issuer("baz".into()) + .build() + .unwrap(); + assert_eq!(offer.issuer(), Some(PrintableString("baz"))); + assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("baz"))); + } + + #[test] + fn builds_offer_with_supported_quantity() { + let ten = NonZeroU64::new(10).unwrap(); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::one()) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(offer.supported_quantity(), Quantity::one()); + assert_eq!(tlv_stream.quantity_max, None); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Unbounded) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(offer.supported_quantity(), Quantity::Unbounded); + assert_eq!(tlv_stream.quantity_max, Some(0)); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Bounded(ten)) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten)); + assert_eq!(tlv_stream.quantity_max, Some(10)); + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Bounded(ten)) + .supported_quantity(Quantity::one()) + .build() + .unwrap(); + let tlv_stream = offer.as_tlv_stream(); + assert_eq!(offer.supported_quantity(), Quantity::one()); + assert_eq!(tlv_stream.quantity_max, None); + } +} diff --git a/lightning/src/onion_message/blinded_route.rs b/lightning/src/onion_message/blinded_route.rs index 29d78d6ab58..8a8d9924f9f 100644 --- a/lightning/src/onion_message/blinded_route.rs +++ b/lightning/src/onion_message/blinded_route.rs @@ -28,33 +28,33 @@ use crate::prelude::*; /// Onion messages can be sent and received to blinded routes, which serve to hide the identity of /// the recipient. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct BlindedRoute { /// To send to a blinded route, the sender first finds a route to the unblinded /// `introduction_node_id`, which can unblind its [`encrypted_payload`] to find out the onion /// message's next hop and forward it along. /// /// [`encrypted_payload`]: BlindedHop::encrypted_payload - pub(super) introduction_node_id: PublicKey, + pub(crate) introduction_node_id: PublicKey, /// Used by the introduction node to decrypt its [`encrypted_payload`] to forward the onion /// message. /// /// [`encrypted_payload`]: BlindedHop::encrypted_payload - pub(super) blinding_point: PublicKey, + pub(crate) blinding_point: PublicKey, /// The hops composing the blinded route. pub(crate) blinded_hops: Vec, } /// Used to construct the blinded hops portion of a blinded route. These hops cannot be identified /// by outside observers and thus can be used to hide the identity of the recipient. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct BlindedHop { /// The blinded node id of this hop in a blinded route. pub(crate) blinded_node_id: PublicKey, /// The encrypted payload intended for this hop in a blinded route. // The node sending to this blinded route will later encode this payload into the onion packet for // this hop. - pub(super) encrypted_payload: Vec, + pub(crate) encrypted_payload: Vec, } impl BlindedRoute { diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 37fade88835..7c4ba7fd50f 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -22,6 +22,7 @@ use core::ops::Deref; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::secp256k1::constants::{PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, COMPACT_SIGNATURE_SIZE}; use bitcoin::secp256k1::ecdsa::Signature; +use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; use bitcoin::consensus; @@ -366,8 +367,7 @@ impl Readable for BigSize { /// In TLV we occasionally send fields which only consist of, or potentially end with, a /// variable-length integer which is simply truncated by skipping high zero bytes. This type /// encapsulates such integers implementing Readable/Writeable for them. -#[cfg_attr(test, derive(PartialEq, Eq))] -#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] pub(crate) struct HighZeroBytesDroppedBigSize(pub T); macro_rules! impl_writeable_primitive { @@ -485,7 +485,7 @@ macro_rules! impl_array { ); } -impl_array!(3); // for rgb +impl_array!(3); // for rgb, ISO 4712 code impl_array!(4); // for IPv4 impl_array!(12); // for OnionV2 impl_array!(16); // for IPv6 @@ -884,6 +884,19 @@ impl Readable for BlockHash { } } +impl Writeable for ChainHash { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + w.write_all(self.as_bytes()) + } +} + +impl Readable for ChainHash { + fn read(r: &mut R) -> Result { + let buf: [u8; 32] = Readable::read(r)?; + Ok(ChainHash::from(&buf[..])) + } +} + impl Writeable for OutPoint { fn write(&self, w: &mut W) -> Result<(), io::Error> { self.txid.write(w)?; diff --git a/lightning/src/util/string.rs b/lightning/src/util/string.rs index 1af04e085fd..42eb96f5bf6 100644 --- a/lightning/src/util/string.rs +++ b/lightning/src/util/string.rs @@ -13,6 +13,7 @@ use core::fmt; /// A string that displays only printable characters, replacing control characters with /// [`core::char::REPLACEMENT_CHARACTER`]. +#[derive(Debug, PartialEq)] pub struct PrintableString<'a>(pub &'a str); impl<'a> fmt::Display for PrintableString<'a> {