Implement `Hash` for `Offer` and `Refund`
[rust-lightning] / lightning / src / offers / offer.rs
index 1044ac9606d5acbe07e736204a97632c08b6758f..d71ec2afd04e9642c11d164e4a8c0f6a02e49dda 100644 (file)
@@ -13,6 +13,8 @@
 //! published as a QR code to be scanned by a customer. The customer uses the offer to request an
 //! invoice from the merchant to be paid.
 //!
+//! # Example
+//!
 //! ```
 //! extern crate bitcoin;
 //! extern crate core;
 //! # Ok(())
 //! # }
 //! ```
+//!
+//! # Note
+//!
+//! If constructing an [`Offer`] for use with a [`ChannelManager`], use
+//! [`ChannelManager::create_offer_builder`] instead of [`OfferBuilder::new`].
+//!
+//! [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+//! [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder
 
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
 use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self};
 use core::convert::TryFrom;
+use core::hash::{Hash, Hasher};
 use core::num::NonZeroU64;
 use core::ops::Deref;
 use core::str::FromStr;
@@ -77,6 +88,7 @@ use core::time::Duration;
 use crate::sign::EntropySource;
 use crate::io;
 use crate::blinded_path::BlindedPath;
+use crate::ln::channelmanager::PaymentId;
 use crate::ln::features::OfferFeatures;
 use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
 use crate::ln::msgs::MAX_VALUE_MSAT;
