Add some no-exporting of more offers code
[rust-lightning] / lightning / src / offers / invoice.rs
index 57d37a17b8726c75729e4ff915780cee65853ae5..7d24c2e46a994c54789d30b5dfdb495825e85f66 100644 (file)
@@ -29,7 +29,7 @@
 //!
 //! # use lightning::ln::PaymentHash;
 //! # use lightning::offers::invoice::BlindedPayInfo;
-//! # use lightning::onion_message::BlindedPath;
+//! # use lightning::blinded_path::BlindedPath;
 //! #
 //! # fn create_payment_paths() -> Vec<(BlindedPath, BlindedPayInfo)> { unimplemented!() }
 //! # fn create_payment_hash() -> PaymentHash { unimplemented!() }
@@ -97,25 +97,27 @@ use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::hash_types::{WPubkeyHash, WScriptHash};
 use bitcoin::hashes::Hash;
 use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
+use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self};
 use bitcoin::secp256k1::schnorr::Signature;
 use bitcoin::util::address::{Address, Payload, WitnessVersion};
 use bitcoin::util::schnorr::TweakedPublicKey;
-use core::convert::TryFrom;
+use core::convert::{Infallible, TryFrom};
 use core::time::Duration;
 use crate::io;
+use crate::blinded_path::BlindedPath;
 use crate::ln::PaymentHash;
 use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
 use crate::ln::inbound_payment::ExpandedKey;
 use crate::ln::msgs::DecodeError;
-use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
+use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
 use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, WithoutSignatures, self};
-use crate::offers::offer::{Amount, OfferTlvStream, OfferTlvStreamRef};
+use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
-use crate::offers::payer::{PayerTlvStream, PayerTlvStreamRef};
-use crate::offers::refund::{Refund, RefundContents};
-use crate::onion_message::BlindedPath;
+use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef};
+use crate::offers::refund::{IV_BYTES as REFUND_IV_BYTES, Refund, RefundContents};
+use crate::offers::signer;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer};
+use crate::util::string::PrintableString;
 
 use crate::prelude::*;
 
@@ -132,65 +134,141 @@ pub(super) const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "
 ///
 /// See [module-level documentation] for usage.
 ///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 /// [`Refund`]: crate::offers::refund::Refund
 /// [module-level documentation]: self
-pub struct InvoiceBuilder<'a> {
+pub struct InvoiceBuilder<'a, S: SigningPubkeyStrategy> {
        invreq_bytes: &'a Vec<u8>,
        invoice: InvoiceContents,
+       keys: Option<KeyPair>,
+       signing_pubkey_strategy: core::marker::PhantomData<S>,
 }
 
-impl<'a> InvoiceBuilder<'a> {
+/// Indicates how [`Invoice::signing_pubkey`] was set.
+///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+pub trait SigningPubkeyStrategy {}
+
+/// [`Invoice::signing_pubkey`] was explicitly set.
+///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+pub struct ExplicitSigningPubkey {}
+
+/// [`Invoice::signing_pubkey`] was derived.
+///
+/// This is not exported to bindings users as builder patterns don't map outside of move semantics.
+pub struct DerivedSigningPubkey {}
+
+impl SigningPubkeyStrategy for ExplicitSigningPubkey {}
+impl SigningPubkeyStrategy for DerivedSigningPubkey {}
+
+impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> {
        pub(super) fn for_offer(
                invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
                created_at: Duration, payment_hash: PaymentHash
        ) -> Result<Self, SemanticError> {
-               let amount_msats = match invoice_request.amount_msats() {
-                       Some(amount_msats) => amount_msats,
-                       None => match invoice_request.contents.inner.offer.amount() {
-                               Some(Amount::Bitcoin { amount_msats }) => {
-                                       amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1))
-                                               .ok_or(SemanticError::InvalidAmount)?
-                               },
-                               Some(Amount::Currency { .. }) => return Err(SemanticError::UnsupportedCurrency),
-                               None => return Err(SemanticError::MissingAmount),
-                       },
-               };
-
+               let amount_msats = Self::check_amount_msats(invoice_request)?;
+               let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey();
                let contents = InvoiceContents::ForOffer {
                        invoice_request: invoice_request.contents.clone(),
-                       fields: InvoiceFields {
-                               payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
-                               fallbacks: None, features: Bolt12InvoiceFeatures::empty(),
-                               signing_pubkey: invoice_request.contents.inner.offer.signing_pubkey(),
-                       },
+                       fields: Self::fields(
+                               payment_paths, created_at, payment_hash, amount_msats, signing_pubkey
+                       ),
                };
 
-               Self::new(&invoice_request.bytes, contents)
+               Self::new(&invoice_request.bytes, contents, None)
        }
 
        pub(super) fn for_refund(
                refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
                payment_hash: PaymentHash, signing_pubkey: PublicKey
        ) -> Result<Self, SemanticError> {
+               let amount_msats = refund.amount_msats();
                let contents = InvoiceContents::ForRefund {
                        refund: refund.contents.clone(),
-                       fields: InvoiceFields {
-                               payment_paths, created_at, relative_expiry: None, payment_hash,
-                               amount_msats: refund.amount_msats(), fallbacks: None,
-                               features: Bolt12InvoiceFeatures::empty(), signing_pubkey,
-                       },
+                       fields: Self::fields(
+                               payment_paths, created_at, payment_hash, amount_msats, signing_pubkey
+                       ),
+               };
+
+               Self::new(&refund.bytes, contents, None)
+       }
+}
+
+impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
+       pub(super) fn for_offer_using_keys(
+               invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
+               created_at: Duration, payment_hash: PaymentHash, keys: KeyPair
+       ) -> Result<Self, SemanticError> {
+               let amount_msats = Self::check_amount_msats(invoice_request)?;
+               let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey();
+               let contents = InvoiceContents::ForOffer {
+                       invoice_request: invoice_request.contents.clone(),
+                       fields: Self::fields(
+                               payment_paths, created_at, payment_hash, amount_msats, signing_pubkey
+                       ),
                };
 
-               Self::new(&refund.bytes, contents)
+               Self::new(&invoice_request.bytes, contents, Some(keys))
        }
 
