Skip to content

Commit f3c61ed

Browse files
shaavancodex
andcommitted
[test] Cover async invoice refresh after channel changes
Used async receive offers may already be published, so changes to their payment paths must update server-side static invoices without waiting for the normal age threshold. Cover channel-opening and counterparty forwarding-update flows. Verify that a newly usable channel adds another payment path and that a changed forwarding fee is encoded in the replacement invoice. Both flows confirm that the server receives the replacement invoice for the same offer slot. Ignore concurrent OfferPathsRequest messages so assertions remain focused on ServeStaticInvoice updates. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent 10b1988 commit f3c61ed

1 file changed

Lines changed: 179 additions & 1 deletion

File tree

lightning/src/ln/async_payments_tests.rs

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ use crate::sign::NodeSigner;
6060
use crate::sync::Mutex;
6161
use crate::types::features::Bolt12InvoiceFeatures;
6262
use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
63-
use crate::util::config::{HTLCInterceptionFlags, UserConfig};
63+
use crate::util::config::{ChannelConfigUpdate, HTLCInterceptionFlags, UserConfig};
6464
use crate::util::ser::Writeable;
6565
use bitcoin::constants::ChainHash;
6666
use bitcoin::network::Network;
@@ -416,6 +416,55 @@ fn extract_static_invoice_om<'a>(
416416
(peer_id, om, static_invoice.unwrap())
417417
}
418418

