Unsigned BOLT 12 message parsing and serialization
authorJeffrey Czyz <jkczyz@gmail.com>
Sat, 12 Aug 2023 03:13:36 +0000 (22:13 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Tue, 22 Aug 2023 00:14:28 +0000 (19:14 -0500)
lightning/src/offers/invoice.rs
lightning/src/offers/invoice_request.rs
lightning/src/offers/merkle.rs

index bb62e71c5aab67a26e3e2ebf66b83b3c63c65e8b..4bc2ac6e7070512ce60a16461261b24b6bb48dd9 100644 (file)
@@ -368,6 +368,11 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
 }
 
 /// A semantically valid [`Bolt12Invoice`] that hasn't been signed.
+///
+/// # Serialization
+///
+/// This is serialized as a TLV stream, which includes TLV records from the originating message. As
+/// such, it may include unknown, odd TLV records.
 pub struct UnsignedBolt12Invoice {
        bytes: Vec<u8>,
        contents: InvoiceContents,
@@ -396,7 +401,9 @@ impl UnsignedBolt12Invoice {
                self.contents.fields().signing_pubkey
        }
 
-       /// Signs the invoice using the given function.
+       /// Signs the [`TaggedHash`] of the invoice using the given function.
+       ///
+       /// Note: The hash computation may have included unknown, odd TLV records.
        ///
        /// This is not exported to bindings users as functions aren't currently mapped.
        pub fn sign<F, E>(mut self, sign: F) -> Result<Bolt12Invoice, SignError<E>>
@@ -733,6 +740,12 @@ impl InvoiceFields {
        }
 }
 
+impl Writeable for UnsignedBolt12Invoice {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               WithoutLength(&self.bytes).write(writer)
+       }
+}
+
 impl Writeable for Bolt12Invoice {
        fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
                WithoutLength(&self.bytes).write(writer)
@@ -745,6 +758,25 @@ impl Writeable for InvoiceContents {
        }
 }
 
+impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
+       type Error = Bolt12ParseError;
+
+       fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+               let invoice = ParsedMessage::<PartialInvoiceTlvStream>::try_from(bytes)?;
+               let ParsedMessage { bytes, tlv_stream } = invoice;
+               let (
+                       payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
+               ) = tlv_stream;
+               let contents = InvoiceContents::try_from(
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
+               )?;
+
+               let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes);
+
+               Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash })
+       }
+}
+
 impl TryFrom<Vec<u8>> for Bolt12Invoice {
        type Error = Bolt12ParseError;
 
@@ -857,6 +889,17 @@ type PartialInvoiceTlvStreamRef<'a> = (
        InvoiceTlvStreamRef<'a>,
 );
 
+impl SeekReadable for PartialInvoiceTlvStream {
+       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 invoice = SeekReadable::read(r)?;
+
+               Ok((payer, offer, invoice_request, invoice))
+       }
+}
+
 impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Bolt12Invoice {
        type Error = Bolt12ParseError;
 
@@ -961,7 +1004,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
 
 #[cfg(test)]
 mod tests {
-       use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG};
+       use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice};
 
        use bitcoin::blockdata::script::Script;
        use bitcoin::hashes::Hash;
@@ -1007,15 +1050,27 @@ mod tests {
                let payment_paths = payment_paths();
                let payment_hash = payment_hash();
                let now = now();
-               let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
+               let unsigned_invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
                        .build().unwrap()
                        .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
                        .build().unwrap()
                        .sign(payer_sign).unwrap()
                        .respond_with_no_std(payment_paths.clone(), payment_hash, now).unwrap()
-                       .build().unwrap()
-                       .sign(recipient_sign).unwrap();
+                       .build().unwrap();
+
+               let mut buffer = Vec::new();
+               unsigned_invoice.write(&mut buffer).unwrap();
+
+               match UnsignedBolt12Invoice::try_from(buffer) {
+                       Err(e) => panic!("error parsing unsigned invoice: {:?}", e),
+                       Ok(parsed) => {
+                               assert_eq!(parsed.bytes, unsigned_invoice.bytes);
+                               assert_eq!(parsed.tagged_hash, unsigned_invoice.tagged_hash);
+                       },
+               }
+
+               let invoice = unsigned_invoice.sign(recipient_sign).unwrap();
 
                let mut buffer = Vec::new();
                invoice.write(&mut buffer).unwrap();
index 978ae77bfb7ae1e3e862780369b13eb857259a93..b05f9af8481bff6ed333856cc74e7c28e5f43d59 100644 (file)
@@ -344,6 +344,11 @@ impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a
 }
 
 /// A semantically valid [`InvoiceRequest`] that hasn't been signed.
