Merge pull request #2714 from TheBlueMatt/2023-11-one-less-alloc
[rust-lightning] / lightning / src / offers / offer.rs
index 405e2e278d8073eac345a801f88906422989f173..ab7fe62cb50e073bbb67e21fb32071027dff9808 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;
 //!
 //! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
 //! use lightning::offers::offer::{Offer, OfferBuilder, Quantity};
-//! use lightning::offers::parse::ParseError;
+//! use lightning::offers::parse::Bolt12ParseError;
 //! use lightning::util::ser::{Readable, Writeable};
 //!
-//! # use lightning::onion_message::BlindedPath;
+//! # use lightning::blinded_path::BlindedPath;
 //! # #[cfg(feature = "std")]
 //! # use std::time::SystemTime;
 //! #
@@ -35,7 +37,7 @@
 //! # fn create_another_blinded_path() -> BlindedPath { unimplemented!() }
 //! #
 //! # #[cfg(feature = "std")]
-//! # fn build() -> Result<(), ParseError> {
+//! # fn build() -> Result<(), Bolt12ParseError> {
 //! let secp_ctx = Secp256k1::new();
 //! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
 //! let pubkey = PublicKey::from(keys);
 //! # 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::PublicKey;
+use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self};
 use core::convert::TryFrom;
 use core::num::NonZeroU64;
+use core::ops::Deref;
 use core::str::FromStr;
 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;
-use crate::offers::invoice_request::InvoiceRequestBuilder;
-use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
-use crate::onion_message::BlindedPath;
+use crate::offers::invoice_request::{DerivedPayerId, ExplicitPayerId, InvoiceRequestBuilder};
+use crate::offers::merkle::TlvStream;
+use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
+use crate::offers::signer::{Metadata, MetadataMaterial, self};
 use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
 
@@ -87,39 +103,124 @@ use crate::prelude::*;
 #[cfg(feature = "std")]
 use std::time::SystemTime;
 
+pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~";
+
 /// Builds an [`Offer`] for the "offer to be paid" flow.
 ///
 /// See [module-level documentation] for usage.
 ///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+///
 /// [module-level documentation]: self
-pub struct OfferBuilder {
+pub struct OfferBuilder<'a, M: MetadataStrategy, T: secp256k1::Signing> {
        offer: OfferContents,
+       metadata_strategy: core::marker::PhantomData<M>,
+       secp_ctx: Option<&'a Secp256k1<T>>,
 }
 
-impl OfferBuilder {
+/// Indicates how [`Offer::metadata`] may be set.
+///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+pub trait MetadataStrategy {}
+
+/// [`Offer::metadata`] may be explicitly set or left empty.
+///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+pub struct ExplicitMetadata {}
+
+/// [`Offer::metadata`] will be derived.
+///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+pub struct DerivedMetadata {}
+
+impl MetadataStrategy for ExplicitMetadata {}
+impl MetadataStrategy for DerivedMetadata {}
+
+impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> {
        /// Creates a new builder for an offer setting the [`Offer::description`] and using the
        /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered
        /// while the offer is valid.
        ///
        /// Use a different pubkey per offer to avoid correlating offers.
+       ///
+       /// # 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 {
-               let offer = OfferContents {
-                       chains: None, metadata: None, amount: None, description,
-                       features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
-                       supported_quantity: Quantity::One, signing_pubkey,
-               };
-               OfferBuilder { offer }
+               OfferBuilder {
+                       offer: OfferContents {
+                               chains: None, metadata: None, amount: None, description,
+                               features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
+                               supported_quantity: Quantity::One, signing_pubkey,
+                       },
+                       metadata_strategy: core::marker::PhantomData,
+                       secp_ctx: None,
+               }
        }
 
+       /// Sets the [`Offer::metadata`] to the given bytes.
+       ///
+       /// Successive calls to this method will override the previous setting.
+       pub fn metadata(mut self, metadata: Vec<u8>) -> Result<Self, Bolt12SemanticError> {
+               self.offer.metadata = Some(Metadata::Bytes(metadata));
+               Ok(self)
+       }
+}
+
+impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> {
+       /// Similar to [`OfferBuilder::new`] except, if [`OfferBuilder::path`] is called, the signing
+       /// pubkey is derived from the given [`ExpandedKey`] and [`EntropySource`]. This provides
+       /// recipient privacy by using a different signing pubkey for each offer. Otherwise, the
+       /// provided `node_id` is used for the signing pubkey.
+       ///
+       /// Also, sets the metadata when [`OfferBuilder::build`] is called such that it can be used by
+       /// [`InvoiceRequest::verify`] to determine if the request was produced for the offer given an
+       /// [`ExpandedKey`].
+       ///
+       /// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify
+       /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
+       pub fn deriving_signing_pubkey<ES: Deref>(
+               description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+               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, None);
+               let metadata = Metadata::DerivedSigningPubkey(derivation_material);
+               OfferBuilder {
+                       offer: OfferContents {
+                               chains: None, metadata: Some(metadata), amount: None, description,
+                               features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
+                               supported_quantity: Quantity::One, signing_pubkey: node_id,
+                       },
+                       metadata_strategy: core::marker::PhantomData,
+                       secp_ctx: Some(secp_ctx),
+               }
+       }
+}
+
+impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
        /// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called,
        /// the chain hash of [`Network::Bitcoin`] is assumed to be the only one supported.
        ///
        /// See [`Offer::chains`] on how this relates to the payment currency.
        ///
        /// Successive calls to this method will add another chain hash.
