From e523e581522e3c1e1a13a79f339931b5662ca13f Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 4 Oct 2021 11:39:59 -0500 Subject: [PATCH] Support paying zero-value invoices --- lightning-invoice/src/payment.rs | 91 ++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index 61a0e350a..e639b6799 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -197,6 +197,29 @@ where /// Pays the given [`Invoice`], caching it for later use in case a retry is needed. pub fn pay_invoice(&self, invoice: &Invoice) -> Result { + if invoice.amount_milli_satoshis().is_none() { + Err(PaymentError::Invoice("amount missing")) + } else { + self.pay_invoice_internal(invoice, None) + } + } + + /// Pays the given zero-value [`Invoice`] using the given amount, caching it for later use in + /// case a retry is needed. + pub fn pay_zero_value_invoice( + &self, invoice: &Invoice, amount_msats: u64 + ) -> Result { + if invoice.amount_milli_satoshis().is_some() { + Err(PaymentError::Invoice("amount unexpected")) + } else { + self.pay_invoice_internal(invoice, Some(amount_msats)) + } + } + + fn pay_invoice_internal( + &self, invoice: &Invoice, amount_msats: Option + ) -> Result { + debug_assert!(invoice.amount_milli_satoshis().is_some() ^ amount_msats.is_some()); let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner()); let mut payment_cache = self.payment_cache.lock().unwrap(); match payment_cache.entry(payment_hash) { @@ -207,11 +230,9 @@ where if let Some(features) = invoice.features() { payee = payee.with_features(features.clone()); } - let final_value_msat = invoice.amount_milli_satoshis() - .ok_or(PaymentError::Invoice("amount missing"))?; let params = RouteParameters { payee, - final_value_msat, + final_value_msat: invoice.amount_milli_satoshis().or(amount_msats).unwrap(), final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32, }; let first_hops = self.payer.first_hops(); @@ -342,6 +363,21 @@ mod tests { .unwrap() } + fn zero_value_invoice(payment_preimage: PaymentPreimage) -> Invoice { + let payment_hash = Sha256::hash(&payment_preimage.0); + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + InvoiceBuilder::new(Currency::Bitcoin) + .description("test".into()) + .payment_hash(payment_hash) + .payment_secret(PaymentSecret([0; 32])) + .current_timestamp() + .min_final_cltv_expiry(144) + .build_signed(|hash| { + Secp256k1::new().sign_recoverable(hash, &private_key) + }) + .unwrap() + } + #[test] fn pays_invoice_on_first_attempt() { let event_handled = core::cell::RefCell::new(false); @@ -681,6 +717,55 @@ mod tests { } } + #[test] + fn pays_zero_value_invoice_using_amount() { + let event_handled = core::cell::RefCell::new(false); + let event_handler = |_: &_| { *event_handled.borrow_mut() = true; }; + + let payment_preimage = PaymentPreimage([1; 32]); + let invoice = zero_value_invoice(payment_preimage); + let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner()); + let final_value_msat = 100; + + let payer = TestPayer::new().expect_value_msat(final_value_msat); + let router = TestRouter {}; + let logger = TestLogger::new(); + let invoice_payer = + InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(0)); + + let payment_id = + Some(invoice_payer.pay_zero_value_invoice(&invoice, final_value_msat).unwrap()); + assert_eq!(*payer.attempts.borrow(), 1); + + invoice_payer.handle_event(&Event::PaymentSent { + payment_id, payment_preimage, payment_hash + }); + assert_eq!(*event_handled.borrow(), true); + assert_eq!(*payer.attempts.borrow(), 1); + } + + #[test] + fn fails_paying_zero_value_invoice_with_amount() { + let event_handled = core::cell::RefCell::new(false); + let event_handler = |_: &_| { *event_handled.borrow_mut() = true; }; + + let payer = TestPayer::new(); + let router = TestRouter {}; + let logger = TestLogger::new(); + let invoice_payer = + InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(0)); + + let payment_preimage = PaymentPreimage([1; 32]); + let invoice = invoice(payment_preimage); + + // Cannot repay an invoice pending payment. + match invoice_payer.pay_zero_value_invoice(&invoice, 100) { + Err(PaymentError::Invoice("amount unexpected")) => {}, + Err(_) => panic!("unexpected error"), + Ok(_) => panic!("expected invoice error"), + } + } + struct TestRouter; impl TestRouter { -- 2.39.5