Invoice request raw byte encoding and decoding
authorJeffrey Czyz <jkczyz@gmail.com>
Tue, 23 Aug 2022 22:31:46 +0000 (17:31 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 9 Dec 2022 14:53:46 +0000 (08:53 -0600)
When reading an offer, an `invoice_request` message is sent over the
wire. Implement Writeable for encoding the message and TryFrom for
decoding it by defining in terms of TLV streams. These streams represent
content for the payer metadata (0), reflected `offer` (1-79),
`invoice_request` (80-159), and signature (240).

lightning/src/offers/invoice_request.rs
lightning/src/offers/merkle.rs
lightning/src/offers/offer.rs
lightning/src/offers/parse.rs
lightning/src/offers/payer.rs
lightning/src/util/ser_macros.rs

index 43ae18d2c00d4c92da0318434fb907247e68eea7..6cedf3eda59d68b2d18ac8aaa3bde05aabfb10b6 100644 (file)
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::secp256k1::PublicKey;
 use bitcoin::secp256k1::schnorr::Signature;
+use core::convert::TryFrom;
+use crate::io;
 use crate::ln::features::InvoiceRequestFeatures;
-use crate::offers::offer::OfferContents;
-use crate::offers::payer::PayerContents;
+use crate::ln::msgs::DecodeError;
+use crate::offers::merkle::{SignatureTlvStream, self};
+use crate::offers::offer::{Amount, OfferContents, OfferTlvStream};
+use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
+use crate::offers::payer::{PayerContents, PayerTlvStream};
+use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
 
 use crate::prelude::*;
@@ -34,7 +40,7 @@ pub struct InvoiceRequest {
 
 /// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`.
 #[derive(Clone, Debug)]
-pub(crate) struct InvoiceRequestContents {
+pub(super) struct InvoiceRequestContents {
        payer: PayerContents,
        offer: OfferContents,
        chain: Option<ChainHash>,
@@ -75,9 +81,9 @@ impl InvoiceRequest {
                &self.contents.features
        }
 
-       /// The quantity of the offer's item conforming to [`Offer::supported_quantity`].
+       /// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`].
        ///
-       /// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity
+       /// [`Offer::is_valid_quantity`]: crate::offers::offer::Offer::is_valid_quantity
        pub fn quantity(&self) -> Option<u64> {
                self.contents.quantity
        }
@@ -99,3 +105,114 @@ impl InvoiceRequest {
                self.signature
        }
 }
+
+impl Writeable for InvoiceRequest {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               WithoutLength(&self.bytes).write(writer)
+       }
+}
+
+tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
+       (80, chain: ChainHash),
+       (82, amount: (u64, HighZeroBytesDroppedBigSize)),
+       (84, features: InvoiceRequestFeatures),
+       (86, quantity: (u64, HighZeroBytesDroppedBigSize)),
+       (88, payer_id: PublicKey),
+       (89, payer_note: (String, WithoutLength)),
+});
+
+type FullInvoiceRequestTlvStream =
+       (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream);
+
+impl SeekReadable for FullInvoiceRequestTlvStream {
+       fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
+               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);
+
+impl TryFrom<Vec<u8>> for InvoiceRequest {
+       type Error = ParseError;
+
+       fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+               let invoice_request = ParsedMessage::<FullInvoiceRequestTlvStream>::try_from(bytes)?;
+               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 {
+                       let tag = concat!("lightning", "invoice_request", "signature");
+                       merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?;
+               }
+
+               Ok(InvoiceRequest { bytes, contents, signature })
+       }
+}
+
+impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
+       type Error = SemanticError;
+
+       fn try_from(tlv_stream: PartialInvoiceRequestTlvStream) -> Result<Self, Self::Error> {
+               let (
+                       PayerTlvStream { metadata },
+                       offer_tlv_stream,
+                       InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note },
+               ) = tlv_stream;
+
+               let payer = match metadata {
+                       None => return Err(SemanticError::MissingPayerMetadata),
+                       Some(metadata) => PayerContents(metadata),
+               };
+               let offer = OfferContents::try_from(offer_tlv_stream)?;
+
+               if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) {
+                       return Err(SemanticError::UnsupportedChain);
+               }
+
+               let amount_msats = match (offer.amount(), amount) {
+                       (None, None) => return Err(SemanticError::MissingAmount),
+                       (Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency),
+                       (_, amount_msats) => amount_msats,
+               };
+
+               let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
+
+               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,
+               })
+       }
+}
index c7aafb0cfef2516fc8a4cd5965c155652d2594aa..95183bea20d2a50c29dc62f2890c0e0381cb402d 100644 (file)
@@ -10,6 +10,8 @@
 //! 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};
 