-       pub fn chain(mut self, network: Network) -> Self {
+       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);
                }
@@ -127,14 +228,6 @@ impl OfferBuilder {
                self
        }
 
-       /// Sets the [`Offer::metadata`].
-       ///
-       /// Successive calls to this method will override the previous setting.
-       pub fn metadata(mut self, metadata: Vec<u8>) -> Self {
-               self.offer.metadata = Some(metadata);
-               self
-       }
-
        /// Sets the [`Offer::amount`] as an [`Amount::Bitcoin`].
        ///
        /// Successive calls to this method will override the previous setting.
@@ -187,14 +280,14 @@ impl OfferBuilder {
        }
 
        /// Builds an [`Offer`] from the builder's settings.
-       pub fn build(mut self) -> Result<Offer, SemanticError> {
+       pub fn build(mut self) -> Result<Offer, Bolt12SemanticError> {
                match self.offer.amount {
                        Some(Amount::Bitcoin { amount_msats }) => {
                                if amount_msats > MAX_VALUE_MSAT {
-                                       return Err(SemanticError::InvalidAmount);
+                                       return Err(Bolt12SemanticError::InvalidAmount);
                                }
                        },
-                       Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
+                       Some(Amount::Currency { .. }) => return Err(Bolt12SemanticError::UnsupportedCurrency),
                        None => {},
                }
 
@@ -204,36 +297,58 @@ impl OfferBuilder {
                        }
                }
 
+               Ok(self.build_without_checks())
+       }
+
+       fn build_without_checks(mut self) -> Offer {
+               // Create the metadata for stateless verification of an InvoiceRequest.
+               if let Some(mut metadata) = self.offer.metadata.take() {
+                       if metadata.has_derivation_material() {
+                               if self.offer.paths.is_none() {
+                                       metadata = metadata.without_keys();
+                               }
+
+                               let mut tlv_stream = self.offer.as_tlv_stream();
+                               debug_assert_eq!(tlv_stream.metadata, None);
+                               tlv_stream.metadata = None;
+                               if metadata.derives_recipient_keys() {
+                                       tlv_stream.node_id = None;
+                               }
+
+                               let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
+                               metadata = derived_metadata;
+                               if let Some(keys) = keys {
+                                       self.offer.signing_pubkey = keys.public_key();
+                               }
+                       }
+
+                       self.offer.metadata = Some(metadata);
+               }
+
                let mut bytes = Vec::new();
                self.offer.write(&mut bytes).unwrap();
 
-               Ok(Offer {
-                       bytes,
-                       contents: self.offer,
-               })
+               Offer { bytes, contents: self.offer }
        }
 }
 
 #[cfg(test)]
