Reverse (BlindedPath, BlindedPayInfo) tuple order in offers invoice.
[rust-lightning] / lightning / src / offers / refund.rs
index bee6cc7f5f61ddd39a89744bf33f9d6bc8ea66f5..07e759917acc54d5973c613e3e8d9826297f805c 100644 (file)
@@ -18,7 +18,7 @@
 //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 //! [`Offer`]: crate::offers::offer::Offer
 //!
-//! ```ignore
+//! ```
 //! extern crate bitcoin;
 //! extern crate core;
 //! extern crate lightning;
@@ -32,7 +32,7 @@
 //! use lightning::offers::refund::{Refund, RefundBuilder};
 //! use lightning::util::ser::{Readable, Writeable};
 //!
-//! # use lightning::onion_message::BlindedPath;
+//! # use lightning::blinded_path::BlindedPath;
 //! # #[cfg(feature = "std")]
 //! # use std::time::SystemTime;
 //! #
 
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::PublicKey;
+use bitcoin::secp256k1::{PublicKey, Secp256k1, self};
 use core::convert::TryFrom;
+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::PaymentHash;
 use crate::ln::features::InvoiceRequestFeatures;
+use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
 use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
-use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
+use crate::offers::invoice::{BlindedPayInfo, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder};
 use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
 use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
 use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
-use crate::onion_message::BlindedPath;
+use crate::offers::signer::{Metadata, MetadataMaterial, self};
 use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
 
@@ -95,16 +99,21 @@ use crate::prelude::*;
 #[cfg(feature = "std")]
 use std::time::SystemTime;
 
+pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Refund ~~~~~";
+
 /// Builds a [`Refund`] for the "offer for money" 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 RefundBuilder {
+pub struct RefundBuilder<'a, T: secp256k1::Signing> {
        refund: RefundContents,
+       secp_ctx: Option<&'a Secp256k1<T>>,
 }
 
-impl RefundBuilder {
+impl<'a> RefundBuilder<'a, secp256k1::SignOnly> {
        /// Creates a new builder for a refund using the [`Refund::payer_id`] for the public node id to
        /// send to if no [`Refund::paths`] are set. Otherwise, it may be a transient pubkey.
        ///
@@ -117,13 +126,48 @@ impl RefundBuilder {
                        return Err(SemanticError::InvalidAmount);
                }
 
-               let refund = RefundContents {
-                       payer: PayerContents(metadata), metadata: None, description, absolute_expiry: None,
-                       issuer: None, paths: None, chain: None, amount_msats,
-                       features: InvoiceRequestFeatures::empty(), payer_id, payer_note: None,
-               };
+               let metadata = Metadata::Bytes(metadata);
+               Ok(Self {
+                       refund: RefundContents {
+                               payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
+                               paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
+                               quantity: None, payer_id, payer_note: None,
+                       },
+                       secp_ctx: None,
+               })
+       }
+}
+
+impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
+       /// Similar to [`RefundBuilder::new`] except, if [`RefundBuilder::path`] is called, the payer id
+       /// is derived from the given [`ExpandedKey`] and nonce. This provides sender privacy by using a
+       /// different payer id for each refund, assuming a different nonce is used.  Otherwise, the
+       /// provided `node_id` is used for the payer id.
+       ///
+       /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used to
+       /// verify that an [`InvoiceRequest`] was produced for the refund given an [`ExpandedKey`].
+       ///
+       /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+       /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
+       pub fn deriving_payer_id<ES: Deref>(
+               description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES,
+               secp_ctx: &'a Secp256k1<T>, amount_msats: u64
+       ) -> Result<Self, SemanticError> where ES::Target: EntropySource {
+               if amount_msats > MAX_VALUE_MSAT {
+                       return Err(SemanticError::InvalidAmount);
+               }
 
-               Ok(RefundBuilder { refund })
+               let nonce = Nonce::from_entropy_source(entropy_source);
+               let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
+               let metadata = Metadata::DerivedSigningPubkey(derivation_material);
+               Ok(Self {
+                       refund: RefundContents {
+                               payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
+                               paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
+                               quantity: None, payer_id: node_id, payer_note: None,
+                       },
+                       secp_ctx: Some(secp_ctx),
+               })
        }
 
        /// Sets the [`Refund::absolute_expiry`] as seconds since the Unix epoch. Any expiry that has