+///
+/// # Serialization
+///
+/// This is serialized as a TLV stream, which includes TLV records from the originating message. As
+/// such, it may include unknown, odd TLV records.
 pub struct UnsignedInvoiceRequest {
        bytes: Vec<u8>,
        contents: InvoiceRequestContents,
@@ -367,7 +372,9 @@ impl UnsignedInvoiceRequest {
                Self { bytes, contents, tagged_hash }
        }
 
-       /// Signs the invoice request using the given function.
+       /// Signs the [`TaggedHash`] of the invoice request using the given function.
+       ///
+       /// Note: The hash computation may have included unknown, odd TLV records.
        ///
        /// This is not exported to bindings users as functions are not yet mapped.
        pub fn sign<F, E>(mut self, sign: F) -> Result<InvoiceRequest, SignError<E>>
@@ -664,6 +671,12 @@ impl InvoiceRequestContentsWithoutPayerId {
        }
 }
 
+impl Writeable for UnsignedInvoiceRequest {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               WithoutLength(&self.bytes).write(writer)
+       }
+}
+
 impl Writeable for InvoiceRequest {
        fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
                WithoutLength(&self.bytes).write(writer)
@@ -723,6 +736,25 @@ type PartialInvoiceRequestTlvStreamRef<'a> = (
        InvoiceRequestTlvStreamRef<'a>,
 );
 
+impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
+       type Error = Bolt12ParseError;
+
+       fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+               let invoice_request = ParsedMessage::<PartialInvoiceRequestTlvStream>::try_from(bytes)?;
+               let ParsedMessage { bytes, tlv_stream } = invoice_request;
+               let (
+                       payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
+               ) = tlv_stream;
+               let contents = InvoiceRequestContents::try_from(
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
+               )?;
+
+               let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes);
+
+               Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash })
+       }
+}
+
 impl TryFrom<Vec<u8>> for InvoiceRequest {
        type Error = Bolt12ParseError;
 
@@ -792,7 +824,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
 
 #[cfg(test)]
 mod tests {
-       use super::{InvoiceRequest, InvoiceRequestTlvStreamRef, SIGNATURE_TAG};
+       use super::{InvoiceRequest, InvoiceRequestTlvStreamRef, SIGNATURE_TAG, UnsignedInvoiceRequest};
 
        use bitcoin::blockdata::constants::ChainHash;
        use bitcoin::network::constants::Network;
@@ -816,12 +848,24 @@ mod tests {
 
        #[test]
        fn builds_invoice_request_with_defaults() {
-               let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
+               let unsigned_invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
                        .build().unwrap()
                        .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
-                       .build().unwrap()
-                       .sign(payer_sign).unwrap();
+                       .build().unwrap();
+
+               let mut buffer = Vec::new();
+               unsigned_invoice_request.write(&mut buffer).unwrap();
+
+               match UnsignedInvoiceRequest::try_from(buffer) {
+                       Err(e) => panic!("error parsing unsigned invoice request: {:?}", e),
+                       Ok(parsed) => {
+                               assert_eq!(parsed.bytes, unsigned_invoice_request.bytes);
+                               assert_eq!(parsed.tagged_hash, unsigned_invoice_request.tagged_hash);
+                       },
+               }
+
+               let invoice_request = unsigned_invoice_request.sign(payer_sign).unwrap();
 
                let mut buffer = Vec::new();
                invoice_request.write(&mut buffer).unwrap();
index d15039cd317d3ed8589ed6230cd3ccdd6487c7b6..b3867bf6f65a0d55d2b6969e32172f0919618249 100644 (file)
@@ -30,6 +30,7 @@ tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, {
 ///
 /// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
 /// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation
+#[derive(Debug, PartialEq)]
 pub struct TaggedHash(Message);
 
 impl TaggedHash {