@@ -131,6 +143,14 @@ impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> {
        /// while the offer is valid.
        ///
        /// Use a different pubkey per offer to avoid correlating offers.
+       ///
+       /// # Note
+       ///
+       /// If constructing an [`Offer`] for use with a [`ChannelManager`], use
+       /// [`ChannelManager::create_offer_builder`] instead of [`OfferBuilder::new`].
+       ///
+       /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
+       /// [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder
        pub fn new(description: String, signing_pubkey: PublicKey) -> Self {
                OfferBuilder {
                        offer: OfferContents {
@@ -169,7 +189,7 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
                secp_ctx: &'a Secp256k1<T>
        ) -> Self where ES::Target: EntropySource {
                let nonce = Nonce::from_entropy_source(entropy_source);
-               let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
+               let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, None);
                let metadata = Metadata::DerivedSigningPubkey(derivation_material);
                OfferBuilder {
                        offer: OfferContents {
@@ -190,9 +210,18 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
        /// See [`Offer::chains`] on how this relates to the payment currency.
        ///
        /// Successive calls to this method will add another chain hash.
-       pub fn chain(mut self, network: Network) -> Self {
+       pub fn chain(self, network: Network) -> Self {
+               self.chain_hash(ChainHash::using_genesis_block(network))
+       }
+
+       /// Adds the [`ChainHash`] to [`Offer::chains`]. If not called, the chain hash of
+       /// [`Network::Bitcoin`] is assumed to be the only one supported.
+       ///
+       /// See [`Offer::chains`] on how this relates to the payment currency.
+       ///
+       /// Successive calls to this method will add another chain hash.
+       pub(crate) fn chain_hash(mut self, chain: ChainHash) -> Self {
                let chains = self.offer.chains.get_or_insert_with(Vec::new);
-               let chain = ChainHash::using_genesis_block(network);
                if !chains.contains(&chain) {
                        chains.push(chain);
                }
@@ -283,7 +312,7 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
                                let mut tlv_stream = self.offer.as_tlv_stream();
                                debug_assert_eq!(tlv_stream.metadata, None);
                                tlv_stream.metadata = None;
-                               if metadata.derives_keys() {
+                               if metadata.derives_recipient_keys() {
                                        tlv_stream.node_id = None;
                                }
 
@@ -311,6 +340,11 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
                self
        }
 
+       pub(crate) fn clear_paths(mut self) -> Self {
+               self.offer.paths = None;
+               self
+       }
+
        pub(super) fn build_unchecked(self) -> Offer {
                self.build_without_checks()
        }
@@ -330,7 +364,6 @@ impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
 #[derive(Clone, Debug)]
-#[cfg_attr(test, derive(PartialEq))]
 pub struct Offer {
        // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
        // fields.
@@ -365,7 +398,7 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => {
        /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet).
        /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats)
        /// for the selected chain.
-       pub fn chains(&$self) -> Vec<$crate::bitcoin::blockdata::constants::ChainHash> {
+       pub fn chains(&$self) -> Vec<bitcoin::blockdata::constants::ChainHash> {
                $contents.chains()
        }
 
@@ -417,7 +450,7 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => {
        }
 
        /// The public key used by the recipient to sign invoices.
-       pub fn signing_pubkey(&$self) -> $crate::bitcoin::secp256k1::PublicKey {
+       pub fn signing_pubkey(&$self) -> bitcoin::secp256k1::PublicKey {
                $contents.signing_pubkey()
        }
 } }
@@ -440,6 +473,11 @@ impl Offer {
                self.contents.is_expired()
        }
 
+       /// Whether the offer has expired given the duration since the Unix epoch.
+       pub fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool {
+               self.contents.is_expired_no_std(duration_since_epoch)
+       }
+
        /// 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)
@@ -454,21 +492,24 @@ impl Offer {
 
        /// Similar to [`Offer::request_invoice`] except it:
        /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
-       ///   request, and
-       /// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
-       ///   that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was requested
-       ///   using a base [`ExpandedKey`] from which the payer id was derived.
+       ///   request,
+       /// - sets [`InvoiceRequest::payer_metadata`] when [`InvoiceRequestBuilder::build`] is called
+       ///   such that it can be used by [`Bolt12Invoice::verify`] to determine if the invoice was
+       ///   requested using a base [`ExpandedKey`] from which the payer id was derived, and
+       /// - includes the [`PaymentId`] encrypted in [`InvoiceRequest::payer_metadata`] so that it can
+       ///   be used when sending the payment for the requested invoice.
        ///
        /// Useful to protect the sender's privacy.
        ///
        /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
        ///
        /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
-       /// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata
+       /// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_metadata
        /// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify
        /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
        pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>(
-               &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
+               &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>,
+               payment_id: PaymentId
        ) -> Result<InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>, Bolt12SemanticError>
        where
                ES::Target: EntropySource,
@@ -477,7 +518,9 @@ impl Offer {
                        return Err(Bolt12SemanticError::UnknownRequiredFeatures);
                }
 
-               Ok(InvoiceRequestBuilder::deriving_payer_id(self, expanded_key, entropy_source, secp_ctx))
+               Ok(InvoiceRequestBuilder::deriving_payer_id(
+                       self, expanded_key, entropy_source, secp_ctx, payment_id
+               ))
        }
 
        /// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the
@@ -489,7 +532,8 @@ impl Offer {
        ///
        /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
        pub fn request_invoice_deriving_metadata<ES: Deref>(
-               &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
+               &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+               payment_id: PaymentId
        ) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, Bolt12SemanticError>
        where
                ES::Target: EntropySource,
@@ -498,7 +542,9 @@ impl Offer {
                        return Err(Bolt12SemanticError::UnknownRequiredFeatures);
                }
 
-               Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source))
+               Ok(InvoiceRequestBuilder::deriving_metadata(
+                       self, payer_id, expanded_key, entropy_source, payment_id
+               ))
        }
 
        /// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`,
@@ -538,6 +584,20 @@ impl AsRef<[u8]> for Offer {
        }
 }
 
+impl PartialEq for Offer {
+       fn eq(&self, other: &Self) -> bool {
+               self.bytes.eq(&other.bytes)
+       }
+}
+
+impl Eq for Offer {}
+
+impl Hash for Offer {
+       fn hash<H: Hasher>(&self, state: &mut H) {
+               self.bytes.hash(state);
+       }
+}
+
 impl OfferContents {
        pub fn chains(&self) -> Vec<ChainHash> {
                self.chains.as_ref().cloned().unwrap_or_else(|| vec![self.implied_chain()])
@@ -573,13 +633,16 @@ impl OfferContents {
 
        #[cfg(feature = "std")]
        pub(super) fn is_expired(&self) -> bool {
-               match self.absolute_expiry {
-                       Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
-                               Ok(elapsed) => elapsed > seconds_from_epoch,
-                               Err(_) => false,
-                       },
-                       None => false,
-               }
+               SystemTime::UNIX_EPOCH
+                       .elapsed()
+                       .map(|duration_since_epoch| self.is_expired_no_std(duration_since_epoch))
+                       .unwrap_or(false)
+       }
+
+       pub(super) fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool {
+               self.absolute_expiry
+                       .map(|absolute_expiry| duration_since_epoch > absolute_expiry)
+                       .unwrap_or(false)
        }
 
        pub fn issuer(&self) -> Option<PrintableString> {
@@ -661,11 +724,13 @@ impl OfferContents {
                                let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| {
                                        match record.r#type {
                                                OFFER_METADATA_TYPE => false,
-                                               OFFER_NODE_ID_TYPE => !self.metadata.as_ref().unwrap().derives_keys(),
+                                               OFFER_NODE_ID_TYPE => {
+                                                       !self.metadata.as_ref().unwrap().derives_recipient_keys()
+                                               },
                                                _ => true,
                                        }
                                });
-                               signer::verify_metadata(
+                               signer::verify_recipient_metadata(
                                        metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
                                )
                        },
@@ -1151,6 +1216,7 @@ mod tests {
        fn builds_offer_with_absolute_expiry() {
                let future_expiry = Duration::from_secs(u64::max_value());
                let past_expiry = Duration::from_secs(0);
+               let now = future_expiry - Duration::from_secs(1_000);
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
                        .absolute_expiry(future_expiry)
@@ -1158,6 +1224,7 @@ mod tests {
                        .unwrap();
                #[cfg(feature = "std")]
                assert!(!offer.is_expired());
+               assert!(!offer.is_expired_no_std(now));
                assert_eq!(offer.absolute_expiry(), Some(future_expiry));
                assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs()));
 
@@ -1168,6 +1235,7 @@ mod tests {
                        .unwrap();
                #[cfg(feature = "std")]
                assert!(offer.is_expired());
+               assert!(offer.is_expired_no_std(now));
                assert_eq!(offer.absolute_expiry(), Some(past_expiry));
                assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(past_expiry.as_secs()));
        }
@@ -1470,28 +1538,60 @@ mod tests {
 }
 
 #[cfg(test)]
-mod bech32_tests {
-       use super::{Bolt12ParseError, Offer};
-       use bitcoin::bech32;
+mod bolt12_tests {
+       use super::{Bolt12ParseError, Bolt12SemanticError, Offer};
        use crate::ln::msgs::DecodeError;
 
-       #[test]
-       fn encodes_offer_as_bech32_without_checksum() {
-               let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg";
-               let offer = dbg!(encoded_offer.parse::<Offer>().unwrap());
-               let reencoded_offer = offer.to_string();
-               dbg!(reencoded_offer.parse::<Offer>().unwrap());
-               assert_eq!(reencoded_offer, encoded_offer);
-       }
-
        #[test]
        fn parses_bech32_encoded_offers() {
                let offers = [
-                       // BOLT 12 test vectors
-                       "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
-                       "l+no1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
-                       "lno1pqps7sjqpgt+yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+5k7msjzfpy7nz5yqcn+ygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+5xvxg",
-                       "lno1pqps7sjqpgt+ yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+  5k7msjzfpy7nz5yqcn+\nygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+\r\n 5xvxg",
+                       // Minimal bolt12 offer
+                       "lno1pgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+
+                       // for testnet
+                       "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj",
+
+                       // for bitcoin (redundant)
+                       "lno1qgsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj",
+
+                       // for bitcoin or liquidv1
+                       "lno1qfqpge38tqmzyrdjj3x2qkdr5y80dlfw56ztq6yd9sme995g3gsxqqm0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq9qc4r9wd6zqan9vd6x7unnzcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese",
+
+                       // with metadata
+                       "lno1qsgqqqqqqqqqqqqqqqqqqqqqqqqqqzsv23jhxapqwejkxar0wfe3vggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs",
+
+                       // with amount
+                       "lno1pqpzwyq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj",
+
+                       // with currency
+                       "lno1qcp4256ypqpzwyq2p32x2um5ypmx2cm5dae8x93pqthvwfzadd7jejes8q9lhc4rvjxd022zv5l44g6qah82ru5rdpnpj",
+
+                       // with expiry
+                       "lno1pgx9getnwss8vetrw3hhyucwq3ay997czcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese",
+
+                       // with issuer
+                       "lno1pgx9getnwss8vetrw3hhyucjy358garswvaz7tmzdak8gvfj9ehhyeeqgf85c4p3xgsxjmnyw4ehgunfv4e3vggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs",
+
+                       // with quantity
+                       "lno1pgx9getnwss8vetrw3hhyuc5qyz3vggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs",
+
+                       // with unlimited (or unknown) quantity
+                       "lno1pgx9getnwss8vetrw3hhyuc5qqtzzqhwcuj966ma9n9nqwqtl032xeyv6755yeflt235pmww58egx6rxry",
+
+                       // with single quantity (weird but valid)
+                       "lno1pgx9getnwss8vetrw3hhyuc5qyq3vggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs",
+
+                       // with feature
+                       "lno1pgx9getnwss8vetrw3hhyucvp5yqqqqqqqqqqqqqqqqqqqqkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
+
+                       // with blinded path via Bob (0x424242...), blinding 020202...
+                       "lno1pgx9getnwss8vetrw3hhyucs5ypjgef743p5fzqq9nqxh0ah7y87rzv3ud0eleps9kl2d5348hq2k8qzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgqpqqqqqqqqqqqqqqqqqqqqqqqqqqqzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqqzq3zyg3zyg3zyg3vggzamrjghtt05kvkvpcp0a79gmy3nt6jsn98ad2xs8de6sl9qmgvcvs",
+
+                       // ... and with second blinded path via Carol (0x434343...), blinding 020202...
+                       "lno1pgx9getnwss8vetrw3hhyucsl5q5yqeyv5l2cs6y3qqzesrth7mlzrlp3xg7xhulusczm04x6g6nms9trspqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqqsqqqqqqqqqqqqqqqqqqqqqqqqqqpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsqpqg3zyg3zyg3zygz0uc7h32x9s0aecdhxlk075kn046aafpuuyw8f5j652t3vha2yqrsyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsqzqqqqqqqqqqqqqqqqqqqqqqqqqqqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqqyzyg3zyg3zyg3zzcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese",
+
+                       // unknown odd field
+                       "lno1pgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxfppf5x2mrvdamk7unvvs",
                ];
                for encoded_offer in &offers {
                        if let Err(e) = encoded_offer.parse::<Offer>() {
@@ -1501,48 +1601,155 @@ mod bech32_tests {
        }
 
        #[test]
-       fn fails_parsing_bech32_encoded_offers_with_invalid_continuations() {
-               let offers = [
-                       // BOLT 12 test vectors
-                       "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+",
-                       "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+ ",
-                       "+lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
-                       "+ lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
-                       "ln++o1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg",
-               ];
-               for encoded_offer in &offers {
-                       match encoded_offer.parse::<Offer>() {
-                               Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                               Err(e) => assert_eq!(e, Bolt12ParseError::InvalidContinuation),
-                       }
-               }
+       fn fails_parsing_bech32_encoded_offers() {
+               // Malformed: fields out of order
+               assert_eq!(
+                       "lno1zcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszpgz5znzfgdzs".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
 
-       }
+               // Malformed: unknown even TLV type 78
+               assert_eq!(
+                       "lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpysgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
+               );
 
-       #[test]
-       fn fails_parsing_bech32_encoded_offer_with_invalid_hrp() {
-               let encoded_offer = "lni1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg";
-               match encoded_offer.parse::<Offer>() {
-                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                       Err(e) => assert_eq!(e, Bolt12ParseError::InvalidBech32Hrp),
-               }
-       }
+               // Malformed: empty
+               assert_eq!(
+                       "lno1".parse::<Offer>(),
+                       Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)),
+               );
 
-       #[test]
-       fn fails_parsing_bech32_encoded_offer_with_invalid_bech32_data() {
-               let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxo";
-               match encoded_offer.parse::<Offer>() {
-                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                       Err(e) => assert_eq!(e, Bolt12ParseError::Bech32(bech32::Error::InvalidChar('o'))),
-               }
-       }
+               // Malformed: truncated at type
+               assert_eq!(
+                       "lno1pg".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
 
-       #[test]
-       fn fails_parsing_bech32_encoded_offer_with_invalid_tlv_data() {
-               let encoded_offer = "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxgqqqqq";
-               match encoded_offer.parse::<Offer>() {
-                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                       Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
-               }
+               // Malformed: truncated in length
+               assert_eq!(
+                       "lno1pt7s".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: truncated after length
+               assert_eq!(
+                       "lno1pgpq".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: truncated in description
+               assert_eq!(
+                       "lno1pgpyz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: invalid offer_chains length
+               assert_eq!(
+                       "lno1qgqszzs9g9xyjs69zcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: truncated currency UTF-8
+               assert_eq!(
+                       "lno1qcqcqzs9g9xyjs69zcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: invalid currency UTF-8
+               assert_eq!(
+                       "lno1qcpgqsg2q4q5cj2rg5tzzqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqg".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: truncated description UTF-8
+               assert_eq!(
+                       "lno1pgqcq93pqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqy".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
+
+               // Malformed: invalid description UTF-8
+               assert_eq!(
+                       "lno1pgpgqsgkyypqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
+
+               // Malformed: truncated offer_paths
+               assert_eq!(
+                       "lno1pgz5znzfgdz3qqgpzcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: zero num_hops in blinded_path
+               assert_eq!(
+                       "lno1pgz5znzfgdz3qqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsqzcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: truncated onionmsg_hop in blinded_path
+               assert_eq!(
+                       "lno1pgz5znzfgdz3qqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqspqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqgkyypqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: bad first_node_id in blinded_path
+               assert_eq!(
+                       "lno1pgz5znzfgdz3qqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqspqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqgqzcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: bad blinding in blinded_path
+               assert_eq!(
+                       "lno1pgz5znzfgdz3qqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcpqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqgqzcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: bad blinded_node_id in onionmsg_hop
+               assert_eq!(
+                       "lno1pgz5znzfgdz3qqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqspqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqgqzcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::ShortRead)),
+               );
+
+               // Malformed: truncated issuer UTF-8
+               assert_eq!(
+                       "lno1pgz5znzfgdz3yqvqzcssyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsz".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
+
+               // Malformed: invalid issuer UTF-8
+               assert_eq!(
+                       "lno1pgz5znzfgdz3yq5qgytzzqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqg".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
+
+               // Malformed: invalid offer_node_id
+               assert_eq!(
+                       "lno1pgz5znzfgdz3vggzqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvps".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
+
+               // Contains type >= 80
+               assert_eq!(
+                       "lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp9qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq".parse::<Offer>(),
+                       Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               );
+
+               // TODO: Resolved in spec https://github.com/lightning/bolts/pull/798/files#r1334851959
+               // Contains unknown feature 22
+               assert!(
+                       "lno1pgx9getnwss8vetrw3hhyucvqdqqqqqkyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg".parse::<Offer>().is_ok()
+               );
+
+               // Missing offer_description
+               assert_eq!(
+                       "lno1zcss9mk8y3wkklfvevcrszlmu23kfrxh49px20665dqwmn4p72pksese".parse::<Offer>(),
+                       Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription)),
+               );
+
+               // Missing offer_node_id"
+               assert_eq!(
+                       "lno1pgx9getnwss8vetrw3hhyuc".parse::<Offer>(),
+                       Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey)),
+               );
        }
 }