From 3d25f109d36790adb1f9ee0a7f4c50aad513b86a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 30 Jan 2025 17:27:23 -0500 Subject: [PATCH 01/18] Remove Copy implementation from UserConfig We need to add a Vec to the config as part of async receive support, see upcoming commits. The vec will contain blinded paths to reach an always-online node that will serve static invoices on our behalf. None of those things implement Copy so remove support for the trait. --- lightning/src/ln/async_signer_tests.rs | 2 +- lightning/src/ln/chanmon_update_fail_tests.rs | 6 ++--- lightning/src/ln/channelmanager.rs | 6 ++--- lightning/src/ln/functional_test_utils.rs | 4 +-- lightning/src/ln/functional_tests.rs | 12 ++++----- lightning/src/ln/invoice_utils.rs | 2 +- lightning/src/ln/monitor_tests.rs | 26 +++++++++---------- lightning/src/ln/onion_route_tests.rs | 10 +++---- lightning/src/ln/payment_tests.rs | 10 +++---- lightning/src/ln/priv_short_conf_tests.rs | 14 +++++----- lightning/src/ln/reorg_tests.rs | 4 +-- lightning/src/ln/shutdown_tests.rs | 2 +- lightning/src/util/config.rs | 2 +- 13 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs index 0cd78738e9b..d5d8b955efa 100644 --- a/lightning/src/ln/async_signer_tests.rs +++ b/lightning/src/ln/async_signer_tests.rs @@ -817,7 +817,7 @@ fn do_test_async_holder_signatures(anchors: bool, remote_commitment: bool) { 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, &[Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let closing_node = if remote_commitment { &nodes[1] } else { &nodes[0] }; diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index 497f724a243..48ffae1c9bb 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -2591,7 +2591,7 @@ fn test_temporary_error_during_shutdown() { 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, &[Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (_, _, channel_id, funding_tx) = create_announced_chan_between_nodes(&nodes, 0, 1); @@ -2762,7 +2762,7 @@ fn do_test_outbound_reload_without_init_mon(use_0conf: bool) { chan_config.manually_accept_inbound_channels = true; chan_config.channel_handshake_limits.trust_own_funding_0conf = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(chan_config), Some(chan_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(chan_config.clone()), Some(chan_config)]); let nodes_0_deserialized; let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -2853,7 +2853,7 @@ fn do_test_inbound_reload_without_init_mon(use_0conf: bool, lock_commitment: boo chan_config.manually_accept_inbound_channels = true; chan_config.channel_handshake_limits.trust_own_funding_0conf = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(chan_config), Some(chan_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(chan_config.clone()), Some(chan_config)]); let nodes_1_deserialized; let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 666258d383c..fc5229e3f50 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1744,7 +1744,7 @@ where /// let default_config = UserConfig::default(); /// let channel_manager = ChannelManager::new( /// fee_estimator, chain_monitor, tx_broadcaster, router, message_router, logger, -/// entropy_source, node_signer, signer_provider, default_config, params, current_timestamp, +/// entropy_source, node_signer, signer_provider, default_config.clone(), params, current_timestamp, /// ); /// /// // Restart from deserialized data @@ -16130,7 +16130,7 @@ mod tests { let chanmon_cfg = create_chanmon_cfgs(2); let node_cfg = create_node_cfgs(2, &chanmon_cfg); let mut user_config = test_default_channel_config(); - let node_chanmgr = create_node_chanmgrs(2, &node_cfg, &[Some(user_config), Some(user_config)]); + let node_chanmgr = create_node_chanmgrs(2, &node_cfg, &[Some(user_config.clone()), Some(user_config.clone())]); let nodes = create_network(2, &node_cfg, &node_chanmgr); let _ = create_announced_chan_between_nodes(&nodes, 0, 1); let channel = &nodes[0].node.list_channels()[0]; @@ -16217,7 +16217,7 @@ mod tests { let chanmon_cfg = create_chanmon_cfgs(2); let node_cfg = create_node_cfgs(2, &chanmon_cfg); let user_config = test_default_channel_config(); - let node_chanmgr = create_node_chanmgrs(2, &node_cfg, &[Some(user_config), Some(user_config)]); + let node_chanmgr = create_node_chanmgrs(2, &node_cfg, &[Some(user_config.clone()), Some(user_config)]); let nodes = create_network(2, &node_cfg, &node_chanmgr); let error_message = "Channel force-closed"; diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 2d485a345a9..e1328b2ddc8 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -719,7 +719,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { let mut w = test_utils::TestVecWriter(Vec::new()); self.node.write(&mut w).unwrap(); <(BlockHash, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut io::Cursor::new(w.0), ChannelManagerReadArgs { - default_config: *self.node.get_current_default_configuration(), + default_config: self.node.get_current_default_configuration().clone(), entropy_source: self.keys_manager, node_signer: self.keys_manager, signer_provider: self.keys_manager, @@ -3971,7 +3971,7 @@ pub fn create_batch_channel_funding<'a, 'b, 'c>( let temp_chan_id = funding_node.node.create_channel( other_node.node.get_our_node_id(), *channel_value_satoshis, *push_msat, *user_channel_id, None, - *override_config, + override_config.clone(), ).unwrap(); let open_channel_msg = get_event_msg!(funding_node, MessageSendEvent::SendOpenChannel, other_node.node.get_our_node_id()); other_node.node.handle_open_channel(funding_node.node.get_our_node_id(), &open_channel_msg); diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index ff0646bfa6c..dd8ecb32828 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -104,7 +104,7 @@ fn test_insane_channel_opens() { cfg.channel_handshake_limits.max_funding_satoshis = TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1; 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, Some(cfg)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(cfg.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); // Instantiate channel parameters where we push the maximum msats given our @@ -2903,7 +2903,7 @@ fn test_multiple_package_conflicts() { user_cfg.manually_accept_inbound_channels = true; let node_chanmgrs = - create_node_chanmgrs(3, &node_cfgs, &[Some(user_cfg), Some(user_cfg), Some(user_cfg)]); + create_node_chanmgrs(3, &node_cfgs, &[Some(user_cfg.clone()), Some(user_cfg.clone()), Some(user_cfg)]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); // Since we're using anchor channels, make sure each node has a UTXO for paying fees. @@ -10622,7 +10622,7 @@ fn test_nondust_htlc_excess_fees_are_dust() { config.channel_handshake_config.our_htlc_minimum_msat = 1; config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config), Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config)]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); // Leave enough on the funder side to let it pay the mining fees for a commit tx with tons of htlcs @@ -11810,7 +11810,7 @@ fn do_test_funding_and_commitment_tx_confirm_same_block(confirm_remote_commitmen let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let mut min_depth_1_block_cfg = test_default_channel_config(); min_depth_1_block_cfg.channel_handshake_config.minimum_depth = 1; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(min_depth_1_block_cfg), Some(min_depth_1_block_cfg)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(min_depth_1_block_cfg.clone()), Some(min_depth_1_block_cfg)]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); let funding_tx = create_chan_between_nodes_with_value_init(&nodes[0], &nodes[1], 1_000_000, 0); @@ -11899,7 +11899,7 @@ fn test_manual_funding_abandon() { cfg.channel_handshake_config.minimum_depth = 1; 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, &[Some(cfg), Some(cfg)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(cfg.clone()), Some(cfg)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); assert!(nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 42, None, None).is_ok()); @@ -11941,7 +11941,7 @@ fn test_funding_signed_event() { cfg.channel_handshake_config.minimum_depth = 1; 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, &[Some(cfg), Some(cfg)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(cfg.clone()), Some(cfg)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); assert!(nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 42, None, None).is_ok()); diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 5be0b3f4b97..69d53573611 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -939,7 +939,7 @@ mod test { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let mut config = test_default_channel_config(); config.channel_handshake_config.minimum_depth = 1; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); // Create a private channel with lots of capacity and a lower value public channel (without diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index 95958e81c38..d482fd7a1c0 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -171,7 +171,7 @@ fn archive_fully_resolved_monitors() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let mut user_config = test_default_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (_, _, chan_id, funding_tx) = @@ -315,7 +315,7 @@ fn do_chanmon_claim_value_coop_close(anchors: bool) { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (_, _, chan_id, funding_tx) = @@ -459,7 +459,7 @@ fn do_test_claim_value_force_close(anchors: bool, prev_commitment_tx: bool) { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let coinbase_tx = Transaction { @@ -862,7 +862,7 @@ fn do_test_balances_on_local_commitment_htlcs(anchors: bool) { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); let coinbase_tx = Transaction { @@ -1359,7 +1359,7 @@ fn do_test_revoked_counterparty_commitment_balances(anchors: bool, confirm_htlc_ user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (_, _, chan_id, funding_tx) = @@ -1645,7 +1645,7 @@ fn do_test_revoked_counterparty_htlc_tx_balances(anchors: bool) { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let coinbase_tx = Transaction { @@ -1946,7 +1946,7 @@ fn do_test_revoked_counterparty_aggregated_claims(anchors: bool) { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let coinbase_tx = Transaction { @@ -2236,7 +2236,7 @@ fn do_test_claimable_balance_correct_while_payment_pending(outbound_payment: boo user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(user_config), Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(user_config.clone()), Some(user_config.clone()), Some(user_config)]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); let coinbase_tx = Transaction { @@ -2401,7 +2401,7 @@ fn do_test_monitor_rebroadcast_pending_claims(anchors: bool) { config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (_, _, _, chan_id, funding_tx) = create_chan_between_nodes_with_value( @@ -2533,7 +2533,7 @@ fn do_test_yield_anchors_events(have_htlcs: bool) { anchors_config.channel_handshake_config.announce_for_forwarding = true; anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; anchors_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config), Some(anchors_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config.clone()), Some(anchors_config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (_, _, chan_id, funding_tx) = create_announced_chan_between_nodes_with_value( @@ -2731,7 +2731,7 @@ fn test_anchors_aggregated_revoked_htlc_tx() { anchors_config.channel_handshake_config.announce_for_forwarding = true; anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; anchors_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config), Some(anchors_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config.clone()), Some(anchors_config.clone())]); let bob_deserialized; let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -3032,7 +3032,7 @@ fn do_test_anchors_monitor_fixes_counterparty_payment_script_on_reload(confirm_c let mut user_config = test_default_channel_config(); user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config.clone())]); let node_deserialized; let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -3120,7 +3120,7 @@ fn do_test_monitor_claims_with_random_signatures(anchors: bool, confirm_counterp user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config), Some(user_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(user_config.clone()), Some(user_config)]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); let coinbase_tx = Transaction { diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index cee8bd1d301..cd617e5c7ce 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -290,7 +290,7 @@ fn test_fee_failures() { 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, &[Some(config), Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config)]); let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); let channels = [create_announced_chan_between_nodes(&nodes, 0, 1), create_announced_chan_between_nodes(&nodes, 1, 2)]; @@ -346,7 +346,7 @@ fn test_onion_failure() { 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, &[Some(config), Some(config), Some(node_2_cfg)]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config), Some(node_2_cfg)]); let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); let channels = [create_announced_chan_between_nodes(&nodes, 0, 1), create_announced_chan_between_nodes(&nodes, 1, 2)]; for node in nodes.iter() { @@ -728,7 +728,7 @@ fn test_onion_failure() { fn test_overshoot_final_cltv() { 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; 3]); + 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(&nodes, 0, 1); create_announced_chan_between_nodes(&nodes, 1, 2); @@ -892,7 +892,7 @@ fn do_test_onion_failure_stale_channel_update(announce_for_forwarding: bool) { .find(|channel| channel.channel_id == channel_to_update.0).unwrap() .config.unwrap(); config.forwarding_fee_base_msat = u32::max_value(); - let msg = update_and_get_channel_update(&config, true, None, false).unwrap(); + let msg = update_and_get_channel_update(&config.clone(), true, None, false).unwrap(); // The old policy should still be in effect until a new block is connected. send_along_route_with_secret(&nodes[0], route.clone(), &[&[&nodes[1], &nodes[2]]], PAYMENT_AMT, @@ -929,7 +929,7 @@ fn do_test_onion_failure_stale_channel_update(announce_for_forwarding: bool) { let config_after_restart = { let chan_1_monitor_serialized = get_monitor!(nodes[1], other_channel.3).encode(); let chan_2_monitor_serialized = get_monitor!(nodes[1], channel_to_update.0).encode(); - reload_node!(nodes[1], *nodes[1].node.get_current_default_configuration(), &nodes[1].node.encode(), + reload_node!(nodes[1], nodes[1].node.get_current_default_configuration().clone(), &nodes[1].node.encode(), &[&chan_1_monitor_serialized, &chan_2_monitor_serialized], persister, chain_monitor, channel_manager_1_deserialized); nodes[1].node.list_channels().iter() .find(|channel| channel.channel_id == channel_to_update.0).unwrap() diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 66322cf107e..b12db7268bf 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -182,7 +182,7 @@ fn mpp_retry_overpay() { let mut limited_config_2 = user_config.clone(); limited_config_2.channel_handshake_config.our_htlc_minimum_msat = 34_500_000; let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, - &[Some(user_config), Some(limited_config_1), Some(limited_config_2), Some(user_config)]); + &[Some(user_config.clone()), Some(limited_config_1), Some(limited_config_2), Some(user_config)]); let nodes = create_network(4, &node_cfgs, &node_chanmgrs); let (chan_1_update, _, _, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 40_000, 0); @@ -3706,7 +3706,7 @@ fn test_custom_tlvs_explicit_claim() { fn do_test_custom_tlvs(spontaneous: bool, even_tlvs: bool, known_tlvs: bool) { 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; 2]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes(&nodes, 0, 1); @@ -4032,7 +4032,7 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { let mut config = test_default_channel_config(); config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 50; - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, Some(config), Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, Some(config.clone()), Some(config.clone()), Some(config.clone())]); let nodes_0_deserialized; let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); @@ -4193,7 +4193,7 @@ fn test_htlc_forward_considers_anchor_outputs_value() { // discovery of this bug. 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, &[Some(config), Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config.clone())]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); const CHAN_AMT: u64 = 1_000_000; @@ -4328,7 +4328,7 @@ fn test_non_strict_forwarding() { let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let mut config = test_default_channel_config(); config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config), Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(config.clone()), Some(config.clone()), Some(config)]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); // Create a routing node with two outbound channels, each of which can forward 2 payments of diff --git a/lightning/src/ln/priv_short_conf_tests.rs b/lightning/src/ln/priv_short_conf_tests.rs index 97d3c68f9f6..1d3e13e12e6 100644 --- a/lightning/src/ln/priv_short_conf_tests.rs +++ b/lightning/src/ln/priv_short_conf_tests.rs @@ -38,7 +38,7 @@ fn test_priv_forwarding_rejection() { no_announce_cfg.accept_forwards_to_priv_channels = false; let persister; let new_chain_monitor; - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(no_announce_cfg), None]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(no_announce_cfg.clone()), None]); let nodes_1_deserialized; let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); @@ -577,7 +577,7 @@ fn test_0conf_channel_with_async_monitor() { let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let mut chan_config = test_default_channel_config(); chan_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(chan_config), None]); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(chan_config.clone()), None]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); @@ -744,7 +744,7 @@ fn test_0conf_close_no_early_chan_update() { let mut chan_config = test_default_channel_config(); chan_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let error_message = "Channel force-closed"; @@ -769,7 +769,7 @@ fn test_public_0conf_channel() { let mut chan_config = test_default_channel_config(); chan_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); // This is the default but we force it on anyway @@ -823,7 +823,7 @@ fn test_0conf_channel_reorg() { let mut chan_config = test_default_channel_config(); chan_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); // This is the default but we force it on anyway @@ -902,7 +902,7 @@ fn test_zero_conf_accept_reject() { // 2.1 First try the non-0conf method to manually accept nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100000, 10001, 42, - None, Some(manually_accept_conf)).unwrap(); + None, Some(manually_accept_conf.clone())).unwrap(); let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id()); @@ -1014,7 +1014,7 @@ fn test_0conf_ann_sigs_racing_conf() { let mut chan_config = test_default_channel_config(); chan_config.manually_accept_inbound_channels = true; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(chan_config.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); // This is the default but we force it on anyway diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs index b1b4f77c590..e6c5994b7a2 100644 --- a/lightning/src/ln/reorg_tests.rs +++ b/lightning/src/ln/reorg_tests.rs @@ -317,7 +317,7 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ let nodes_0_serialized = nodes[0].node.encode(); let chan_0_monitor_serialized = get_monitor!(nodes[0], chan.2).encode(); - reload_node!(nodes[0], *nodes[0].node.get_current_default_configuration(), &nodes_0_serialized, &[&chan_0_monitor_serialized], persister, new_chain_monitor, nodes_0_deserialized); + reload_node!(nodes[0], nodes[0].node.get_current_default_configuration().clone(), &nodes_0_serialized, &[&chan_0_monitor_serialized], persister, new_chain_monitor, nodes_0_deserialized); } if reorg_after_reload { @@ -793,7 +793,7 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(anchors: bool, revoked_c } let persister; let new_chain_monitor; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); let nodes_1_deserialized; let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); diff --git a/lightning/src/ln/shutdown_tests.rs b/lightning/src/ln/shutdown_tests.rs index c068d7f12d6..5e217fb6c96 100644 --- a/lightning/src/ln/shutdown_tests.rs +++ b/lightning/src/ln/shutdown_tests.rs @@ -981,7 +981,7 @@ fn test_unsupported_anysegwit_shutdown_script() { config.channel_handshake_config.announce_for_forwarding = true; config.channel_handshake_limits.force_announced_channel_preference = false; config.channel_handshake_config.commit_upfront_shutdown_pubkey = false; - let user_cfgs = [None, Some(config), None]; + let user_cfgs = [None, Some(config.clone()), None]; let chanmon_cfgs = create_chanmon_cfgs(3); let mut node_cfgs = create_node_cfgs(3, &chanmon_cfgs); *node_cfgs[0].override_init_features.borrow_mut() = Some(channelmanager::provided_init_features(&config).clear_shutdown_anysegwit()); diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index ef35df1a0b1..612be819c98 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -810,7 +810,7 @@ impl crate::util::ser::Readable for LegacyChannelConfig { /// /// `Default::default()` provides sane defaults for most configurations /// (but currently with zero relay fees!) -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct UserConfig { /// Channel handshake config that we propose to our counterparty. pub channel_handshake_config: ChannelHandshakeConfig, From 6782d6aa3bf4490d44411763769f6c29cb802f32 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 16:19:18 -0500 Subject: [PATCH 02/18] ChannelManager: DRY queueing onion messages The new utility will be used in upcoming commits as part of supporting static invoice server. --- lightning/src/ln/channelmanager.rs | 71 ++++++++++++++---------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fc5229e3f50..21a93ef6d99 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -78,6 +78,7 @@ use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailab use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; +use crate::onion_message::packet::OnionMessageContents; use crate::sign::{EntropySource, NodeSigner, Recipient, SignerProvider}; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::util::config::{ChannelConfig, ChannelConfigUpdate, ChannelConfigOverrides, UserConfig}; @@ -4870,19 +4871,10 @@ where }; let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); - const HTLC_AVAILABLE_LIMIT: usize = 10; - reply_paths - .iter() - .flat_map(|reply_path| invoice.message_paths().iter().map(move |invoice_path| (invoice_path, reply_path))) - .take(HTLC_AVAILABLE_LIMIT) - .for_each(|(invoice_path, reply_path)| { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::BlindedPath(invoice_path.clone()), - reply_path: reply_path.clone(), - }; - let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); - pending_async_payments_messages.push((message, instructions)); - }); + let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); + queue_onion_message_with_reply_paths( + message, invoice.message_paths(), reply_paths, &mut pending_async_payments_messages + ); NotifyOption::DoPersist }); @@ -10437,18 +10429,10 @@ where ) -> Result<(), Bolt12SemanticError> { let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); if !invoice_request.paths().is_empty() { - reply_paths - .iter() - .flat_map(|reply_path| invoice_request.paths().iter().map(move |path| (path, reply_path))) - .take(OFFERS_MESSAGE_REQUEST_LIMIT) - .for_each(|(path, reply_path)| { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::BlindedPath(path.clone()), - reply_path: reply_path.clone(), - }; - let message = OffersMessage::InvoiceRequest(invoice_request.clone()); - pending_offers_messages.push((message, instructions)); - }); + let message = OffersMessage::InvoiceRequest(invoice_request.clone()); + queue_onion_message_with_reply_paths( + message, invoice_request.paths(), reply_paths, &mut pending_offers_messages + ); } else if let Some(node_id) = invoice_request.issuer_signing_pubkey() { for reply_path in reply_paths { let instructions = MessageSendInstructions::WithSpecifiedReplyPath { @@ -10546,18 +10530,10 @@ where pending_offers_messages.push((message, instructions)); } } else { - reply_paths - .iter() - .flat_map(|reply_path| refund.paths().iter().map(move |path| (path, reply_path))) - .take(OFFERS_MESSAGE_REQUEST_LIMIT) - .for_each(|(path, reply_path)| { - let instructions = MessageSendInstructions::WithSpecifiedReplyPath { - destination: Destination::BlindedPath(path.clone()), - reply_path: reply_path.clone(), - }; - let message = OffersMessage::Invoice(invoice.clone()); - pending_offers_messages.push((message, instructions)); - }); + let message = OffersMessage::Invoice(invoice.clone()); + queue_onion_message_with_reply_paths( + message, refund.paths(), reply_paths, &mut pending_offers_messages + ); } Ok(invoice) @@ -12692,6 +12668,27 @@ where } } +fn queue_onion_message_with_reply_paths( + message: T, message_paths: &[BlindedMessagePath], reply_paths: Vec, + queue: &mut Vec<(T, MessageSendInstructions)> +) { + reply_paths + .iter() + .flat_map(|reply_path| + message_paths + .iter() + .map(move |path| (path.clone(), reply_path)) + ) + .take(OFFERS_MESSAGE_REQUEST_LIMIT) + .for_each(|(path, reply_path)| { + let instructions = MessageSendInstructions::WithSpecifiedReplyPath { + destination: Destination::BlindedPath(path.clone()), + reply_path: reply_path.clone(), + }; + queue.push((message.clone(), instructions)); + }); +} + /// Fetches the set of [`NodeFeatures`] flags that are provided by or required by /// [`ChannelManager`]. pub(crate) fn provided_node_features(config: &UserConfig) -> NodeFeatures { From 3dbfc328a9b5f3f736f6a31b1e074bab06834d03 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 4 Feb 2025 17:02:30 -0800 Subject: [PATCH 03/18] OnionMessenger: DRY extracting message context We're about to add more onion message types, so this pre-emptively DRYs the code to extract the expected message context taken from the blinded path. --- lightning/src/onion_message/messenger.rs | 38 +++++++++++------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 012b7978053..4ac2ae90639 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1820,6 +1820,18 @@ where { fn handle_onion_message(&self, peer_node_id: PublicKey, msg: &OnionMessage) { let logger = WithContext::from(&self.logger, Some(peer_node_id), None, None); + macro_rules! extract_expected_context { + ($context: expr, $expected_context_type: path) => { + match $context { + Some($expected_context_type(context)) => context, + Some(_) => { + debug_assert!(false, "Checked in peel_onion_message"); + return; + }, + None => return, + } + }; + } match self.peel_onion_message(msg) { Ok(PeeledOnion::Receive(message, context, reply_path)) => { log_trace!( @@ -1850,14 +1862,8 @@ where 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 context = + extract_expected_context!(context, MessageContext::AsyncPayments); let response_instructions = self .async_payments_handler .handle_held_htlc_available(msg, context, responder); @@ -1869,14 +1875,8 @@ where ParsedOnionMessageContents::AsyncPayments( AsyncPaymentsMessage::ReleaseHeldHtlc(msg), ) => { - let context = match context { - Some(MessageContext::AsyncPayments(context)) => context, - Some(_) => { - debug_assert!(false, "Checked in peel_onion_message"); - return; - }, - None => return, - }; + let context = + extract_expected_context!(context, MessageContext::AsyncPayments); self.async_payments_handler.handle_release_held_htlc(msg, context); }, ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECQuery( @@ -1891,10 +1891,8 @@ where ParsedOnionMessageContents::DNSResolver(DNSResolverMessage::DNSSECProof( msg, )) => { - let context = match context { - Some(MessageContext::DNSResolver(context)) => context, - _ => return, - }; + let context = + extract_expected_context!(context, MessageContext::DNSResolver); self.dns_resolver_handler.handle_dnssec_proof(msg, context); }, ParsedOnionMessageContents::Custom(msg) => { From 2e39b0615b2ee4bf87ec995b9cd02e9c95633f90 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 5 Feb 2025 16:33:27 -0800 Subject: [PATCH 04/18] Un-cfg-gate StaticInvoices We need to include static invoices in the public API as part of the onion messages we're adding for static invoice server support. Utilities to create these static invoices and other parts of the async receive API will remain cfg-gated for now. Generally, we can't totally avoid exposing half baked async receive support in the public API because OnionMessenger is parameterized by an async payments message handler, which can't be cfg-gated easily. --- lightning/src/offers/mod.rs | 1 - lightning/src/offers/offer.rs | 1 - lightning/src/offers/static_invoice.rs | 2 ++ 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index e4fe7d789db..49a95b96f86 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -25,7 +25,6 @@ pub mod parse; mod payer; pub mod refund; pub(crate) mod signer; -#[cfg(async_payments)] pub mod static_invoice; #[cfg(test)] pub(crate) mod test_utils; diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index e9a2d9428e2..d93bda8e72a 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -742,7 +742,6 @@ impl Offer { .chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES)) } - #[cfg(async_payments)] pub(super) fn verify( &self, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1, ) -> Result<(OfferId, Option), ()> { diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index e61ae781bc1..091b06170a4 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -22,6 +22,7 @@ use crate::offers::invoice::{ #[cfg(test)] use crate::offers::invoice_macros::invoice_builder_methods_test_common; use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; +#[cfg(async_payments)] use crate::offers::invoice_request::InvoiceRequest; use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, @@ -379,6 +380,7 @@ impl StaticInvoice { self.signature } + #[cfg(async_payments)] pub(crate) fn from_same_offer(&self, invreq: &InvoiceRequest) -> bool { let invoice_offer_tlv_stream = Offer::tlv_stream_iter(&self.bytes).map(|tlv_record| tlv_record.record_bytes); From 7d017d614128622c1d97498bc4c8c3a863a1c006 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 16:24:03 -0800 Subject: [PATCH 05/18] Persist cached async receive offer in ChannelManager In future commits, as part of being an async recipient, we will interactively build an offer and static invoice with an always-online node that will serve static invoices on our behalf. Once the offer is built and the static invoice is confirmed as persisted by the server, we will use this struct to cache the offer in ChannelManager. --- lightning/src/ln/channelmanager.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 21a93ef6d99..ad82bb5cd45 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1501,6 +1501,22 @@ struct PendingInboundPayment { min_value_msat: Option, } +/// If we are an async recipient, on startup we interactively build an offer and static invoice with +/// an always-online node that will serve static invoices on our behalf. Once the offer is built and +/// the static invoice is confirmed as persisted by the server, use this struct to cache the offer +/// in `ChannelManager`. +struct AsyncReceiveOffer { + offer: Option, + /// Used to limit the number of times we request paths for our offer from the static invoice + /// server. + offer_paths_request_attempts: u8, +} + +impl_writeable_tlv_based!(AsyncReceiveOffer, { + (0, offer, option), + (2, offer_paths_request_attempts, (static_value, 0)), +}); + /// [`SimpleArcChannelManager`] is useful when you need a [`ChannelManager`] with a static lifetime, e.g. /// when you're using `lightning-net-tokio` (since `tokio::spawn` requires parameters with static /// lifetimes). Other times you can afford a reference, which is more efficient, in which case @@ -2663,6 +2679,7 @@ where #[cfg(any(test, feature = "_test_utils"))] pub(crate) pending_offers_messages: Mutex>, pending_async_payments_messages: Mutex>, + async_receive_offer_cache: Mutex, /// Tracks the message events that are to be broadcasted when we are connected to some peer. pending_broadcast_messages: Mutex>, @@ -3617,6 +3634,7 @@ where pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), + async_receive_offer_cache: Mutex::new(AsyncReceiveOffer { offer: None, offer_paths_request_attempts: 0 }), pending_broadcast_messages: Mutex::new(Vec::new()), last_days_feerates: Mutex::new(VecDeque::new()), @@ -13405,6 +13423,7 @@ where (15, self.inbound_payment_id_secret, required), (17, in_flight_monitor_updates, required), (19, peer_storage_dir, optional_vec), + (21, *self.async_receive_offer_cache.lock().unwrap(), required), }); Ok(()) @@ -13934,6 +13953,7 @@ where let mut decode_update_add_htlcs: Option>> = None; let mut inbound_payment_id_secret = None; let mut peer_storage_dir: Option)>> = None; + let mut async_receive_offer_cache = AsyncReceiveOffer { offer: None, offer_paths_request_attempts: 0 }; read_tlv_fields!(reader, { (1, pending_outbound_payments_no_retry, option), (2, pending_intercepted_htlcs, option), @@ -13951,6 +13971,7 @@ where (15, inbound_payment_id_secret, option), (17, in_flight_monitor_updates, required), (19, peer_storage_dir, optional_vec), + (21, async_receive_offer_cache, (default_value, AsyncReceiveOffer { offer: None, offer_paths_request_attempts: 0 })), }); let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map()); let peer_storage_dir: Vec<(PublicKey, Vec)> = peer_storage_dir.unwrap_or_else(Vec::new); @@ -14646,6 +14667,7 @@ where pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), + async_receive_offer_cache: Mutex::new(async_receive_offer_cache), pending_broadcast_messages: Mutex::new(Vec::new()), @@ -15073,8 +15095,8 @@ mod tests { let nodes = create_network(2, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes(&nodes, 0, 1); - - // Since we do not send peer storage, we manually simulate receiving a dummy + + // Since we do not send peer storage, we manually simulate receiving a dummy // `PeerStorage` from the channel partner. nodes[0].node.handle_peer_storage(nodes[1].node.get_our_node_id(), msgs::PeerStorage{data: vec![0; 100]}); From 606d4b0f6cdd5d66882e3b11c9637e539ffefea3 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 17:12:06 -0800 Subject: [PATCH 06/18] Add config for paths to a static invoice server As part of being an async recipient, we need to support interactively building an offer and static invoice with an always-online node that will serve static invoices on our behalf. Add a config field containing blinded message paths that async recipients can use to request blinded paths that will be included in their offer. Payers will forward invoice requests over the paths returned by the server, and receive a static invoice in response if the recipient is offline. --- lightning/src/util/config.rs | 9 +++++++++ lightning/src/util/ser.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 612be819c98..40a1c5ef58f 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -10,8 +10,10 @@ //! Various user-configurable channel limits and settings which ChannelManager //! applies for you. +use crate::blinded_path::message::BlindedMessagePath; use crate::ln::channel::MAX_FUNDING_SATOSHIS_NO_WUMBO; use crate::ln::channelmanager::{BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT}; +use crate::prelude::*; #[cfg(fuzzing)] use crate::util::ser::Readable; @@ -878,6 +880,11 @@ pub struct UserConfig { /// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment pub manually_handle_bolt12_invoices: bool, + /// [`BlindedMessagePath`]s to reach an always-online node that will serve [`StaticInvoice`]s on + /// our behalf. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + pub paths_to_static_invoice_server: Vec, } impl Default for UserConfig { @@ -891,6 +898,7 @@ impl Default for UserConfig { manually_accept_inbound_channels: false, accept_intercept_htlcs: false, manually_handle_bolt12_invoices: false, + paths_to_static_invoice_server: Vec::new(), } } } @@ -910,6 +918,7 @@ impl Readable for UserConfig { manually_accept_inbound_channels: Readable::read(reader)?, accept_intercept_htlcs: Readable::read(reader)?, manually_handle_bolt12_invoices: Readable::read(reader)?, + paths_to_static_invoice_server: Vec::new(), }) } } diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 21997c09c1a..ed4082c89d2 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -1057,6 +1057,7 @@ impl_for_vec!(crate::chain::channelmonitor::ChannelMonitorUpdate); impl_for_vec!(crate::ln::channelmanager::MonitorUpdateCompletionAction); impl_for_vec!(crate::ln::channelmanager::PaymentClaimDetails); impl_for_vec!(crate::ln::msgs::SocketAddress); +impl_for_vec!(crate::blinded_path::message::BlindedMessagePath); impl_for_vec!((A, B), A, B); impl_writeable_for_vec!(&crate::routing::router::BlindedTail); impl_readable_for_vec!(crate::routing::router::BlindedTail); From 9d3d326cf20fd6e3d4ede9ee0aa20d392800ecdb Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 5 Feb 2025 17:19:51 -0800 Subject: [PATCH 07/18] Add static invoice server messages and boilerplate Because async recipients are not online to respond to invoice requests, the plan is for another node on the network that is always-online to serve static invoices on their behalf. The protocol is as follows: - Recipient is configured with blinded message paths to reach the static invoice server - On startup, recipient requests blinded message paths for inclusion in their offer from the static invoice server over the configured paths - Server replies with offer paths for the recipient - Recipient builds their offer using these paths and the corresponding static invoice and replies with the invoice - Server persists the invoice and confirms that they've persisted it, causing the recipient to cache the interactively built offer for use At pay-time, the payer sends an invoice request to the static invoice server, who replies with the static invoice after forwarding the invreq to the recipient (to give them a chance to provide a fresh invoice in case they're online). Here we add the requisite trait methods and onion messages to support this protocol. --- fuzz/src/onion_message.rs | 26 ++- lightning/src/ln/channelmanager.rs | 26 ++- lightning/src/ln/peer_handler.rs | 18 +- lightning/src/offers/static_invoice.rs | 9 +- lightning/src/onion_message/async_payments.rs | 180 +++++++++++++++++- .../src/onion_message/functional_tests.rs | 24 ++- lightning/src/onion_message/messenger.rs | 41 ++++ 7 files changed, 317 insertions(+), 7 deletions(-) diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 10a667fb594..7efbede4504 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -15,7 +15,8 @@ use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::script::ShutdownScript; use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::onion_message::async_payments::{ - AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc, + AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc, + ServeStaticInvoice, StaticInvoicePersisted, }; use lightning::onion_message::messenger::{ CustomOnionMessageHandler, Destination, MessageRouter, MessageSendInstructions, @@ -121,6 +122,29 @@ impl OffersMessageHandler for TestOffersMessageHandler { struct TestAsyncPaymentsMessageHandler {} impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, + responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + let responder = match responder { + Some(resp) => resp, + None => return None, + }; + Some((OfferPaths { paths: Vec::new() }, responder.respond())) + } + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + ) { + } + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) { + } fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, responder: Option, diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ad82bb5cd45..9e2b47d14bb 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -74,7 +74,10 @@ use crate::offers::offer::{Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; use crate::offers::refund::{Refund, RefundBuilder}; use crate::offers::signer; -use crate::onion_message::async_payments::{AsyncPaymentsMessage, HeldHtlcAvailable, ReleaseHeldHtlc, AsyncPaymentsMessageHandler}; +use crate::onion_message::async_payments::{ + AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, + OfferPathsRequest, ReleaseHeldHtlc, ServeStaticInvoice, StaticInvoicePersisted +}; use crate::onion_message::dns_resolution::HumanReadableName; use crate::onion_message::messenger::{Destination, MessageRouter, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; @@ -12562,6 +12565,27 @@ where MR::Target: MessageRouter, L::Target: Logger, { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, + _responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + None + } + + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + ) {} + + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) {} + fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, _responder: Option diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index aca1afbff39..d334590f104 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -29,7 +29,7 @@ use crate::util::ser::{VecWriter, Writeable, Writer}; use crate::ln::peer_channel_encryptor::{PeerChannelEncryptor, NextNoiseStep, MessageBuf, MSG_BUF_ALLOC_SIZE}; use crate::ln::wire; use crate::ln::wire::{Encode, Type}; -use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, ReleaseHeldHtlc, StaticInvoicePersisted}; use crate::onion_message::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage, DNSSECProof, DNSSECQuery}; use crate::onion_message::messenger::{CustomOnionMessageHandler, Responder, ResponseInstruction, MessageSendInstructions}; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; @@ -151,6 +151,22 @@ impl OffersMessageHandler for IgnoringMessageHandler { } } impl AsyncPaymentsMessageHandler for IgnoringMessageHandler { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + None + } + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext + ) {} + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) {} fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, _responder: Option, diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 091b06170a4..8d87c6684a0 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -34,7 +34,7 @@ use crate::offers::offer::{ }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::types::features::{Bolt12InvoiceFeatures, OfferFeatures}; -use crate::util::ser::{CursorReadable, Iterable, WithoutLength, Writeable, Writer}; +use crate::util::ser::{CursorReadable, Iterable, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; use bitcoin::address::Address; use bitcoin::constants::ChainHash; @@ -530,6 +530,13 @@ impl InvoiceContents { } } +impl Readable for StaticInvoice { + fn read(reader: &mut R) -> Result { + let bytes: WithoutLength> = Readable::read(reader)?; + Self::try_from(bytes.0).map_err(|_| DecodeError::InvalidValue) + } +} + impl Writeable for StaticInvoice { fn write(&self, writer: &mut W) -> Result<(), io::Error> { WithoutLength(&self.bytes).write(writer) diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 7a473c90e8f..6d19304a081 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -9,15 +9,23 @@ //! Message handling for async payments. -use crate::blinded_path::message::AsyncPaymentsContext; +use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath}; use crate::io; use crate::ln::msgs::DecodeError; +use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::packet::OnionMessageContents; use crate::prelude::*; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; +use core::time::Duration; + // TLV record types for the `onionmsg_tlv` TLV stream as defined in BOLT 4. +// TODO: document static invoice server onion message payload types in a bLIP. +const OFFER_PATHS_REQ_TLV_TYPE: u64 = 65538; +const OFFER_PATHS_TLV_TYPE: u64 = 65540; +const SERVE_INVOICE_TLV_TYPE: u64 = 65542; +const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544; const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72; const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74; @@ -25,6 +33,42 @@ const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74; /// /// [`OnionMessage`]: crate::ln::msgs::OnionMessage pub trait AsyncPaymentsMessageHandler { + /// Handle an [`OfferPathsRequest`] message. If the message was sent over paths that we previously + /// provided to an async recipient via [`UserConfig::paths_to_static_invoice_server`], an + /// [`OfferPaths`] message should be returned. + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + fn handle_offer_paths_request( + &self, message: OfferPathsRequest, context: AsyncPaymentsContext, + responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)>; + + /// Handle an [`OfferPaths`] message. If this is in response to an [`OfferPathsRequest`] that + /// we previously sent as an async recipient, we should build an [`Offer`] containing the + /// included [`OfferPaths::paths`] and a corresponding [`StaticInvoice`], and reply with + /// [`ServeStaticInvoice`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + fn handle_offer_paths( + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)>; + + /// Handle a [`ServeStaticInvoice`] message. If this is in response to an [`OfferPaths`] message + /// we previously sent, a [`StaticInvoicePersisted`] message should be sent once the message is + /// handled. + fn handle_serve_static_invoice( + &self, message: ServeStaticInvoice, context: AsyncPaymentsContext, + ); + + /// Handle a [`StaticInvoicePersisted`] message. If this is in response to a + /// [`ServeStaticInvoice`] message we previously sent as an async recipient, then the offer we + /// generated on receipt of a previous [`OfferPaths`] message is now ready to be used for async + /// payments. + fn handle_static_invoice_persisted( + &self, message: StaticInvoicePersisted, context: AsyncPaymentsContext, + ); + /// Handle a [`HeldHtlcAvailable`] message. A [`ReleaseHeldHtlc`] should be returned to release /// the held funds. fn handle_held_htlc_available( @@ -50,6 +94,25 @@ pub trait AsyncPaymentsMessageHandler { /// [`OnionMessage`]: crate::ln::msgs::OnionMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsMessage { + /// A request for [`BlindedMessagePath`]s from a static invoice server. + OfferPathsRequest(OfferPathsRequest), + + /// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent in + /// response to an [`OfferPathsRequest`]. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + OfferPaths(OfferPaths), + + /// A request to serve a [`StaticInvoice`] on behalf of an async recipient. + ServeStaticInvoice(ServeStaticInvoice), + + /// Confirms that a [`StaticInvoice`] was persisted by a static invoice server and the + /// corresponding [`Offer`] is ready to be used to receive async payments. Sent in response to a + /// [`ServeStaticInvoice`] message. + /// + /// [`Offer`]: crate::offers::offer::Offer + StaticInvoicePersisted(StaticInvoicePersisted), + /// An HTLC is being held upstream for the often-offline recipient, to be released via /// [`ReleaseHeldHtlc`]. HeldHtlcAvailable(HeldHtlcAvailable), @@ -58,6 +121,52 @@ pub enum AsyncPaymentsMessage { ReleaseHeldHtlc(ReleaseHeldHtlc), } +/// A request for [`BlindedMessagePath`]s from a static invoice server. These paths will be used +/// in the async recipient's [`Offer::paths`], so payers can request [`StaticInvoice`]s from the +/// static invoice server. +/// +/// [`Offer::paths`]: crate::offers::offer::Offer::paths +#[derive(Clone, Debug)] +pub struct OfferPathsRequest {} + +/// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent in +/// response to an [`OfferPathsRequest`]. +/// +/// [`Offer::paths`]: crate::offers::offer::Offer::paths +#[derive(Clone, Debug)] +pub struct OfferPaths { + /// The paths that should be included in the async recipient's [`Offer::paths`]. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + pub paths: Vec, + /// The time as duration since the Unix epoch at which the [`Self::paths`] expire. + pub paths_absolute_expiry: Option, +} + +/// Indicates that a [`StaticInvoice`] should be provided by a static invoice server in response to +/// [`InvoiceRequest`]s from payers behalf of an async recipient. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +#[derive(Clone, Debug)] +pub struct ServeStaticInvoice { + /// The invoice that should be served by the static invoice server. + pub invoice: StaticInvoice, + /// Once [`Self::invoice`] has been persisted, these paths should be used to send + /// [`StaticInvoicePersisted`] messages to the recipient to confirm that the offer corresponding + /// to the invoice is ready to receive async payments. + pub invoice_persisted_paths: Vec, + // TODO: include blinded paths to forward the invreq to the async recipient + // pub invoice_request_paths: Vec, +} + +/// Confirms that a [`StaticInvoice`] was persisted by a static invoice server and the +/// corresponding [`Offer`] is ready to be used to receive async payments. Sent in response to a +/// [`ServeStaticInvoice`] message. +/// +/// [`Offer`]: crate::offers::offer::Offer +#[derive(Clone, Debug)] +pub struct StaticInvoicePersisted {} + /// An HTLC destined for the recipient of this message is being held upstream. The reply path /// accompanying this onion message should be used to send a [`ReleaseHeldHtlc`] response, which /// will cause the upstream HTLC to be released. @@ -68,6 +177,34 @@ pub struct HeldHtlcAvailable {} #[derive(Clone, Debug)] pub struct ReleaseHeldHtlc {} +impl OnionMessageContents for OfferPaths { + fn tlv_type(&self) -> u64 { + OFFER_PATHS_TLV_TYPE + } + #[cfg(c_bindings)] + fn msg_type(&self) -> String { + "Offer Paths".to_string() + } + #[cfg(not(c_bindings))] + fn msg_type(&self) -> &'static str { + "Offer Paths" + } +} + +impl OnionMessageContents for ServeStaticInvoice { + fn tlv_type(&self) -> u64 { + SERVE_INVOICE_TLV_TYPE + } + #[cfg(c_bindings)] + fn msg_type(&self) -> String { + "Serve Static Invoice".to_string() + } + #[cfg(not(c_bindings))] + fn msg_type(&self) -> &'static str { + "Serve Static Invoice" + } +} + impl OnionMessageContents for ReleaseHeldHtlc { fn tlv_type(&self) -> u64 { RELEASE_HELD_HTLC_TLV_TYPE @@ -82,6 +219,20 @@ impl OnionMessageContents for ReleaseHeldHtlc { } } +impl_writeable_tlv_based!(OfferPathsRequest, {}); + +impl_writeable_tlv_based!(OfferPaths, { + (0, paths, required_vec), + (2, paths_absolute_expiry, option), +}); + +impl_writeable_tlv_based!(ServeStaticInvoice, { + (0, invoice, required), + (2, invoice_persisted_paths, required), +}); + +impl_writeable_tlv_based!(StaticInvoicePersisted, {}); + impl_writeable_tlv_based!(HeldHtlcAvailable, {}); impl_writeable_tlv_based!(ReleaseHeldHtlc, {}); @@ -90,7 +241,12 @@ impl AsyncPaymentsMessage { /// Returns whether `tlv_type` corresponds to a TLV record for async payment messages. pub fn is_known_type(tlv_type: u64) -> bool { match tlv_type { - HELD_HTLC_AVAILABLE_TLV_TYPE | RELEASE_HELD_HTLC_TLV_TYPE => true, + OFFER_PATHS_REQ_TLV_TYPE + | OFFER_PATHS_TLV_TYPE + | SERVE_INVOICE_TLV_TYPE + | INVOICE_PERSISTED_TLV_TYPE + | HELD_HTLC_AVAILABLE_TLV_TYPE + | RELEASE_HELD_HTLC_TLV_TYPE => true, _ => false, } } @@ -99,6 +255,10 @@ impl AsyncPaymentsMessage { impl OnionMessageContents for AsyncPaymentsMessage { fn tlv_type(&self) -> u64 { match self { + Self::OfferPathsRequest(_) => OFFER_PATHS_REQ_TLV_TYPE, + Self::OfferPaths(msg) => msg.tlv_type(), + Self::ServeStaticInvoice(msg) => msg.tlv_type(), + Self::StaticInvoicePersisted(_) => INVOICE_PERSISTED_TLV_TYPE, Self::HeldHtlcAvailable(_) => HELD_HTLC_AVAILABLE_TLV_TYPE, Self::ReleaseHeldHtlc(msg) => msg.tlv_type(), } @@ -106,6 +266,10 @@ impl OnionMessageContents for AsyncPaymentsMessage { #[cfg(c_bindings)] fn msg_type(&self) -> String { match &self { + Self::OfferPathsRequest(_) => "Offer Paths Request".to_string(), + Self::OfferPaths(msg) => msg.msg_type(), + Self::ServeStaticInvoice(msg) => msg.msg_type(), + Self::StaticInvoicePersisted(_) => "Static Invoice Persisted".to_string(), Self::HeldHtlcAvailable(_) => "Held HTLC Available".to_string(), Self::ReleaseHeldHtlc(msg) => msg.msg_type(), } @@ -113,6 +277,10 @@ impl OnionMessageContents for AsyncPaymentsMessage { #[cfg(not(c_bindings))] fn msg_type(&self) -> &'static str { match &self { + Self::OfferPathsRequest(_) => "Offer Paths Request", + Self::OfferPaths(msg) => msg.msg_type(), + Self::ServeStaticInvoice(msg) => msg.msg_type(), + Self::StaticInvoicePersisted(_) => "Static Invoice Persisted", Self::HeldHtlcAvailable(_) => "Held HTLC Available", Self::ReleaseHeldHtlc(msg) => msg.msg_type(), } @@ -122,6 +290,10 @@ impl OnionMessageContents for AsyncPaymentsMessage { impl Writeable for AsyncPaymentsMessage { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { + Self::OfferPathsRequest(message) => message.write(w), + Self::OfferPaths(message) => message.write(w), + Self::ServeStaticInvoice(message) => message.write(w), + Self::StaticInvoicePersisted(message) => message.write(w), Self::HeldHtlcAvailable(message) => message.write(w), Self::ReleaseHeldHtlc(message) => message.write(w), } @@ -131,6 +303,10 @@ impl Writeable for AsyncPaymentsMessage { impl ReadableArgs for AsyncPaymentsMessage { fn read(r: &mut R, tlv_type: u64) -> Result { match tlv_type { + OFFER_PATHS_REQ_TLV_TYPE => Ok(Self::OfferPathsRequest(Readable::read(r)?)), + OFFER_PATHS_TLV_TYPE => Ok(Self::OfferPaths(Readable::read(r)?)), + SERVE_INVOICE_TLV_TYPE => Ok(Self::ServeStaticInvoice(Readable::read(r)?)), + INVOICE_PERSISTED_TLV_TYPE => Ok(Self::StaticInvoicePersisted(Readable::read(r)?)), HELD_HTLC_AVAILABLE_TLV_TYPE => Ok(Self::HeldHtlcAvailable(Readable::read(r)?)), RELEASE_HELD_HTLC_TLV_TYPE => Ok(Self::ReleaseHeldHtlc(Readable::read(r)?)), _ => Err(DecodeError::InvalidValue), diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 239625de9e4..5b18615456e 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -9,7 +9,10 @@ //! Onion message testing and test utilities live here. -use super::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc}; +use super::async_payments::{ + AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc, + ServeStaticInvoice, StaticInvoicePersisted, +}; use super::dns_resolution::{ DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, }; @@ -90,6 +93,25 @@ impl OffersMessageHandler for TestOffersMessageHandler { struct TestAsyncPaymentsMessageHandler {} impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler { + fn handle_offer_paths_request( + &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, + _responder: Option, + ) -> Option<(OfferPaths, ResponseInstruction)> { + None + } + fn handle_offer_paths( + &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + None + } + fn handle_serve_static_invoice( + &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, + ) { + } + fn handle_static_invoice_persisted( + &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + ) { + } fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, _responder: Option, diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 4ac2ae90639..838c6208782 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1859,6 +1859,47 @@ where } }, #[cfg(async_payments)] + ParsedOnionMessageContents::AsyncPayments( + AsyncPaymentsMessage::OfferPathsRequest(msg), + ) => { + let context = + extract_expected_context!(context, MessageContext::AsyncPayments); + let response_instructions = self + .async_payments_handler + .handle_offer_paths_request(msg, context, responder); + if let Some((msg, instructions)) = response_instructions { + let _ = self.handle_onion_message_response(msg, instructions); + } + }, + #[cfg(async_payments)] + ParsedOnionMessageContents::AsyncPayments( + AsyncPaymentsMessage::OfferPaths(msg), + ) => { + let context = + extract_expected_context!(context, MessageContext::AsyncPayments); + let response_instructions = + self.async_payments_handler.handle_offer_paths(msg, context, responder); + if let Some((msg, instructions)) = response_instructions { + let _ = self.handle_onion_message_response(msg, instructions); + } + }, + #[cfg(async_payments)] + ParsedOnionMessageContents::AsyncPayments( + AsyncPaymentsMessage::ServeStaticInvoice(msg), + ) => { + let context = + extract_expected_context!(context, MessageContext::AsyncPayments); + self.async_payments_handler.handle_serve_static_invoice(msg, context); + }, + #[cfg(async_payments)] + ParsedOnionMessageContents::AsyncPayments( + AsyncPaymentsMessage::StaticInvoicePersisted(msg), + ) => { + let context = + extract_expected_context!(context, MessageContext::AsyncPayments); + self.async_payments_handler.handle_static_invoice_persisted(msg, context); + }, + #[cfg(async_payments)] ParsedOnionMessageContents::AsyncPayments( AsyncPaymentsMessage::HeldHtlcAvailable(msg), ) => { From 10874f37254947bd9e76da5c2022e7b67ba5ef85 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 6 Feb 2025 11:48:15 -0800 Subject: [PATCH 08/18] Check and refresh async receive offer As an async recipient, we need to interactively build a static invoice that an always-online node will serve to payers on our behalf. At the start of this process, we send a request for paths to include in our offer to the always-online node on startup and refresh the cached offer when it expires. --- lightning/src/blinded_path/message.rs | 31 +++++++ lightning/src/ln/channelmanager.rs | 82 +++++++++++++++++++ lightning/src/offers/signer.rs | 18 ++++ lightning/src/onion_message/async_payments.rs | 6 ++ 4 files changed, 137 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 441a2c2a625..a4d49f435ce 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -393,6 +393,32 @@ pub enum OffersContext { /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsContext { + /// Context used by a reply path to an [`OfferPathsRequest`], provided back to us in corresponding + /// [`OfferPaths`] messages. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + OfferPaths { + /// A nonce used for authenticating that an [`OfferPaths`] message is valid for a preceding + /// [`OfferPathsRequest`]. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + nonce: Nonce, + /// Authentication code for the [`OfferPaths`] message. + /// + /// Prevents nodes from creating their own blinded path to us and causing us to cache an + /// unintended async receive offer. + /// + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Used to time out a static invoice server from providing offer paths if the async recipient + /// is no longer configured to accept paths from them. + path_absolute_expiry: core::time::Duration, + }, /// Context contained within the reply [`BlindedMessagePath`] we put in outbound /// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`] /// messages. @@ -475,6 +501,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, hmac, required), (4, path_absolute_expiry, required), }, + (2, OfferPaths) => { + (0, nonce, required), + (2, hmac, required), + (4, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9e2b47d14bb..658440b1ef1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -94,6 +94,7 @@ use crate::util::errors::APIError; #[cfg(async_payments)] use { crate::offers::offer::Amount, crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, + crate::onion_message::async_payments::REPLY_PATH_RELATIVE_EXPIRY, }; #[cfg(feature = "dnssec")] @@ -1515,6 +1516,35 @@ struct AsyncReceiveOffer { offer_paths_request_attempts: u8, } +impl AsyncReceiveOffer { + // If we have more than three hours before our offer expires, don't bother requesting new + // paths. + #[cfg(async_payments)] + const OFFER_RELATIVE_EXPIRY_BUFFER: Duration = Duration::from_secs(3 * 60 * 60); + + /// Removes the offer from our cache if it's expired. + #[cfg(async_payments)] + fn check_expire_offer(&mut self, duration_since_epoch: Duration) { + if let Some(ref mut offer) = self.offer { + if offer.is_expired_no_std(duration_since_epoch) { + self.offer.take(); + self.offer_paths_request_attempts = 0; + } + } + } + + #[cfg(async_payments)] + fn should_refresh_offer(&self, duration_since_epoch: Duration) -> bool { + if let Some(ref offer) = self.offer { + let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX); + if offer_expiry > duration_since_epoch.saturating_add(Self::OFFER_RELATIVE_EXPIRY_BUFFER) { + return false + } + } + return true + } +} + impl_writeable_tlv_based!(AsyncReceiveOffer, { (0, offer, option), (2, offer_paths_request_attempts, (static_value, 0)), @@ -2432,6 +2462,8 @@ where // // `pending_async_payments_messages` // +// `async_receive_offer_cache` +// // `total_consistency_lock` // | // |__`forward_htlcs` @@ -4852,6 +4884,50 @@ where ) } + #[cfg(async_payments)] + fn check_refresh_async_receive_offer(&self) { + if self.default_configuration.paths_to_static_invoice_server.is_empty() { return } + + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let duration_since_epoch = self.duration_since_epoch(); + + { + let mut offer_cache = self.async_receive_offer_cache.lock().unwrap(); + offer_cache.check_expire_offer(duration_since_epoch); + if !offer_cache.should_refresh_offer(duration_since_epoch) { + return + } + + const MAX_ATTEMPTS: u8 = 3; + if offer_cache.offer_paths_request_attempts > MAX_ATTEMPTS { return } + } + + let reply_paths = { + let nonce = Nonce::from_entropy_source(entropy); + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths { + nonce, + hmac: signer::hmac_for_offer_paths_context(nonce, expanded_key), + path_absolute_expiry: duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY), + }); + match self.create_blinded_paths(context) { + Ok(paths) => paths, + Err(()) => { + log_error!(self.logger, "Failed to create blinded paths when requesting async receive offer paths"); + return + } + } + }; + + + self.async_receive_offer_cache.lock().unwrap().offer_paths_request_attempts += 1; + let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {}); + queue_onion_message_with_reply_paths( + message, &self.default_configuration.paths_to_static_invoice_server[..], reply_paths, + &mut self.pending_async_payments_messages.lock().unwrap() + ); + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -6801,6 +6877,9 @@ where duration_since_epoch, &self.pending_events ); + #[cfg(async_payments)] + self.check_refresh_async_receive_offer(); + // Technically we don't need to do this here, but if we have holding cell entries in a // channel that need freeing, it's better to do that here and block a background task // than block the message queueing pipeline. @@ -12085,6 +12164,9 @@ where return NotifyOption::SkipPersistHandleEvents; //TODO: Also re-broadcast announcement_signatures }); + + #[cfg(async_payments)] + self.check_refresh_async_receive_offer(); res } diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 84540fc13e0..9d596385dc2 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -55,6 +55,11 @@ const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16]; #[cfg(async_payments)] const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; +// HMAC input used in `AsyncPaymentsContext::OfferPaths` to authenticate inbound offer_paths onion +// messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -555,3 +560,16 @@ pub(crate) fn verify_held_htlc_available_context( Err(()) } } + +#[cfg(async_payments)] +pub(crate) fn hmac_for_offer_paths_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer Paths~"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_OFFER_PATHS_INPUT); + + Hmac::from_engine(hmac) +} diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 6d19304a081..ecd02b00577 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -29,6 +29,12 @@ const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544; const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72; const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74; +// Used to expire reply paths used in exchanging static invoice server onion messages. We expect +// these onion messages to be exchanged quickly, but add some buffer for no-std users who rely on +// block timestamps. +#[cfg(async_payments)] +pub(crate) const REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60); + /// A handler for an [`OnionMessage`] containing an async payments message as its payload. /// /// [`OnionMessage`]: crate::ln::msgs::OnionMessage From 63ddee81e2ec0ac626169152fc78e360cfcb1ebc Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 14 Feb 2025 18:26:48 -0500 Subject: [PATCH 09/18] Send static invoice in response to offer paths As an async recipient, we need to interactively build a static invoice that an always-online node will serve to payers on our behalf. As part of this process, the static invoice server sends us blinded message paths to include in our offer so they'll receive invoice requests from senders trying to pay us while we're offline. On receipt of these paths, create an offer and static invoice and send the invoice back to the server so they can provide the invoice to payers. --- lightning/src/blinded_path/message.rs | 39 +++++++++++ lightning/src/ln/channelmanager.rs | 93 +++++++++++++++++++++++++++ lightning/src/offers/signer.rs | 29 +++++++++ 3 files changed, 161 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index a4d49f435ce..b9362784d19 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -23,6 +23,7 @@ use crate::ln::channelmanager::PaymentId; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; use crate::offers::nonce::Nonce; +use crate::offers::offer::Offer; use crate::onion_message::packet::ControlTlvs; use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, Recipient}; @@ -419,6 +420,38 @@ pub enum AsyncPaymentsContext { /// is no longer configured to accept paths from them. path_absolute_expiry: core::time::Duration, }, + /// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in + /// corresponding [`StaticInvoicePersisted`] messages. + /// + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + StaticInvoicePersisted { + /// The offer corresponding to the [`StaticInvoice`] that has been persisted. This invoice is + /// now ready to be provided by the static invoice server in response to [`InvoiceRequest`]s. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + offer: Offer, + /// A nonce used for authenticating that a [`StaticInvoicePersisted`] message is valid for a + /// preceding [`ServeStaticInvoice`] message. + /// + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + nonce: Nonce, + /// Authentication code for the [`StaticInvoicePersisted`] message. + /// + /// Prevents nodes from creating their own blinded path to us and causing us to cache an + /// unintended async receive offer. + /// + /// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted + hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Prevents a static invoice server from causing an async recipient to cache an old offer if + /// the recipient is no longer configured to use that server. + path_absolute_expiry: core::time::Duration, + }, /// Context contained within the reply [`BlindedMessagePath`] we put in outbound /// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`] /// messages. @@ -506,6 +539,12 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, hmac, required), (4, path_absolute_expiry, required), }, + (3, StaticInvoicePersisted) => { + (0, offer, required), + (2, nonce, required), + (4, hmac, required), + (6, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 658440b1ef1..a455f21884c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12657,6 +12657,99 @@ where fn handle_offer_paths( &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { + #[cfg(async_payments)] { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + let secp_ctx = &self.secp_ctx; + let duration_since_epoch = self.duration_since_epoch(); + + match _context { + AsyncPaymentsContext::OfferPaths { nonce, hmac, path_absolute_expiry } => { + if let Err(()) = signer::verify_offer_paths_context(nonce, hmac, expanded_key) { + return None + } + if duration_since_epoch > path_absolute_expiry { return None } + }, + _ => return None + } + + if !self.async_receive_offer_cache.lock().unwrap().should_refresh_offer(duration_since_epoch) { + return None + } + + // Require at least two hours before we'll need to start the process of creating a new offer. + const MIN_OFFER_PATHS_RELATIVE_EXPIRY: Duration = + Duration::from_secs(2 * 60 * 60).saturating_add(AsyncReceiveOffer::OFFER_RELATIVE_EXPIRY_BUFFER); + let min_offer_paths_absolute_expiry = + duration_since_epoch.saturating_add(MIN_OFFER_PATHS_RELATIVE_EXPIRY); + let offer_paths_absolute_expiry = + _message.paths_absolute_expiry.unwrap_or(Duration::from_secs(u64::MAX)); + if offer_paths_absolute_expiry < min_offer_paths_absolute_expiry { + log_error!(self.logger, "Received offer paths with too-soon absolute Unix epoch expiry: {}", offer_paths_absolute_expiry.as_secs()); + return None + } + + // Expire the offer at the same time as the static invoice so we automatically refresh both + // at the same time. + let offer_and_invoice_absolute_expiry = Duration::from_secs(core::cmp::min( + offer_paths_absolute_expiry.as_secs(), + duration_since_epoch.saturating_add(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY).as_secs() + )); + + let (offer, offer_nonce) = { + let (offer_builder, offer_nonce) = + match self.create_async_receive_offer_builder(_message.paths) { + Ok((builder, nonce)) => (builder, nonce), + Err(e) => { + log_error!(self.logger, "Failed to create offer builder when replying to OfferPaths message: {:?}", e); + return None + }, + }; + match offer_builder.absolute_expiry(offer_and_invoice_absolute_expiry).build() { + Ok(offer) => (offer, offer_nonce), + Err(e) => { + log_error!(self.logger, "Failed to build offer when replying to OfferPaths message: {:?}", e); + return None + }, + } + }; + + let static_invoice_relative_expiry = + offer_and_invoice_absolute_expiry.saturating_sub(duration_since_epoch); + let static_invoice = { + let invoice_res = self.create_static_invoice_builder( + &offer, offer_nonce, Some(static_invoice_relative_expiry ) + ).and_then(|builder| builder.build_and_sign(secp_ctx)); + match invoice_res { + Ok(invoice) => invoice, + Err(e) => { + log_error!(self.logger, "Failed to create static invoice when replying to OfferPaths message: {:?}", e); + return None + }, + } + }; + + let invoice_persisted_paths = { + let nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_static_invoice_persisted_context(nonce, expanded_key); + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted { + offer, nonce, hmac, + path_absolute_expiry: duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY) + }); + match self.create_blinded_paths(context) { + Ok(paths) => paths, + Err(()) => { + log_error!(self.logger, "Failed to create blinded paths when replying to OfferPaths message"); + return None + }, + } + }; + + let reply = ServeStaticInvoice { invoice: static_invoice, invoice_persisted_paths }; + return _responder.map(|responder| (reply, responder.respond())) + } + + #[cfg(not(async_payments))] None } diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 9d596385dc2..b2dac54b8db 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -60,6 +60,11 @@ const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; #[cfg(async_payments)] const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16]; +// HMAC input used in `AsyncPaymentsContext::StaticInvoicePersisted` to authenticate inbound +// static_invoice_persisted onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT: &[u8; 16] = &[11; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -573,3 +578,27 @@ pub(crate) fn hmac_for_offer_paths_context( Hmac::from_engine(hmac) } + +#[cfg(async_payments)] +pub(crate) fn verify_offer_paths_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_offer_paths_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} + +#[cfg(async_payments)] +pub(crate) fn hmac_for_static_invoice_persisted_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK InvPersisted"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT); + + Hmac::from_engine(hmac) +} From 2f9beb6dbcc52b068b06d60a2bec8fedfb81112e Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 14:52:45 -0500 Subject: [PATCH 10/18] Cache offer on StaticInvoicePersisted onion message As an async recipient, we need to interactively build a static invoice that an always-online node will serve on our behalf. Once this invoice is built and persisted by the static invoice server, they will send us a confirmation onion message. At this time, cache the corresponding offer and mark it as ready to receive async payments. --- lightning/src/ln/channelmanager.rs | 30 +++++++++++++++++++++++++++++- lightning/src/offers/signer.rs | 11 +++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a455f21884c..5c9f6bf950b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12759,7 +12759,35 @@ where fn handle_static_invoice_persisted( &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, - ) {} + ) { + #[cfg(async_payments)] { + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + + let mut new_offer = match _context { + AsyncPaymentsContext::StaticInvoicePersisted { + offer, nonce, hmac, path_absolute_expiry + } => { + if let Err(()) = signer::verify_static_invoice_persisted_context(nonce, hmac, expanded_key) { + return + } + if duration_since_epoch > path_absolute_expiry { return } + if offer.is_expired_no_std(duration_since_epoch) { return } + Some(offer) + }, + _ => return + }; + + PersistenceNotifierGuard::optionally_notify(self, || { + let mut offer_cache = self.async_receive_offer_cache.lock().unwrap(); + if !offer_cache.should_refresh_offer(duration_since_epoch) { + return NotifyOption::SkipPersistNoEvents + } + offer_cache.offer = new_offer.take(); + NotifyOption::DoPersist + }); + } + } fn handle_held_htlc_available( &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index b2dac54b8db..f3e2958bf8c 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -602,3 +602,14 @@ pub(crate) fn hmac_for_static_invoice_persisted_context( Hmac::from_engine(hmac) } + +#[cfg(async_payments)] +pub(crate) fn verify_static_invoice_persisted_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_static_invoice_persisted_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} From 20f349f31ab3a443b77b69535deddc49c54ede26 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 15:14:47 -0500 Subject: [PATCH 11/18] Add API to retrieve cached async receive offer Over the past several commits we've implemented interactively building an async receive offer with a static invoice server that will service invoice requests on our behalf as an async recipient. Here we add an API to retrieve this resulting offer so we can receive payments when we're offline. --- lightning/src/ln/channelmanager.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 5c9f6bf950b..9686044127e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10318,9 +10318,23 @@ where #[cfg(c_bindings)] create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder); + /// Retrieve our cached [`Offer`] for receiving async payments as an often-offline recipient. Will + /// only be set if [`UserConfig::paths_to_static_invoice_server`] is set and we succeeded in + /// interactively building a [`StaticInvoice`] with the static invoice server. + #[cfg(async_payments)] + pub fn get_cached_async_receive_offer(&self) -> Option { + let mut offer_cache = self.async_receive_offer_cache.lock().unwrap(); + offer_cache.check_expire_offer(self.duration_since_epoch()); + + offer_cache.offer.clone() + } + /// 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: + /// Instead of using this method, prefer to set [`UserConfig::paths_to_static_invoice_server`] and + /// retrieve the automatically built offer via [`Self::get_cached_async_receive_offer`]. + /// + /// If you want to build the [`StaticInvoice`] manually using this method instead, 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 @@ -10354,6 +10368,10 @@ where /// 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`]. + /// + /// Instead of using this method to manually build the invoice, prefer to set + /// [`UserConfig::paths_to_static_invoice_server`] and retrieve the automatically built offer via + /// [`Self::get_cached_async_receive_offer`]. #[cfg(async_payments)] pub fn create_static_invoice_builder<'a>( &self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option From 182a1e3388dd8df8707ef0a7a778d0aeb2687046 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 20 Feb 2025 16:45:58 -0500 Subject: [PATCH 12/18] BOLT 12 {Static}Invoices: expose more is_expired methods In upcoming commits, we need to check whether a static invoice or its underlying offer is expired in no-std builds. Here we expose the methods to do so. The methods could instead be kept private to the crate, but they seem potentially useful. --- lightning/src/offers/invoice.rs | 4 ++++ lightning/src/offers/invoice_macros.rs | 5 +++++ lightning/src/offers/static_invoice.rs | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index be3886346c5..6af523897be 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1223,6 +1223,10 @@ impl InvoiceContents { is_expired(self.created_at(), self.relative_expiry()) } + fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.created_at().saturating_add(self.relative_expiry()) < duration_since_epoch + } + fn payment_hash(&self) -> PaymentHash { self.fields().payment_hash } diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index 2b276a37d29..e07b0c94d0f 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -131,6 +131,11 @@ macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice $contents.is_expired() } + /// Whether the invoice has expired given the current time as duration since the Unix epoch. + pub fn is_expired_no_std(&$self, duration_since_epoch: Duration) -> bool { + $contents.is_expired_no_std(duration_since_epoch) + } + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to /// least-preferred. pub fn fallbacks(&$self) -> Vec
{ diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 8d87c6684a0..b784994fb85 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -380,6 +380,18 @@ impl StaticInvoice { self.signature } + /// Whether the [`Offer`] that this invoice is based on is expired. + #[cfg(feature = "std")] + pub fn is_offer_expired(&self) -> bool { + self.contents.is_expired() + } + + /// Whether the [`Offer`] that this invoice is based on is expired, given the current time as + /// duration since the Unix epoch. + pub fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.contents.is_offer_expired_no_std(duration_since_epoch) + } + #[cfg(async_payments)] pub(crate) fn from_same_offer(&self, invreq: &InvoiceRequest) -> bool { let invoice_offer_tlv_stream = @@ -396,7 +408,6 @@ impl InvoiceContents { self.offer.is_expired() } - #[cfg(not(feature = "std"))] fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { self.offer.is_expired_no_std(duration_since_epoch) } @@ -513,6 +524,10 @@ impl InvoiceContents { is_expired(self.created_at(), self.relative_expiry()) } + fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.created_at().saturating_add(self.relative_expiry()) < duration_since_epoch + } + fn fallbacks(&self) -> Vec
{ let chain = self.chain(); self.fallbacks From ac6e640e8949f82428d840ddb6c8095fdaee55bf Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 15:26:01 -0800 Subject: [PATCH 13/18] Util for blinded paths to configure an async recipient As part of serving static invoices to payers on behalf of often-offline recipients, these recipients need a way to contact the static invoice server to retrieve blinded paths to include in their offers. Add a utility to create blinded paths for this purpose as a static invoice server. The recipient will be configured with the resulting paths and use them to request offer paths on startup. --- lightning/src/blinded_path/message.rs | 33 +++++++++++++++++ lightning/src/ln/channelmanager.rs | 35 ++++++++++++++++++- lightning/src/offers/signer.rs | 17 +++++++++ lightning/src/onion_message/async_payments.rs | 6 ++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index b9362784d19..56103558af0 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -394,6 +394,34 @@ pub enum OffersContext { /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsContext { + /// Context used by a [`BlindedMessagePath`] that an async recipient is configured with in + /// [`UserConfig::paths_to_static_invoice_server`], provided back to us in corresponding + /// [`OfferPathsRequest`]s. + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + OfferPathsRequest { + /// An identifier for the async recipient that is requesting blinded paths to include in their + /// [`Offer::paths`]. This ID is intended to be included in the reply path to our [`OfferPaths`] + /// response, and subsequently rate limit [`ServeStaticInvoice`] messages from recipients. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + recipient_id_nonce: Nonce, + /// Authentication code for the [`OfferPathsRequest`]. + /// + /// Prevents nodes from requesting offer paths from us without having been previously configured + /// with a [`BlindedMessagePath`] that we generated. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: core::time::Duration, + }, /// Context used by a reply path to an [`OfferPathsRequest`], provided back to us in corresponding /// [`OfferPaths`] messages. /// @@ -545,6 +573,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (4, hmac, required), (6, path_absolute_expiry, required), }, + (4, OfferPathsRequest) => { + (0, recipient_id_nonce, required), + (2, hmac, required), + (4, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9686044127e..83187574044 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -94,7 +94,7 @@ use crate::util::errors::APIError; #[cfg(async_payments)] use { crate::offers::offer::Amount, crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder}, - crate::onion_message::async_payments::REPLY_PATH_RELATIVE_EXPIRY, + crate::onion_message::async_payments::{DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY, REPLY_PATH_RELATIVE_EXPIRY}, }; #[cfg(feature = "dnssec")] @@ -10825,6 +10825,39 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, &self.inbound_payment_key) } + /// [`BlindedMessagePath`]s that an async recipient will be configured with via + /// [`UserConfig::paths_to_static_invoice_server`], enabling the recipient to request blinded + /// paths from us for inclusion in their [`Offer::paths`]. + /// + /// If `relative_expiry` is unset, the [`BlindedMessagePath`]s expiry will default to + /// [`DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY`]. + /// + /// Returns the paths to be included in the recipient's + /// [`UserConfig::paths_to_static_invoice_server`] as well as a nonce that uniquely identifies the + /// recipient that has been configured with these paths. // TODO link to events that surface this nonce + /// + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + /// [`DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY`]: crate::onion_message::async_payments::DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY + #[cfg(async_payments)] + pub fn blinded_paths_for_async_recipient( + &self, relative_expiry: Option + ) -> Result<(Vec, Nonce), ()> { + let expanded_key = &self.inbound_payment_key; + let entropy = &*self.entropy_source; + + let path_absolute_expiry = relative_expiry.unwrap_or(DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY) + .saturating_add(self.duration_since_epoch()); + + let recipient_id_nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_offer_paths_request_context(recipient_id_nonce, expanded_key); + + let context = MessageContext::AsyncPayments( + AsyncPaymentsContext::OfferPathsRequest { recipient_id_nonce, hmac, path_absolute_expiry } + ); + self.create_blinded_paths(context).map(|paths| (paths, recipient_id_nonce)) + } + /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on /// the path's intended lifetime. /// diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index f3e2958bf8c..d135aabdb00 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -65,6 +65,10 @@ const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16]; #[cfg(async_payments)] const ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT: &[u8; 16] = &[11; 16]; +/// +#[cfg(async_payments)] +const ASYNC_PAYMENTS_OFFER_PATHS_REQUEST_INPUT: &[u8; 16] = &[12; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -566,6 +570,19 @@ pub(crate) fn verify_held_htlc_available_context( } } +#[cfg(async_payments)] +pub(crate) fn hmac_for_offer_paths_request_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Paths Please"; // TODO + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_OFFER_PATHS_REQUEST_INPUT); + + Hmac::from_engine(hmac) +} + #[cfg(async_payments)] pub(crate) fn hmac_for_offer_paths_context( nonce: Nonce, expanded_key: &ExpandedKey, diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index ecd02b00577..f8b2bbc29e7 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -29,6 +29,12 @@ const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544; const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72; const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74; +/// Used to expire the blinded paths created by the static invoice server that the async recipient +/// is configured with via [`UserConfig::paths_to_static_invoice_server`]. +/// +/// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server +pub const DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(30 * 24 * 60 * 60); + // Used to expire reply paths used in exchanging static invoice server onion messages. We expect // these onion messages to be exchanged quickly, but add some buffer for no-std users who rely on // block timestamps. From a26550b2748d39e949e70a2de1ff77c7e28e0cb3 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 6 Feb 2025 16:29:42 -0800 Subject: [PATCH 14/18] Send offer paths in response to requests As part of serving static invoices to payers on behalf of often-offline recipients, we need to provide the async recipient with blinded message paths to include in their offers. Support responding to inbound requests for offer paths from async recipients. --- lightning/src/blinded_path/message.rs | 91 +++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 50 +++++++++++++++ lightning/src/offers/signer.rs | 43 +++++++++++++ 3 files changed, 184 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 56103558af0..a93db1a60bc 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -35,6 +35,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use core::mem; use core::ops::Deref; +use core::time::Duration; /// A blinded path to be used for sending or receiving a message, hiding the identity of the /// recipient. @@ -331,6 +332,47 @@ pub enum OffersContext { /// [`Offer`]: crate::offers::offer::Offer nonce: Nonce, }, + /// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient on behalf + /// of whom we are serving [`StaticInvoice`]s. + /// + /// This variant is intended to be received when handling an [`InvoiceRequest`] on behalf of said + /// async recipient. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + StaticInvoiceRequested { + /// An identifier for the async recipient for whom we are serving [`StaticInvoice`]s. Used to + /// look up a corresponding [`StaticInvoice`] to return to the payer if the recipient is offline. + /// + /// Also useful to rate limit the number of [`InvoiceRequest`]s we will respond to on + /// recipient's behalf. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + recipient_id_nonce: Nonce, + + /// A nonce used for authenticating that a received [`InvoiceRequest`] is valid for a preceding + /// [`OfferPaths`] message that we sent. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + nonce: Nonce, + + /// Authentication code for the [`InvoiceRequest`]. + /// + /// Prevents nodes from creating their own blinded path to us and causing us to unintentionally + /// hit our database looking for a [`StaticInvoice`] to return. + /// + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + hmac: Hmac, + + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: Duration, + }, /// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an /// [`InvoiceRequest`]. /// @@ -448,6 +490,43 @@ pub enum AsyncPaymentsContext { /// is no longer configured to accept paths from them. path_absolute_expiry: core::time::Duration, }, + /// Context used by a reply path to an [`OfferPaths`] message, provided back to us in + /// corresponding [`ServeStaticInvoice`] messages. + /// + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + ServeStaticInvoice { + /// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served + /// on their behalf. + /// + /// Useful as a key to retrieve the invoice when payers send an [`InvoiceRequest`] over the + /// paths that we previously created for the recipient's [`Offer::paths`]. Also useful to rate + /// limit the invoices being persisted on behalf of a particular recipient. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + recipient_id_nonce: Nonce, + /// A nonce used for authenticating that a [`ServeStaticInvoice`] message is valid for a preceding + /// [`OfferPaths`] message. + /// + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + nonce: Nonce, + /// Authentication code for the [`ServeStaticInvoice`] message. + /// + /// Prevents nodes from creating their own blinded path to us and causing us to persist an + /// unintended [`StaticInvoice`]. + /// + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + hmac: Hmac, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: core::time::Duration, + }, /// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in /// corresponding [`StaticInvoicePersisted`] messages. /// @@ -549,6 +628,12 @@ impl_writeable_tlv_based_enum!(OffersContext, (1, nonce, required), (2, hmac, required) }, + (3, StaticInvoiceRequested) => { + (0, recipient_id_nonce, required), + (2, nonce, required), + (4, hmac, required), + (6, path_absolute_expiry, required), + }, ); impl_writeable_tlv_based_enum!(AsyncPaymentsContext, @@ -578,6 +663,12 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (2, hmac, required), (4, path_absolute_expiry, required), }, + (5, ServeStaticInvoice) => { + (0, recipient_id_nonce, required), + (2, nonce, required), + (4, hmac, required), + (6, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 83187574044..3391131084f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12702,6 +12702,56 @@ where &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option, ) -> Option<(OfferPaths, ResponseInstruction)> { + #[cfg(async_payments)] { + let entropy = &*self.entropy_source; + let expanded_key = &self.inbound_payment_key; + let duration_since_epoch = self.duration_since_epoch(); + + let recipient_id_nonce = match _context { + AsyncPaymentsContext::OfferPathsRequest { recipient_id_nonce, hmac, path_absolute_expiry } => { + if let Err(()) = signer::verify_offer_paths_request_context( + recipient_id_nonce, hmac, expanded_key + ) { return None } + if duration_since_epoch > path_absolute_expiry { + return None } + recipient_id_nonce + }, + _ => return None + }; + + let (offer_paths, paths_expiry) = { + // TODO: support longer-lived offers + const OFFER_PATH_EXPIRY: Duration = Duration::from_secs(30 * 24 * 60 * 60); + let path_absolute_expiry = + duration_since_epoch.saturating_add(OFFER_PATH_EXPIRY); + let nonce = Nonce::from_entropy_source(entropy); + let hmac = signer::hmac_for_async_recipient_invreq_context(nonce, expanded_key); + let context = OffersContext::StaticInvoiceRequested { + recipient_id_nonce, nonce, hmac, path_absolute_expiry + }; + match self.create_blinded_paths_using_absolute_expiry( + context, Some(path_absolute_expiry) + ) { + Ok(paths) => (paths, path_absolute_expiry), + Err(()) => return None, + } + }; + + let reply_path_context = { + let nonce = Nonce::from_entropy_source(entropy); + let path_absolute_expiry = duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY); + let hmac = signer::hmac_for_serve_static_invoice_context(nonce, expanded_key); + MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice { + nonce, recipient_id_nonce, hmac, path_absolute_expiry + }) + }; + + let offer_paths_om = OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry) }; + return _responder + .map(|responder| (offer_paths_om, responder.respond_with_reply_path(reply_path_context))) + } + + #[cfg(not(async_payments))] None } diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index d135aabdb00..8167ba866b9 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -69,6 +69,16 @@ const ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT: &[u8; 16] = &[11; 16]; #[cfg(async_payments)] const ASYNC_PAYMENTS_OFFER_PATHS_REQUEST_INPUT: &[u8; 16] = &[12; 16]; +/// HMAC input used in `OffersContext::StaticInvoiceRequested` to authenticate inbound invoice +/// requests that are being serviced on behalf of async recipients. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_INVREQ: &[u8; 16] = &[13; 16]; + +/// HMAC input used in `AsyncPaymentsContext::ServeStaticInvoice` to authenticate inbound +/// serve_static_invoice onion messages. +#[cfg(async_payments)] +const ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT: &[u8; 16] = &[14; 16]; + /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -583,6 +593,13 @@ pub(crate) fn hmac_for_offer_paths_request_context( Hmac::from_engine(hmac) } +#[cfg(async_payments)] +pub(crate) fn verify_offer_paths_request_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_offer_paths_request_context(nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } +} + #[cfg(async_payments)] pub(crate) fn hmac_for_offer_paths_context( nonce: Nonce, expanded_key: &ExpandedKey, @@ -607,6 +624,19 @@ pub(crate) fn verify_offer_paths_context( } } +#[cfg(async_payments)] +pub(crate) fn hmac_for_serve_static_invoice_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Serve Inv~~~"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT); + + Hmac::from_engine(hmac) +} + #[cfg(async_payments)] pub(crate) fn hmac_for_static_invoice_persisted_context( nonce: Nonce, expanded_key: &ExpandedKey, @@ -630,3 +660,16 @@ pub(crate) fn verify_static_invoice_persisted_context( Err(()) } } + +#[cfg(async_payments)] +pub(crate) fn hmac_for_async_recipient_invreq_context( + nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Async Invreq"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(ASYNC_PAYMENTS_INVREQ); + + Hmac::from_engine(hmac) +} From 6458c6aa489fb89d5101f8def96b709dded1ba83 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 17:30:08 -0500 Subject: [PATCH 15/18] Static invoice server: persist invoices once built As part of serving static invoices to payers on behalf of often-offline recipients, the recipient will send us the final static invoice once it's done being interactively built. We will then persist this invoice and confirm to them that the corresponding offer is ready to be used for async payments. Surface an event once the invoice is received and expose an API to tell the recipient that it's ready for payments. --- lightning/src/events/mod.rs | 58 +++++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 42 ++++++++++++++++++- lightning/src/offers/signer.rs | 11 +++++ lightning/src/offers/static_invoice.rs | 8 ++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index b5dad96a979..2d907955a47 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -46,6 +46,12 @@ use core::time::Duration; use core::ops::Deref; use crate::sync::Arc; +#[cfg(async_payments)] use { + crate::blinded_path::message::BlindedMessagePath, + crate::offers::nonce::Nonce, + crate::offers::static_invoice::StaticInvoice, +}; + #[allow(unused_imports)] use crate::prelude::*; @@ -1494,7 +1500,32 @@ pub enum Event { /// The node id of the peer we just connected to, who advertises support for /// onion messages. peer_node_id: PublicKey, - } + }, + /// We received a [`StaticInvoice`] from an async recipient that wants us to serve the invoice to + /// payers on their behalf when they are offline. This event will only be generated if we + /// previously created paths using [`ChannelManager::blinded_paths_for_async_recipient`] and + /// configured the recipient with them via [`UserConfig::paths_to_static_invoice_server`]. + /// + /// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + #[cfg(async_payments)] + PersistStaticInvoice { + /// The invoice that should be persisted and later provided to payers when handling a future + /// `Event::StaticInvoiceRequested`. + invoice: StaticInvoice, + /// An identifier for the recipient, originally surfaced in + /// [`ChannelManager::blinded_paths_for_async_recipient`]. When an + /// `Event::StaticInvoiceRequested` comes in for this invoice, this id will be surfaced so the + /// persisted invoice can be retrieved from the database. + recipient_id_nonce: Nonce, + /// Once the [`StaticInvoice`] is persisted, [`ChannelManager::static_invoice_persisted`] should + /// be called with these paths to confirm to the recipient that their [`Offer`] is ready to be used + /// for async payments. + /// + /// [`ChannelManager::static_invoice_persisted`]: crate::ln::channelmanager::ChannelManager::static_invoice_persisted + /// [`Offer`]: crate::offers::offer::Offer + invoice_persisted_paths: Vec, + }, } impl Writeable for Event { @@ -1828,6 +1859,15 @@ impl Writeable for Event { (8, former_temporary_channel_id, required), }); }, + #[cfg(async_payments)] + &Event::PersistStaticInvoice { ref invoice, ref recipient_id_nonce, ref invoice_persisted_paths } => { + 45u8.write(writer)?; + write_tlv_fields!(writer, { + (0, invoice, required), + (2, recipient_id_nonce, required), + (4, invoice_persisted_paths, required), + }); + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2341,6 +2381,22 @@ impl MaybeReadable for Event { former_temporary_channel_id: former_temporary_channel_id.0.unwrap(), })) }, + #[cfg(async_payments)] + 45u8 => { + let mut f = || { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, invoice, required), + (2, recipient_id_nonce, required), + (4, invoice_persisted_paths, required), + }); + Ok(Some(Event::PersistStaticInvoice { + invoice: _init_tlv_based_struct_field!(invoice, required), + recipient_id_nonce: _init_tlv_based_struct_field!(recipient_id_nonce, required), + invoice_persisted_paths: _init_tlv_based_struct_field!(invoice_persisted_paths, required), + })) + }; + f() + }, // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 3391131084f..6f0fb220a5b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4928,6 +4928,22 @@ where ); } + /// + #[cfg(async_payments)] + pub fn static_invoice_persisted(&self, message_paths: Vec) -> Result<(), ()> { + let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); + + let message = AsyncPaymentsMessage::StaticInvoicePersisted(StaticInvoicePersisted {}); + for path in message_paths.into_iter().take(OFFERS_MESSAGE_REQUEST_LIMIT) { + let instructions = MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(path), + }; + pending_async_payments_messages.push((message.clone(), instructions)); + } + + Ok(()) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -12856,7 +12872,31 @@ where fn handle_serve_static_invoice( &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, - ) {} + ) { + #[cfg(async_payments)] { + let expanded_key = &self.inbound_payment_key; + let recipient_id_nonce = match _context { + AsyncPaymentsContext::ServeStaticInvoice { + recipient_id_nonce, nonce, hmac, path_absolute_expiry + } => { + if let Err(()) = signer::verify_serve_static_invoice_context(nonce, hmac, expanded_key) { + return + } + if self.duration_since_epoch() > path_absolute_expiry { return } + recipient_id_nonce + }, + _ => return + }; + + PersistenceNotifierGuard::notify_on_drop(self); + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((Event::PersistStaticInvoice { + invoice: _message.invoice, + recipient_id_nonce, + invoice_persisted_paths: _message.invoice_persisted_paths + }, None)); + } + } fn handle_static_invoice_persisted( &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 8167ba866b9..1953570993a 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -637,6 +637,17 @@ pub(crate) fn hmac_for_serve_static_invoice_context( Hmac::from_engine(hmac) } +#[cfg(async_payments)] +pub(crate) fn verify_serve_static_invoice_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_serve_static_invoice_context(nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} + #[cfg(async_payments)] pub(crate) fn hmac_for_static_invoice_persisted_context( nonce: Nonce, expanded_key: &ExpandedKey, diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index b784994fb85..76f69c7ea83 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -177,6 +177,14 @@ impl<'a> StaticInvoiceBuilder<'a> { invoice_builder_methods_test_common!(self, Self, self.invoice, Self, self, mut); } +impl PartialEq for StaticInvoice { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +impl Eq for StaticInvoice {} + /// A semantically valid [`StaticInvoice`] that hasn't been signed. pub struct UnsignedStaticInvoice { bytes: Vec, From 8614fb2604eaf65de4f15af436e170743deb6876 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 19 Feb 2025 19:18:13 -0500 Subject: [PATCH 16/18] Static invoice server: forward static invoices to payers Here we implement serving static invoices to payers on behalf of often-offline recipients. These recipients previously encoded blinded paths terminating at our node in their offer, so we receive invoice requests on their behalf. Handle those inbound invreqs by retrieving a static invoice we previously persisted on behalf of the payee, and forward it to the payer as a reply to their invreq. --- lightning/src/events/mod.rs | 54 +++++++++++++++++++++++- lightning/src/ln/channelmanager.rs | 40 ++++++++++++++++++ lightning/src/offers/signer.rs | 7 +++ lightning/src/onion_message/messenger.rs | 5 +++ 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 2d907955a47..7ac0e132d6f 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1511,11 +1511,11 @@ pub enum Event { #[cfg(async_payments)] PersistStaticInvoice { /// The invoice that should be persisted and later provided to payers when handling a future - /// `Event::StaticInvoiceRequested`. + /// [`Event::StaticInvoiceRequested`]. invoice: StaticInvoice, /// An identifier for the recipient, originally surfaced in /// [`ChannelManager::blinded_paths_for_async_recipient`]. When an - /// `Event::StaticInvoiceRequested` comes in for this invoice, this id will be surfaced so the + /// [`Event::StaticInvoiceRequested`] comes in for this invoice, this id will be surfaced so the /// persisted invoice can be retrieved from the database. recipient_id_nonce: Nonce, /// Once the [`StaticInvoice`] is persisted, [`ChannelManager::static_invoice_persisted`] should @@ -1526,6 +1526,34 @@ pub enum Event { /// [`Offer`]: crate::offers::offer::Offer invoice_persisted_paths: Vec, }, + /// We received an [`InvoiceRequest`] on behalf of an often-offline recipient for whom we are + /// serving [`StaticInvoice`]s. + /// + /// This event will only be generated if we previously created paths using + /// [`ChannelManager::blinded_paths_for_async_recipient`] and configured the recipient with them + /// via [`UserConfig::paths_to_static_invoice_server`]. + /// + /// If we previously persisted a [`StaticInvoice`] from an [`Event::PersistStaticInvoice`] that + /// matches the contained [`Event::StaticInvoiceRequested::recipient_id_nonce`], that + /// invoice should be retrieved now and forwarded to the payer via + /// [`ChannelManager::send_static_invoice`]. + /// + /// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient + /// [`UserConfig::paths_to_static_invoice_server`]: crate::util::config::UserConfig::paths_to_static_invoice_server + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + #[cfg(async_payments)] + StaticInvoiceRequested { + /// An identifier for the recipient previously surfaced in + /// [`Event::PersistStaticInvoice::recipient_id_nonce`]. Useful to retrieve the [`StaticInvoice`] + /// requested by the payer. + recipient_id_nonce: Nonce, + /// The path over which the [`StaticInvoice`] will be sent to the payer, which should be + /// provided to [`ChannelManager::send_static_invoice`] along with the invoice. + /// + /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + reply_path: BlindedMessagePath, + }, } impl Writeable for Event { @@ -1868,6 +1896,14 @@ impl Writeable for Event { (4, invoice_persisted_paths, required), }); }, + #[cfg(async_payments)] + &Event::StaticInvoiceRequested { ref recipient_id_nonce, ref reply_path } => { + 47u8.write(writer)?; + write_tlv_fields!(writer, { + (0, recipient_id_nonce, required), + (2, reply_path, required), + }); + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2397,6 +2433,20 @@ impl MaybeReadable for Event { }; f() }, + #[cfg(async_payments)] + 47u8 => { + let mut f = || { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, recipient_id_nonce, required), + (2, reply_path, required), + }); + Ok(Some(Event::StaticInvoiceRequested { + recipient_id_nonce: _init_tlv_based_struct_field!(recipient_id_nonce, required), + reply_path: _init_tlv_based_struct_field!(reply_path, required), + })) + }; + f() + }, // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6f0fb220a5b..59a688dc753 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4944,6 +4944,27 @@ where Ok(()) } + /// Forwards a [`StaticInvoice`] that was previously persisted by us from an + /// [`Event::PersistStaticInvoice`], in response to an [`Event::StaticInvoiceRequested`]. + #[cfg(async_payments)] + pub fn send_static_invoice( + &self, invoice: StaticInvoice, path: BlindedMessagePath + ) -> Result<(), ()> { + let duration_since_epoch = self.duration_since_epoch(); + if invoice.is_expired_no_std(duration_since_epoch) { return Err(()) } + if invoice.is_offer_expired_no_std(duration_since_epoch) { return Err(()) } + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + + let message = OffersMessage::StaticInvoice(invoice); + // TODO include reply path for invoice error + let instructions = MessageSendInstructions::WithoutReplyPath { + destination: Destination::BlindedPath(path), + }; + pending_offers_messages.push((message, instructions)); + + Ok(()) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -12533,6 +12554,25 @@ where let nonce = match context { None if invoice_request.metadata().is_some() => None, Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + #[cfg(async_payments)] + Some(OffersContext::StaticInvoiceRequested { + recipient_id_nonce, nonce, hmac, path_absolute_expiry + }) => { + // TODO: vet invreq more? + if signer::verify_async_recipient_invreq_context(nonce, hmac, expanded_key).is_err() { + return None + } + if path_absolute_expiry < self.duration_since_epoch() { + return None + } + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back((Event::StaticInvoiceRequested { + recipient_id_nonce, reply_path: responder.reply_path().clone() + }, None)); + + return None + }, _ => return None, }; diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 1953570993a..b273d3e7f65 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -684,3 +684,10 @@ pub(crate) fn hmac_for_async_recipient_invreq_context( Hmac::from_engine(hmac) } + +#[cfg(async_payments)] +pub(crate) fn verify_async_recipient_invreq_context( + nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_async_recipient_invreq_context(nonce, expanded_key) == hmac { Ok(()) } else { Err(()) } +} diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 838c6208782..e3ed0f89421 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -428,6 +428,11 @@ impl Responder { context: Some(context), } } + + #[cfg(async_payments)] + pub(crate) fn reply_path(&self) -> &BlindedMessagePath { + &self.reply_path + } } /// Instructions for how and where to send the response to an onion message. From 8493fa01c5362a07c2eeca2512c07f437a3a8240 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 19 Feb 2025 18:20:15 -0500 Subject: [PATCH 17/18] Adapt async payments tests for static invoice server We were manually creating the static invoice in tests, but now we can use the static invoice server protocol to interactively build the invoice. --- lightning/src/ln/async_payments_tests.rs | 353 +++++++++++++++++----- lightning/src/ln/functional_test_utils.rs | 2 + lightning/src/onion_message/messenger.rs | 10 + 3 files changed, 287 insertions(+), 78 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 8f45313c516..7a59fd0afcc 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -38,6 +38,7 @@ use crate::sign::NodeSigner; use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use crate::util::ser::Writeable; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1; @@ -46,6 +47,103 @@ use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; +// Reload the recipient node, now configured with blinded paths to reach the static invoice +// server. +macro_rules! reload_payee_with_async_receive_cfg { + ($server_node: expr, $payee_node: expr, $new_persister: ident, $new_chain_monitor: ident, + $payee_node_deserialized: ident, $chan_ids: expr + ) => { + let offer_paths_request_paths = + $server_node.node.blinded_paths_for_async_recipient(None).unwrap().0; + let mut async_payee_cfg = test_default_channel_config(); + async_payee_cfg.paths_to_static_invoice_server = offer_paths_request_paths; + + $server_node.node.peer_disconnected($payee_node.node.get_our_node_id()); + + let mut serialized_monitor_vecs = Vec::with_capacity($chan_ids.len()); + for chan_id in $chan_ids { + serialized_monitor_vecs.push(get_monitor!($payee_node, *chan_id).encode()); + } + let mut serialized_monitors = Vec::with_capacity($chan_ids.len()); + for vec in serialized_monitor_vecs.iter() { + serialized_monitors.push(&vec[..]); + } + + reload_node!( + $payee_node, + async_payee_cfg, + $payee_node.node.encode(), + &serialized_monitors[..], + $new_persister, + $new_chain_monitor, + $payee_node_deserialized + ); + + let mut reconnect_args = ReconnectArgs::new(&$server_node, &$payee_node); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); + }; +} + +// +fn pass_static_invoice_server_messages(server: &Node, recipient: &Node) -> StaticInvoice { + // The recipient may send several offer paths requests depending on how many peers they have. + loop { + let offer_paths_req_opt = + recipient.onion_messenger.next_onion_message_for_peer(server.node.get_our_node_id()); + if let Some(offer_paths_req) = offer_paths_req_opt { + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + } else { + break; + } + } + + loop { + let offer_paths_opt = + server.onion_messenger.next_onion_message_for_peer(recipient.node.get_our_node_id()); + if let Some(offer_paths) = offer_paths_opt { + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &offer_paths); + } else { + break; + } + } + + let serve_static_invoice_om = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice_om); + + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (static_invoice, ack_paths) = match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice, invoice_persisted_paths, recipient_id_nonce: _ } => { + (invoice, invoice_persisted_paths) + }, + _ => panic!(), + }; + + assert!(recipient.node.get_cached_async_receive_offer().is_none()); + server.node.static_invoice_persisted(ack_paths).unwrap(); + + let invoice_persisted_om = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); + assert!(recipient.node.get_cached_async_receive_offer().is_some()); + + static_invoice +} + // Goes through the async receive onion message flow, returning the final release_held_htlc OM. // // Assumes the held_htlc_available message will be sent: @@ -59,23 +157,25 @@ fn pass_async_payments_oms( let sender_node_id = sender.node.get_our_node_id(); let always_online_node_id = always_online_recipient_counterparty.node.get_our_node_id(); - // 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 = sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); - let invreq_reply_path = - offers_tests::extract_invoice_request(always_online_recipient_counterparty, &invreq_om).1; - always_online_recipient_counterparty .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) + .handle_onion_message(sender_node_id, &invreq_om); + + let mut events = always_online_recipient_counterparty.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let reply_path = match events.pop().unwrap() { + Event::StaticInvoiceRequested { recipient_id_nonce: _, reply_path } => { + // TODO check recipient nonce? + reply_path + }, + _ => panic!(), + }; + + always_online_recipient_counterparty + .node + .send_static_invoice(static_invoice, reply_path) .unwrap(); let static_invoice_om = always_online_recipient_counterparty .onion_messenger @@ -390,16 +490,35 @@ fn ignore_unexpected_static_invoice() { #[test] fn async_receive_flow_success() { // Test that an always-online sender can successfully pay an async receiver. - let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); + + let static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[2]); + assert!(static_invoice.invoice_features().supports_basic_mpp()); + let offer = nodes[2].node.get_cached_async_receive_offer().unwrap(); // Set the random bytes so we can predict the payment preimage and hash. let hardcoded_random_bytes = [42; 32]; @@ -407,12 +526,6 @@ fn async_receive_flow_success() { let payment_hash: PaymentHash = keysend_preimage.into(); *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); - 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]); let params = RouteParametersConfig::default(); @@ -449,18 +562,28 @@ fn async_receive_flow_success() { #[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 new_persister; + let new_chain_monitor; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); - 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 static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[2]); + let offer = nodes[2].node.get_cached_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -474,20 +597,16 @@ fn expired_static_invoice_fail() { .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(); + nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); + + let mut events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let reply_path = match events.pop().unwrap() { + Event::StaticInvoiceRequested { recipient_id_nonce: _, reply_path } => reply_path, + _ => panic!(), + }; + + nodes[1].node.send_static_invoice(static_invoice.clone(), reply_path).unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -496,7 +615,7 @@ fn expired_static_invoice_fail() { // 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, + (static_invoice.created_at() + static_invoice.relative_expiry()).as_secs() as u32 + 1u32, Vec::new(), ); connect_block(&nodes[0], &block); @@ -513,15 +632,17 @@ fn expired_static_invoice_fail() { }, _ => 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. + // TODO: 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. } #[test] fn async_receive_mpp() { - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs( @@ -529,7 +650,8 @@ fn async_receive_mpp() { &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg.clone()), Some(allow_priv_chan_fwds_cfg), None], ); - let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); // Create this network topology: // n1 @@ -539,9 +661,26 @@ fn async_receive_mpp() { // n2 create_announced_chan_between_nodes(&nodes, 0, 1); create_announced_chan_between_nodes(&nodes, 0, 2); - create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + let chan_id_1_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0).0.channel_id; + let chan_id_2_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0).0.channel_id; + + nodes[2].node.peer_disconnected(nodes[3].node.get_our_node_id()); + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[3], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_3, chan_id_2_3] + ); + let mut reconnect_args = ReconnectArgs::new(&nodes[2], &nodes[3]); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); + + let static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[3]); + let offer = nodes[3].node.get_cached_async_receive_offer().unwrap(); // In other tests we hardcode the sender's random bytes so we can predict the keysend preimage to // check later in the test, but that doesn't work for MPP because it causes the session_privs for @@ -600,6 +739,9 @@ fn amount_doesnt_match_invreq() { let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; // Make one blinded path's fees slightly higher so they are tried in a deterministic order. @@ -610,7 +752,8 @@ fn amount_doesnt_match_invreq() { &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), Some(higher_fee_chan_cfg), None], ); - let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); // Create this network topology so nodes[0] has a blinded route hint to retry over. // n1 @@ -620,10 +763,26 @@ fn amount_doesnt_match_invreq() { // n2 create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 1_000_000, 0); - create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + let chan_id_1_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0).0.channel_id; + let chan_id_2_3 = + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0).0.channel_id; + + nodes[2].node.peer_disconnected(nodes[3].node.get_our_node_id()); + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[3], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_3, chan_id_2_3] + ); + let mut reconnect_args = ReconnectArgs::new(&nodes[2], &nodes[3]); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + let static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[3]); + let offer = nodes[3].node.get_cached_async_receive_offer().unwrap(); // Set the random bytes so we can predict the payment preimage and hash. let hardcoded_random_bytes = [42; 32]; @@ -811,13 +970,31 @@ fn invalid_async_receive_with_retry( let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); + + // Set the random bytes so we can predict the offer nonce. + let hardcoded_random_bytes = [42; 32]; + *nodes[2].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); let blinded_paths_to_always_online_node = nodes[1] .message_router @@ -850,12 +1027,8 @@ fn invalid_async_receive_with_retry( } nodes[2].router.expect_blinded_payment_paths(static_invoice_paths); - let static_invoice = nodes[2] - .node - .create_static_invoice_builder(&offer, offer_nonce, None) - .unwrap() - .build_and_sign(&secp_ctx) - .unwrap(); + let static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[2]); + let offer = nodes[2].node.get_cached_async_receive_offer().unwrap(); // Set the random bytes so we can predict the payment preimage and hash. let hardcoded_random_bytes = [42; 32]; @@ -930,27 +1103,34 @@ fn invalid_async_receive_with_retry( claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } -#[cfg(not(feature = "std"))] +#[cfg_attr(feature = "std", ignore)] #[test] fn expired_static_invoice_message_path() { // Test that if we receive a held_htlc_available message over an expired blinded path, we'll // ignore it. - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + let mut 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 (offer, static_invoice) = create_static_invoice( - &nodes[1], - &nodes[2], - Some(Duration::from_secs(INVOICE_EXPIRY_SECS as u64)), - &secp_ctx, + let chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] ); + let static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[2]); + let offer = nodes[2].node.get_cached_async_receive_offer().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -966,7 +1146,7 @@ fn expired_static_invoice_message_path() { // After the invoice is expired, ignore inbound held_htlc_available messages over the path. let path_absolute_expiry = crate::ln::inbound_payment::calculate_absolute_expiry( nodes[2].node.duration_since_epoch().as_secs(), - INVOICE_EXPIRY_SECS, + crate::offers::static_invoice::DEFAULT_RELATIVE_EXPIRY.as_secs() as u32, ); let block = create_dummy_block( nodes[2].best_block_hash(), @@ -991,13 +1171,28 @@ fn expired_static_invoice_payment_path() { let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let new_persister; + let new_chain_monitor; + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let payee_node_deserialized; + + let mut 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 chan_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister, + new_chain_monitor, + payee_node_deserialized, + &[chan_id_1_2] + ); // Make sure all nodes are at the same block height in preparation for CLTV timeout things. let node_max_height = @@ -1055,7 +1250,9 @@ fn expired_static_invoice_payment_path() { ); connect_blocks(&nodes[2], final_max_cltv_expiry - nodes[2].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + let static_invoice = pass_static_invoice_server_messages(&nodes[1], &nodes[2]); + let offer = nodes[2].node.get_cached_async_receive_offer().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e1328b2ddc8..3031feb12e8 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1184,6 +1184,8 @@ macro_rules! reload_node { $new_channelmanager = _reload_node(&$node, $new_config, &chanman_encoded, $monitors_encoded); $node.node = &$new_channelmanager; $node.onion_messenger.set_offers_handler(&$new_channelmanager); + $node.onion_messenger.set_async_payments_handler(&$new_channelmanager); + // $node.onion_messenger.set_dns_resolver_handler(&$new_channelmanager); }; ($node: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => { reload_node!($node, $crate::util::config::UserConfig::default(), $chanman_encoded, $monitors_encoded, $persister, $new_chain_monitor, $new_channelmanager); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index e3ed0f89421..76e37a52153 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1303,6 +1303,16 @@ where self.offers_handler = offers_handler; } + #[cfg(test)] + pub(crate) fn set_async_payments_handler(&mut self, async_payments_handler: APH) { + self.async_payments_handler = async_payments_handler; + } + + // #[cfg(test)] + // pub(crate) fn set_dns_resolver_handler(&mut self, dns_resolver_handler: DRH) { + // self.dns_resolver_handler = dns_resolver_handler; + // } + /// Sends an [`OnionMessage`] based on its [`MessageSendInstructions`]. pub fn send_onion_message( &self, contents: T, instructions: MessageSendInstructions, From 1c6073b60316407d8ee831c270c967fb6c258429 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 26 Feb 2025 13:51:12 -0500 Subject: [PATCH 18/18] WIP: Test static invoice server --- lightning/src/ln/async_payments_tests.rs | 223 ++++++++++++++++++++++- lightning/src/onion_message/messenger.rs | 2 +- 2 files changed, 222 insertions(+), 3 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 7a59fd0afcc..502a9ec00d3 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -28,8 +28,13 @@ use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::Offer; use crate::offers::static_invoice::StaticInvoice; -use crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler}; -use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; +use crate::onion_message::async_payments::{ + AsyncPaymentsMessage, AsyncPaymentsMessageHandler, DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY, + REPLY_PATH_RELATIVE_EXPIRY, +}; +use crate::onion_message::messenger::{ + Destination, MessageRouter, MessageSendInstructions, PeeledOnion, +}; use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::prelude::*; @@ -1284,3 +1289,217 @@ fn expired_static_invoice_payment_path() { 1, ); } + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_static_invoice_server_message() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let new_persister_1; + let new_persister_2; + let new_chain_monitor_1; + let new_chain_monitor_2; + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let payee_node_deserialized_1; + let payee_node_deserialized_2; + 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_id_1_2 = + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0).0.channel_id; + + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister_1, + new_chain_monitor_1, + payee_node_deserialized_1, + &[chan_id_1_2] + ); + + // If we receive an offer_paths_request over an expired path, it should be ignored. + let offer_paths_req = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + assert!(matches!( + nodes[1].onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::Receive( + ParsedOnionMessageContents::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_)), + _, + _ + ) + )); + nodes[2].onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + + let configured_path_absolute_expiry = (nodes[1].node.duration_since_epoch() + + DEFAULT_CONFIG_PATH_RELATIVE_EXPIRY) + .as_secs() as u32; + let block = create_dummy_block( + nodes[1].best_block_hash(), + configured_path_absolute_expiry + 1u32, + Vec::new(), + ); + connect_block(&nodes[1], &block); + connect_block(&nodes[2], &block); + + nodes[1] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &offer_paths_req); + assert!(nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[2].node.get_our_node_id()) + .is_none()); + + // The payee's configured paths are expired, so reload them again with fresh paths. + reload_payee_with_async_receive_cfg!( + nodes[1], + nodes[2], + new_persister_2, + new_chain_monitor_2, + payee_node_deserialized_2, + &[chan_id_1_2] + ); + + // If we receive an offer_paths message over an expired reply path, it should be ignored. + nodes[2].node.timer_tick_occurred(); + let offer_paths_req = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + nodes[2].onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + nodes[1] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &offer_paths_req); + let offer_paths = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[2].node.get_our_node_id()) + .unwrap(); + assert!(matches!( + nodes[2].onion_messenger.peel_onion_message(&offer_paths).unwrap(), + PeeledOnion::Receive( + ParsedOnionMessageContents::AsyncPayments(AsyncPaymentsMessage::OfferPaths(_)), + _, + _ + ) + )); + + let block = create_dummy_block( + nodes[2].best_block_hash(), + (nodes[2].node.duration_since_epoch() + REPLY_PATH_RELATIVE_EXPIRY).as_secs() as u32 + 1u32, + Vec::new(), + ); + connect_block(&nodes[2], &block); + + nodes[2].onion_messenger.handle_onion_message(nodes[1].node.get_our_node_id(), &offer_paths); + assert!(nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .is_none()); + + // If we receive a serve_static_invoice message over an expired reply path, it should be ignored. + nodes[2].node.timer_tick_occurred(); + let offer_paths_req = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + nodes[1] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &offer_paths_req); + let offer_paths = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[2].node.get_our_node_id()) + .unwrap(); + nodes[2].onion_messenger.handle_onion_message(nodes[1].node.get_our_node_id(), &offer_paths); + let serve_static_invoice = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + assert!(matches!( + nodes[1].onion_messenger.peel_onion_message(&serve_static_invoice).unwrap(), + PeeledOnion::Receive( + ParsedOnionMessageContents::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_)), + _, + _ + ) + )); + + let block = create_dummy_block( + nodes[1].best_block_hash(), + (nodes[1].node.duration_since_epoch() + REPLY_PATH_RELATIVE_EXPIRY).as_secs() as u32 + 1u32, + Vec::new(), + ); + connect_block(&nodes[1], &block); + + nodes[1] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &serve_static_invoice); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[2].node.get_our_node_id()) + .is_none()); + + // If we receive a static_invoice_persisted message to an expired path, it should be ignored. + nodes[2].node.timer_tick_occurred(); + let offer_paths_req = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + nodes[1] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &offer_paths_req); + let offer_paths = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[2].node.get_our_node_id()) + .unwrap(); + nodes[2].onion_messenger.handle_onion_message(nodes[1].node.get_our_node_id(), &offer_paths); + let serve_static_invoice = nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .unwrap(); + nodes[1] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &serve_static_invoice); + let mut events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let ack_paths = match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice_persisted_paths, .. } => invoice_persisted_paths, + _ => panic!(), + }; + + nodes[1].node.static_invoice_persisted(ack_paths).unwrap(); + let invoice_persisted = nodes[1] + .onion_messenger + .next_onion_message_for_peer(nodes[2].node.get_our_node_id()) + .unwrap(); + assert!(matches!( + nodes[2].onion_messenger.peel_onion_message(&invoice_persisted).unwrap(), + PeeledOnion::Receive( + ParsedOnionMessageContents::AsyncPayments( + AsyncPaymentsMessage::StaticInvoicePersisted(_) + ), + _, + _ + ) + )); + + let block = create_dummy_block( + nodes[2].best_block_hash(), + (nodes[2].node.duration_since_epoch() + REPLY_PATH_RELATIVE_EXPIRY).as_secs() as u32 + 1u32, + Vec::new(), + ); + connect_block(&nodes[1], &block); + connect_block(&nodes[2], &block); + nodes[2] + .onion_messenger + .handle_onion_message(nodes[1].node.get_our_node_id(), &invoice_persisted); + assert!(nodes[2].node.get_cached_async_receive_offer().is_none()); + + // The recipient won't try to ask for offer paths again as the maximum number of attempts has been + // exceeded. + nodes[2].node.timer_tick_occurred(); + assert!(nodes[2] + .onion_messenger + .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) + .is_none()); +} diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 76e37a52153..830fc48d518 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1527,7 +1527,7 @@ where } #[cfg(test)] - pub(super) fn release_pending_msgs(&self) -> HashMap> { + pub(crate) fn release_pending_msgs(&self) -> HashMap> { let mut message_recipients = self.message_recipients.lock().unwrap(); let mut msgs = new_hash_map(); // We don't want to disconnect the peers by removing them entirely from the original map, so we