X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Finvoice.rs;h=7d24c2e46a994c54789d30b5dfdb495825e85f66;hb=8d7615b6a3255f75b5a61c66e029add83b3b7da8;hp=57d37a17b8726c75729e4ff915780cee65853ae5;hpb=2298af4d0b008d844eed12444948339ba7557de7;p=rust-lightning diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 57d37a17..7d24c2e4 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -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, invoice: InvoiceContents, + keys: Option, + signing_pubkey_strategy: core::marker::PhantomData, } -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 { - 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 { + 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 { + 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, contents: InvoiceContents) -> Result { + pub(super) fn for_refund_using_keys( + refund: &'a Refund, payment_paths: Vec<(BlindedPath, BlindedPayInfo)>, created_at: Duration, + payment_hash: PaymentHash, keys: KeyPair, + ) -> Result { + 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 { + 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, contents: InvoiceContents, keys: Option + ) -> Result { 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, 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( + self, secp_ctx: &Secp256k1 + ) -> Result { + #[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, @@ -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(self, sign: F) -> Result> where F: FnOnce(&Message) -> Result @@ -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
{ 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( &self, tlv_stream: TlvStream<'_>, key: &ExpandedKey, secp_ctx: &Secp256k1 ) -> 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; @@ -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();