-impl OfferBuilder {
+impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> {
        fn features_unchecked(mut self, features: OfferFeatures) -> Self {
                self.offer.features = features;
                self
        }
 
        pub(super) fn build_unchecked(self) -> Offer {
-               let mut bytes = Vec::new();
-               self.offer.write(&mut bytes).unwrap();
-
-               Offer { bytes, contents: self.offer }
+               self.build_without_checks()
        }
 }
 
 /// An `Offer` is a potentially long-lived proposal for payment of a good or service.
 ///
 /// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
-/// customer may request an [`Invoice`] for a specific quantity and using an amount sufficient to
-/// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
+/// customer may request an [`Bolt12Invoice`] for a specific quantity and using an amount sufficient
+/// to cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
 ///
 /// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
 /// latter.
@@ -241,8 +356,9 @@ impl OfferBuilder {
 /// Through the use of [`BlindedPath`]s, offers provide recipient privacy.
 ///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
-/// [`Invoice`]: crate::offers::invoice::Invoice
-#[derive(Clone, Debug, PartialEq)]
+/// [`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.
@@ -250,14 +366,16 @@ pub struct Offer {
        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 a
+/// [`Bolt12Invoice`].
 ///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
-/// [`Invoice`]: crate::offers::invoice::Invoice
-#[derive(Clone, Debug, PartialEq)]
+/// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice
+#[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub(super) struct OfferContents {
        chains: Option<Vec<ChainHash>>,
-       metadata: Option<Vec<u8>>,
+       metadata: Option<Metadata>,
        amount: Option<Amount>,
        description: String,
        features: OfferFeatures,
@@ -268,77 +386,91 @@ pub(super) struct OfferContents {
        signing_pubkey: PublicKey,
 }
 
-impl Offer {
+macro_rules! offer_accessors { ($self: ident, $contents: expr) => {
        // TODO: Return a slice once ChainHash has constants.
        // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1283
        // - https://github.com/rust-bitcoin/rust-bitcoin/pull/1286
        /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet).
        /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats)
        /// for the selected chain.
-       pub fn chains(&self) -> Vec<ChainHash> {
-               self.contents.chains()
-       }
-
-       pub(super) fn implied_chain(&self) -> ChainHash {
-               self.contents.implied_chain()
-       }
-
-       /// Returns whether the given chain is supported by the offer.
-       pub fn supports_chain(&self, chain: ChainHash) -> bool {
-               self.contents.supports_chain(chain)
+       pub fn chains(&$self) -> Vec<bitcoin::blockdata::constants::ChainHash> {
+               $contents.chains()
        }
 
        // TODO: Link to corresponding method in `InvoiceRequest`.
        /// Opaque bytes set by the originator. Useful for authentication and validating fields since it
        /// is reflected in `invoice_request` messages along with all the other fields from the `offer`.
-       pub fn metadata(&self) -> Option<&Vec<u8>> {
-               self.contents.metadata.as_ref()
+       pub fn metadata(&$self) -> Option<&Vec<u8>> {
+               $contents.metadata()
        }
 
        /// The minimum amount required for a successful payment of a single item.
-       pub fn amount(&self) -> Option<&Amount> {
-               self.contents.amount()
+       pub fn amount(&$self) -> Option<&$crate::offers::offer::Amount> {
+               $contents.amount()
        }
 
        /// A complete description of the purpose of the payment. Intended to be displayed to the user
        /// but with the caveat that it has not been verified in any way.
-       pub fn description(&self) -> PrintableString {
-               PrintableString(&self.contents.description)
+       pub fn description(&$self) -> $crate::util::string::PrintableString {
+               $contents.description()
        }
 
        /// Features pertaining to the offer.
-       pub fn features(&self) -> &OfferFeatures {
-               &self.contents.features
+       pub fn offer_features(&$self) -> &$crate::ln::features::OfferFeatures {
+               &$contents.features()
        }
 
        /// Duration since the Unix epoch when an invoice should no longer be requested.
        ///
        /// If `None`, the offer does not expire.
-       pub fn absolute_expiry(&self) -> Option<Duration> {
-               self.contents.absolute_expiry
-       }
-
-       /// Whether the offer has expired.
-       #[cfg(feature = "std")]
-       pub fn is_expired(&self) -> bool {
-               self.contents.is_expired()
+       pub fn absolute_expiry(&$self) -> Option<core::time::Duration> {
+               $contents.absolute_expiry()
        }
 
        /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. Intended to be
        /// displayed to the user but with the caveat that it has not been verified in any way.
-       pub fn issuer(&self) -> Option<PrintableString> {
-               self.contents.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str()))
+       pub fn issuer(&$self) -> Option<$crate::util::string::PrintableString> {
+               $contents.issuer()
        }
 
        /// Paths to the recipient originating from publicly reachable nodes. Blinded paths provide
        /// recipient privacy by obfuscating its node id.
-       pub fn paths(&self) -> &[BlindedPath] {
-               self.contents.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[])
+       pub fn paths(&$self) -> &[$crate::blinded_path::BlindedPath] {
+               $contents.paths()
        }
 
        /// The quantity of items supported.
-       pub fn supported_quantity(&self) -> Quantity {
-               self.contents.supported_quantity()
+       pub fn supported_quantity(&$self) -> $crate::offers::offer::Quantity {
+               $contents.supported_quantity()
+       }
+
+       /// The public key used by the recipient to sign invoices.
+       pub fn signing_pubkey(&$self) -> bitcoin::secp256k1::PublicKey {
+               $contents.signing_pubkey()
+       }
+} }
+
+impl Offer {
+       offer_accessors!(self, self.contents);
+
+       pub(super) fn implied_chain(&self) -> ChainHash {
+               self.contents.implied_chain()
+       }
+
+       /// Returns whether the given chain is supported by the offer.
+       pub fn supports_chain(&self, chain: ChainHash) -> bool {
+               self.contents.supports_chain(chain)
+       }
+
+       /// Whether the offer has expired.
+       #[cfg(feature = "std")]
+       pub fn is_expired(&self) -> bool {
+               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.
@@ -353,13 +485,65 @@ impl Offer {
                self.contents.expects_quantity()
        }
 
-       /// The public key used by the recipient to sign invoices.
-       pub fn signing_pubkey(&self) -> PublicKey {
-               self.contents.signing_pubkey()
+       /// Similar to [`Offer::request_invoice`] except it:
+       /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
+       ///   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::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>,
+               payment_id: PaymentId
+       ) -> Result<InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>, Bolt12SemanticError>
+       where
+               ES::Target: EntropySource,
+       {
+               if self.offer_features().requires_unknown_bits() {
+                       return Err(Bolt12SemanticError::UnknownRequiredFeatures);
+               }
+
+               Ok(InvoiceRequestBuilder::deriving_payer_id(
+                       self, expanded_key, entropy_source, secp_ctx, payment_id
+               ))
        }
 
-       /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
-       /// will be reflected in the `Invoice` response.
+       /// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the
+       /// [`InvoiceRequest::payer_id`] instead of deriving a different key for each request.
+       ///
+       /// Useful for recurring payments using the same `payer_id` with different invoices.
+       ///
+       /// 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
+       pub fn request_invoice_deriving_metadata<ES: Deref>(
+               &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+               payment_id: PaymentId
+       ) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, Bolt12SemanticError>
+       where
+               ES::Target: EntropySource,
+       {
+               if self.offer_features().requires_unknown_bits() {
+                       return Err(Bolt12SemanticError::UnknownRequiredFeatures);
+               }
+
+               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`,
+       /// which will be reflected in the `Bolt12Invoice` response.
        ///
        /// The `metadata` is useful for including information about the derivation of `payer_id` such
        /// that invoice response handling can be stateless. Also serves as payer-provided entropy while
@@ -370,12 +554,14 @@ impl Offer {
        ///
        /// Errors if the offer contains unknown required features.
        ///
+       /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+       ///
        /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
        pub fn request_invoice(
                &self, metadata: Vec<u8>, payer_id: PublicKey
-       ) -> Result<InvoiceRequestBuilderSemanticError> {
-               if self.features().requires_unknown_bits() {
-                       return Err(SemanticError::UnknownRequiredFeatures);
+       ) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, Bolt12SemanticError> {
+               if self.offer_features().requires_unknown_bits() {
+                       return Err(Bolt12SemanticError::UnknownRequiredFeatures);
                }
 
                Ok(InvoiceRequestBuilder::new(self, metadata, payer_id))
@@ -406,41 +592,68 @@ impl OfferContents {
                self.chains().contains(&chain)
        }
 
-       #[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,
-               }
+       pub fn metadata(&self) -> Option<&Vec<u8>> {
+               self.metadata.as_ref().and_then(|metadata| metadata.as_bytes())
        }
 
        pub fn amount(&self) -> Option<&Amount> {
                self.amount.as_ref()
        }
 
+       pub fn description(&self) -> PrintableString {
+               PrintableString(&self.description)
+       }
+
+       pub fn features(&self) -> &OfferFeatures {
+               &self.features
+       }
+
+       pub fn absolute_expiry(&self) -> Option<Duration> {
+               self.absolute_expiry
+       }
+
+       #[cfg(feature = "std")]
+       pub(super) fn is_expired(&self) -> bool {
+               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> {
+               self.issuer.as_ref().map(|issuer| PrintableString(issuer.as_str()))
+       }
+
+       pub fn paths(&self) -> &[BlindedPath] {
+               self.paths.as_ref().map(|paths| paths.as_slice()).unwrap_or(&[])
+       }
+
        pub(super) fn check_amount_msats_for_quantity(
                &self, amount_msats: Option<u64>, quantity: Option<u64>
-       ) -> Result<(), SemanticError> {
+       ) -> Result<(), Bolt12SemanticError> {
                let offer_amount_msats = match self.amount {
                        None => 0,
                        Some(Amount::Bitcoin { amount_msats }) => amount_msats,
-                       Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
+                       Some(Amount::Currency { .. }) => return Err(Bolt12SemanticError::UnsupportedCurrency),
                };
 
                if !self.expects_quantity() || quantity.is_some() {
                        let expected_amount_msats = offer_amount_msats.checked_mul(quantity.unwrap_or(1))
-                               .ok_or(SemanticError::InvalidAmount)?;
+                               .ok_or(Bolt12SemanticError::InvalidAmount)?;
                        let amount_msats = amount_msats.unwrap_or(expected_amount_msats);
 
                        if amount_msats < expected_amount_msats {
-                               return Err(SemanticError::InsufficientAmount);
+                               return Err(Bolt12SemanticError::InsufficientAmount);
                        }
 
                        if amount_msats > MAX_VALUE_MSAT {
-                               return Err(SemanticError::InvalidAmount);
+                               return Err(Bolt12SemanticError::InvalidAmount);
                        }
                }
 
@@ -451,13 +664,13 @@ impl OfferContents {
                self.supported_quantity
        }
 
-       pub(super) fn check_quantity(&self, quantity: Option<u64>) -> Result<(), SemanticError> {
+       pub(super) fn check_quantity(&self, quantity: Option<u64>) -> Result<(), Bolt12SemanticError> {
                let expects_quantity = self.expects_quantity();
                match quantity {
-                       None if expects_quantity => Err(SemanticError::MissingQuantity),
-                       Some(_) if !expects_quantity => Err(SemanticError::UnexpectedQuantity),
+                       None if expects_quantity => Err(Bolt12SemanticError::MissingQuantity),
+                       Some(_) if !expects_quantity => Err(Bolt12SemanticError::UnexpectedQuantity),
                        Some(quantity) if !self.is_valid_quantity(quantity) => {
-                               Err(SemanticError::InvalidQuantity)
+                               Err(Bolt12SemanticError::InvalidQuantity)
                        },
                        _ => Ok(()),
                }
@@ -483,6 +696,29 @@ impl OfferContents {
                self.signing_pubkey
        }
 
+       /// Verifies that the offer metadata was produced from the offer in the TLV stream.
+       pub(super) fn verify<T: secp256k1::Signing>(
+               &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1<T>
+       ) -> Result<Option<KeyPair>, ()> {
+               match self.metadata() {
+                       Some(metadata) => {
+                               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_recipient_keys()
+                                               },
+                                               _ => true,
+                                       }
+                               });
+                               signer::verify_recipient_metadata(
+                                       metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx
+                               )
+                       },
+                       None => Err(()),
+               }
+       }
+
        pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
                let (currency, amount) = match &self.amount {
                        None => (None, None),
@@ -498,7 +734,7 @@ impl OfferContents {
 
                OfferTlvStreamRef {
                        chains: self.chains.as_ref(),
-                       metadata: self.metadata.as_ref(),
+                       metadata: self.metadata(),
                        currency,
                        amount,
                        description: Some(&self.description),
@@ -570,9 +806,18 @@ impl Quantity {
        }
 }
 
-tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
+/// Valid type range for offer TLV records.
+pub(super) const OFFER_TYPES: core::ops::Range<u64> = 1..80;
+
+/// TLV record type for [`Offer::metadata`].
+const OFFER_METADATA_TYPE: u64 = 4;
+
+/// TLV record type for [`Offer::signing_pubkey`].
+const OFFER_NODE_ID_TYPE: u64 = 22;
+
+tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
        (2, chains: (Vec<ChainHash>, WithoutLength)),
-       (4, metadata: (Vec<u8>, WithoutLength)),
+       (OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
        (6, currency: CurrencyCode),
        (8, amount: (u64, HighZeroBytesDroppedBigSize)),
        (10, description: (String, WithoutLength)),
@@ -581,7 +826,7 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, 1..80, {
        (16, paths: (Vec<BlindedPath>, WithoutLength)),
        (18, issuer: (String, WithoutLength)),
        (20, quantity_max: (u64, HighZeroBytesDroppedBigSize)),
-       (22, node_id: PublicKey),
+       (OFFER_NODE_ID_TYPE, node_id: PublicKey),
 });
 
 impl Bech32Encode for Offer {
@@ -589,7 +834,7 @@ impl Bech32Encode for Offer {
 }
 
 impl FromStr for Offer {
-       type Err = ParseError;
+       type Err = Bolt12ParseError;
 
        fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
                Self::from_bech32_str(s)
@@ -597,7 +842,7 @@ impl FromStr for Offer {
 }
 
 impl TryFrom<Vec<u8>> for Offer {
-       type Error = ParseError;
+       type Error = Bolt12ParseError;
 
        fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
                let offer = ParsedMessage::<OfferTlvStream>::try_from(bytes)?;
@@ -608,7 +853,7 @@ impl TryFrom<Vec<u8>> for Offer {
 }
 
 impl TryFrom<OfferTlvStream> for OfferContents {
-       type Error = SemanticError;
+       type Error = Bolt12SemanticError;
 
        fn try_from(tlv_stream: OfferTlvStream) -> Result<Self, Self::Error> {
                let OfferTlvStream {
@@ -616,18 +861,20 @@ impl TryFrom<OfferTlvStream> for OfferContents {
                        issuer, quantity_max, node_id,
                } = tlv_stream;
 
+               let metadata = metadata.map(|metadata| Metadata::Bytes(metadata));
+
                let amount = match (currency, amount) {
                        (None, None) => None,
                        (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => {
-                               return Err(SemanticError::InvalidAmount);
+                               return Err(Bolt12SemanticError::InvalidAmount);
                        },
                        (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }),
-                       (Some(_), None) => return Err(SemanticError::MissingAmount),
+                       (Some(_), None) => return Err(Bolt12SemanticError::MissingAmount),
                        (Some(iso4217_code), Some(amount)) => Some(Amount::Currency { iso4217_code, amount }),
                };
 
                let description = match description {
-                       None => return Err(SemanticError::MissingDescription),
+                       None => return Err(Bolt12SemanticError::MissingDescription),
                        Some(description) => description,
                };
 
@@ -643,7 +890,7 @@ impl TryFrom<OfferTlvStream> for OfferContents {
                };
 
                let signing_pubkey = match node_id {
-                       None => return Err(SemanticError::MissingSigningPubkey),
+                       None => return Err(Bolt12SemanticError::MissingSigningPubkey),
                        Some(node_id) => node_id,
                };
 
@@ -666,26 +913,20 @@ mod tests {
 
        use bitcoin::blockdata::constants::ChainHash;
        use bitcoin::network::constants::Network;
-       use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
+       use bitcoin::secp256k1::Secp256k1;
        use core::convert::TryFrom;
        use core::num::NonZeroU64;
        use core::time::Duration;
+       use crate::blinded_path::{BlindedHop, BlindedPath};
+       use crate::sign::KeyMaterial;
        use crate::ln::features::OfferFeatures;
+       use crate::ln::inbound_payment::ExpandedKey;
        use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
-       use crate::offers::parse::{ParseError, SemanticError};
-       use crate::onion_message::{BlindedHop, BlindedPath};
+       use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
+       use crate::offers::test_utils::*;
        use crate::util::ser::{BigSize, Writeable};
        use crate::util::string::PrintableString;
 
-       fn pubkey(byte: u8) -> PublicKey {
-               let secp_ctx = Secp256k1::new();
-               PublicKey::from_secret_key(&secp_ctx, &privkey(byte))
-       }
-
-       fn privkey(byte: u8) -> SecretKey {
-               SecretKey::from_slice(&[byte; 32]).unwrap()
-       }
-
        #[test]
        fn builds_offer_with_defaults() {
                let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap();
@@ -699,7 +940,7 @@ mod tests {
                assert_eq!(offer.metadata(), None);
                assert_eq!(offer.amount(), None);
                assert_eq!(offer.description(), PrintableString("foo"));
-               assert_eq!(offer.features(), &OfferFeatures::empty());
+               assert_eq!(offer.offer_features(), &OfferFeatures::empty());
                assert_eq!(offer.absolute_expiry(), None);
                #[cfg(feature = "std")]
                assert!(!offer.is_expired());
@@ -774,21 +1015,125 @@ mod tests {
        #[test]
        fn builds_offer_with_metadata() {
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .metadata(vec![42; 32])
+                       .metadata(vec![42; 32]).unwrap()
                        .build()
                        .unwrap();
                assert_eq!(offer.metadata(), Some(&vec![42; 32]));
                assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
-                       .metadata(vec![42; 32])
-                       .metadata(vec![43; 32])
+                       .metadata(vec![42; 32]).unwrap()
+                       .metadata(vec![43; 32]).unwrap()
                        .build()
                        .unwrap();
                assert_eq!(offer.metadata(), Some(&vec![43; 32]));
                assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
        }
 
+       #[test]
+       fn builds_offer_with_metadata_derived() {
+               let desc = "foo".to_string();
+               let node_id = recipient_pubkey();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer = OfferBuilder
+                       ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+                       .amount_msats(1000)
+                       .build().unwrap();
+               assert_eq!(offer.signing_pubkey(), node_id);
+
+               let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok());
+
+               // Fails verification with altered offer field
+               let mut tlv_stream = offer.as_tlv_stream();
+               tlv_stream.amount = Some(100);
+
+               let mut encoded_offer = Vec::new();
+               tlv_stream.write(&mut encoded_offer).unwrap();
+
+               let invoice_request = Offer::try_from(encoded_offer).unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err());
+
+               // Fails verification with altered metadata
+               let mut tlv_stream = offer.as_tlv_stream();
+               let metadata = tlv_stream.metadata.unwrap().iter().copied().rev().collect();
+               tlv_stream.metadata = Some(&metadata);
+
+               let mut encoded_offer = Vec::new();
+               tlv_stream.write(&mut encoded_offer).unwrap();
+
+               let invoice_request = Offer::try_from(encoded_offer).unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err());
+       }
+
+       #[test]
+       fn builds_offer_with_derived_signing_pubkey() {
+               let desc = "foo".to_string();
+               let node_id = recipient_pubkey();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let blinded_path = BlindedPath {
+                       introduction_node_id: pubkey(40),
+                       blinding_point: pubkey(41),
+                       blinded_hops: vec![
+                               BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
+                               BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
+                       ],
+               };
+
+               let offer = OfferBuilder
+                       ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+                       .amount_msats(1000)
+                       .path(blinded_path)
+                       .build().unwrap();
+               assert_ne!(offer.signing_pubkey(), node_id);
+
+               let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok());
+
+               // Fails verification with altered offer field
+               let mut tlv_stream = offer.as_tlv_stream();
+               tlv_stream.amount = Some(100);
+
+               let mut encoded_offer = Vec::new();
+               tlv_stream.write(&mut encoded_offer).unwrap();
+
+               let invoice_request = Offer::try_from(encoded_offer).unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err());
+
+               // Fails verification with altered signing pubkey
+               let mut tlv_stream = offer.as_tlv_stream();
+               let signing_pubkey = pubkey(1);
+               tlv_stream.node_id = Some(&signing_pubkey);
+
+               let mut encoded_offer = Vec::new();
+               tlv_stream.write(&mut encoded_offer).unwrap();
+
+               let invoice_request = Offer::try_from(encoded_offer).unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+               assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err());
+       }
+
        #[test]
        fn builds_offer_with_amount() {
                let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 };
@@ -811,7 +1156,7 @@ mod tests {
                assert_eq!(tlv_stream.currency, Some(b"USD"));
                match builder.build() {
                        Ok(_) => panic!("expected error"),
-                       Err(e) => assert_eq!(e, SemanticError::UnsupportedCurrency),
+                       Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency),
                }
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
@@ -826,7 +1171,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, SemanticError::InvalidAmount),
+                       Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount),
                }
        }
 
@@ -836,7 +1181,7 @@ mod tests {
                        .features_unchecked(OfferFeatures::unknown())
                        .build()
                        .unwrap();
-               assert_eq!(offer.features(), &OfferFeatures::unknown());
+               assert_eq!(offer.offer_features(), &OfferFeatures::unknown());
                assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown()));
 
                let offer = OfferBuilder::new("foo".into(), pubkey(42))
@@ -844,7 +1189,7 @@ mod tests {
                        .features_unchecked(OfferFeatures::empty())
                        .build()
                        .unwrap();
-               assert_eq!(offer.features(), &OfferFeatures::empty());
+               assert_eq!(offer.offer_features(), &OfferFeatures::empty());
                assert_eq!(offer.as_tlv_stream().features, None);
        }
 
@@ -852,6 +1197,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)
@@ -859,6 +1205,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()));
 
@@ -869,6 +1216,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()));
        }
@@ -980,7 +1328,7 @@ mod tests {
                        .request_invoice(vec![1; 32], pubkey(43))
                {
                        Ok(_) => panic!("expected error"),
-                       Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures),
+                       Err(e) => assert_eq!(e, Bolt12SemanticError::UnknownRequiredFeatures),
                }
        }
 
@@ -1026,7 +1374,7 @@ mod tests {
 
                match Offer::try_from(encoded_offer) {
                        Ok(_) => panic!("expected error"),
-                       Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingAmount)),
+                       Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingAmount)),
                }
 
                let mut tlv_stream = offer.as_tlv_stream();
@@ -1038,7 +1386,7 @@ mod tests {
 
                match Offer::try_from(encoded_offer) {
                        Ok(_) => panic!("expected error"),
-                       Err(e) => assert_eq!(e, ParseError::InvalidSemantics(SemanticError::InvalidAmount)),
+                       Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)),
                }
        }
 
@@ -1058,7 +1406,7 @@ mod tests {
                match Offer::try_from(encoded_offer) {
                        Ok(_) => panic!("expected error"),
                        Err(e) => {
-                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingDescription));
+                               assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingDescription));
                        },
                }
        }
@@ -1148,7 +1496,7 @@ mod tests {
                match Offer::try_from(encoded_offer) {
                        Ok(_) => panic!("expected error"),
                        Err(e) => {
-                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::MissingSigningPubkey));
+                               assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey));
                        },
                }
        }
@@ -1165,41 +1513,66 @@ mod tests {
 
                match Offer::try_from(encoded_offer) {
                        Ok(_) => panic!("expected error"),
-                       Err(e) => assert_eq!(e, ParseError::Decode(DecodeError::InvalidValue)),
+                       Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
                }
        }
 }
 
 #[cfg(test)]
-mod bech32_tests {
-       use super::{Offer, ParseError};
-       use bitcoin::bech32;
+mod bolt12_tests {
+       use super::{Bolt12ParseError, Bolt12SemanticError, Offer};
        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::<Offer>().unwrap());
-               let reencoded_offer = offer.to_string();
-               dbg!(reencoded_offer.parse::<Offer>().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",
+                       // 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>() {
@@ -1209,48 +1582,155 @@ mod bech32_tests {
        }
 
        #[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::<Offer>() {
-                               Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                               Err(e) => assert_eq!(e, ParseError::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 = "lni1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsy";
-               match encoded_offer.parse::<Offer>() {
-                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                       Err(e) => assert_eq!(e, ParseError::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 = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qso";
-               match encoded_offer.parse::<Offer>() {
-                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                       Err(e) => assert_eq!(e, ParseError::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 = "lno1qcp4256ypqpq86q2pucnq42ngssx2an9wfujqerp0y2pqun4wd68jtn00fkxzcnn9ehhyec6qgqsz83qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqsp9nyu4phcg6dqhlhzgxagfu7zh3d9re0sqp9ts2yfugvnnm9gxkcnnnkdpa084a6t520h5zhkxsdnghvpukvd43lastpwuh73k29qsyqqqqq";
-               match encoded_offer.parse::<Offer>() {
-                       Ok(_) => panic!("Valid offer: {}", encoded_offer),
-                       Err(e) => assert_eq!(e, ParseError::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)),
+               );
        }
 }