diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5734c9aa073..9ee464e2575 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -35,7 +35,15 @@ use crate::util::ser::WithoutLength; use crate::util::test_utils; use lightning_invoice::RawBolt11Invoice; #[cfg(async_payments)] use { + crate::blinded_path::BlindedHop, + crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath, OffersContext}, + crate::ln::channelmanager::Verification, crate::ln::inbound_payment, + crate::ln::msgs::OnionMessageHandler, + crate::offers::nonce::Nonce, + crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler, ReleaseHeldHtlc}, + crate::onion_message::offers::{OffersMessage, OffersMessageHandler}, + crate::types::features::Bolt12InvoiceFeatures, crate::types::payment::PaymentPreimage, }; @@ -1416,6 +1424,209 @@ fn custom_tlvs_to_blinded_path() { ); } +#[test] +#[cfg(async_payments)] +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); + + // Use a dummy blinded path because we don't support retrieving the static invoice from the + // recipient's LSP yet. + let dummy_blinded_path_to_always_online_node = BlindedMessagePath::from_raw( + nodes[1].node.get_our_node_id(), test_utils::pubkey(42), + vec![BlindedHop { blinded_node_id: test_utils::pubkey(42), encrypted_payload: vec![42; 32] }] + ); + let (offer_builder, nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node]).unwrap(); + let offer = offer_builder.build().unwrap(); + let static_invoice_unknown_req_features = nodes[2].node.create_static_invoice_builder_for_async_receive_offer( + &offer, nonce, None + ) + .unwrap() + .features_unchecked(Bolt12InvoiceFeatures::unknown()) + .build_and_sign(&secp_ctx).unwrap(); + + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + // Set the random bytes so we can predict the offer outbound payment context nonce. + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some([42; 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 just provide the invoice directly to the payer. + let _invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); + + let inbound_payment_key = inbound_payment::ExpandedKey::new( + &nodes[0].keys_manager.get_inbound_payment_key_material() + ); + let offer_outbound_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager); + let hmac = payment_id.hmac_for_offer_payment(offer_outbound_context_nonce, &inbound_payment_key); + if nodes[0].node.handle_message( + OffersMessage::StaticInvoice(static_invoice_unknown_req_features), + Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None + ).is_some() { panic!() } + + 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] +#[cfg(async_payments)] +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 dummy_blinded_path_to_always_online_node = BlindedMessagePath::from_raw( + nodes[1].node.get_our_node_id(), test_utils::pubkey(42), + vec![BlindedHop { blinded_node_id: test_utils::pubkey(42), encrypted_payload: vec![42; 32] }] + ); + let (offer_builder, offer_nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node.clone()]).unwrap(); + let offer = offer_builder.build().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + // Set the random bytes so we can predict the offer outbound payment context nonce. + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some([42; 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 just provide the invoice directly to the payer. + let _invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); + + // Create a static invoice with the same payment_id but corresponding to a different offer. + let unexpected_static_invoice = { + let (offer_builder, nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node]).unwrap(); + let sender_unintended_offer = offer_builder.build().unwrap(); + + nodes[2].node.create_static_invoice_builder_for_async_receive_offer( + &sender_unintended_offer, nonce, None + ).unwrap().build_and_sign(&secp_ctx).unwrap() + }; + + // Check that we'll ignore the unexpected static invoice. + let inbound_payment_key = inbound_payment::ExpandedKey::new( + &nodes[0].keys_manager.get_inbound_payment_key_material() + ); + let offer_outbound_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager); + let hmac = payment_id.hmac_for_offer_payment(offer_outbound_context_nonce, &inbound_payment_key); + assert!(nodes[0].node.handle_message( + OffersMessage::StaticInvoice(unexpected_static_invoice), + Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None + ).is_none()); + 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_for_async_receive_offer( + &offer, offer_nonce, None + ).unwrap().build_and_sign(&secp_ctx).unwrap(); + + assert!(nodes[0].node.handle_message( + OffersMessage::StaticInvoice(valid_static_invoice.clone()), + Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None + ).is_none()); + 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. + assert!(nodes[0].node.handle_message( + OffersMessage::StaticInvoice(valid_static_invoice), + Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None + ).is_none()); + let async_pmts_msgs = AsyncPaymentsMessageHandler::release_pending_messages(nodes[0].node); + assert!(async_pmts_msgs.is_empty()); +} + +#[test] +#[cfg(async_payments)] +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 dummy_blinded_path_to_always_online_node = BlindedMessagePath::from_raw( + nodes[1].node.get_our_node_id(), test_utils::pubkey(42), + vec![BlindedHop { blinded_node_id: test_utils::pubkey(42), encrypted_payload: vec![42; 32] }] + ); + let (offer_builder, offer_nonce) = nodes[2].node.create_async_receive_offer_builder(vec![dummy_blinded_path_to_always_online_node.clone()]).unwrap(); + let offer = offer_builder.build().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + // Set the random bytes so we can predict the offer outbound payment context nonce. + *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some([42; 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 just provide the invoice directly to the payer. + let _invreq_om = nodes[0].onion_messenger.next_onion_message_for_peer(nodes[1].node.get_our_node_id()).unwrap(); + + let inbound_payment_key = inbound_payment::ExpandedKey::new( + &nodes[0].keys_manager.get_inbound_payment_key_material() + ); + let offer_outbound_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager); + let hmac = payment_id.hmac_for_offer_payment(offer_outbound_context_nonce, &inbound_payment_key); + + let static_invoice = nodes[2].node.create_static_invoice_builder_for_async_receive_offer( + &offer, offer_nonce, None + ).unwrap().build_and_sign(&secp_ctx).unwrap(); + + assert!(nodes[0].node.handle_message( + OffersMessage::StaticInvoice(static_invoice), + Some(OffersContext::OutboundPayment { payment_id, nonce: offer_outbound_context_nonce, hmac: Some(hmac) }), None + ).is_none()); + 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(_)))); + + // Manually create the message and context releasing the HTLC since the recipient doesn't support + // responding themselves yet. + let outbound_async_payment_context_nonce = Nonce::from_entropy_source(nodes[0].keys_manager); + let outbound_async_payment_context = AsyncPaymentsContext::OutboundPayment { + payment_id, + nonce: outbound_async_payment_context_nonce, + hmac: payment_id.hmac_for_async_payment(outbound_async_payment_context_nonce, &inbound_payment_key), + }; + nodes[0].node.handle_release_held_htlc(ReleaseHeldHtlc {}, outbound_async_payment_context.clone()); + + // 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].node.handle_release_held_htlc(ReleaseHeldHtlc {}, outbound_async_payment_context.clone()); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); +} + fn secret_from_hex(hex: &str) -> SecretKey { SecretKey::from_slice(&>::from_hex(hex).unwrap()).unwrap() }