@@ -18,6 +20,25 @@ use crate::prelude::*;
 /// Valid type range for signature TLV records.
 const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
 
+tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, {
+       (240, signature: Signature),
+});
+
+/// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message
+/// digest.
+///
+/// Panics if `bytes` is not a well-formed TLV stream containing at least one TLV record.
+pub(super) fn verify_signature(
+       signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey,
+) -> Result<(), secp256k1::Error> {
+       let tag = sha256::Hash::hash(tag.as_bytes());
+       let merkle_root = root_hash(bytes);
+       let digest = Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap();
+       let pubkey = pubkey.into();
+       let secp_ctx = Secp256k1::verification_only();
+       secp_ctx.verify_schnorr(signature, &digest, &pubkey)
+}
+
 /// 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 {
index 1ac8b0bdebf26377d4cb9440f208595b1f4d2c83..1403fbd223ec19ab8888f0e2fdaae8432dcd0cb8 100644 (file)
@@ -270,6 +270,11 @@ impl Offer {
                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`.
        /// 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`.
@@ -279,7 +284,7 @@ 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()
        }
 
        /// A complete description of the purpose of the payment. Intended to be displayed to the user
@@ -329,6 +334,18 @@ 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()
@@ -355,10 +372,48 @@ impl OfferContents {
                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
        }
 
+       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,
+               }
+       }
+
        fn as_tlv_stream(&self) -> OfferTlvStreamRef {
                let (currency, amount) = match &self.amount {
                        None => (None, None),
@@ -572,6 +627,7 @@ mod tests {
 
                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"));
@@ -610,6 +666,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);
 
@@ -617,6 +674,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]));
 
@@ -625,6 +683,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]));
 
@@ -633,6 +692,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]));
        }
index 013970981767acb6f51062f9eefd9af445f71593..b9815b8117767938bee0ba544f1dd9df73db891f 100644 (file)
@@ -11,6 +11,7 @@
 
 use bitcoin::bech32;
 use bitcoin::bech32::{FromBase32, ToBase32};
+use bitcoin::secp256k1;
 use core::convert::TryFrom;
 use core::fmt;
 use crate::io;
@@ -115,23 +116,37 @@ pub enum ParseError {
        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,
        /// An amount was expected but was missing.
        MissingAmount,
        /// The amount exceeded the total bitcoin supply.
        InvalidAmount,
+       /// An amount was provided but was not sufficient in value.
+       InsufficientAmount,
        /// A currency was provided that is not supported.
        UnsupportedCurrency,
        /// A required description was not provided.
        MissingDescription,
        /// A signing pubkey was not provided.
        MissingSigningPubkey,
+       /// A quantity was expected but was missing.
+       MissingQuantity,
        /// An unsupported quantity was provided.
        InvalidQuantity,
+       /// A quantity or quantity bounds was provided but was not expected.
+       UnexpectedQuantity,
+       /// Payer metadata was expected but was missing.
+       MissingPayerMetadata,
+       /// A payer id was expected but was missing.
+       MissingPayerId,
 }
 
 impl From<bech32::Error> for ParseError {
@@ -151,3 +166,9 @@ impl From<SemanticError> for ParseError {
                Self::InvalidSemantics(error)
        }
 }
+
+impl From<secp256k1::Error> for ParseError {
+       fn from(error: secp256k1::Error) -> Self {
+               Self::InvalidSignature(error)
+       }
+}
index 1705be85ed8f53bb316154f81edacbd9f2f391ab..e389a8f6d5d5daf36dd8e8c3b259c59438472b95 100644 (file)
@@ -9,6 +9,8 @@
 
 //! 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
@@ -16,4 +18,8 @@ use crate::prelude::*;
 ///
 /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
 #[derive(Clone, Debug)]
-pub(crate) struct PayerContents(pub Vec<u8>);
+pub(super) struct PayerContents(pub Vec<u8>);
+
+tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
+       (0, metadata: (Vec<u8>, WithoutLength)),
+});
index 6cc9d947753440bf61a55aa0e013d7213086cc6d..231320ac1591dd392e8e415073c10b9a30bd2bc6 100644 (file)
@@ -506,7 +506,7 @@ macro_rules! tlv_stream {
                #[derive(Debug)]
                pub(super) struct $name {
                        $(
-                               $field: Option<tlv_record_type!($fieldty)>,
+                               pub(super) $field: Option<tlv_record_type!($fieldty)>,
                        )*
                }