-       fn new(invreq_bytes: &'a Vec<u8>, contents: InvoiceContents) -> Result<Self, SemanticError> {
+       pub(super) fn for_refund_using_keys(
+               refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
+               payment_hash: PaymentHash, keys: KeyPair,
+       ) -> Result<Self, SemanticError> {
+               let amount_msats = refund.amount_msats();
+               let signing_pubkey = keys.public_key();
+               let contents = InvoiceContents::ForRefund {
+                       refund: refund.contents.clone(),
+                       fields: Self::fields(
+                               payment_paths, created_at, payment_hash, amount_msats, signing_pubkey
+                       ),
+               };
+
+               Self::new(&refund.bytes, contents, Some(keys))
+       }
+}
+
+impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> {
+       fn check_amount_msats(invoice_request: &InvoiceRequest) -> Result<u64, SemanticError> {
+               match invoice_request.amount_msats() {
+                       Some(amount_msats) => Ok(amount_msats),
+                       None => match invoice_request.contents.inner.offer.amount() {
+                               Some(Amount::Bitcoin { amount_msats }) => {
+                                       amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1))
+                                               .ok_or(SemanticError::InvalidAmount)
+                               },
+                               Some(Amount::Currency { .. }) => Err(SemanticError::UnsupportedCurrency),
+                               None => Err(SemanticError::MissingAmount),
+                       },
+               }
+       }
+
+       fn fields(
+               payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration,
+               payment_hash: PaymentHash, amount_msats: u64, signing_pubkey: PublicKey
+       ) -> InvoiceFields {
+               InvoiceFields {
+                       payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
+                       fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey,
+               }
+       }
+
+       fn new(
+               invreq_bytes: &'a Vec<u8>, contents: InvoiceContents, keys: Option<KeyPair>
+       ) -> Result<Self, SemanticError> {
                if contents.fields().payment_paths.is_empty() {
                        return Err(SemanticError::MissingPaths);
                }
 
-               Ok(Self { invreq_bytes, invoice: contents })
+               Ok(Self {
+                       invreq_bytes,
+                       invoice: contents,
+                       keys,
+                       signing_pubkey_strategy: core::marker::PhantomData,
+               })
        }
 
        /// Sets the [`Invoice::relative_expiry`] as seconds since [`Invoice::created_at`]. Any expiry
@@ -233,6 +311,8 @@ impl<'a> InvoiceBuilder<'a> {
        ///
        /// Successive calls to this method will add another address. Caller is responsible for not
        /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses.
+       ///
+       /// This is not exported to bindings users as TweakedPublicKey isn't yet mapped.
        pub fn fallback_v1_p2tr_tweaked(mut self, output_key: &TweakedPublicKey) -> Self {
                let address = FallbackAddress {
                        version: WitnessVersion::V1.to_num(),
@@ -247,7 +327,9 @@ impl<'a> InvoiceBuilder<'a> {
                self.invoice.fields_mut().features.set_basic_mpp_optional();
                self
        }
+}
 
+impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> {
        /// Builds an unsigned [`Invoice`] after checking for valid semantics. It can be signed by
        /// [`UnsignedInvoice::sign`].
        pub fn build(self) -> Result<UnsignedInvoice<'a>, SemanticError> {
@@ -257,11 +339,33 @@ impl<'a> InvoiceBuilder<'a> {
                        }
                }
 
-               let InvoiceBuilder { invreq_bytes, invoice } = self;
+               let InvoiceBuilder { invreq_bytes, invoice, .. } = self;
                Ok(UnsignedInvoice { invreq_bytes, invoice })
        }
 }
 
+impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> {
+       /// Builds a signed [`Invoice`] after checking for valid semantics.
+       pub fn build_and_sign<T: secp256k1::Signing>(
+               self, secp_ctx: &Secp256k1<T>
+       ) -> Result<Invoice, SemanticError> {
+               #[cfg(feature = "std")] {
+                       if self.invoice.is_offer_or_refund_expired() {
+                               return Err(SemanticError::AlreadyExpired);
+                       }
+               }
+
+               let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self;
+               let unsigned_invoice = UnsignedInvoice { invreq_bytes, invoice };
+
+               let keys = keys.unwrap();
+               let invoice = unsigned_invoice
+                       .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
+                       .unwrap();
+               Ok(invoice)
+       }
+}
+
 /// A semantically valid [`Invoice`] that hasn't been signed.
 pub struct UnsignedInvoice<'a> {
        invreq_bytes: &'a Vec<u8>,
@@ -275,6 +379,8 @@ impl<'a> UnsignedInvoice<'a> {
        }
 
        /// Signs the invoice using the given function.
+       ///
+       /// This is not exported to bindings users as functions aren't currently mapped.
        pub fn sign<F, E>(self, sign: F) -> Result<Invoice, SignError<E>>
        where
                F: FnOnce(&Message) -> Result<Signature, E>
@@ -311,6 +417,8 @@ impl<'a> UnsignedInvoice<'a> {
 /// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
 /// directly after scanning a refund. It includes all the information needed to pay a recipient.
 ///
+/// This is not exported to bindings users as its name conflicts with the BOLT 11 Invoice type.
+///
 /// [`Offer`]: crate::offers::offer::Offer
 /// [`Refund`]: crate::offers::refund::Refund
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
@@ -359,11 +467,19 @@ struct InvoiceFields {
 }
 
 impl Invoice {
+       /// A complete description of the purpose of the originating offer or 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 {
+               self.contents.description()
+       }
+
        /// Paths to the recipient originating from publicly reachable nodes, including information
        /// needed for routing payments across them.
        ///
        /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this
        /// privacy is lost if a public node id is used for [`Invoice::signing_pubkey`].
+       ///
+       /// This is not exported to bindings users as a slice of tuples isn't exportable.
        pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
                &self.contents.fields().payment_paths[..]
        }