@@ -162,6 +206,20 @@ impl RefundBuilder {
                self
        }
 
+       /// Sets [`Refund::quantity`] of items. This is purely for informational purposes. It is useful
+       /// when the refund pertains to an [`Invoice`] that paid for more than one item from an
+       /// [`Offer`] as specified by [`InvoiceRequest::quantity`].
+       ///
+       /// Successive calls to this method will override the previous setting.
+       ///
+       /// [`Invoice`]: crate::offers::invoice::Invoice
+       /// [`InvoiceRequest::quantity`]: crate::offers::invoice_request::InvoiceRequest::quantity
+       /// [`Offer`]: crate::offers::offer::Offer
+       pub fn quantity(mut self, quantity: u64) -> Self {
+               self.refund.quantity = Some(quantity);
+               self
+       }
+
        /// Sets the [`Refund::payer_note`].
        ///
        /// Successive calls to this method will override the previous setting.
@@ -176,18 +234,38 @@ impl RefundBuilder {
                        self.refund.chain = None;
                }
 
+               // Create the metadata for stateless verification of an Invoice.
+               if self.refund.payer.0.has_derivation_material() {
+                       let mut metadata = core::mem::take(&mut self.refund.payer.0);
+
+                       if self.refund.paths.is_none() {
+                               metadata = metadata.without_keys();
+                       }
+
+                       let mut tlv_stream = self.refund.as_tlv_stream();
+                       tlv_stream.0.metadata = None;
+                       if metadata.derives_keys() {
+                               tlv_stream.2.payer_id = None;
+                       }
+
+                       let (derived_metadata, keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
+                       metadata = derived_metadata;
+                       if let Some(keys) = keys {
+                               self.refund.payer_id = keys.public_key();
+                       }
+
+                       self.refund.payer.0 = metadata;
+               }
+
                let mut bytes = Vec::new();
                self.refund.write(&mut bytes).unwrap();
 
-               Ok(Refund {
-                       bytes,
-                       contents: self.refund,
-               })
+               Ok(Refund { bytes, contents: self.refund })
        }
 }
 
 #[cfg(test)]
