diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index c2a49b8ee24..26c9fb2177d 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/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 94da4d09be5..10a667fb594 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -122,7 +122,8 @@ struct TestAsyncPaymentsMessageHandler {} impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { fn handle_held_htlc_available( - &self, _message: HeldHtlcAvailable, responder: Option, + &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, + responder: Option, ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { let responder = match responder { Some(resp) => resp, 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..a2d7755eac7 --- /dev/null +++ b/lightning/src/ln/async_payments_tests.rs @@ -0,0 +1,485 @@ +// 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::get_blinded_route_parameters; +use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; +use crate::ln::functional_test_utils::*; +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::offers::offer::Offer; +use crate::offers::static_invoice::StaticInvoice; +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::types::features::Bolt12InvoiceFeatures; +use crate::types::payment::{PaymentPreimage, PaymentSecret}; +use bitcoin::secp256k1; +use bitcoin::secp256k1::Secp256k1; + +use core::convert::Infallible; +use core::time::Duration; + +fn create_static_invoice( + always_online_counterparty: &Node, recipient: &Node, relative_expiry: Option, + secp_ctx: &Secp256k1, +) -> (Offer, StaticInvoice) { + let blinded_paths_to_always_online_node = always_online_counterparty + .message_router + .create_blinded_paths( + always_online_counterparty.node.get_our_node_id(), + MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + Vec::new(), + &secp_ctx, + ) + .unwrap(); + let (offer_builder, offer_nonce) = recipient + .node + .create_async_receive_offer_builder(blinded_paths_to_always_online_node) + .unwrap(); + let offer = offer_builder.build().unwrap(); + let static_invoice = recipient + .node + .create_static_invoice_builder(&offer, offer_nonce, relative_expiry) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + (offer, static_invoice) +} + +#[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 (offer, valid_static_invoice) = + create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + 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 = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx).1; + + // 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. + 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 relative_expiry = Duration::from_secs(1000); + let (offer, static_invoice) = + create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); + assert!(static_invoice.invoice_features().supports_basic_mpp()); + assert_eq!(static_invoice.relative_expiry(), relative_expiry); + + 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, + )), + 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()); +} + +#[cfg(not(feature = "std"))] +#[test] +fn expired_static_invoice_fail() { + // Test that if we receive an expired static invoice we'll fail the payment. + 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); + + const INVOICE_EXPIRY_SECS: u32 = 10; + let relative_expiry = Duration::from_secs(INVOICE_EXPIRY_SECS as u64); + let (offer, static_invoice) = + create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); + + 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(); + + 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; + // TODO: update to not manually send here when we add support for being the recipient's + // always-online counterparty + 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(); + + // Wait until the static invoice expires before providing it to the sender. + let block = create_dummy_block( + nodes[0].best_block_hash(), + nodes[0].node.duration_since_epoch().as_secs() as u32 + INVOICE_EXPIRY_SECS + 1, + Vec::new(), + ); + connect_block(&nodes[0], &block); + 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_id: ev_payment_id, reason, .. } => { + assert_eq!(reason.unwrap(), PaymentFailureReason::PaymentExpired); + assert_eq!(ev_payment_id, payment_id); + }, + _ => panic!(), + } + // The sender doesn't reply with InvoiceError right now because the always-online node doesn't + // currently provide them with a reply path to do so. +} diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index c1fad65c14f..46e301bc06b 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 @@ -109,6 +105,37 @@ pub fn get_blinded_route_parameters( ) } +pub fn fail_blinded_htlc_backwards( + payment_hash: PaymentHash, intro_node_idx: usize, nodes: &[&Node], +) { + for i in (0..nodes.len()).rev() { + match i { + 0 => { + let mut payment_failed_conditions = PaymentFailedConditions::new() + .expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32]); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + }, + i if i <= intro_node_idx => { + let unblinded_node_updates = get_htlc_update_msgs!(nodes[i], nodes[i-1].node.get_our_node_id()); + assert_eq!(unblinded_node_updates.update_fail_htlcs.len(), 1); + nodes[i-1].node.handle_update_fail_htlc( + nodes[i].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[i-1] + ); + do_commitment_signed_dance(&nodes[i-1], &nodes[i], &unblinded_node_updates.commitment_signed, false, false); + }, + _ => { + let blinded_node_updates = get_htlc_update_msgs!(nodes[i], nodes[i-1].node.get_our_node_id()); + assert_eq!(blinded_node_updates.update_fail_malformed_htlcs.len(), 1); + let update_malformed = &blinded_node_updates.update_fail_malformed_htlcs[0]; + assert_eq!(update_malformed.sha256_of_onion, [0; 32]); + assert_eq!(update_malformed.failure_code, INVALID_ONION_BLINDING); + nodes[i-1].node.handle_update_fail_malformed_htlc(nodes[i].node.get_our_node_id(), update_malformed); + do_commitment_signed_dance(&nodes[i-1], &nodes[i], &blinded_node_updates.commitment_signed, true, false); + } + } + } +} + #[test] fn one_hop_blinded_path() { do_one_hop_blinded_path(true); @@ -365,11 +392,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { do_commitment_signed_dance(&nodes[1], &nodes[0], &updates_0_1.commitment_signed, true, true); if intro_fails { - let mut updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]); - do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, - PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32])); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]]); return } @@ -551,13 +574,8 @@ fn do_forward_fail_in_process_pending_htlc_fwds(check: ProcessPendingHTLCsCheck, if intro_fails { cause_error!(nodes[0], nodes[1], nodes[2], chan_id_1_2, chan_upd_1_2.short_channel_id); - let mut updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]); check_added_monitors!(nodes[1], 1); - do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); - - expect_payment_failed_conditions(&nodes[0], payment_hash, false, - PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32])); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]]); return } @@ -647,14 +665,8 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) { nodes[1].node.fail_intercepted_htlc(intercept_id).unwrap(); expect_pending_htlcs_forwardable_and_htlc_handling_failed_ignore!(nodes[1], vec![HTLCDestination::UnknownNextHop { requested_forward_scid: intercept_scid }]); nodes[1].node.process_pending_htlc_forwards(); - let update_fail = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); check_added_monitors!(&nodes[1], 1); - assert!(update_fail.update_fail_htlcs.len() == 1); - let fail_msg = update_fail.update_fail_htlcs[0].clone(); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &fail_msg); - commitment_signed_dance!(nodes[0], nodes[1], update_fail.commitment_signed, false); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, - PaymentFailedConditions::new().expected_htlc_error_data(INVALID_ONION_BLINDING, &[0; 32])); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]]); return } @@ -760,29 +772,7 @@ fn three_hop_blinded_path_fail() { ); nodes[3].node.process_pending_htlc_forwards(); check_added_monitors!(nodes[3], 1); - - let updates_3_2 = get_htlc_update_msgs!(nodes[3], nodes[2].node.get_our_node_id()); - assert_eq!(updates_3_2.update_fail_malformed_htlcs.len(), 1); - let update_malformed = &updates_3_2.update_fail_malformed_htlcs[0]; - assert_eq!(update_malformed.sha256_of_onion, [0; 32]); - assert_eq!(update_malformed.failure_code, INVALID_ONION_BLINDING); - nodes[2].node.handle_update_fail_malformed_htlc(nodes[3].node.get_our_node_id(), update_malformed); - do_commitment_signed_dance(&nodes[2], &nodes[3], &updates_3_2.commitment_signed, true, false); - - 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])); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2], &nodes[3]]); } #[derive(PartialEq)] @@ -1226,149 +1216,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 21ab790bf9c..8426b857090 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -74,8 +74,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}; @@ -90,6 +88,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; @@ -237,6 +240,11 @@ pub enum PendingHTLCRouting { /// [`PaymentSecret`] and should verify it using our /// [`NodeSigner::get_inbound_payment_key`]. has_recipient_created_payment_secret: bool, + /// The context of the payment included by the recipient in a blinded path, or `None` if a + /// blinded path was not used. + /// + /// Used in part to determine the [`events::PaymentPurpose`]. + payment_context: Option, }, } @@ -4769,8 +4777,8 @@ where let best_block_height = self.best_block.read().unwrap().height; let features = self.bolt12_invoice_features(); let outbound_pmts_res = self.pending_outbound_payments.static_invoice_received( - invoice, payment_id, features, best_block_height, &*self.entropy_source, - &self.pending_events + invoice, payment_id, features, best_block_height, self.duration_since_epoch(), + &*self.entropy_source, &self.pending_events ); match outbound_pmts_res { Ok(()) => {}, @@ -6045,7 +6053,7 @@ where PendingHTLCRouting::ReceiveKeysend { payment_data, payment_preimage, payment_metadata, incoming_cltv_expiry, custom_tlvs, requires_blinded_error: _, - has_recipient_created_payment_secret, + has_recipient_created_payment_secret, payment_context, } => { let onion_fields = RecipientOnionFields { payment_secret: payment_data.as_ref().map(|data| data.payment_secret), @@ -6053,7 +6061,8 @@ where custom_tlvs, }; (incoming_cltv_expiry, OnionPayload::Spontaneous(payment_preimage), - payment_data, None, None, onion_fields, has_recipient_created_payment_secret) + payment_data, payment_context, None, onion_fields, + has_recipient_created_payment_secret) }, _ => { panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); @@ -6237,14 +6246,25 @@ 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) => { + if payment_context.is_some() { + if !matches!(payment_context, Some(PaymentContext::AsyncBolt12Offer(_))) { + log_trace!(self.logger, "Failing new HTLC with payment_hash {}: received a keysend payment to a non-async payments context {:#?}", payment_hash, payment_context); + } + fail_htlc!(claimable_htlc, payment_hash); + } let purpose = events::PaymentPurpose::SpontaneousPayment(preimage); check_total_value!(purpose); } @@ -9953,6 +9973,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. @@ -10016,6 +10116,7 @@ where let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), nonce, + needs_retry: true, }; self.pending_outbound_payments .add_new_awaiting_invoice( @@ -10151,7 +10252,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)?; @@ -10458,7 +10559,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; @@ -10466,8 +10568,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, @@ -11897,7 +12004,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, @@ -12029,7 +12136,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(()) => { @@ -12172,14 +12279,20 @@ where L::Target: Logger, { fn handle_held_htlc_available( - &self, _message: HeldHtlcAvailable, _responder: Option + &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, + _responder: Option ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { None } 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!( @@ -12232,6 +12345,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)) @@ -12376,6 +12490,7 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (4, payment_data, option), // Added in 0.0.116 (5, custom_tlvs, optional_vec), (7, has_recipient_created_payment_secret, (default_value, false)), + (9, payment_context, option), }, ); diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 1f6a1096e11..201f2ecfeff 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..52ded477fc1 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -47,8 +47,8 @@ use crate::blinded_path::IntroductionNode; use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; use crate::blinded_path::message::{MessageContext, OffersContext}; -use crate::events::{ClosureReason, Event, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose}; -use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, Retry, self}; +use crate::events::{ClosureReason, Event, HTLCDestination, MessageSendEventsProvider, PaymentFailureReason, PaymentPurpose}; +use crate::ln::channelmanager::{Bolt12PaymentError, MAX_SHORT_LIVED_RELATIVE_EXPIRY, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; @@ -62,6 +62,7 @@ use crate::onion_message::messenger::{Destination, PeeledOnion, MessageSendInstr use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::routing::gossip::{NodeAlias, NodeId}; +use crate::routing::router::{PaymentParameters, RouteParameters}; use crate::sign::{NodeSigner, Recipient}; use crate::util::ser::Writeable; @@ -199,7 +200,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) { @@ -2255,6 +2256,71 @@ fn fails_paying_invoice_with_unknown_required_features() { } } +#[test] +fn rejects_keysend_to_non_static_invoice_path() { + // Test that we'll fail a keysend payment that was sent over a non-static BOLT 12 invoice path. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + + // First pay the offer and save the payment preimage and invoice. + let offer = nodes[1].node.create_offer_builder(None).unwrap().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(1), None).unwrap(); + let invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); + nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); + let invoice_om = nodes[1].onion_messenger.next_onion_message_for_peer(nodes[0].node.get_our_node_id()).unwrap(); + let invoice = extract_invoice(&nodes[0], &invoice_om).0; + nodes[0].onion_messenger.handle_onion_message(nodes[1].node.get_our_node_id(), &invoice_om); + + route_bolt12_payment(&nodes[0], &[&nodes[1]], &invoice); + expect_recent_payment!(nodes[0], RecentPaymentDetails::Pending, payment_id); + + let payment_preimage = match get_event!(nodes[1], Event::PaymentClaimable) { + Event::PaymentClaimable { purpose, .. } => purpose.preimage().unwrap(), + _ => panic!() + }; + + claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); + expect_recent_payment!(&nodes[0], RecentPaymentDetails::Fulfilled, payment_id); + + // Time out the payment from recent payments so we can attempt to pay it again via keysend. + for _ in 0..=IDEMPOTENCY_TIMEOUT_TICKS { + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + } + + // Pay the invoice via keysend now that we have the preimage and make sure the recipient fails it + // due to incorrect payment context. + let pay_params = PaymentParameters::from_bolt12_invoice(&invoice); + let route_params = RouteParameters::from_payment_params_and_value(pay_params, amt_msat); + let keysend_payment_id = PaymentId([2; 32]); + let payment_hash = nodes[0].node.send_spontaneous_payment( + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), keysend_payment_id, + route_params, Retry::Attempts(0) + ).unwrap(); + check_added_monitors!(nodes[0], 1); + 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 route: &[&[&Node]] = &[&[&nodes[1]]]; + + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_payment_preimage(payment_preimage) + .expect_failure(HTLCDestination::FailedPayment { payment_hash }); + do_pass_along_path(args); + let mut updates = get_htlc_update_msgs!(nodes[1], nodes[0].node.get_our_node_id()); + nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]); + do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); + expect_payment_failed_conditions(&nodes[0], payment_hash, true, PaymentFailedConditions::new()); + nodes[1].logger.assert_log_contains( + "lightning::ln::channelmanager", "received a keysend payment to a non-async payments context", 1 + ); +} + #[test] fn no_double_pay_with_stale_channelmanager() { // This tests the following bug: diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 193cdd1582a..203a528a8fb 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -235,6 +235,7 @@ pub(super) fn create_recv_pending_htlc_info( custom_tlvs, requires_blinded_error, has_recipient_created_payment_secret, + payment_context, } } else if let Some(data) = payment_data { PendingHTLCRouting::Receive { diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index c0dea7df52d..743fff45e62 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 { @@ -1003,22 +1011,21 @@ impl OutboundPayments { #[cfg(async_payments)] pub(super) fn static_invoice_received( &self, invoice: &StaticInvoice, payment_id: PaymentId, features: Bolt12InvoiceFeatures, - best_block_height: u32, entropy_source: ES, + best_block_height: u32, duration_since_epoch: Duration, entropy_source: ES, pending_events: &Mutex)>> ) -> 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(); } } @@ -1038,6 +1045,11 @@ impl OutboundPayments { abandon_with_entry!(entry, PaymentFailureReason::UnknownRequiredFeatures); return Err(Bolt12PaymentError::UnknownRequiredFeatures) } + if duration_since_epoch > invoice.created_at().saturating_add(invoice.relative_expiry()) { + abandon_with_entry!(entry, PaymentFailureReason::PaymentExpired); + return Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::PaymentExpired)) + } + let amount_msat = match InvoiceBuilder::::amount_msats(invreq) { Ok(amt) => amt, Err(_) => { @@ -2229,11 +2241,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/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index dbce9ca0498..80b92cec1bd 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -149,7 +149,8 @@ impl OffersMessageHandler for IgnoringMessageHandler { } impl AsyncPaymentsMessageHandler for IgnoringMessageHandler { fn handle_held_htlc_available( - &self, _message: HeldHtlcAvailable, _responder: Option, + &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, + _responder: Option, ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { 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/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index d81010e5d5f..7a473c90e8f 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -28,7 +28,8 @@ pub trait AsyncPaymentsMessageHandler { /// Handle a [`HeldHtlcAvailable`] message. A [`ReleaseHeldHtlc`] should be returned to release /// the held funds. fn handle_held_htlc_available( - &self, message: HeldHtlcAvailable, responder: Option, + &self, message: HeldHtlcAvailable, context: AsyncPaymentsContext, + responder: Option, ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)>; /// Handle a [`ReleaseHeldHtlc`] message. If authentication of the message succeeds, an HTLC diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index f9d73f05ff3..57a7bc8afd5 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -85,7 +85,8 @@ struct TestAsyncPaymentsMessageHandler {} impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { fn handle_held_htlc_available( - &self, _message: HeldHtlcAvailable, _responder: Option, + &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, + _responder: Option, ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { None } diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f076e6a9da4..7b0bae82ba8 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1646,8 +1646,16 @@ where }, #[cfg(async_payments)] ParsedOnionMessageContents::AsyncPayments(AsyncPaymentsMessage::HeldHtlcAvailable(msg)) => { + let context = match context { + Some(MessageContext::AsyncPayments(context)) => context, + Some(_) => { + debug_assert!(false, "Checked in peel_onion_message"); + return + }, + None => return, + }; let response_instructions = self.async_payments_handler.handle_held_htlc_available( - msg, responder + msg, context, responder ); if let Some((msg, instructions)) = response_instructions { let _ = self.handle_onion_message_response(msg, instructions); 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 07b2b19b0d6..3d3dd587609 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() {