diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index 25eba1d74..1f3ab47b1 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/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 465256d21..95bb1f1cb 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6658,7 +6658,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/ln/features.rs b/lightning/src/ln/features.rs index e8a3e6d26..77d0fa452 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. /// @@ -684,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 { @@ -704,21 +716,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}; diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs new file mode 100644 index 000000000..2f961a0bb --- /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 000000000..bf569fbc0 --- /dev/null +++ b/lightning/src/offers/offer.rs @@ -0,0 +1,709 @@ +// 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. +//! +//! 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::*; + +#[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 +/// 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>, + supported_quantity: Quantity, + 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![self.contents.implied_chain()]) + } + + // 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 { + 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, PartialEq)] +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`]. +#[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 82a325a9e..8a8d9924f 100644 --- a/lightning/src/onion_message/blinded_route.rs +++ b/lightning/src/onion_message/blinded_route.rs @@ -28,31 +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, 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(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, PartialEq)] 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. - pub(super) encrypted_payload: Vec, + pub(crate) encrypted_payload: Vec, } impl BlindedRoute { diff --git a/lightning/src/onion_message/mod.rs b/lightning/src/onion_message/mod.rs index 3c522e30c..89c1545a7 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/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 73f58036c..8fff53642 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/routing/gossip.rs b/lightning/src/routing/gossip.rs index 6b0f88d09..00846a7ba 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/events.rs b/lightning/src/util/events.rs index 5a0a9d0cc..a9687d288 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/mod.rs b/lightning/src/util/mod.rs index 9ffe1b749..730131f22 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/ser.rs b/lightning/src/util/ser.rs index 5ff6dc86a..7c4ba7fd5 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; @@ -283,39 +284,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] @@ -451,6 +419,9 @@ macro_rules! impl_writeable_primitive { } } } + impl From<$val_type> for HighZeroBytesDroppedBigSize<$val_type> { + fn from(val: $val_type) -> Self { Self(val) } + } } } @@ -514,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 @@ -546,6 +517,59 @@ 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 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> { + 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, @@ -860,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/ser_macros.rs b/lightning/src/util/ser_macros.rs index 74b206d3a..3ec084868 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 { @@ -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 { @@ -66,7 +72,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 { @@ -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 { @@ -160,7 +172,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) => {{ @@ -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)),* $(,)*} @@ -453,7 +543,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) => { diff --git a/lightning/src/util/string.rs b/lightning/src/util/string.rs new file mode 100644 index 000000000..42eb96f5b --- /dev/null +++ b/lightning/src/util/string.rs @@ -0,0 +1,42 @@ +// 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`]. +#[derive(Debug, PartialEq)] +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}", + ); + } +}