-impl RefundBuilder {
+impl<'a, T: secp256k1::Signing> RefundBuilder<'a, T> {
        fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self {
                self.refund.features = features;
                self
@@ -203,6 +281,7 @@ impl RefundBuilder {
 /// [`Invoice`]: crate::offers::invoice::Invoice
 /// [`Offer`]: crate::offers::offer::Offer
 #[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub struct Refund {
        pub(super) bytes: Vec<u8>,
        pub(super) contents: RefundContents,
@@ -212,10 +291,10 @@ pub struct Refund {
 ///
 /// [`Invoice`]: crate::offers::invoice::Invoice
 #[derive(Clone, Debug)]
+#[cfg_attr(test, derive(PartialEq))]
 pub(super) struct RefundContents {
        payer: PayerContents,
        // offer fields
-       metadata: Option<Vec<u8>>,
        description: String,
        absolute_expiry: Option<Duration>,
        issuer: Option<String>,
@@ -224,6 +303,7 @@ pub(super) struct RefundContents {
        chain: Option<ChainHash>,
        amount_msats: u64,
        features: InvoiceRequestFeatures,
+       quantity: Option<u64>,
        payer_id: PublicKey,
        payer_note: Option<String>,
 }
@@ -232,7 +312,7 @@ impl Refund {
        /// A complete description of the purpose of the refund. 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)
+               self.contents.description()
        }
 
        /// Duration since the Unix epoch when an invoice should no longer be sent.
@@ -265,7 +345,7 @@ impl Refund {
        ///
        /// [`payer_id`]: Self::payer_id
        pub fn metadata(&self) -> &[u8] {
-               &self.contents.payer.0
+               self.contents.metadata()
        }
 
        /// A chain that the refund is valid for.
@@ -285,6 +365,11 @@ impl Refund {
                &self.contents.features
        }
 
+       /// The quantity of an item that refund is for.
+       pub fn quantity(&self) -> Option<u64> {
+               self.contents.quantity
+       }
+
        /// A public node id to send to in the case where there are no [`paths`]. Otherwise, a possibly
        /// transient pubkey.
        ///
@@ -298,12 +383,32 @@ impl Refund {
                self.contents.payer_note.as_ref().map(|payer_note| PrintableString(payer_note.as_str()))
        }
 
-       /// Creates an [`Invoice`] for the refund with the given required fields.
+       /// Creates an [`InvoiceBuilder`] for the refund with the given required fields and using the
+       /// [`Duration`] since [`std::time::SystemTime::UNIX_EPOCH`] as the creation time.
+       ///
+       /// See [`Refund::respond_with_no_std`] for further details where the aforementioned creation
+       /// time is used for the `created_at` parameter.
+       ///
+       /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+       ///
+       /// [`Duration`]: core::time::Duration
+       #[cfg(feature = "std")]
+       pub fn respond_with(
+               &self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
+               signing_pubkey: PublicKey,
+       ) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
+               let created_at = std::time::SystemTime::now()
+                       .duration_since(std::time::SystemTime::UNIX_EPOCH)
+                       .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
+
+               self.respond_with_no_std(payment_paths, payment_hash, signing_pubkey, created_at)
+       }
+
+       /// Creates an [`InvoiceBuilder`] for the refund with the given required fields.
        ///
        /// Unless [`InvoiceBuilder::relative_expiry`] is set, the invoice will expire two hours after
-       /// calling this method in `std` builds. For `no-std` builds, a final [`Duration`] parameter
-       /// must be given, which is used to set [`Invoice::created_at`] since [`std::time::SystemTime`]
-       /// is not available.
+       /// `created_at`, which is used to set [`Invoice::created_at`]. Useful for `no-std` builds where
+       /// [`std::time::SystemTime`] is not available.
        ///
        /// The caller is expected to remember the preimage of `payment_hash` in order to
        /// claim a payment for the invoice.
@@ -312,28 +417,73 @@ impl Refund {
        /// offer, which does have a `signing_pubkey`.
        ///
        /// The `payment_paths` parameter is useful for maintaining the payment recipient's privacy. It
-       /// must contain one or more elements.
+       /// must contain one or more elements ordered from most-preferred to least-preferred, if there's
+       /// a preference. Note, however, that any privacy is lost if a public node id is used for
+       /// `signing_pubkey`.
        ///
        /// Errors if the request contains unknown required features.
        ///
-       /// [`Invoice`]: crate::offers::invoice::Invoice
+       /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+       ///
        /// [`Invoice::created_at`]: crate::offers::invoice::Invoice::created_at
-       pub fn respond_with(
-               &self, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, payment_hash: PaymentHash,
-               signing_pubkey: PublicKey,
-               #[cfg(any(test, not(feature = "std")))]
-               created_at: Duration
-       ) -> Result<InvoiceBuilder, SemanticError> {
+       pub fn respond_with_no_std(
+               &self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
+               signing_pubkey: PublicKey, created_at: Duration
+       ) -> Result<InvoiceBuilder<ExplicitSigningPubkey>, SemanticError> {
                if self.features().requires_unknown_bits() {
                        return Err(SemanticError::UnknownRequiredFeatures);
                }
 
-               #[cfg(all(not(test), feature = "std"))]
+               InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey)
+       }
+
+       /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses
+       /// derived signing keys to sign the [`Invoice`].
+       ///
+       /// See [`Refund::respond_with`] for further details.
+       ///
+       /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+       ///
+       /// [`Invoice`]: crate::offers::invoice::Invoice
+       #[cfg(feature = "std")]
+       pub fn respond_using_derived_keys<ES: Deref>(
+               &self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
+               expanded_key: &ExpandedKey, entropy_source: ES
+       ) -> Result<InvoiceBuilder<DerivedSigningPubkey>, SemanticError>
+       where
+               ES::Target: EntropySource,
+       {
                let created_at = std::time::SystemTime::now()
                        .duration_since(std::time::SystemTime::UNIX_EPOCH)
                        .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
 
-               InvoiceBuilder::for_refund(self, payment_paths, created_at, payment_hash, signing_pubkey)
+               self.respond_using_derived_keys_no_std(
+                       payment_paths, payment_hash, created_at, expanded_key, entropy_source
+               )
+       }
+
+       /// Creates an [`InvoiceBuilder`] for the refund using the given required fields and that uses
+       /// derived signing keys to sign the [`Invoice`].
+       ///
+       /// See [`Refund::respond_with_no_std`] for further details.
+       ///
+       /// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+       ///
+       /// [`Invoice`]: crate::offers::invoice::Invoice
+       pub fn respond_using_derived_keys_no_std<ES: Deref>(
+               &self, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, payment_hash: PaymentHash,
+               created_at: core::time::Duration, expanded_key: &ExpandedKey, entropy_source: ES
+       ) -> Result<InvoiceBuilder<DerivedSigningPubkey>, SemanticError>
+       where
+               ES::Target: EntropySource,
+       {
+               if self.features().requires_unknown_bits() {
+                       return Err(SemanticError::UnknownRequiredFeatures);
+               }
+
+               let nonce = Nonce::from_entropy_source(entropy_source);
+               let keys = signer::derive_keys(nonce, expanded_key);
+               InvoiceBuilder::for_refund_using_keys(self, payment_paths, created_at, payment_hash, keys)
        }
 
        #[cfg(test)]
@@ -349,6 +499,10 @@ impl AsRef<[u8]> for Refund {
 }
 
 impl RefundContents {
+       pub fn description(&self) -> PrintableString {
+               PrintableString(&self.description)
+       }
+
        #[cfg(feature = "std")]
        pub(super) fn is_expired(&self) -> bool {
                match self.absolute_expiry {
@@ -360,6 +514,10 @@ impl RefundContents {
                }
        }
 
+       pub(super) fn metadata(&self) -> &[u8] {
+               self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
+       }
+
        pub(super) fn chain(&self) -> ChainHash {
                self.chain.unwrap_or_else(|| self.implied_chain())
        }
@@ -368,14 +526,22 @@ impl RefundContents {
                ChainHash::using_genesis_block(Network::Bitcoin)
        }
 
+       pub(super) fn derives_keys(&self) -> bool {
+               self.payer.0.derives_keys()
+       }
+
+       pub(super) fn payer_id(&self) -> PublicKey {
+               self.payer_id
+       }
+
        pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
                let payer = PayerTlvStreamRef {
-                       metadata: Some(&self.payer.0),
+                       metadata: self.payer.0.as_bytes(),
                };
 
                let offer = OfferTlvStreamRef {
                        chains: None,
-                       metadata: self.metadata.as_ref(),
+                       metadata: None,
                        currency: None,
                        amount: None,
                        description: Some(&self.description),
@@ -396,7 +562,7 @@ impl RefundContents {
                        chain: self.chain.as_ref(),
                        amount: Some(self.amount_msats),
                        features,
-                       quantity: None,
+                       quantity: self.quantity,
                        payer_id: Some(&self.payer_id),
                        payer_note: self.payer_note.as_ref(),
                };
@@ -474,9 +640,13 @@ impl TryFrom<RefundTlvStream> for RefundContents {
 
                let payer = match payer_metadata {
                        None => return Err(SemanticError::MissingPayerMetadata),
-                       Some(metadata) => PayerContents(metadata),
+                       Some(metadata) => PayerContents(Metadata::Bytes(metadata)),
                };
 
+               if metadata.is_some() {
+                       return Err(SemanticError::UnexpectedMetadata);
+               }
+
                if chains.is_some() {
                        return Err(SemanticError::UnexpectedChain);
                }
@@ -514,20 +684,14 @@ impl TryFrom<RefundTlvStream> for RefundContents {
 
                let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
 
-               // TODO: Check why this isn't in the spec.
-               if quantity.is_some() {
-                       return Err(SemanticError::UnexpectedQuantity);
-               }
-
                let payer_id = match payer_id {
                        None => return Err(SemanticError::MissingPayerId),
                        Some(payer_id) => payer_id,
                };
 
-               // TODO: Should metadata be included?
                Ok(RefundContents {
-                       payer, metadata, description, absolute_expiry, issuer, paths, chain, amount_msats,
-                       features, payer_id, payer_note,
+                       payer, description, absolute_expiry, issuer, paths, chain, amount_msats, features,
+                       quantity, payer_id, payer_note,
                })
        }
 }
@@ -544,33 +708,22 @@ mod tests {
 
        use bitcoin::blockdata::constants::ChainHash;
        use bitcoin::network::constants::Network;
-       use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey};
+       use bitcoin::secp256k1::{KeyPair, Secp256k1, SecretKey};
        use core::convert::TryFrom;
        use core::time::Duration;
+       use crate::blinded_path::{BlindedHop, BlindedPath};
+       use crate::sign::KeyMaterial;
        use crate::ln::features::{InvoiceRequestFeatures, OfferFeatures};
+       use crate::ln::inbound_payment::ExpandedKey;
        use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
        use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
        use crate::offers::offer::OfferTlvStreamRef;
        use crate::offers::parse::{ParseError, SemanticError};
        use crate::offers::payer::PayerTlvStreamRef;
-       use crate::onion_message::{BlindedHop, BlindedPath};
+       use crate::offers::test_utils::*;
        use crate::util::ser::{BigSize, Writeable};
        use crate::util::string::PrintableString;
 
-       fn payer_pubkey() -> PublicKey {
-               let secp_ctx = Secp256k1::new();
-               KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()).public_key()
-       }
-
-       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()
-       }
-
        trait ToBytes {
                fn to_bytes(&self) -> Vec<u8>;
        }
@@ -646,6 +799,118 @@ mod tests {
                }
        }
 
+       #[test]
+       fn builds_refund_with_metadata_derived() {
+               let desc = "foo".to_string();
+               let node_id = payer_pubkey();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let refund = RefundBuilder
+                       ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000)
+                       .unwrap()
+                       .build().unwrap();
+               assert_eq!(refund.payer_id(), node_id);
+
+               // Fails verification with altered fields
+               let invoice = refund
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(invoice.verify(&expanded_key, &secp_ctx));
+
+               let mut tlv_stream = refund.as_tlv_stream();
+               tlv_stream.2.amount = Some(2000);
+
+               let mut encoded_refund = Vec::new();
+               tlv_stream.write(&mut encoded_refund).unwrap();
+
+               let invoice = Refund::try_from(encoded_refund).unwrap()
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered metadata
+               let mut tlv_stream = refund.as_tlv_stream();
+               let metadata = tlv_stream.0.metadata.unwrap().iter().copied().rev().collect();
+               tlv_stream.0.metadata = Some(&metadata);
+
+               let mut encoded_refund = Vec::new();
+               tlv_stream.write(&mut encoded_refund).unwrap();
+
+               let invoice = Refund::try_from(encoded_refund).unwrap()
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+       }
+
+       #[test]
+       fn builds_refund_with_derived_payer_id() {
+               let desc = "foo".to_string();
+               let node_id = payer_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(43), encrypted_payload: vec![0; 43] },
+                               BlindedHop { blinded_node_id: node_id, encrypted_payload: vec![0; 44] },
+                       ],
+               };
+
+               let refund = RefundBuilder
+                       ::deriving_payer_id(desc, node_id, &expanded_key, &entropy, &secp_ctx, 1000)
+                       .unwrap()
+                       .path(blinded_path)
+                       .build().unwrap();
+               assert_ne!(refund.payer_id(), node_id);
+
+               let invoice = refund
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered fields
+               let mut tlv_stream = refund.as_tlv_stream();
+               tlv_stream.2.amount = Some(2000);
+
+               let mut encoded_refund = Vec::new();
+               tlv_stream.write(&mut encoded_refund).unwrap();
+
+               let invoice = Refund::try_from(encoded_refund).unwrap()
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+
+               // Fails verification with altered payer_id
+               let mut tlv_stream = refund.as_tlv_stream();
+               let payer_id = pubkey(1);
+               tlv_stream.2.payer_id = Some(&payer_id);
+
+               let mut encoded_refund = Vec::new();
+               tlv_stream.write(&mut encoded_refund).unwrap();
+
+               let invoice = Refund::try_from(encoded_refund).unwrap()
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+                       .unwrap()
+                       .build().unwrap()
+                       .sign(recipient_sign).unwrap();
+               assert!(!invoice.verify(&expanded_key, &secp_ctx));
+       }
+
        #[test]
        fn builds_refund_with_absolute_expiry() {
                let future_expiry = Duration::from_secs(u64::max_value());
@@ -755,6 +1020,24 @@ mod tests {
                assert_eq!(tlv_stream.chain, Some(&testnet));
        }
 
+       #[test]
+       fn builds_refund_with_quantity() {
+               let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+                       .quantity(10)
+                       .build().unwrap();
+               let (_, _, tlv_stream) = refund.as_tlv_stream();
+               assert_eq!(refund.quantity(), Some(10));
+               assert_eq!(tlv_stream.quantity, Some(10));
+
+               let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+                       .quantity(10)
+                       .quantity(1)
+                       .build().unwrap();
+               let (_, _, tlv_stream) = refund.as_tlv_stream();
+               assert_eq!(refund.quantity(), Some(1));
+               assert_eq!(tlv_stream.quantity, Some(1));
+       }
+
        #[test]
        fn builds_refund_with_payer_note() {
                let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
@@ -773,6 +1056,18 @@ mod tests {
                assert_eq!(tlv_stream.payer_note, Some(&String::from("baz")));
        }
 
+       #[test]
+       fn fails_responding_with_unknown_required_features() {
+               match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+                       .features_unchecked(InvoiceRequestFeatures::unknown())
+                       .build().unwrap()
+                       .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
+               {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, SemanticError::UnknownRequiredFeatures),
+               }
+       }
+
        #[test]
        fn parses_refund_with_metadata() {
                let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
@@ -888,6 +1183,7 @@ mod tests {
                        .path(paths[1].clone())
                        .chain(Network::Testnet)
                        .features_unchecked(InvoiceRequestFeatures::unknown())
+                       .quantity(10)
                        .payer_note("baz".into())
                        .build()
                        .unwrap();
@@ -900,6 +1196,7 @@ mod tests {
                                assert_eq!(refund.issuer(), Some(PrintableString("bar")));
                                assert_eq!(refund.chain(), ChainHash::using_genesis_block(Network::Testnet));
                                assert_eq!(refund.features(), &InvoiceRequestFeatures::unknown());
+                               assert_eq!(refund.quantity(), Some(10));
                                assert_eq!(refund.payer_note(), Some(PrintableString("baz")));
                        },
                        Err(e) => panic!("error parsing refund: {:?}", e),
@@ -914,6 +1211,17 @@ mod tests {
                        panic!("error parsing refund: {:?}", e);
                }
 
+               let metadata = vec![42; 32];
+               let mut tlv_stream = refund.as_tlv_stream();
+               tlv_stream.1.metadata = Some(&metadata);
+
+               match Refund::try_from(tlv_stream.to_bytes()) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedMetadata));
+                       },
+               }
+
                let chains = vec![ChainHash::using_genesis_block(Network::Testnet)];
                let mut tlv_stream = refund.as_tlv_stream();
                tlv_stream.1.chains = Some(&chains);
@@ -967,16 +1275,6 @@ mod tests {
                                assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedSigningPubkey));
                        },
                }
-
-               let mut tlv_stream = refund.as_tlv_stream();
-               tlv_stream.2.quantity = Some(10);
-
-               match Refund::try_from(tlv_stream.to_bytes()) {
-                       Ok(_) => panic!("expected error"),
-                       Err(e) => {
-                               assert_eq!(e, ParseError::InvalidSemantics(SemanticError::UnexpectedQuantity));
-                       },
-               }
        }
 
        #[test]