@@ -404,6 +520,8 @@ impl Invoice {
 
        /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to
        /// least-preferred.
+       ///
+       /// This is not exported to bindings users as Address is not yet mapped
        pub fn fallbacks(&self) -> Vec<Address> {
                let network = match self.network() {
                        None => return Vec::new(),
@@ -468,6 +586,8 @@ impl Invoice {
        }
 
        /// Signature of the invoice verified using [`Invoice::signing_pubkey`].
+       ///
+       /// This is not exported to bindings users as SIgnature is not yet mapped.
        pub fn signature(&self) -> Signature {
                self.signature
        }
@@ -514,6 +634,15 @@ impl InvoiceContents {
                }
        }
 
+       fn description(&self) -> PrintableString {
+               match self {
+                       InvoiceContents::ForOffer { invoice_request, .. } => {
+                               invoice_request.inner.offer.description()
+                       },
+                       InvoiceContents::ForRefund { refund, .. } => refund.description(),
+               }
+       }
+
        fn fields(&self) -> &InvoiceFields {
                match self {
                        InvoiceContents::ForOffer { fields, .. } => fields,
@@ -531,13 +660,35 @@ impl InvoiceContents {
        fn verify<T: secp256k1::Signing>(
                &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
        ) -> bool {
-               match self {
+               let offer_records = tlv_stream.clone().range(OFFER_TYPES);
+               let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
+                       match record.r#type {
+                               PAYER_METADATA_TYPE => false, // Should be outside range
+                               INVOICE_REQUEST_PAYER_ID_TYPE => !self.derives_keys(),
+                               _ => true,
+                       }
+               });
+               let tlv_stream = offer_records.chain(invreq_records);
+
+               let (metadata, payer_id, iv_bytes) = match self {
                        InvoiceContents::ForOffer { invoice_request, .. } => {
-                               invoice_request.verify(tlv_stream, key, secp_ctx)
+                               (invoice_request.metadata(), invoice_request.payer_id(), INVOICE_REQUEST_IV_BYTES)
                        },
                        InvoiceContents::ForRefund { refund, .. } => {
-                               refund.verify(tlv_stream, key, secp_ctx)
+                               (refund.metadata(), refund.payer_id(), REFUND_IV_BYTES)
                        },
+               };
+
+               match signer::verify_metadata(metadata, key, iv_bytes, payer_id, tlv_stream, secp_ctx) {
+                       Ok(_) => true,
+                       Err(()) => false,
+               }
+       }
+
+       fn derives_keys(&self) -> bool {
+               match self {
+                       InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.derives_keys(),
+                       InvoiceContents::ForRefund { refund, .. } => refund.derives_keys(),
                }
        }
 
@@ -617,7 +768,7 @@ type BlindedPayInfoIter<'a> = core::iter::Map<
 >;
 
 /// Information needed to route a payment across a [`BlindedPath`].
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Hash, Eq, PartialEq)]
 pub struct BlindedPayInfo {
        /// Base fee charged (in millisatoshi) for the entire blinded path.
        pub fee_base_msat: u32,
@@ -811,8 +962,11 @@ mod tests {
        use bitcoin::util::schnorr::TweakedPublicKey;
        use core::convert::TryFrom;
        use core::time::Duration;
-       use crate::ln::msgs::DecodeError;
+       use crate::blinded_path::{BlindedHop, BlindedPath};
+       use crate::chain::keysinterface::KeyMaterial;
        use crate::ln::features::Bolt12InvoiceFeatures;
+       use crate::ln::inbound_payment::ExpandedKey;
+       use crate::ln::msgs::DecodeError;
        use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
        use crate::offers::merkle::{SignError, SignatureTlvStreamRef, self};
        use crate::offers::offer::{OfferBuilder, OfferTlvStreamRef, Quantity};
@@ -821,6 +975,7 @@ mod tests {
        use crate::offers::refund::RefundBuilder;
        use crate::offers::test_utils::*;
        use crate::util::ser::{BigSize, Iterable, Writeable};
+       use crate::util::string::PrintableString;
 
        trait ToBytes {
                fn to_bytes(&self) -> Vec<u8>;
@@ -857,6 +1012,7 @@ mod tests {
                invoice.write(&mut buffer).unwrap();
 
                assert_eq!(invoice.bytes, buffer.as_slice());
+               assert_eq!(invoice.description(), PrintableString("foo"));
                assert_eq!(invoice.payment_paths(), payment_paths.as_slice());
                assert_eq!(invoice.created_at(), now);
                assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY);
@@ -939,6 +1095,7 @@ mod tests {
                invoice.write(&mut buffer).unwrap();
 
                assert_eq!(invoice.bytes, buffer.as_slice());
+               assert_eq!(invoice.description(), PrintableString("foo"));
                assert_eq!(invoice.payment_paths(), payment_paths.as_slice());
                assert_eq!(invoice.created_at(), now);
                assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY);
@@ -1064,6 +1221,87 @@ mod tests {
                }
        }
 
+       #[test]
+       fn builds_invoice_from_offer_using_derived_keys() {
+               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();
+               let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+
+               if let Err(e) = invoice_request
+                       .verify_and_respond_using_derived_keys_no_std(
+                               payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
+                       )
+                       .unwrap()
+                       .build_and_sign(&secp_ctx)
+               {
+                       panic!("error building invoice: {:?}", e);
+               }
+
+               let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32]));
+               match invoice_request.verify_and_respond_using_derived_keys_no_std(
+                       payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
+               ) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, SemanticError::InvalidMetadata),
+               }
+
+               let desc = "foo".to_string();
+               let offer = OfferBuilder
+                       ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx)
+                       .amount_msats(1000)
+                       .build().unwrap();
+               let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
+                       .build().unwrap()
+                       .sign(payer_sign).unwrap();
+
+               match invoice_request.verify_and_respond_using_derived_keys_no_std(
+                       payment_paths(), payment_hash(), now(), &expanded_key, &secp_ctx
+               ) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, SemanticError::InvalidMetadata),
+               }
+       }
+
+       #[test]
+       fn builds_invoice_from_refund_using_derived_keys() {
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap()
+                       .build().unwrap();
+
+               if let Err(e) = refund
+                       .respond_using_derived_keys_no_std(
+                               payment_paths(), payment_hash(), now(), &expanded_key, &entropy
+                       )
+                       .unwrap()
+                       .build_and_sign(&secp_ctx)
+               {
+                       panic!("error building invoice: {:?}", e);
+               }
+       }
+
        #[test]
        fn builds_invoice_with_relative_expiry() {
                let now = now();