419+
/// Extracts the next static invoice update while ignoring unrelated offer-path requests.
420+
fn extract_serve_static_invoice_om<'a>(
421+
recipient: &'a Node, next_hop_nodes: &[&'a Node],
422+
) -> (PublicKey, msgs::OnionMessage, StaticInvoice) {
423+
let mut static_invoice = None;
424+
let mut expected_msg_type = |peeled_onion: &_| match peeled_onion {
425+
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(msg), _, _) => {
426+
static_invoice = Some(msg.invoice.clone());
427+
true
428+
},
429+
_ => false,
430+
};
431+
let expected_msg_type_to_ignore = |peeled_onion: &_| {
432+
matches!(
433+
peeled_onion,
434+
&PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _)
435+
)
436+
};
437+
let (peer_id, om) = extract_expected_om(
438+
recipient,
439+
next_hop_nodes,
440+
expected_msg_type,
441+
expected_msg_type_to_ignore,
442+
)
443+
.pop()
444+
.unwrap();
445+
(peer_id, om, static_invoice.unwrap())
446+
}
447+
448+
/// Delivers a static invoice update and checks that the server persists it in the expected slot.
449+
fn expect_static_invoice_persist_event(
450+
server: &Node, recipient: &Node, serve_static_invoice_om: &msgs::OnionMessage,
451+
expected_invoice: &StaticInvoice, expected_invoice_slot: u16, expected_recipient_id: &[u8],
452+
) {
453+
server
454+
.onion_messenger
455+
.handle_onion_message(recipient.node.get_our_node_id(), serve_static_invoice_om);
456+
let mut events = server.node.get_and_clear_pending_events();
457+
assert_eq!(events.len(), 1);
458+
match events.pop().unwrap() {
459+
Event::PersistStaticInvoice { invoice, invoice_slot, recipient_id, .. } => {
460+
assert_eq!(&invoice, expected_invoice);
461+
assert_eq!(invoice_slot, expected_invoice_slot);
462+
assert_eq!(recipient_id, expected_recipient_id);
463+
},
464+
_ => panic!(),
465+
}
466+
}
467+
419468
fn extract_held_htlc_available_oms<'a>(
420469
payer: &'a Node, next_hop_nodes: &[&'a Node],
421470
) -> Vec<(PublicKey, msgs::OnionMessage)> {
@@ -2507,6 +2556,135 @@ fn refresh_static_invoices_for_used_offers() {
25072556
assert_eq!(res.0, Some(PaidBolt12Invoice::StaticInvoice(updated_invoice)));
25082557
}
25092558

2559+
/// Checks that a used async receive offer gets a fresh server-side static invoice when a new
2560+
/// channel becomes usable. Used offers may already be published, so they should not wait for the
2561+
/// normal invoice refresh threshold after local payment paths change.
2562+
#[test]
2563+
fn refresh_static_invoices_for_used_offers_when_channel_opens() {
2564+
let chanmon_cfgs = create_chanmon_cfgs(3);
2565+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
2566+
2567+
let mut allow_priv_chan_fwds_cfg = test_default_channel_config();
2568+
allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true;
2569+
let node_chanmgrs =
2570+
create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]);
2571+
2572+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
2573+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
2574+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
2575+
let server = &nodes[1];
2576+
let recipient = &nodes[2];
2577+
2578+
let recipient_id = vec![42; 32];
2579+
let inv_server_paths =
2580+
server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap();
2581+
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
2582+
expect_offer_paths_requests(recipient, &[&nodes[0], server]);
2583+
2584+
let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone());
2585+
let original_invoice = flow_res.invoice;
2586+
assert_eq!(original_invoice.payment_paths().len(), 1);
2587+
2588+
// Mark the offer as used so the cache treats it as potentially published by the application.
2589+
let _offer = recipient.node.get_async_receive_offer().unwrap();
2590+
2591+
// Keep onion delivery direct so the test only checks that opening a channel refreshes the
2592+
// invoice after its forwarding information is available.
2593+
server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id());
2594+
recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id());
2595+
2596+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
2597+
let (peer_node_id, serve_static_invoice_om, updated_invoice) =
2598+
extract_serve_static_invoice_om(recipient, &[server]);
2599+
assert_eq!(peer_node_id, server.node.get_our_node_id());
2600+
assert_ne!(original_invoice, updated_invoice);
2601+
assert_eq!(updated_invoice.payment_paths().len(), 2);
2602+
2603+
expect_static_invoice_persist_event(
2604+
server,
2605+
recipient,
2606+
&serve_static_invoice_om,
2607+
&updated_invoice,
2608+
flow_res.invoice_slot,
2609+
&recipient_id,
2610+
);
2611+
}
2612+
2613+
/// Checks that changed forwarding parameters refresh the static invoice for a used offer without
2614+
/// waiting for the normal invoice refresh threshold.
2615+
#[test]
2616+
fn refresh_static_invoices_for_used_offers_when_forwarding_fees_change() {
2617+
let chanmon_cfgs = create_chanmon_cfgs(3);
2618+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
2619+
2620+
let mut allow_priv_chan_fwds_cfg = test_default_channel_config();
2621+
allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true;
2622+
let node_chanmgrs =
2623+
create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]);
2624+
2625+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
2626+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
2627+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0);
2628+
let server = &nodes[1];
2629+
let recipient = &nodes[2];
2630+
2631+
let recipient_id = vec![42; 32];
2632+
let inv_server_paths =
2633+
server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap();
2634+
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
2635+
expect_offer_paths_requests(recipient, &[&nodes[0], server]);
2636+
2637+
let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone());
2638+
let original_invoice = flow_res.invoice;
2639+
let _offer = recipient.node.get_async_receive_offer().unwrap();
2640+
2641+
// Keep onion delivery direct so the test only checks the forwarding update trigger.
2642+
server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id());
2643+
recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id());
2644+
2645+
let channel = server
2646+
.node
2647+
.list_channels()
2648+
.into_iter()
2649+
.find(|channel| channel.counterparty.node_id == recipient.node.get_our_node_id())
2650+
.unwrap();
2651+
let updated_fee_base_msat = channel.config.unwrap().forwarding_fee_base_msat + 10;
2652+
let config_update = ChannelConfigUpdate {
2653+
forwarding_fee_base_msat: Some(updated_fee_base_msat),
2654+
..ChannelConfigUpdate::default()
2655+
};
2656+
server
2657+
.node
2658+
.update_partial_channel_config(
2659+
&recipient.node.get_our_node_id(),
2660+
&[channel.channel_id],
2661+
&config_update,
2662+
)
2663+
.unwrap();
2664+
let channel_update = get_event_msg!(
2665+
server,
2666+
MessageSendEvent::SendChannelUpdate,
2667+
recipient.node.get_our_node_id()
2668+
);
2669+
recipient.node.handle_channel_update(server.node.get_our_node_id(), &channel_update);
2670+
2671+
let (peer_node_id, serve_static_invoice_om, updated_invoice) =
2672+
extract_serve_static_invoice_om(recipient, &[server]);
2673+
assert_eq!(peer_node_id, server.node.get_our_node_id());
2674+
assert_ne!(original_invoice, updated_invoice);
2675+
assert_eq!(updated_invoice.payment_paths().len(), 1);
2676+
assert_eq!(updated_invoice.payment_paths()[0].payinfo.fee_base_msat, updated_fee_base_msat);
2677+
2678+
expect_static_invoice_persist_event(
2679+
server,
2680+
recipient,
2681+
&serve_static_invoice_om,
2682+
&updated_invoice,
2683+
flow_res.invoice_slot,
2684+
&recipient_id,
2685+
);
2686+
}
2687+
25102688
#[cfg_attr(feature = "std", ignore)]
25112689
#[test]
25122690
fn ignore_expired_static_invoice() {

0 commit comments

Comments
 (0)