diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs new file mode 100644 index 00000000000..62ef8ca7476 --- /dev/null +++ b/lightning/src/offers/invoice.rs @@ -0,0 +1,241 @@ +// 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 `invoice_request` messages. + +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::util::address::{Address, Payload, WitnessVersion}; +use core::convert::TryFrom; +use core::time::Duration; +use crate::io; +use crate::ln::PaymentHash; +use crate::ln::features::OfferFeatures; +use crate::ln::msgs::DecodeError; +use crate::offers::merkle::{SignatureTlvStream, self}; +use crate::offers::offer::OfferTlvStream; +use crate::offers::parse::{ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::PayerTlvStream; +use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream}; +use crate::onion_message::BlindedPath; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; + +use crate::prelude::*; + +/// +const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature"); + +/// +pub struct Invoice { + bytes: Vec, + contents: InvoiceContents, + signature: Option, +} + +/// +pub(crate) struct InvoiceContents { + invoice_request: InvoiceRequestContents, + paths: Option>, + payinfo: Option>, + created_at: Duration, + relative_expiry: Option, + payment_hash: PaymentHash, + amount_msats: u64, + fallbacks: Option>, + features: Option, + code: Option, +} + +impl Invoice { + /// + pub fn fallbacks(&self) -> Vec<&Address> { + let is_valid = |address: &&Address| { + if let Address { payload: Payload::WitnessProgram { program, .. }, .. } = address { + if address.is_standard() { + return true; + } else if program.len() < 2 || program.len() > 40 { + return false; + } else { + return true; + } + } + + unreachable!() + }; + self.contents.fallbacks + .as_ref() + .map(|fallbacks| fallbacks.iter().filter(is_valid).collect()) + .unwrap_or_else(Vec::new) + } +} + +impl Writeable for Invoice { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice = ParsedMessage::::try_from(bytes)?; + Invoice::try_from(parsed_invoice) + } +} + +tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { + (160, paths: (Vec, WithoutLength)), + (162, payinfo: (Vec, WithoutLength)), + (164, created_at: (u64, HighZeroBytesDroppedBigSize)), + (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)), + (168, payment_hash: PaymentHash), + (170, amount: (u64, HighZeroBytesDroppedBigSize)), + (172, fallbacks: (Vec, WithoutLength)), + (174, features: OfferFeatures), + (176, code: (String, WithoutLength)), +}); + +/// +#[derive(Debug)] +pub struct BlindedPayInfo { + fee_base_msat: u32, + fee_proportional_millionths: u32, + cltv_expiry_delta: u16, + htlc_minimum_msat: u64, + htlc_maximum_msat: u64, + features_len: u16, + features: OfferFeatures, +} + +impl_writeable!(BlindedPayInfo, { + fee_base_msat, + fee_proportional_millionths, + cltv_expiry_delta, + htlc_minimum_msat, + htlc_maximum_msat, + features_len, + features +}); + +/// +#[derive(Debug)] +pub struct FallbackAddress { + version: WitnessVersion, + program: Vec, +} + +impl_writeable!(FallbackAddress, { version, program }); + +type FullInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let invoice = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, invoice, signature)) + } +} + +type PartialInvoiceTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream); + +impl TryFrom> for Invoice { + type Error = ParseError; + + fn try_from(invoice: ParsedMessage) -> Result { + let ParsedMessage { bytes, tlv_stream } = invoice; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) + )?; + + if let Some(signature) = &signature { + let pubkey = contents.invoice_request.offer.signing_pubkey(); + merkle::verify_signature(signature, SIGNATURE_TAG, &bytes, pubkey)?; + } + + Ok(Invoice { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result { + let ( + payer_tlv_stream, + offer_tlv_stream, + invoice_request_tlv_stream, + InvoiceTlvStream { + paths, payinfo, created_at, relative_expiry, payment_hash, amount, fallbacks, + features, code, + }, + ) = tlv_stream; + + let invoice_request = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + + let (paths, payinfo) = match (paths, payinfo) { + (None, _) => return Err(SemanticError::MissingPaths), + (_, None) => return Err(SemanticError::InvalidPayInfo), + (Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths), + (Some(paths), Some(payinfo)) if paths.len() != payinfo.len() => { + return Err(SemanticError::InvalidPayInfo); + }, + (paths, payinfo) => (paths, payinfo), + }; + + let created_at = match created_at { + None => return Err(SemanticError::MissingCreationTime), + Some(timestamp) => Duration::from_secs(timestamp), + }; + + let relative_expiry = relative_expiry + .map(Into::::into) + .map(Duration::from_secs); + + let payment_hash = match payment_hash { + None => return Err(SemanticError::MissingPaymentHash), + Some(payment_hash) => payment_hash, + }; + + let amount_msats = match amount { + None => return Err(SemanticError::MissingAmount), + Some(amount) => amount, + }; + + let fallbacks = match fallbacks { + None => None, + Some(fallbacks) => { + let mut addresses = Vec::with_capacity(fallbacks.len()); + for FallbackAddress { version, program } in fallbacks { + addresses.push(Address { + payload: Payload::WitnessProgram { version, program }, + network: invoice_request.network(), + }); + } + Some(addresses) + }, + }; + + Ok(InvoiceContents { + invoice_request, paths, payinfo, created_at, relative_expiry, payment_hash, + amount_msats, fallbacks, features, code, + }) + } +} diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs new file mode 100644 index 00000000000..c26cf5e2d6c --- /dev/null +++ b/lightning/src/offers/invoice_request.rs @@ -0,0 +1,538 @@ +// 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 `invoice_request` messages. +//! +//! An [`InvoiceRequest`] can be either built from a parsed [`Offer`] as an "offer to be paid" or +//! built directly as an "offer for money" (e.g., refund, ATM withdrawal). In the former case, it is +//! typically constructed by a customer and sent to the merchant who had published the corresponding +//! offer. In the latter case, an offer doesn't exist as a precursor to the request. Rather the +//! merchant would typically construct the invoice request and presents it to the customer. +//! +//! The recipient of the request responds with an `Invoice`. +//! ``` +//! extern crate bitcoin; +//! extern crate lightning; +//! +//! use bitcoin::network::constants::Network; +//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +//! use lightning::ln::features::OfferFeatures; +//! use lightning::offers::offer::Offer; +//! use lightning::util::ser::Writeable; +//! +//! # fn parse() -> Result<(), lightning::offers::parse::ParseError> { +//! let secp_ctx = Secp256k1::new(); +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let pubkey = PublicKey::from(keys); +//! let mut buffer = Vec::new(); +//! +//! // "offer to be paid" flow +//! "lno1qcp4256ypq" +//! .parse::()? +//! .request_invoice(pubkey) +//! .metadata(vec![42; 64]) +//! .chain(Network::Testnet)? +//! .amount_msats(1000) +//! .quantity(5)? +//! .payer_note("foo".to_string()) +//! .build()? +//! .sign(|digest| secp_ctx.sign_schnorr_no_aux_rand(digest, &keys))? +//! .write(&mut buffer) +//! .unwrap(); +//! # Ok(()) +//! # } +//! ``` + +use bitcoin::blockdata::constants::ChainHash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::{Message, PublicKey, self}; +use bitcoin::secp256k1::schnorr::Signature; +use core::convert::TryFrom; +use core::str::FromStr; +use crate::io; +use crate::ln::features::OfferFeatures; +use crate::ln::msgs::DecodeError; +use crate::offers::merkle::{SignatureTlvStream, SignatureTlvStreamRef, self}; +use crate::offers::offer::{Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, SendInvoiceOfferContents}; +use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; +use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer}; + +use crate::prelude::*; + +const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature"); + +/// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow. +/// +/// See [module-level documentation] for usage. +/// +/// [module-level documentation]: self +pub struct InvoiceRequestBuilder<'a> { + offer: &'a Offer, + invoice_request: InvoiceRequestContents, +} + +impl<'a> InvoiceRequestBuilder<'a> { + pub(super) fn new(offer: &'a Offer, payer_id: PublicKey) -> Self { + Self { + offer, + invoice_request: InvoiceRequestContents { + payer: PayerContents(None), offer: offer.contents.clone(), chain: None, + amount_msats: None, features: None, quantity: None, payer_id, payer_note: None, + }, + } + } + + /// Sets the metadata for the invoice request. Useful for containing information about the + /// derivation of [`InvoiceRequest::payer_id`]. This should not leak any information such as + /// using a simple BIP-32 derivation path. + /// + /// Successive calls to this method will override the previous setting. + pub fn metadata(mut self, metadata: Vec) -> Self { + self.invoice_request.payer = PayerContents(Some(metadata)); + self + } + + /// Sets the chain hash of the given [`Network`] for paying an invoice. If not called, + /// [`Network::Bitcoin`] is assumed. Must be supported by the offer. + /// + /// Successive calls to this method will override the previous setting. + pub fn chain(mut self, network: Network) -> Result { + let chain = ChainHash::using_genesis_block(network); + if !self.offer.supports_chain(chain) { + return Err(SemanticError::UnsupportedChain) + } + + self.invoice_request.chain = Some(chain); + Ok(self) + } + + /// Sets the amount for paying an invoice. Must be at least the base invoice amount (i.e., + /// [`Offer::amount`] times [`quantity`]). + /// + /// Successive calls to this method will override the previous setting. + /// + /// [`quantity`]: Self::quantity + pub fn amount_msats(mut self, amount_msats: u64) -> Self { + self.invoice_request.amount_msats = Some(amount_msats); + self + } + + /// Sets the features for the invoice request. + /// + /// Successive calls to this method will override the previous setting. + #[cfg(test)] + pub fn features(mut self, features: OfferFeatures) -> Self { + self.invoice_request.features = Some(features); + self + } + + /// Sets a quantity of items for the invoice request. If not set, `1` is assumed. Must conform + /// to [`Offer::is_valid_quantity`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn quantity(mut self, quantity: u64) -> Result { + if !self.offer.is_valid_quantity(quantity) { + return Err(SemanticError::InvalidQuantity); + } + + self.invoice_request.quantity = Some(quantity); + Ok(self) + } + + /// Sets a note for the invoice request. + /// + /// Successive calls to this method will override the previous setting. + pub fn payer_note(mut self, payer_note: String) -> Self { + self.invoice_request.payer_note = Some(payer_note); + self + } + + /// Builds an [`InvoiceRequest`] after checking for valid semantics. + pub fn build(self) -> Result, SemanticError> { + if !self.offer.supports_chain(self.invoice_request.chain()) { + return Err(SemanticError::UnsupportedChain); + } + + if let Some(amount) = self.offer.amount() { + if self.invoice_request.amount_msats.is_none() { + return Err(SemanticError::MissingAmount); + } + + if let Amount::Currency { .. } = amount { + return Err(SemanticError::UnsupportedCurrency); + } + } + + if self.offer.expects_quantity() && self.invoice_request.quantity.is_none() { + return Err(SemanticError::InvalidQuantity); + } + + let amount_msats = self.invoice_request.amount_msats.unwrap_or(self.offer.amount_msats()); + let quantity = self.invoice_request.quantity.unwrap_or(1); + if amount_msats < self.offer.expected_invoice_amount_msats(quantity) { + return Err(SemanticError::InsufficientAmount); + } + + let InvoiceRequestBuilder { offer, invoice_request } = self; + Ok(UnsignedInvoiceRequest { offer, invoice_request }) + } +} + +/// A semantically valid [`InvoiceRequest`] that hasn't been signed. +pub struct UnsignedInvoiceRequest<'a> { + offer: &'a Offer, + invoice_request: InvoiceRequestContents, +} + +impl<'a> UnsignedInvoiceRequest<'a> { + /// Signs the invoice request using the given function. + pub fn sign(self, sign: F) -> Result + where F: FnOnce(&Message) -> Signature + { + // Use the offer bytes instead of the offer TLV stream as the offer may have contained + // unknown TLV records, which are not stored in `OfferContents`. + let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) = + self.invoice_request.as_tlv_stream(); + let offer_bytes = WithoutLength(&self.offer.bytes); + let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream); + + let mut bytes = Vec::new(); + unsigned_tlv_stream.write(&mut bytes).unwrap(); + + let pubkey = self.invoice_request.payer_id; + let signature = Some(merkle::sign_message(sign, SIGNATURE_TAG, &bytes, pubkey)?); + + // Append the signature TLV record to the bytes. + let signature_tlv_stream = SignatureTlvStreamRef { + signature: signature.as_ref(), + }; + signature_tlv_stream.write(&mut bytes).unwrap(); + + Ok(InvoiceRequest { + bytes, + contents: self.invoice_request, + signature, + }) + } +} + +/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`]. +/// +/// An offer may provided choices such as quantity, amount, chain, features, etc. An invoice request +/// specifies these such that the recipient can send an invoice for payment. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug)] +pub struct InvoiceRequest { + bytes: Vec, + contents: InvoiceRequestContents, + signature: Option, +} + +/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`. +#[derive(Clone, Debug)] +pub(crate) struct InvoiceRequestContents { + payer: PayerContents, + pub(super) offer: OfferContents, + chain: Option, + amount_msats: Option, + features: Option, + quantity: Option, + payer_id: PublicKey, + payer_note: Option, +} + +impl InvoiceRequest { + /// An unpredictable series of bytes, typically containing information about the derivation of + /// [`payer_id`]. + /// + /// [`payer_id`]: Self::payer_id + pub fn metadata(&self) -> Option<&Vec> { + self.contents.payer.0.as_ref() + } + + /// A chain from [`Offer::chains`] that the offer is valid for. + pub fn chain(&self) -> ChainHash { + self.contents.chain() + } + + /// The amount to pay in msats (i.e., the minimum lightning-payable unit for [`chain`]), which + /// must be greater than or equal to [`Offer::amount`], converted if necessary. + /// + /// [`chain`]: Self::chain + pub fn amount_msats(&self) -> Option { + self.contents.amount_msats + } + + /// Features for paying the invoice. + pub fn features(&self) -> Option<&OfferFeatures> { + self.contents.features.as_ref() + } + + /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`]. + pub fn quantity(&self) -> Option { + self.contents.quantity + } + + /// A transient pubkey used to sign the invoice request. + pub fn payer_id(&self) -> PublicKey { + self.contents.payer_id + } + + /// Payer provided note to include in the invoice. + pub fn payer_note(&self) -> Option<&String> { + self.contents.payer_note.as_ref() + } + + /// Signature of the invoice request using [`payer_id`]. + /// + /// [`payer_id`]: Self::payer_id + pub fn signature(&self) -> Option { + self.signature + } +} + +impl InvoiceRequestContents { + fn chain(&self) -> ChainHash { + self.chain.unwrap_or_else(|| self.offer.implied_chain()) + } + + pub fn network(&self) -> Network { + let chain = self.chain(); + if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Network::Bitcoin + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Network::Testnet + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Network::Signet + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Network::Regtest + } else { + unreachable!() + } + } + + pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef { + let payer = PayerTlvStreamRef { + metadata: self.payer.0.as_ref(), + }; + + let offer = self.offer.as_tlv_stream(); + + let invoice_request = InvoiceRequestTlvStreamRef { + chain: self.chain.as_ref(), + amount: self.amount_msats, + features: self.features.as_ref(), + quantity: self.quantity, + payer_id: Some(&self.payer_id), + payer_note: self.payer_note.as_ref(), + }; + + (payer, offer, invoice_request) + } +} + +impl AsRef<[u8]> for InvoiceRequest { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +impl Writeable for InvoiceRequest { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl Writeable for InvoiceRequestContents { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + self.as_tlv_stream().write(writer) + } +} + +impl TryFrom> for InvoiceRequest { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_invoice_request = ParsedMessage::::try_from(bytes)?; + InvoiceRequest::try_from(parsed_invoice_request) + } +} + +tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, { + (80, chain: ChainHash), + (82, amount: (u64, HighZeroBytesDroppedBigSize)), + (84, features: OfferFeatures), + (86, quantity: (u64, HighZeroBytesDroppedBigSize)), + (88, payer_id: PublicKey), + (89, payer_note: (String, WithoutLength)), +}); + +impl Bech32Encode for InvoiceRequest { + const BECH32_HRP: &'static str = "lnr"; +} + +type FullInvoiceRequestTlvStream = + (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream); + +impl SeekReadable for FullInvoiceRequestTlvStream { + fn read(r: &mut R) -> Result { + let payer = SeekReadable::read(r)?; + let offer = SeekReadable::read(r)?; + let invoice_request = SeekReadable::read(r)?; + let signature = SeekReadable::read(r)?; + + Ok((payer, offer, invoice_request, signature)) + } +} + +type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream); + +type PartialInvoiceRequestTlvStreamRef<'a> = ( + PayerTlvStreamRef<'a>, + OfferTlvStreamRef<'a>, + InvoiceRequestTlvStreamRef<'a>, +); + +impl FromStr for InvoiceRequest { + type Err = ParseError; + + fn from_str(s: &str) -> Result::Err> { + InvoiceRequest::from_bech32_str(s) + } +} + +impl TryFrom> for InvoiceRequest { + type Error = ParseError; + + fn try_from(invoice_request: ParsedMessage) + -> Result + { + let ParsedMessage {bytes, tlv_stream } = invoice_request; + let ( + payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, + SignatureTlvStream { signature }, + ) = tlv_stream; + let contents = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + + if let Some(signature) = &signature { + merkle::verify_signature(signature, SIGNATURE_TAG, &bytes, contents.payer_id)?; + } + + Ok(InvoiceRequest { bytes, contents, signature }) + } +} + +impl TryFrom for InvoiceRequestContents { + type Error = SemanticError; + + fn try_from(tlv_stream: PartialInvoiceRequestTlvStream) -> Result { + let ( + PayerTlvStream { metadata }, + offer_tlv_stream, + InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note }, + ) = tlv_stream; + + let payer = PayerContents(metadata); + let offer = match offer_tlv_stream.node_id { + Some(_) => OfferContents::try_from(offer_tlv_stream)?, + None => SendInvoiceOfferContents::try_from(offer_tlv_stream)?.0, + }; + + if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) { + return Err(SemanticError::UnsupportedChain); + } + + let amount_msats = match (offer.amount(), amount) { + (Some(_), None) => return Err(SemanticError::MissingAmount), + (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency), + (_, amount_msats) => amount_msats, + }; + + if let Some(features) = &features { + if features.requires_unknown_bits() { + return Err(SemanticError::UnknownRequiredFeatures); + } + } + + let expects_quantity = offer.expects_quantity(); + let quantity = match quantity { + None if expects_quantity => return Err(SemanticError::MissingQuantity), + Some(_) if !expects_quantity => return Err(SemanticError::UnexpectedQuantity), + Some(quantity) if !offer.is_valid_quantity(quantity) => { + return Err(SemanticError::InvalidQuantity); + } + quantity => quantity, + }; + + { + let amount_msats = amount_msats.unwrap_or(offer.amount_msats()); + let quantity = quantity.unwrap_or(1); + if amount_msats < offer.expected_invoice_amount_msats(quantity) { + return Err(SemanticError::InsufficientAmount); + } + } + + + let payer_id = match payer_id { + None => return Err(SemanticError::MissingPayerId), + Some(payer_id) => payer_id, + }; + + Ok(InvoiceRequestContents { + payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note, + }) + } +} + +impl core::fmt::Display for InvoiceRequest { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + +#[cfg(test)] +mod tests { + use super::InvoiceRequest; + + use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey}; + use core::convert::TryFrom; + use crate::ln::msgs::DecodeError; + use crate::offers::offer::OfferBuilder; + use crate::offers::parse::ParseError; + use crate::util::ser::{BigSize, Writeable}; + + #[test] + fn fails_parsing_invoice_request_with_extra_tlv_records() { + let secp_ctx = Secp256k1::new(); + let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let invoice_request = OfferBuilder::new("foo".into(), keys.public_key()) + .build() + .unwrap() + .request_invoice(keys.public_key()) + .build() + .unwrap() + .sign(|digest| secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)) + .unwrap(); + + let mut encoded_invoice_request = Vec::new(); + invoice_request.write(&mut encoded_invoice_request).unwrap(); + BigSize(1002).write(&mut encoded_invoice_request).unwrap(); + BigSize(32).write(&mut encoded_invoice_request).unwrap(); + [42u8; 32].write(&mut encoded_invoice_request).unwrap(); + + match InvoiceRequest::try_from(encoded_invoice_request) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } +} diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs new file mode 100644 index 00000000000..1b38d60bc2d --- /dev/null +++ b/lightning/src/offers/merkle.rs @@ -0,0 +1,178 @@ +// 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. + +//! Tagged hashes for use in signature calculation and verification. + +use bitcoin::hashes::{Hash, HashEngine, sha256}; +use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::schnorr::Signature; +use crate::io; +use crate::util::ser::{BigSize, Readable}; + +use crate::prelude::*; + +/// Valid type range for signature TLV records. +const SIGNATURE_TYPES: core::ops::RangeInclusive = 240..=1000; + +tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, { + (240, signature: Signature), +}); + +pub(super) fn sign_message( + sign: F, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result +where + F: FnOnce(&Message) -> Signature +{ + let digest = message_digest(tag, bytes); + let signature = sign(&digest); + + let pubkey = pubkey.into(); + let secp_ctx = Secp256k1::verification_only(); + secp_ctx.verify_schnorr(&signature, &digest, &pubkey)?; + + Ok(signature) +} + +/// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message +/// digest. +pub(super) fn verify_signature( + signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey, +) -> Result<(), secp256k1::Error> { + let digest = message_digest(tag, bytes); + let pubkey = pubkey.into(); + let secp_ctx = Secp256k1::verification_only(); + secp_ctx.verify_schnorr(signature, &digest, &pubkey) +} + +fn message_digest(tag: &str, bytes: &[u8]) -> Message { + let tag = sha256::Hash::hash(tag.as_bytes()); + let merkle_root = root_hash(bytes); + Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap() +} + +/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream +/// containing at least one TLV record. +fn root_hash(data: &[u8]) -> sha256::Hash { + let mut tlv_stream = TlvStream::new(&data[..]).peekable(); + let nonce_tag = tagged_hash_engine(sha256::Hash::from_engine({ + let mut engine = sha256::Hash::engine(); + engine.input("LnNonce".as_bytes()); + engine.input(tlv_stream.peek().unwrap().type_bytes); + engine + })); + let leaf_tag = tagged_hash_engine(sha256::Hash::hash("LnLeaf".as_bytes())); + let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes())); + + let mut leaves = Vec::new(); + for record in tlv_stream { + if !SIGNATURE_TYPES.contains(&record.r#type.0) { + leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record)); + leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); + } + } + + // Calculate the merkle root hash in place. + let num_leaves = leaves.len(); + for level in 0.. { + let step = 2 << level; + let offset = step / 2; + if offset >= num_leaves { + break; + } + + for (i, j) in (0..num_leaves).step_by(step).zip((offset..num_leaves).step_by(step)) { + leaves[i] = tagged_branch_hash_from_engine(branch_tag.clone(), leaves[i], leaves[j]); + } + } + + *leaves.first().unwrap() +} + +fn tagged_hash>(tag: sha256::Hash, msg: T) -> sha256::Hash { + let engine = tagged_hash_engine(tag); + tagged_hash_from_engine(engine, msg) +} + +fn tagged_hash_engine(tag: sha256::Hash) -> sha256::HashEngine { + let mut engine = sha256::Hash::engine(); + engine.input(tag.as_ref()); + engine.input(tag.as_ref()); + engine +} + +fn tagged_hash_from_engine>(mut engine: sha256::HashEngine, msg: T) -> sha256::Hash { + engine.input(msg.as_ref()); + sha256::Hash::from_engine(engine) +} + +fn tagged_branch_hash_from_engine( + mut engine: sha256::HashEngine, leaf1: sha256::Hash, leaf2: sha256::Hash, +) -> sha256::Hash { + if leaf1 < leaf2 { + engine.input(leaf1.as_ref()); + engine.input(leaf2.as_ref()); + } else { + engine.input(leaf2.as_ref()); + engine.input(leaf1.as_ref()); + }; + sha256::Hash::from_engine(engine) +} + +/// [`Iterator`] over a sequence of bytes yielding [`TlvRecord`]s. The input is assumed to be a +/// well-formed TLV stream. +struct TlvStream<'a> { + data: io::Cursor<&'a [u8]>, +} + +impl<'a> TlvStream<'a> { + fn new(data: &'a [u8]) -> Self { + Self { + data: io::Cursor::new(data), + } + } +} + +/// A slice into a [`TlvStream`] for a record. +struct TlvRecord<'a> { + r#type: BigSize, + type_bytes: &'a [u8], + data: &'a [u8], +} + +impl AsRef<[u8]> for TlvRecord<'_> { + fn as_ref(&self) -> &[u8] { &self.data } +} + +impl<'a> Iterator for TlvStream<'a> { + type Item = TlvRecord<'a>; + + fn next(&mut self) -> Option { + if self.data.position() < self.data.get_ref().len() as u64 { + let start = self.data.position(); + + let r#type: BigSize = Readable::read(&mut self.data).unwrap(); + let offset = self.data.position(); + let type_bytes = &self.data.get_ref()[start as usize..offset as usize]; + + let length: BigSize = Readable::read(&mut self.data).unwrap(); + let offset = self.data.position(); + let end = offset + length.0; + + let _value = &self.data.get_ref()[offset as usize..end as usize]; + let data = &self.data.get_ref()[start as usize..end as usize]; + + self.data.set_position(end); + + Some(TlvRecord { r#type, type_bytes, data }) + } else { + None + } + } +} diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 2f961a0bb6e..e479d1e1d8e 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -12,4 +12,9 @@ //! //! Offers are a flexible protocol for Lightning payments. +pub mod invoice; +pub mod invoice_request; +mod merkle; pub mod offer; +pub mod parse; +mod payer; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index bf569fbc022..232c8f12dc7 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -18,13 +18,15 @@ //! extern crate core; //! extern crate lightning; //! +//! use core::convert::TryFrom; //! use core::num::NonZeroU64; //! use core::time::Duration; //! //! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; -//! use lightning::offers::offer::{OfferBuilder, Quantity}; +//! use lightning::offers::offer::{Offer, OfferBuilder, Quantity}; +//! use lightning::offers::parse::ParseError; +//! use lightning::util::ser::{Readable, Writeable}; //! -//! # use bitcoin::secp256k1; //! # use lightning::onion_message::BlindedPath; //! # #[cfg(feature = "std")] //! # use std::time::SystemTime; @@ -33,9 +35,9 @@ //! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() } //! # //! # #[cfg(feature = "std")] -//! # fn build() -> Result<(), secp256k1::Error> { +//! # fn build() -> Result<(), ParseError> { //! let secp_ctx = Secp256k1::new(); -//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); //! let pubkey = PublicKey::from(keys); //! //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); @@ -46,8 +48,20 @@ //! .issuer("Foo Bar".to_string()) //! .path(create_blinded_path()) //! .path(create_another_blinded_path()) -//! .build() -//! .unwrap(); +//! .build()?; +//! +//! // Encode as a bech32 string for use in a QR code. +//! let encoded_offer = offer.to_string(); +//! +//! // Parse from a bech32 string after scanning from a QR code. +//! let offer = encoded_offer.parse::()?; +//! +//! // Encode offer as raw bytes. +//! let mut bytes = Vec::new(); +//! offer.write(&mut bytes).unwrap(); +//! +//! // Decode raw bytes into an offer. +//! let offer = Offer::try_from(bytes)?; //! # Ok(()) //! # } //! ``` @@ -55,11 +69,15 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::PublicKey; +use core::convert::TryFrom; use core::num::NonZeroU64; +use core::str::FromStr; use core::time::Duration; use crate::io; use crate::ln::features::OfferFeatures; use crate::ln::msgs::MAX_VALUE_MSAT; +use crate::offers::invoice_request::InvoiceRequestBuilder; +use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError}; use crate::onion_message::BlindedPath; use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; @@ -177,14 +195,14 @@ impl OfferBuilder { } /// Builds an [`Offer`] from the builder's settings. - pub fn build(mut self) -> Result { + pub fn build(mut self) -> Result { match self.offer.amount { Some(Amount::Bitcoin { amount_msats }) => { if amount_msats > MAX_VALUE_MSAT { - return Err(()); + return Err(SemanticError::InvalidAmount); } }, - Some(Amount::Currency { .. }) => unreachable!(), + Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency), None => {}, } @@ -206,7 +224,7 @@ impl OfferBuilder { /// 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 +/// 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`]. /// @@ -214,15 +232,19 @@ impl OfferBuilder { /// latter. /// /// Through the use of [`BlindedPath`]s, offers provide recipient privacy. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown // fields. - bytes: Vec, - contents: OfferContents, + pub(super) bytes: Vec, + pub(super) contents: OfferContents, } -/// The contents of an [`Offer`], which may be shared with an `InvoiceRequest` or an `Invoice`. +/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] pub(crate) struct OfferContents { chains: Option>, @@ -245,10 +267,12 @@ impl Offer { /// 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()]) + self.contents.chains() + } + + /// Returns whether the given chain is supported by the offer. + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.contents.supports_chain(chain) } // TODO: Link to corresponding method in `InvoiceRequest`. @@ -260,7 +284,17 @@ impl Offer { /// The minimum amount required for a successful payment of a single item. pub fn amount(&self) -> Option<&Amount> { - self.contents.amount.as_ref() + self.contents.amount() + } + + /// The minimum amount in msats required for a successful payment. + pub fn amount_msats(&self) -> u64 { + self.contents.amount_msats() + } + + /// Returns the minimum amount in msats required for the given quantity. + pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 { + self.contents.expected_invoice_amount_msats(quantity) } /// A complete description of the purpose of the payment. Intended to be displayed to the user @@ -310,9 +344,26 @@ impl Offer { self.contents.supported_quantity() } + /// Returns whether the given quantity is valid for the offer. + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + self.contents.is_valid_quantity(quantity) + } + + /// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub fn expects_quantity(&self) -> bool { + self.contents.expects_quantity() + } + /// The public key used by the recipient to sign invoices. pub fn signing_pubkey(&self) -> PublicKey { - self.contents.signing_pubkey.unwrap() + self.contents.signing_pubkey() + } + + /// + pub fn request_invoice(&self, payer_id: PublicKey) -> InvoiceRequestBuilder { + InvoiceRequestBuilder::new(self, payer_id) } #[cfg(test)] @@ -321,16 +372,68 @@ impl Offer { } } +impl AsRef<[u8]> for Offer { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + impl OfferContents { + pub fn chains(&self) -> Vec { + self.chains.as_ref().cloned().unwrap_or_else(|| vec![self.implied_chain()]) + } + pub fn implied_chain(&self) -> ChainHash { ChainHash::using_genesis_block(Network::Bitcoin) } + pub fn supports_chain(&self, chain: ChainHash) -> bool { + self.chains().contains(&chain) + } + + pub fn amount(&self) -> Option<&Amount> { + self.amount.as_ref() + } + + pub fn amount_msats(&self) -> u64 { + match self.amount() { + None => 0, + Some(&Amount::Bitcoin { amount_msats }) => amount_msats, + Some(&Amount::Currency { .. }) => unreachable!(), + } + } + + pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 { + self.amount_msats() * quantity + } + pub fn supported_quantity(&self) -> Quantity { self.supported_quantity } - fn as_tlv_stream(&self) -> OfferTlvStreamRef { + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + match self.supported_quantity { + Quantity::Bounded(n) => { + let n = n.get(); + if n == 1 { false } + else { quantity > 0 && quantity <= n } + }, + Quantity::Unbounded => quantity > 0, + } + } + + pub fn expects_quantity(&self) -> bool { + match self.supported_quantity { + Quantity::Bounded(n) => n.get() != 1, + Quantity::Unbounded => true, + } + } + + pub fn signing_pubkey(&self) -> PublicKey { + self.signing_pubkey.unwrap() + } + + pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { let (currency, amount) = match &self.amount { None => (None, None), Some(Amount::Bitcoin { amount_msats }) => (None, Some(*amount_msats)), @@ -359,12 +462,27 @@ impl OfferContents { } } +impl Writeable for Offer { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + impl Writeable for OfferContents { fn write(&self, writer: &mut W) -> Result<(), io::Error> { self.as_tlv_stream().write(writer) } } +impl TryFrom> for Offer { + type Error = ParseError; + + fn try_from(bytes: Vec) -> Result { + let parsed_message = ParsedMessage::::try_from(bytes)?; + Offer::try_from(parsed_message) + } +} + /// The minimum amount required for an item in an [`Offer`], denominated in either bitcoin or /// another currency. #[derive(Clone, Debug, PartialEq)] @@ -400,6 +518,15 @@ impl Quantity { Quantity::Bounded(NonZeroU64::new(1).unwrap()) } + fn new(quantity: Option) -> Self { + match quantity { + None => Quantity::one(), + Some(0) => Quantity::Unbounded, + Some(1) => unreachable!(), + Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), + } + } + fn to_tlv_record(&self) -> Option { match self { Quantity::Bounded(n) => { @@ -411,7 +538,7 @@ impl Quantity { } } -tlv_stream!(OfferTlvStream, OfferTlvStreamRef, { +tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, { (2, chains: (Vec, WithoutLength)), (4, metadata: (Vec, WithoutLength)), (6, currency: CurrencyCode), @@ -425,19 +552,147 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, { (22, node_id: PublicKey), }); +impl Bech32Encode for Offer { + const BECH32_HRP: &'static str = "lno"; +} + +impl FromStr for Offer { + type Err = ParseError; + + fn from_str(s: &str) -> Result::Err> { + Self::from_bech32_str(s) + } +} + +impl TryFrom> for Offer { + type Error = ParseError; + + fn try_from(offer: ParsedMessage) -> Result { + let ParsedMessage { bytes, tlv_stream } = offer; + let contents = OfferContents::try_from(tlv_stream)?; + Ok(Offer { bytes, contents }) + } +} + +impl TryFrom for OfferContents { + type Error = SemanticError; + + fn try_from(tlv_stream: OfferTlvStream) -> Result { + let OfferTlvStream { + chains, metadata, currency, amount, description, features, absolute_expiry, paths, + issuer, quantity_max, node_id, + } = tlv_stream; + + let amount = match (currency, amount) { + (None, None) => None, + (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }), + (Some(_), None) => return Err(SemanticError::MissingAmount), + (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }), + }; + + let description = match description { + None => return Err(SemanticError::MissingDescription), + Some(description) => description, + }; + + let features = features.unwrap_or_else(OfferFeatures::empty); + + let absolute_expiry = absolute_expiry + .map(|seconds_from_epoch| Duration::from_secs(seconds_from_epoch)); + + let paths = match paths { + Some(paths) if paths.is_empty() => return Err(SemanticError::MissingPaths), + paths => paths, + }; + + let supported_quantity = match quantity_max { + Some(1) => return Err(SemanticError::InvalidQuantity), + _ => Quantity::new(quantity_max), + }; + + if node_id.is_none() { + return Err(SemanticError::MissingNodeId); + } + + Ok(OfferContents { + chains, metadata, amount, description, features, absolute_expiry, issuer, paths, + supported_quantity, signing_pubkey: node_id, + }) + } +} + +/// [`OfferContents`] used with a "send invoice" offer (i.e., a published [`InvoiceRequest`]). +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +pub(super) struct SendInvoiceOfferContents(pub OfferContents); + +impl TryFrom for SendInvoiceOfferContents { + type Error = SemanticError; + + fn try_from(tlv_stream: OfferTlvStream) -> Result { + let OfferTlvStream { + chains, metadata, currency, amount, description, features, absolute_expiry, paths, + issuer, quantity_max, node_id, + } = tlv_stream; + assert!(node_id.is_none()); + + if chains.is_some() { + return Err(SemanticError::UnexpectedChain); + } + + if currency.is_some() || amount.is_some() { + return Err(SemanticError::UnexpectedAmount); + } + + let description = match description { + None => return Err(SemanticError::MissingDescription), + Some(description) => description, + }; + + let features = match features { + None => OfferFeatures::empty(), + Some(_) => return Err(SemanticError::UnexpectedFeatures), + }; + + let absolute_expiry = absolute_expiry.map(Duration::from_secs); + + let paths = match paths { + Some(paths) if paths.is_empty() => return Err(SemanticError::MissingPaths), + paths => paths, + }; + + if quantity_max.is_some() { + return Err(SemanticError::UnexpectedQuantity); + } + + Ok(SendInvoiceOfferContents(OfferContents { + chains: None, metadata, amount: None, description, features, absolute_expiry, issuer, + paths, supported_quantity: Quantity::one(), signing_pubkey: None, + })) + } +} + +impl core::fmt::Display for Offer { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + self.fmt_bech32_str(f) + } +} + #[cfg(test)] mod tests { - use super::{Amount, OfferBuilder, Quantity}; + use super::{Amount, Offer, OfferBuilder, Quantity}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::convert::TryFrom; use core::num::NonZeroU64; use core::time::Duration; use crate::ln::features::OfferFeatures; - use crate::ln::msgs::MAX_VALUE_MSAT; + use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; + use crate::offers::parse::{ParseError, SemanticError}; use crate::onion_message::{BlindedHop, BlindedPath}; - use crate::util::ser::Writeable; + use crate::util::ser::{BigSize, Writeable}; use crate::util::string::PrintableString; fn pubkey(byte: u8) -> PublicKey { @@ -454,10 +709,11 @@ mod tests { 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(); + offer.write(&mut buffer).unwrap(); assert_eq!(offer.bytes, buffer.as_slice()); assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]); + assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin))); assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); assert_eq!(offer.description(), PrintableString("foo")); @@ -481,6 +737,10 @@ mod tests { assert_eq!(tlv_stream.issuer, None); assert_eq!(tlv_stream.quantity_max, None); assert_eq!(tlv_stream.node_id, Some(&pubkey(42))); + + if let Err(e) = Offer::try_from(buffer) { + panic!("error parsing offer: {:?}", e); + } } #[test] @@ -492,6 +752,7 @@ mod tests { .chain(Network::Bitcoin) .build() .unwrap(); + assert!(offer.supports_chain(mainnet)); assert_eq!(offer.chains(), vec![mainnet]); assert_eq!(offer.as_tlv_stream().chains, None); @@ -499,6 +760,7 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); @@ -507,6 +769,7 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); @@ -515,6 +778,8 @@ mod tests { .chain(Network::Testnet) .build() .unwrap(); + assert!(offer.supports_chain(mainnet)); + assert!(offer.supports_chain(testnet)); assert_eq!(offer.chains(), vec![mainnet, testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet])); } @@ -557,6 +822,10 @@ mod tests { assert_eq!(builder.offer.amount, Some(currency_amount.clone())); assert_eq!(tlv_stream.amount, Some(10)); assert_eq!(tlv_stream.currency, Some(b"USD")); + match builder.build() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, SemanticError::UnsupportedCurrency), + } let offer = OfferBuilder::new("foo".into(), pubkey(42)) .amount(currency_amount.clone()) @@ -570,7 +839,7 @@ mod tests { 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, ()), + Err(e) => assert_eq!(e, SemanticError::InvalidAmount), } } @@ -706,4 +975,264 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::one()); assert_eq!(tlv_stream.quantity_max, None); } + + #[test] + fn parses_offer_with_chains() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .chain(Network::Bitcoin) + .chain(Network::Testnet) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + } + + #[test] + fn parses_offer_with_amount() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .amount(Amount::Bitcoin { amount_msats: 1000 }) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.amount = Some(1000); + tlv_stream.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + if let Err(e) = Offer::try_from(encoded_offer) { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.amount = None; + tlv_stream.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)), + } + } + + #[test] + fn parses_offer_with_description() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.description = None; + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingDescription)); + }, + } + } + + #[test] + fn parses_offer_with_paths() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .path(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] }, + ], + }) + .path(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] }, + ], + }) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + builder.offer.paths = Some(vec![]); + + let offer = builder.build().unwrap(); + match offer.to_string().parse::() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingPaths)), + } + } + + #[test] + fn parses_offer_with_quantity() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::one()) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Unbounded) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let offer = OfferBuilder::new("foo".into(), pubkey(42)) + .supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap())) + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.quantity_max = Some(1); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidQuantity)); + }, + } + } + + #[test] + fn parses_offer_with_node_id() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + builder.offer.signing_pubkey = None; + + let offer = builder.build().unwrap(); + match offer.to_string().parse::() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingNodeId)), + } + } + + #[test] + fn fails_parsing_offer_with_extra_tlv_records() { + let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + + let mut encoded_offer = Vec::new(); + offer.write(&mut encoded_offer).unwrap(); + BigSize(80).write(&mut encoded_offer).unwrap(); + BigSize(32).write(&mut encoded_offer).unwrap(); + [42u8; 32].write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } +} + +#[cfg(test)] +mod bolt12_tests { + use super::{Offer, ParseError}; + use bitcoin::bech32; + use crate::ln::msgs::DecodeError; + + // TODO: Remove once test vectors are updated. + #[ignore] + #[test] + fn encodes_offer_as_bech32_without_checksum() { + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy"; + let offer = dbg!(encoded_offer.parse::().unwrap()); + let reencoded_offer = offer.to_string(); + dbg!(reencoded_offer.parse::().unwrap()); + assert_eq!(reencoded_offer, encoded_offer); + } + + // TODO: Remove once test vectors are updated. + #[ignore] + #[test] + fn parses_bech32_encoded_offers() { + let offers = [ + // BOLT 12 test vectors + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "l+no1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "lno1qcp4256ypqpq+86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qs+y", + "lno1qcp4256ypqpq+ 86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn0+ 0fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0+\nsqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43l+\r\nastpwuh73k29qs+\r y", + // Two blinded paths + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0yg06qg2qdd7t628sgykwj5kuc837qmlv9m9gr7sq8ap6erfgacv26nhp8zzcqgzhdvttlk22pw8fmwqqrvzst792mj35ypylj886ljkcmug03wg6heqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6muh550qsfva9fdes0ruph7ctk2s8aqq06r4jxj3msc448wzwy9sqs9w6ckhlv55zuwnkuqqxc9qhu24h9rggzflyw04l9d3hcslzu340jqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + ]; + for encoded_offer in &offers { + if let Err(e) = encoded_offer.parse::() { + panic!("Invalid offer ({:?}): {}", e, encoded_offer); + } + } + } + + #[test] + fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() { + let offers = [ + // BOLT 12 test vectors + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+", + "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy+ ", + "+lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "+ lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + "ln++o1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy", + ]; + for encoded_offer in &offers { + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::InvalidContinuation), + } + } + + } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() { + let encoded_offer = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::InvalidBech32Hrp), + } + } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() { + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::Bech32(bech32::Error::InvalidChar('o'))), + } + } + + #[test] + fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() { + let encoded_offer = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq"; + match encoded_offer.parse::() { + Ok(_) => panic!("Valid offer: {}", encoded_offer), + Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)), + } + } } diff --git a/lightning/src/offers/parse.rs b/lightning/src/offers/parse.rs new file mode 100644 index 00000000000..73192a81435 --- /dev/null +++ b/lightning/src/offers/parse.rs @@ -0,0 +1,165 @@ +// 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. + +//! Parsing and formatting for bech32 message encoding. + +use bitcoin::bech32; +use bitcoin::bech32::{FromBase32, ToBase32}; +use bitcoin::secp256k1; +use core::convert::TryFrom; +use core::fmt; +use crate::io; +use crate::ln::msgs::DecodeError; +use crate::util::ser::SeekReadable; + +use crate::prelude::*; + +/// Indicates a message can be encoded using bech32. +pub(crate) trait Bech32Encode: AsRef<[u8]> + TryFrom, Error=ParseError> { + /// Human readable part of the message's bech32 encoding. + const BECH32_HRP: &'static str; + + /// Parses a bech32-encoded message into a TLV stream. + fn from_bech32_str(s: &str) -> Result { + // Offer encoding may be split by '+' followed by optional whitespace. + for chunk in s.split('+') { + let chunk = chunk.trim_start(); + if chunk.is_empty() || chunk.contains(char::is_whitespace) { + return Err(ParseError::InvalidContinuation); + } + } + + let s = s.chars().filter(|c| *c != '+' && !c.is_whitespace()).collect::(); + let (hrp, data) = bech32::decode_without_checksum(&s)?; + + if hrp != Self::BECH32_HRP { + return Err(ParseError::InvalidBech32Hrp); + } + + let data = Vec::::from_base32(&data)?; + Self::try_from(data) + } + + /// Formats the message using bech32-encoding. + fn fmt_bech32_str(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + bech32::encode_without_checksum_to_fmt(f, Self::BECH32_HRP, self.as_ref().to_base32()) + .expect("HRP is valid").unwrap(); + + Ok(()) + } +} + +/// A wrapper for reading a message as a TLV stream `T` from a byte sequence, while still +/// maintaining ownership of the bytes for later use. +pub(crate) struct ParsedMessage { + pub bytes: Vec, + pub tlv_stream: T, +} + +impl TryFrom> for ParsedMessage { + type Error = DecodeError; + + fn try_from(bytes: Vec) -> Result { + let mut cursor = crate::io::Cursor::new(bytes); + let tlv_stream: T = SeekReadable::read(&mut cursor)?; + + if cursor.position() < cursor.get_ref().len() as u64 { + return Err(DecodeError::InvalidValue); + } + + let bytes = cursor.into_inner(); + Ok(Self { bytes, tlv_stream }) + } +} + +/// Error when parsing a bech32 encoded message using [`str::parse`]. +#[derive(Debug, PartialEq)] +pub enum ParseError { + /// The bech32 encoding does not conform to the BOLT 12 requirements for continuing messages + /// across multiple parts (i.e., '+' followed by whitespace). + InvalidContinuation, + /// The bech32 encoding's human-readable part does not match what was expected for the message + /// being parsed. + InvalidBech32Hrp, + /// The string could not be bech32 decoded. + Bech32(bech32::Error), + /// The bech32 decoded string could not be decoded as the expected message type. + Decode(DecodeError), + /// The parsed message has invalid semantics. + InvalidSemantics(SemanticError), + /// The parsed message has an invalid signature. + InvalidSignature(secp256k1::Error), +} + +/// Error when interpreting a TLV stream as a specific type. +#[derive(Debug, PartialEq)] +pub enum SemanticError { + /// The provided chain hash does not correspond to a supported chain. + UnsupportedChain, + /// A chain was provided but was not expected. + UnexpectedChain, + /// An amount was not provided. + MissingAmount, + /// An amount exceeded the maximum number of bitcoin. + InvalidAmount, + /// An amount was provided but was not sufficient in value. + InsufficientAmount, + /// An amount was provided but was not expected. + UnexpectedAmount, + /// A currency was provided that is not supported. + UnsupportedCurrency, + /// A feature was required but is unknown. + UnknownRequiredFeatures, + /// Features were provided but were not expected. + UnexpectedFeatures, + /// A required description was not provided. + MissingDescription, + /// A node id was not provided. + MissingNodeId, + /// An empty set of blinded paths was provided. + MissingPaths, + /// A quantity was not provided. + MissingQuantity, + /// An unsupported quantity was provided. + InvalidQuantity, + /// A quantity or quantity bounds was provided but was not expected. + UnexpectedQuantity, + /// A payer id was expected but was missing. + MissingPayerId, + /// + InvalidPayInfo, + /// + MissingCreationTime, + /// + MissingPaymentHash, +} + +impl From for ParseError { + fn from(error: bech32::Error) -> Self { + Self::Bech32(error) + } +} + +impl From for ParseError { + fn from(error: DecodeError) -> Self { + Self::Decode(error) + } +} + +impl From for ParseError { + fn from(error: SemanticError) -> Self { + Self::InvalidSemantics(error) + } +} + +impl From for ParseError { + fn from(error: secp256k1::Error) -> Self { + Self::InvalidSignature(error) + } +} diff --git a/lightning/src/offers/payer.rs b/lightning/src/offers/payer.rs new file mode 100644 index 00000000000..a8ff43bb6e2 --- /dev/null +++ b/lightning/src/offers/payer.rs @@ -0,0 +1,25 @@ +// 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 `invoice_request_metadata` records. + +use crate::util::ser::WithoutLength; + +use crate::prelude::*; + +/// An unpredictable sequence of bytes typically containing information needed to derive +/// [`InvoiceRequestContents::payer_id`]. +/// +/// [`InvoiceRequestContents::payer_id`]: invoice_request::InvoiceRequestContents::payer_id +#[derive(Clone, Debug)] +pub(crate) struct PayerContents(pub Option>); + +tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, { + (0, metadata: (Vec, WithoutLength)), +}); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 7c4ba7fd50f..f0f00ca1165 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -11,7 +11,7 @@ //! as ChannelsManagers and ChannelMonitors. use crate::prelude::*; -use crate::io::{self, Read, Write}; +use crate::io::{self, Read, Seek, Write}; use crate::io_extras::{copy, sink}; use core::hash::Hash; use crate::sync::Mutex; @@ -20,8 +20,9 @@ use core::convert::TryFrom; 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::secp256k1::constants::{PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, COMPACT_SIGNATURE_SIZE, SCHNORR_SIGNATURE_SIZE}; +use bitcoin::secp256k1::ecdsa; +use bitcoin::secp256k1::schnorr; use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut}; @@ -29,6 +30,7 @@ use bitcoin::consensus; use bitcoin::consensus::Encodable; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hash_types::{Txid, BlockHash}; +use bitcoin::util::address::WitnessVersion; use core::marker::Sized; use core::time::Duration; use crate::ln::msgs::DecodeError; @@ -219,6 +221,13 @@ pub trait Readable fn read(reader: &mut R) -> Result; } +/// A trait that various rust-lightning types implement allowing them to be read in from a +/// `Read + Seek`. +pub(crate) trait SeekReadable where Self: Sized { + /// Reads a Self in from the given Read + fn read(reader: &mut R) -> Result; +} + /// A trait that various higher-level rust-lightning types implement allowing them to be read in /// from a Read given some additional set of arguments which is required to deserialize. /// @@ -491,7 +500,7 @@ impl_array!(12); // for OnionV2 impl_array!(16); // for IPv6 impl_array!(32); // for channel id & hmac impl_array!(PUBLIC_KEY_SIZE); // for PublicKey -impl_array!(COMPACT_SIGNATURE_SIZE); // for Signature +impl_array!(64); // for ecdsa::Signature and schnorr::Signature impl_array!(1300); // for OnionPacket.hop_data impl Writeable for [u16; 8] { @@ -656,7 +665,7 @@ impl Readable for Vec { Ok(ret) } } -impl Writeable for Vec { +impl Writeable for Vec { #[inline] fn write(&self, w: &mut W) -> Result<(), io::Error> { (self.len() as u16).write(w)?; @@ -667,7 +676,7 @@ impl Writeable for Vec { } } -impl Readable for Vec { +impl Readable for Vec { #[inline] fn read(r: &mut R) -> Result { let len: u16 = Readable::read(r)?; @@ -756,7 +765,7 @@ impl Readable for Sha256dHash { } } -impl Writeable for Signature { +impl Writeable for ecdsa::Signature { fn write(&self, w: &mut W) -> Result<(), io::Error> { self.serialize_compact().write(w) } @@ -766,10 +775,30 @@ impl Writeable for Signature { } } -impl Readable for Signature { +impl Readable for ecdsa::Signature { fn read(r: &mut R) -> Result { let buf: [u8; COMPACT_SIGNATURE_SIZE] = Readable::read(r)?; - match Signature::from_compact(&buf) { + match ecdsa::Signature::from_compact(&buf) { + Ok(sig) => Ok(sig), + Err(_) => return Err(DecodeError::InvalidValue), + } + } +} + +impl Writeable for schnorr::Signature { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.as_ref().write(w) + } + #[inline] + fn serialized_length(&self) -> usize { + SCHNORR_SIGNATURE_SIZE + } +} + +impl Readable for schnorr::Signature { + fn read(r: &mut R) -> Result { + let buf: [u8; SCHNORR_SIGNATURE_SIZE] = Readable::read(r)?; + match schnorr::Signature::from_slice(&buf) { Ok(sig) => Ok(sig), Err(_) => return Err(DecodeError::InvalidValue), } @@ -942,6 +971,22 @@ macro_rules! impl_consensus_ser { impl_consensus_ser!(Transaction); impl_consensus_ser!(TxOut); +impl Writeable for WitnessVersion { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_num().write(w) + } +} + +impl Readable for WitnessVersion { + fn read(r: &mut R) -> Result { + let num: u8 = Readable::read(r)?; + match WitnessVersion::try_from(num) { + Ok(version) => Ok(version), + Err(_) => Err(DecodeError::InvalidValue), + } + } +} + impl Readable for Mutex { fn read(r: &mut R) -> Result { let t: T = Readable::read(r)?; @@ -984,6 +1029,44 @@ impl Writeable for (A, B, C) { } } +impl Readable for (A, B, C, D) { + fn read(r: &mut R) -> Result { + let a: A = Readable::read(r)?; + let b: B = Readable::read(r)?; + let c: C = Readable::read(r)?; + let d: D = Readable::read(r)?; + Ok((a, b, c, d)) + } +} +impl Writeable for (A, B, C, D) { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w)?; + self.1.write(w)?; + self.2.write(w)?; + self.3.write(w) + } +} + +impl Readable for (A, B, C, D, E) { + fn read(r: &mut R) -> Result { + let a: A = Readable::read(r)?; + let b: B = Readable::read(r)?; + let c: C = Readable::read(r)?; + let d: D = Readable::read(r)?; + let e: E = Readable::read(r)?; + Ok((a, b, c, d, e)) + } +} +impl Writeable for (A, B, C, D, E) { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w)?; + self.1.write(w)?; + self.2.write(w)?; + self.3.write(w)?; + self.4.write(w) + } +} + impl Writeable for () { fn write(&self, _: &mut W) -> Result<(), io::Error> { Ok(()) diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index 3ec0848680f..31978d5e4c7 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -201,6 +201,17 @@ macro_rules! decode_tlv { // `Ok(false)` if the message type is unknown, and `Err(DecodeError)` if parsing fails. macro_rules! decode_tlv_stream { ($stream: expr, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} + $(, $decode_custom_tlv: expr)?) => { { + let rewind = |_, _| { unreachable!() }; + use core::ops::RangeBounds; + decode_tlv_stream_range!( + $stream, .., rewind, {$(($type, $field, $fieldty)),*} $(, $decode_custom_tlv)? + ); + } } +} + +macro_rules! decode_tlv_stream_range { + ($stream: expr, $range: expr, $rewind: ident, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} $(, $decode_custom_tlv: expr)?) => { { use $crate::ln::msgs::DecodeError; let mut last_seen_type: Option = None; @@ -215,7 +226,7 @@ macro_rules! decode_tlv_stream { // UnexpectedEof. This should in every case be largely cosmetic, but its nice to // pass the TLV test vectors exactly, which requre this distinction. let mut tracking_reader = ser::ReadTrackingReader::new(&mut stream_ref); - match $crate::util::ser::Readable::read(&mut tracking_reader) { + match <$crate::util::ser::BigSize as $crate::util::ser::Readable>::read(&mut tracking_reader) { Err(DecodeError::ShortRead) => { if !tracking_reader.have_read { break 'tlv_read; @@ -224,7 +235,13 @@ macro_rules! decode_tlv_stream { } }, Err(e) => return Err(e), - Ok(t) => t, + Ok(t) => if $range.contains(&t.0) { t } else { + use $crate::util::ser::Writeable; + drop(tracking_reader); + let bytes_read = t.serialized_length(); + $rewind(stream_ref, bytes_read); + break 'tlv_read; + }, } }; @@ -472,13 +489,13 @@ macro_rules! impl_writeable_tlv_based { /// [`Readable`]: crate::util::ser::Readable /// [`Writeable`]: crate::util::ser::Writeable macro_rules! tlv_stream { - ($name:ident, $nameref:ident, { + ($name:ident, $nameref:ident, $range:expr, { $(($type:expr, $field:ident : $fieldty:tt)),* $(,)* }) => { #[derive(Debug)] - struct $name { + pub(crate) struct $name { $( - $field: Option, + pub(crate) $field: Option, )* } @@ -497,12 +514,15 @@ macro_rules! tlv_stream { } } - impl $crate::util::ser::Readable for $name { - fn read(reader: &mut R) -> Result { + impl $crate::util::ser::SeekReadable for $name { + fn read(reader: &mut R) -> Result { $( init_tlv_field_var!($field, option); )* - decode_tlv_stream!(reader, { + let rewind = |cursor: &mut R, offset: usize| { + cursor.seek($crate::io::SeekFrom::Current(-(offset as i64))).expect(""); + }; + decode_tlv_stream_range!(reader, $range, rewind, { $(($type, $field, (option, encoding: $fieldty))),* });