diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ca3f0028f3a..aac26a7cfb8 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -133,7 +133,7 @@ impl Router for FuzzRouter { fn create_blinded_payment_paths( &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, - _amount_msats: u64, _secp_ctx: &Secp256k1, + _amount_msats: Option, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index c1f2dd11b1e..a11dae31baf 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -160,7 +160,7 @@ impl Router for FuzzRouter { fn create_blinded_payment_paths( &self, _recipient: PublicKey, _first_hops: Vec, _tlvs: ReceiveTlvs, - _amount_msats: u64, _secp_ctx: &Secp256k1, + _amount_msats: Option, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 4d96434dd63..51494e1b21e 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -402,6 +402,24 @@ pub enum AsyncPaymentsContext { /// containing the expected [`PaymentId`]. hmac: Hmac, }, + /// Context contained within the [`BlindedMessagePath`]s we put in static invoices, provided back + /// to us in corresponding [`HeldHtlcAvailable`] messages. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + InboundPayment { + /// A nonce used for authenticating that a [`HeldHtlcAvailable`] message is valid for a + /// preceding static invoice. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + nonce: Nonce, + /// Authentication code for the [`HeldHtlcAvailable`] message. + /// + /// Prevents nodes from creating their own blinded path to us, sending a [`HeldHtlcAvailable`] + /// message and trivially getting notified whenever we come online. + /// + /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable + hmac: Hmac, + }, } impl_writeable_tlv_based_enum!(MessageContext, @@ -433,6 +451,10 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, nonce, required), (4, hmac, required), }, + (1, InboundPayment) => { + (0, nonce, required), + (2, hmac, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index e3a81927146..cf5af7d784e 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -349,6 +349,11 @@ pub enum PaymentContext { /// [`Offer`]: crate::offers::offer::Offer Bolt12Offer(Bolt12OfferContext), + /// The payment was made for a static invoice requested from a BOLT 12 [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + AsyncBolt12Offer(AsyncBolt12OfferContext), + /// The payment was made for an invoice sent for a BOLT 12 [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund @@ -378,6 +383,18 @@ pub struct Bolt12OfferContext { pub invoice_request: InvoiceRequestFields, } +/// The context of a payment made for a static invoice requested from a BOLT 12 [`Offer`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AsyncBolt12OfferContext { + /// The [`Nonce`] used to verify that an inbound [`InvoiceRequest`] corresponds to this static + /// invoice's offer. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + pub offer_nonce: Nonce, +} + /// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund @@ -627,6 +644,7 @@ impl_writeable_tlv_based_enum_legacy!(PaymentContext, // 0 for Unknown removed in version 0.1. (1, Bolt12Offer), (2, Bolt12Refund), + (3, AsyncBolt12Offer), ); impl<'a> Writeable for PaymentContextRef<'a> { @@ -651,6 +669,10 @@ impl_writeable_tlv_based!(Bolt12OfferContext, { (2, invoice_request, required), }); +impl_writeable_tlv_based!(AsyncBolt12OfferContext, { + (0, offer_nonce, required), +}); + impl_writeable_tlv_based!(Bolt12RefundContext, {}); #[cfg(test)] diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 5bc446f9724..0a2a2093cb7 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -181,27 +181,32 @@ impl PaymentPurpose { pub(crate) fn from_parts( payment_preimage: Option, payment_secret: PaymentSecret, payment_context: Option, - ) -> Self { + ) -> Result { match payment_context { None => { - PaymentPurpose::Bolt11InvoicePayment { + Ok(PaymentPurpose::Bolt11InvoicePayment { payment_preimage, payment_secret, - } + }) }, Some(PaymentContext::Bolt12Offer(context)) => { - PaymentPurpose::Bolt12OfferPayment { + Ok(PaymentPurpose::Bolt12OfferPayment { payment_preimage, payment_secret, payment_context: context, - } + }) }, Some(PaymentContext::Bolt12Refund(context)) => { - PaymentPurpose::Bolt12RefundPayment { + Ok(PaymentPurpose::Bolt12RefundPayment { payment_preimage, payment_secret, payment_context: context, - } + }) + }, + Some(PaymentContext::AsyncBolt12Offer(_context)) => { + // This code will change to return Self::Bolt12OfferPayment when we add support for async + // receive. + Err(()) }, } } @@ -1865,7 +1870,8 @@ impl MaybeReadable for Event { (13, payment_id, option), }); let purpose = match payment_secret { - Some(secret) => PaymentPurpose::from_parts(payment_preimage, secret, payment_context), + Some(secret) => PaymentPurpose::from_parts(payment_preimage, secret, payment_context) + .map_err(|()| msgs::DecodeError::InvalidValue)?, None if payment_preimage.is_some() => PaymentPurpose::SpontaneousPayment(payment_preimage.unwrap()), None => return Err(msgs::DecodeError::InvalidValue), }; diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs new file mode 100644 index 00000000000..424d76da6c2 --- /dev/null +++ b/lightning/src/ln/async_payments_tests.rs @@ -0,0 +1,601 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::blinded_path::message::{MessageContext, OffersContext}; +use crate::events::{Event, HTLCDestination, MessageSendEventsProvider, PaymentFailureReason}; +use crate::ln::blinded_payment_tests::{blinded_payment_path, get_blinded_route_parameters}; +use crate::ln::channelmanager; +use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; +use crate::ln::functional_test_utils::*; +use crate::ln::inbound_payment; +use crate::ln::msgs::ChannelMessageHandler; +use crate::ln::msgs::OnionMessageHandler; +use crate::ln::offers_tests; +use crate::ln::onion_utils::INVALID_ONION_BLINDING; +use crate::ln::outbound_payment::Retry; +use crate::offers::nonce::Nonce; +use crate::onion_message::async_payments::{ + AsyncPaymentsMessage, AsyncPaymentsMessageHandler, ReleaseHeldHtlc, +}; +use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; +use crate::onion_message::offers::OffersMessage; +use crate::onion_message::packet::ParsedOnionMessageContents; +use crate::prelude::*; +use crate::routing::router::{PaymentParameters, RouteParameters}; +use crate::sign::NodeSigner; +use crate::types::features::Bolt12InvoiceFeatures; +use crate::types::payment::{PaymentPreimage, PaymentSecret}; +use crate::util::config::UserConfig; +use bitcoin::secp256k1::Secp256k1; + +use core::convert::Infallible; +use core::time::Duration; + +#[test] +fn blinded_keysend() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let chan_upd_1_2 = + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; + + let inbound_payment_key = nodes[2].keys_manager.get_inbound_payment_key(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &inbound_payment_key, + None, + u32::MAX, + nodes[2].node.duration_since_epoch().as_secs(), + None, + ) + .unwrap(); + + let amt_msat = 5000; + let keysend_preimage = PaymentPreimage([42; 32]); + let route_params = get_blinded_route_parameters( + amt_msat, + payment_secret, + 1, + 1_0000_0000, + nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), + &[&chan_upd_1_2], + &chanmon_cfgs[2].keys_manager, + ); + + let payment_hash = nodes[0] + .node + .send_spontaneous_payment( + Some(keysend_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(keysend_preimage.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + pass_along_path( + &nodes[0], + expected_route[0], + amt_msat, + payment_hash, + Some(payment_secret), + ev.clone(), + true, + Some(keysend_preimage), + ); + claim_payment_along_route(ClaimAlongRouteArgs::new( + &nodes[0], + expected_route, + keysend_preimage, + )); +} + +#[test] +fn blinded_mpp_keysend() { + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes(&nodes, 0, 1); + create_announced_chan_between_nodes(&nodes, 0, 2); + let chan_1_3 = create_announced_chan_between_nodes(&nodes, 1, 3); + let chan_2_3 = create_announced_chan_between_nodes(&nodes, 2, 3); + + let inbound_payment_key = nodes[3].keys_manager.get_inbound_payment_key(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &inbound_payment_key, + None, + u32::MAX, + nodes[3].node.duration_since_epoch().as_secs(), + None, + ) + .unwrap(); + + let amt_msat = 15_000_000; + let keysend_preimage = PaymentPreimage([42; 32]); + let route_params = { + let pay_params = PaymentParameters::blinded(vec![ + blinded_payment_path( + payment_secret, + 1, + 1_0000_0000, + vec![nodes[1].node.get_our_node_id(), nodes[3].node.get_our_node_id()], + &[&chan_1_3.0.contents], + &chanmon_cfgs[3].keys_manager, + ), + blinded_payment_path( + payment_secret, + 1, + 1_0000_0000, + vec![nodes[2].node.get_our_node_id(), nodes[3].node.get_our_node_id()], + &[&chan_2_3.0.contents], + &chanmon_cfgs[3].keys_manager, + ), + ]) + .with_bolt12_features(channelmanager::provided_bolt12_invoice_features( + &UserConfig::default(), + )) + .unwrap(); + RouteParameters::from_payment_params_and_value(pay_params, amt_msat) + }; + + let payment_hash = nodes[0] + .node + .send_spontaneous_payment( + Some(keysend_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(keysend_preimage.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors!(nodes[0], 2); + + let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + pass_along_path( + &nodes[0], + expected_route[0], + amt_msat, + payment_hash.clone(), + Some(payment_secret), + ev.clone(), + false, + Some(keysend_preimage), + ); + + let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); + pass_along_path( + &nodes[0], + expected_route[1], + amt_msat, + payment_hash.clone(), + Some(payment_secret), + ev.clone(), + true, + Some(keysend_preimage), + ); + claim_payment_along_route(ClaimAlongRouteArgs::new( + &nodes[0], + expected_route, + keysend_preimage, + )); +} + +#[test] +fn invalid_keysend_payment_secret() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let chan_upd_1_2 = + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; + + let invalid_payment_secret = PaymentSecret([42; 32]); + let amt_msat = 5000; + let keysend_preimage = PaymentPreimage([42; 32]); + let route_params = get_blinded_route_parameters( + amt_msat, + invalid_payment_secret, + 1, + 1_0000_0000, + nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), + &[&chan_upd_1_2], + &chanmon_cfgs[2].keys_manager, + ); + + let payment_hash = nodes[0] + .node + .send_spontaneous_payment( + Some(keysend_preimage), + RecipientOnionFields::spontaneous_empty(), + PaymentId(keysend_preimage.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let args = + PassAlongPathArgs::new(&nodes[0], &expected_route[0], amt_msat, payment_hash, ev.clone()) + .with_payment_secret(invalid_payment_secret) + .with_payment_preimage(keysend_preimage) + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + + let updates_2_1 = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); + assert_eq!(updates_2_1.update_fail_malformed_htlcs.len(), 1); + let update_malformed = &updates_2_1.update_fail_malformed_htlcs[0]; + assert_eq!(update_malformed.sha256_of_onion, [0; 32]); + assert_eq!(update_malformed.failure_code, INVALID_ONION_BLINDING); + nodes[1] + .node + .handle_update_fail_malformed_htlc(nodes[2].node.get_our_node_id(), update_malformed); + do_commitment_signed_dance(&nodes[1], &nodes[2], &updates_2_1.commitment_signed, true, false); + + let updates_1_0 = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); + assert_eq!(updates_1_0.update_fail_htlcs.len(), 1); + nodes[0].node.handle_update_fail_htlc( + nodes[1].node.get_our_node_id(), + &updates_1_0.update_fail_htlcs[0], + ); + do_commitment_signed_dance(&nodes[0], &nodes[1], &updates_1_0.commitment_signed, false, false); + expect_payment_failed_conditions( + &nodes[0], + payment_hash, + false, + PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32]), + ); +} + +#[test] +fn static_invoice_unknown_required_features() { + // Test that we will fail to pay a static invoice with unsupported required features. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let blinded_paths_to_always_online_node = nodes[1] + .message_router + .create_blinded_paths( + nodes[1].node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let static_invoice_unknown_req_features = nodes[2] + .node + .create_static_invoice_builder(&offer, nonce, None) + .unwrap() + .features_unchecked(Bolt12InvoiceFeatures::unknown()) + .build_and_sign(&secp_ctx) + .unwrap(); + + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) + .unwrap(); + + // Don't forward the invreq since we don't support retrieving the static invoice from the + // recipient's LSP yet, instead manually construct the response. + let invreq_om = nodes[0] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + static_invoice_unknown_req_features, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + + let static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::PaymentFailed { payment_hash, payment_id: ev_payment_id, reason } => { + assert_eq!(payment_hash, None); + assert_eq!(payment_id, ev_payment_id); + assert_eq!(reason, Some(PaymentFailureReason::UnknownRequiredFeatures)); + }, + _ => panic!(), + } +} + +#[test] +fn ignore_unexpected_static_invoice() { + // Test that we'll ignore unexpected static invoices, invoices that don't match our invoice + // request, and duplicate invoices. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + // Initiate payment to the sender's intended offer. + let blinded_paths_to_always_online_node = nodes[1] + .message_router + .create_blinded_paths( + nodes[1].node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, offer_nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node.clone()) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) + .unwrap(); + + // Don't forward the invreq since we don't support retrieving the static invoice from the + // recipient's LSP yet, instead manually construct the responses below. + let invreq_om = nodes[0] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + + // Create a static invoice to be sent over the reply path containing the original payment_id, but + // the static invoice corresponds to a different offer than was originally paid. + let unexpected_static_invoice = { + let (offer_builder, nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let sender_unintended_offer = offer_builder.build().unwrap(); + + nodes[2] + .node + .create_static_invoice_builder(&sender_unintended_offer, nonce, None) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap() + }; + + // Check that we'll ignore the unexpected static invoice. + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + unexpected_static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path.clone()), + }, + ) + .unwrap(); + let unexpected_static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &unexpected_static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(async_pmts_msgs.is_empty()); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + // A valid static invoice corresponding to the correct offer will succeed and cause us to send a + // held_htlc_available onion message. + let valid_static_invoice = nodes[2] + .node + .create_static_invoice_builder(&offer, offer_nonce, None) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + valid_static_invoice.clone(), + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path.clone()), + }, + ) + .unwrap(); + let static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(!async_pmts_msgs.is_empty()); + assert!(async_pmts_msgs + .into_iter() + .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); + + // Receiving a duplicate invoice will have no effect. + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + valid_static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + let dup_static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &dup_static_invoice_om); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(async_pmts_msgs.is_empty()); +} + +#[test] +fn pays_static_invoice() { + // Test that we support the async payments flow up to and including sending the actual payment. + // Async receive is not yet supported so we don't complete the payment yet. + let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let blinded_paths_to_always_online_node = nodes[1] + .message_router + .create_blinded_paths( + nodes[1].node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, offer_nonce) = nodes[2] + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let relative_expiry = Duration::from_secs(1000); + let static_invoice = nodes[2] + .node + .create_static_invoice_builder(&offer, offer_nonce, Some(relative_expiry)) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + assert!(static_invoice.invoice_features().supports_basic_mpp()); + assert_eq!(static_invoice.relative_expiry(), relative_expiry); + + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), None) + .unwrap(); + + // Don't forward the invreq since we don't support retrieving the static invoice from the + // recipient's LSP yet, instead manually construct the response. + let invreq_om = nodes[0] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + + nodes[1] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( + static_invoice, + )), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(invreq_reply_path), + }, + ) + .unwrap(); + let static_invoice_om = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &static_invoice_om); + let mut async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(!async_pmts_msgs.is_empty()); + assert!(async_pmts_msgs + .iter() + .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); + + // Manually send the message and context releasing the HTLC since the recipient doesn't support + // responding themselves yet. + let held_htlc_avail_reply_path = match async_pmts_msgs.pop().unwrap().1 { + MessageSendInstructions::WithSpecifiedReplyPath { reply_path, .. } => reply_path, + _ => panic!(), + }; + nodes[2] + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::AsyncPayments( + AsyncPaymentsMessage::ReleaseHeldHtlc(ReleaseHeldHtlc {}), + ), + MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(held_htlc_avail_reply_path), + }, + ) + .unwrap(); + + let release_held_htlc_om = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) + .unwrap(); + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + // Check that we've queued the HTLCs of the async keysend payment. + let htlc_updates = get_htlc_update_msgs!(nodes[0], nodes[1].node.get_our_node_id()); + assert_eq!(htlc_updates.update_add_htlcs.len(), 1); + check_added_monitors!(nodes[0], 1); + + // Receiving a duplicate release_htlc message doesn't result in duplicate payment. + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); +} diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index c1fad65c14f..96e6e76ac2c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -36,12 +36,8 @@ use crate::util::config::UserConfig; use crate::util::ser::WithoutLength; use crate::util::test_utils; use lightning_invoice::RawBolt11Invoice; -#[cfg(async_payments)] use { - crate::ln::inbound_payment, - crate::types::payment::PaymentPreimage, -}; -fn blinded_payment_path( +pub fn blinded_payment_path( payment_secret: PaymentSecret, intro_node_min_htlc: u64, intro_node_max_htlc: u64, node_ids: Vec, channel_upds: &[&msgs::UnsignedChannelUpdate], keys_manager: &test_utils::TestKeysInterface @@ -1226,149 +1222,6 @@ fn conditionally_round_fwd_amt() { expect_payment_sent(&nodes[0], payment_preimage, Some(Some(expected_fee)), true, true); } -#[test] -#[cfg(async_payments)] -fn blinded_keysend() { - let chanmon_cfgs = create_chanmon_cfgs(3); - let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let chan_upd_1_2 = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; - - let inbound_payment_key = nodes[2].keys_manager.get_inbound_payment_key(); - let payment_secret = inbound_payment::create_for_spontaneous_payment( - &inbound_payment_key, None, u32::MAX, nodes[2].node.duration_since_epoch().as_secs(), None - ).unwrap(); - - let amt_msat = 5000; - let keysend_preimage = PaymentPreimage([42; 32]); - let route_params = get_blinded_route_parameters(amt_msat, payment_secret, 1, - 1_0000_0000, - nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), - &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); - - let payment_hash = nodes[0].node.send_spontaneous_payment(Some(keysend_preimage), RecipientOnionFields::spontaneous_empty(), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0)).unwrap(); - check_added_monitors(&nodes[0], 1); - - let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - - let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - pass_along_path(&nodes[0], expected_route[0], amt_msat, payment_hash, Some(payment_secret), ev.clone(), true, Some(keysend_preimage)); - claim_payment_along_route( - ClaimAlongRouteArgs::new(&nodes[0], expected_route, keysend_preimage) - ); -} - -#[test] -#[cfg(async_payments)] -fn blinded_mpp_keysend() { - let chanmon_cfgs = create_chanmon_cfgs(4); - let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); - let nodes = create_network(4, &node_cfgs, &node_chanmgrs); - - create_announced_chan_between_nodes(&nodes, 0, 1); - create_announced_chan_between_nodes(&nodes, 0, 2); - let chan_1_3 = create_announced_chan_between_nodes(&nodes, 1, 3); - let chan_2_3 = create_announced_chan_between_nodes(&nodes, 2, 3); - - let inbound_payment_key = nodes[3].keys_manager.get_inbound_payment_key(); - let payment_secret = inbound_payment::create_for_spontaneous_payment( - &inbound_payment_key, None, u32::MAX, nodes[3].node.duration_since_epoch().as_secs(), None - ).unwrap(); - - let amt_msat = 15_000_000; - let keysend_preimage = PaymentPreimage([42; 32]); - let route_params = { - let pay_params = PaymentParameters::blinded( - vec![ - blinded_payment_path(payment_secret, 1, 1_0000_0000, - vec![nodes[1].node.get_our_node_id(), nodes[3].node.get_our_node_id()], &[&chan_1_3.0.contents], - &chanmon_cfgs[3].keys_manager - ), - blinded_payment_path(payment_secret, 1, 1_0000_0000, - vec![nodes[2].node.get_our_node_id(), nodes[3].node.get_our_node_id()], &[&chan_2_3.0.contents], - &chanmon_cfgs[3].keys_manager - ), - ] - ) - .with_bolt12_features(channelmanager::provided_bolt12_invoice_features(&UserConfig::default())) - .unwrap(); - RouteParameters::from_payment_params_and_value(pay_params, amt_msat) - }; - - let payment_hash = nodes[0].node.send_spontaneous_payment(Some(keysend_preimage), RecipientOnionFields::spontaneous_empty(), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0)).unwrap(); - check_added_monitors!(nodes[0], 2); - - let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 2); - - let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - pass_along_path(&nodes[0], expected_route[0], amt_msat, payment_hash.clone(), - Some(payment_secret), ev.clone(), false, Some(keysend_preimage)); - - let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); - pass_along_path(&nodes[0], expected_route[1], amt_msat, payment_hash.clone(), - Some(payment_secret), ev.clone(), true, Some(keysend_preimage)); - claim_payment_along_route( - ClaimAlongRouteArgs::new(&nodes[0], expected_route, keysend_preimage) - ); -} - -#[test] -#[cfg(async_payments)] -fn invalid_keysend_payment_secret() { - let chanmon_cfgs = create_chanmon_cfgs(3); - let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let chan_upd_1_2 = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.contents; - - let invalid_payment_secret = PaymentSecret([42; 32]); - let amt_msat = 5000; - let keysend_preimage = PaymentPreimage([42; 32]); - let route_params = get_blinded_route_parameters( - amt_msat, invalid_payment_secret, 1, 1_0000_0000, - nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], - &chanmon_cfgs[2].keys_manager - ); - - let payment_hash = nodes[0].node.send_spontaneous_payment(Some(keysend_preimage), RecipientOnionFields::spontaneous_empty(), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0)).unwrap(); - check_added_monitors(&nodes[0], 1); - - let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - - let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - let args = PassAlongPathArgs::new( - &nodes[0], &expected_route[0], amt_msat, payment_hash, ev.clone() - ) - .with_payment_secret(invalid_payment_secret) - .with_payment_preimage(keysend_preimage) - .expect_failure(HTLCDestination::FailedPayment { payment_hash }); - do_pass_along_path(args); - - let updates_2_1 = get_htlc_update_msgs!(nodes[2], nodes[1].node.get_our_node_id()); - assert_eq!(updates_2_1.update_fail_malformed_htlcs.len(), 1); - let update_malformed = &updates_2_1.update_fail_malformed_htlcs[0]; - assert_eq!(update_malformed.sha256_of_onion, [0; 32]); - assert_eq!(update_malformed.failure_code, INVALID_ONION_BLINDING); - nodes[1].node.handle_update_fail_malformed_htlc(nodes[2].node.get_our_node_id(), update_malformed); - do_commitment_signed_dance(&nodes[1], &nodes[2], &updates_2_1.commitment_signed, true, false); - - let updates_1_0 = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); - assert_eq!(updates_1_0.update_fail_htlcs.len(), 1); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates_1_0.update_fail_htlcs[0]); - do_commitment_signed_dance(&nodes[0], &nodes[1], &updates_1_0.commitment_signed, false, false); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, - PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32])); -} #[test] fn custom_tlvs_to_blinded_path() { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index b312e0055ee..755198a8e52 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -72,8 +72,6 @@ use crate::offers::offer::{Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; use crate::offers::signer; -#[cfg(async_payments)] -use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler}; use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; @@ -88,6 +86,11 @@ use crate::util::ser::{BigSize, FixedLengthReader, Readable, ReadableArgs, Maybe use crate::util::ser::TransactionU16LenLimited; use crate::util::logger::{Level, Logger, WithContext}; use crate::util::errors::APIError; +#[cfg(async_payments)] use { + crate::blinded_path::payment::AsyncBolt12OfferContext, + crate::offers::offer::Amount, + crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, +}; #[cfg(feature = "dnssec")] use crate::blinded_path::message::DNSResolverContext; @@ -6253,11 +6256,16 @@ where match claimable_htlc.onion_payload { OnionPayload::Invoice { .. } => { let payment_data = payment_data.unwrap(); - let purpose = events::PaymentPurpose::from_parts( + let purpose = match events::PaymentPurpose::from_parts( payment_preimage, payment_data.payment_secret, payment_context, - ); + ) { + Ok(purpose) => purpose, + Err(()) => { + fail_htlc!(claimable_htlc, payment_hash); + }, + }; check_total_value!(purpose); }, OnionPayload::Spontaneous(preimage) => { @@ -9983,6 +9991,86 @@ where #[cfg(c_bindings)] create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder); + /// Create an offer for receiving async payments as an often-offline recipient. + /// + /// Because we may be offline when the payer attempts to request an invoice, you MUST: + /// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will + /// serve the [`StaticInvoice`] created from this offer on our behalf. + /// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this + /// [`Offer`] plus the returned [`Nonce`], and provide the static invoice to the + /// aforementioned always-online node. + #[cfg(async_payments)] + pub fn create_async_receive_offer_builder( + &self, message_paths_to_always_online_node: Vec + ) -> Result<(OfferBuilder, Nonce), Bolt12SemanticError> { + if message_paths_to_always_online_node.is_empty() { + return Err(Bolt12SemanticError::MissingPaths) + } + + let node_id = self.get_our_node_id(); + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let nonce = Nonce::from_entropy_source(entropy); + let mut builder = OfferBuilder::deriving_signing_pubkey( + node_id, expanded_key, nonce, secp_ctx + ).chain_hash(self.chain_hash); + + for path in message_paths_to_always_online_node { + builder = builder.path(path); + } + + Ok((builder.into(), nonce)) + } + + /// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were + /// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the + /// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`]. + #[cfg(async_payments)] + pub fn create_static_invoice_builder<'a>( + &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option + ) -> Result, Bolt12SemanticError> { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + + let payment_context = PaymentContext::AsyncBolt12Offer( + AsyncBolt12OfferContext { offer_nonce } + ); + let amount_msat = offer.amount().and_then(|amount| { + match amount { + Amount::Bitcoin { amount_msats } => Some(amount_msats), + Amount::Currency { .. } => None + } + }); + + let relative_expiry = relative_expiry.unwrap_or(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY); + let relative_expiry_secs: u32 = relative_expiry.as_secs().try_into().unwrap_or(u32::MAX); + + let created_at = self.duration_since_epoch(); + let payment_secret = inbound_payment::create_for_spontaneous_payment( + &self.inbound_payment_key, amount_msat, relative_expiry_secs, created_at.as_secs(), None + ).map_err(|()| Bolt12SemanticError::InvalidAmount)?; + + let payment_paths = self.create_blinded_payment_paths( + amount_msat, payment_secret, payment_context, relative_expiry_secs + ).map_err(|()| Bolt12SemanticError::MissingPaths)?; + + let nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); + let context = MessageContext::AsyncPayments( + AsyncPaymentsContext::InboundPayment { nonce, hmac } + ); + let async_receive_message_paths = self.create_blinded_paths(context) + .map_err(|()| Bolt12SemanticError::MissingPaths)?; + + StaticInvoiceBuilder::for_offer_using_derived_keys( + offer, payment_paths, async_receive_message_paths, created_at, expanded_key, + offer_nonce, secp_ctx + ).map(|inv| inv.allow_mpp().relative_expiry(relative_expiry_secs)) + } + /// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and /// enqueuing it to be sent via an onion message. [`ChannelManager`] will pay the actual /// [`Bolt12Invoice`] once it is received. @@ -10046,6 +10134,7 @@ where let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), nonce, + needs_retry: true, }; self.pending_outbound_payments .add_new_awaiting_invoice( @@ -10181,7 +10270,7 @@ where Ok((payment_hash, payment_secret)) => { let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); let payment_paths = self.create_blinded_payment_paths( - amount_msats, payment_secret, payment_context + Some(amount_msats), payment_secret, payment_context, relative_expiry, ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -10488,7 +10577,8 @@ where /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to /// [`Router::create_blinded_payment_paths`]. fn create_blinded_payment_paths( - &self, amount_msats: u64, payment_secret: PaymentSecret, payment_context: PaymentContext + &self, amount_msats: Option, payment_secret: PaymentSecret, payment_context: PaymentContext, + relative_expiry_seconds: u32 ) -> Result, ()> { let expanded_key = &self.inbound_payment_key; let entropy = &*self.entropy_source; @@ -10496,8 +10586,13 @@ where let first_hops = self.list_usable_channels(); let payee_node_id = self.get_our_node_id(); - let max_cltv_expiry = self.best_block.read().unwrap().height + CLTV_FAR_FAR_AWAY - + LATENCY_GRACE_PERIOD_BLOCKS; + + // Assume shorter than usual block times to avoid spuriously failing payments too early. + const SECONDS_PER_BLOCK: u32 = 9 * 60; + let relative_expiry_blocks = relative_expiry_seconds / SECONDS_PER_BLOCK; + let max_cltv_expiry = core::cmp::max(relative_expiry_blocks, CLTV_FAR_FAR_AWAY) + .saturating_add(LATENCY_GRACE_PERIOD_BLOCKS) + .saturating_add(self.best_block.read().unwrap().height); let payee_tlvs = UnauthenticatedReceiveTlvs { payment_secret, @@ -11915,7 +12010,7 @@ where .pending_outbound_payments .release_invoice_requests_awaiting_invoice() { - let RetryableInvoiceRequest { invoice_request, nonce } = retryable_invoice_request; + let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, @@ -12047,7 +12142,7 @@ where invoice_request: invoice_request.fields(), }); let payment_paths = match self.create_blinded_payment_paths( - amount_msats, payment_secret, payment_context + Some(amount_msats), payment_secret, payment_context, relative_expiry ) { Ok(payment_paths) => payment_paths, Err(()) => { @@ -12197,7 +12292,12 @@ where fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { #[cfg(async_payments)] { - let AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } = _context; + let (payment_id, nonce, hmac) = match _context { + AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } => { + (payment_id, nonce, hmac) + }, + _ => return + }; if payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key).is_err() { return } if let Err(e) = self.send_payment_for_static_invoice(payment_id) { log_trace!( @@ -12250,6 +12350,7 @@ where let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), nonce, + needs_retry: true, }; self.pending_outbound_payments .received_offer(payment_id, Some(retryable_invoice_request)) diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index e1631a2892c..c1bdc554c00 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -55,6 +55,9 @@ pub use onion_utils::create_payment_onion; #[cfg(test)] #[allow(unused_mut)] mod blinded_payment_tests; +#[cfg(all(test, async_payments))] +#[allow(unused_mut)] +mod async_payments_tests; #[cfg(test)] #[allow(unused_mut)] mod functional_tests; diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6455a60b139..2cbf9e56647 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -199,7 +199,7 @@ fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessa } } -fn extract_invoice_request<'a, 'b, 'c>( +pub(super) fn extract_invoice_request<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, message: &OnionMessage ) -> (InvoiceRequest, BlindedMessagePath) { match node.onion_messenger.peel_onion_message(message) { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 80d93387ac3..803aa615288 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -134,13 +134,16 @@ pub(crate) enum PendingOutboundPayment { }, } +#[derive(Clone)] pub(crate) struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, pub(crate) nonce: Nonce, + pub(super) needs_retry: bool, } impl_writeable_tlv_based!(RetryableInvoiceRequest, { (0, invoice_request, required), + (1, needs_retry, (default_value, true)), (2, nonce, required), }); @@ -760,7 +763,12 @@ pub(super) struct OutboundPayments { impl OutboundPayments { pub(super) fn new(pending_outbound_payments: HashMap) -> Self { let has_invoice_requests = pending_outbound_payments.values().any(|payment| { - matches!(payment, PendingOutboundPayment::AwaitingInvoice { retryable_invoice_request: Some(_), .. }) + matches!( + payment, + PendingOutboundPayment::AwaitingInvoice { + retryable_invoice_request: Some(invreq), .. + } if invreq.needs_retry + ) }); Self { @@ -1008,17 +1016,16 @@ impl OutboundPayments { ) -> Result<(), Bolt12PaymentError> where ES::Target: EntropySource { macro_rules! abandon_with_entry { ($payment: expr, $reason: expr) => { - $payment.get_mut().mark_abandoned($reason); - if let PendingOutboundPayment::Abandoned { reason, .. } = $payment.get() { - if $payment.get().remaining_parts() == 0 { - pending_events.lock().unwrap().push_back((events::Event::PaymentFailed { - payment_id, - payment_hash: None, - reason: *reason, - }, None)); - $payment.remove(); - } - } + assert!( + matches!($payment.get(), PendingOutboundPayment::AwaitingInvoice { .. }), + "Generating PaymentFailed for unexpected outbound payment type can result in funds loss" + ); + pending_events.lock().unwrap().push_back((events::Event::PaymentFailed { + payment_id, + payment_hash: None, + reason: Some($reason), + }, None)); + $payment.remove(); } } @@ -2228,11 +2235,12 @@ impl OutboundPayments { .iter_mut() .filter_map(|(payment_id, payment)| { if let PendingOutboundPayment::AwaitingInvoice { - retryable_invoice_request, .. + retryable_invoice_request: Some(invreq), .. } = payment { - retryable_invoice_request.take().map(|retryable_invoice_request| { - (*payment_id, retryable_invoice_request) - }) + if invreq.needs_retry { + invreq.needs_retry = false; + Some((*payment_id, invreq.clone())) + } else { None } } else { None } diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index fa9fdfa3467..7deff734b34 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -50,6 +50,11 @@ const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16]; // HMAC input for `ReceiveTlvs`. The HMAC is used in `blinded_path::payment::PaymentContext`. const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16]; +// HMAC input used in `AsyncPaymentsContext::InboundPayment` to authenticate inbound +// held_htlc_available onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -483,3 +488,16 @@ pub(crate) fn verify_payment_tlvs( ) -> Result<(), ()> { if hmac_for_payment_tlvs(receive_tlvs, nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } } + +#[cfg(async_payments)] +pub(crate) fn hmac_for_held_htlc_available_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Held HTLC OM"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT); + + Hmac::from_engine(hmac) +} diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 39c17eb3bcc..fdeffcb0c20 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -49,7 +49,7 @@ use crate::offers::invoice::is_expired; use crate::prelude::*; /// Static invoices default to expiring after 2 weeks. -const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14); +pub const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(3600 * 24 * 14); /// Tag for the hash function used when signing a [`StaticInvoice`]'s merkle root. pub const SIGNATURE_TAG: &'static str = concat!("lightning", "static_invoice", "signature"); @@ -102,8 +102,8 @@ pub struct StaticInvoiceBuilder<'a> { impl<'a> StaticInvoiceBuilder<'a> { /// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`]. /// - /// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours - /// after `created_at`. + /// The invoice's expiration will default to [`DEFAULT_RELATIVE_EXPIRY`] after `created_at` unless + /// overridden by [`StaticInvoiceBuilder::relative_expiry`]. pub fn for_offer_using_derived_keys( offer: &'a Offer, payment_paths: Vec, message_paths: Vec, created_at: Duration, expanded_key: &ExpandedKey, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 78a93aa0d39..a257d2b4abe 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -95,7 +95,7 @@ impl>, L: Deref, ES: Deref, S: Deref, SP: Size T: secp256k1::Signing + secp256k1::Verification > ( &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, - amount_msats: u64, secp_ctx: &Secp256k1 + amount_msats: Option, secp_ctx: &Secp256k1 ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PAYMENT_PATHS: usize = 3; @@ -120,9 +120,9 @@ impl>, L: Deref, ES: Deref, S: Deref, SP: Size let paths = first_hops.into_iter() .filter(|details| details.counterparty.features.supports_route_blinding()) - .filter(|details| amount_msats <= details.inbound_capacity_msat) - .filter(|details| amount_msats >= details.inbound_htlc_minimum_msat.unwrap_or(0)) - .filter(|details| amount_msats <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX)) + .filter(|details| amount_msats.unwrap_or(0) <= details.inbound_capacity_msat) + .filter(|details| amount_msats.unwrap_or(u64::MAX) >= details.inbound_htlc_minimum_msat.unwrap_or(0)) + .filter(|details| amount_msats.unwrap_or(0) <= details.inbound_htlc_maximum_msat.unwrap_or(u64::MAX)) // Limit to peers with announced channels unless the recipient is unannounced. .filter(|details| network_graph .node(&NodeId::from_pubkey(&details.counterparty.node_id)) @@ -218,7 +218,7 @@ pub trait Router { T: secp256k1::Signing + secp256k1::Verification > ( &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, - amount_msats: u64, secp_ctx: &Secp256k1 + amount_msats: Option, secp_ctx: &Secp256k1 ) -> Result, ()>; } diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 442a709866c..9db85508a5b 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -252,7 +252,7 @@ impl<'a> Router for TestRouter<'a> { T: secp256k1::Signing + secp256k1::Verification >( &self, recipient: PublicKey, first_hops: Vec, tlvs: ReceiveTlvs, - amount_msats: u64, secp_ctx: &Secp256k1, + amount_msats: Option, secp_ctx: &Secp256k1, ) -> Result, ()> { let mut expected_paths = self.next_blinded_payment_paths.lock().unwrap(); if expected_paths.is_empty() {