From 3d373947da03cdb82cc923f8a328442289c3bd7a Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 30 Jan 2025 17:27:23 -0500 Subject: [PATCH 01/11] 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 8c0b7ba7b1a..efbe2813c16 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 @@ -16127,7 +16127,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]; @@ -16214,7 +16214,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 ee171fc1c62c23ee4c96e1aca6ef91c9a3ed96b9 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 16:19:18 -0500 Subject: [PATCH 02/11] 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 efbe2813c16..85bc88e1576 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}; @@ -4867,19 +4868,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 }); @@ -10434,18 +10426,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 { @@ -10543,18 +10527,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) @@ -12689,6 +12665,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 e62b5ec0ab97f8d0ff44dd28744e41940e05720b Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 4 Feb 2025 17:02:30 -0800 Subject: [PATCH 03/11] 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 890ce7da939dbbaf928b226a6d157221307a5f7f Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 5 Feb 2025 16:33:27 -0800 Subject: [PATCH 04/11] 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/invoice_request.rs | 2 +- lightning/src/offers/mod.rs | 1 - lightning/src/offers/offer.rs | 1 - lightning/src/offers/static_invoice.rs | 1 + 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index a374a055e70..e1a5d79a2eb 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -857,7 +857,7 @@ impl InvoiceRequest { ); invoice_request_verify_method!(self, Self); - #[cfg(async_payments)] + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag pub(super) fn bytes(&self) -> &Vec { &self.bytes } 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..93d2692420a 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -379,6 +379,7 @@ impl StaticInvoice { self.signature } + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag 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 9e0f2dabb0e299b7e13ea00f6b1b4b42a4a0a8f3 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 16:24:03 -0800 Subject: [PATCH 05/11] 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 ++++++++++++++++++++++++-- lightning/src/util/ser.rs | 1 + 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 85bc88e1576..0538d7180ee 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 @@ -2660,6 +2676,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>, @@ -3614,6 +3631,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()), @@ -13402,6 +13420,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(()) @@ -13931,6 +13950,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), @@ -13948,6 +13968,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); @@ -14643,6 +14664,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()), @@ -15070,8 +15092,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]}); 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 b52f1b4b7b3948b347f3273b50e328e8b45497f8 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 17:12:06 -0800 Subject: [PATCH 06/11] 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 +++++++++ 1 file changed, 9 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(), }) } } From e99e05b741b97c8b518bd214be7ae9ef79b44850 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 5 Feb 2025 17:19:51 -0800 Subject: [PATCH 07/11] 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 0538d7180ee..077874bdf12 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}; @@ -12559,6 +12562,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 93d2692420a..078a4b35f3b 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -33,7 +33,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; @@ -529,6 +529,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..300ff4c9c06 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 { + "Persist Static Invoice".to_string() + } + #[cfg(not(c_bindings))] + fn msg_type(&self) -> &'static str { + "Persist 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.tlv_type(), + Self::ServeStaticInvoice(_) => msg.tlv_type(), + Self::StaticInvoicePersisted(msg) => "Static Invoice Persisted", 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..8f78d198c89 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, + ServeStaticInvoice, ReleaseHeldHtlc, 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 41f00887a4cad20dac099bb2ba04e30306f32917 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 6 Feb 2025 11:48:15 -0800 Subject: [PATCH 08/11] 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 | 75 +++++++++++++++++++++++++++ lightning/src/offers/signer.rs | 18 +++++++ 3 files changed, 124 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 077874bdf12..4f1333b659c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1515,6 +1515,19 @@ struct AsyncReceiveOffer { offer_paths_request_attempts: u8, } +impl AsyncReceiveOffer { + /// 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; + } + } + } +} + impl_writeable_tlv_based!(AsyncReceiveOffer, { (0, offer, option), (2, offer_paths_request_attempts, (static_value, 0)), @@ -2429,6 +2442,8 @@ where // // `pending_async_payments_messages` // +// `async_receive_offer_cache` +// // `total_consistency_lock` // | // |__`forward_htlcs` @@ -4849,6 +4864,60 @@ 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 let Some(ref offer) = offer_cache.offer { + // If we have more than three hours before our offer expires, don't bother requesting new + // paths. + const PATHS_EXPIRY_BUFFER: Duration = Duration::from_secs(60 * 60 * 3); + let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX); + if offer_expiry > duration_since_epoch.saturating_add(PATHS_EXPIRY_BUFFER) { + return + } + } + + const MAX_ATTEMPTS: u8 = 3; + if offer_cache.offer_paths_request_attempts > MAX_ATTEMPTS { return } + } + + let reply_paths = { + // We expect the static invoice server to respond quickly to our request for offer paths, but + // add some buffer for no-std users that rely on block timestamps. + const REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60); + 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 @@ -6798,6 +6867,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. @@ -12082,6 +12154,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) +} From b67220b06cf17df7f80594621d46fb71407dfe64 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Fri, 14 Feb 2025 18:26:48 -0500 Subject: [PATCH 09/11] 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 | 122 ++++++++++++++++++++++++-- lightning/src/offers/signer.rs | 29 ++++++ 3 files changed, 181 insertions(+), 9 deletions(-) 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 4f1333b659c..a503c32b41e 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1516,6 +1516,11 @@ struct AsyncReceiveOffer { } 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(60 * 60 * 3); + /// Removes the offer from our cache if it's expired. #[cfg(async_payments)] fn check_expire_offer(&mut self, duration_since_epoch: Duration) { @@ -1526,6 +1531,17 @@ impl AsyncReceiveOffer { } } } + + #[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, { @@ -4875,15 +4891,8 @@ where { let mut offer_cache = self.async_receive_offer_cache.lock().unwrap(); offer_cache.check_expire_offer(duration_since_epoch); - - if let Some(ref offer) = offer_cache.offer { - // If we have more than three hours before our offer expires, don't bother requesting new - // paths. - const PATHS_EXPIRY_BUFFER: Duration = Duration::from_secs(60 * 60 * 3); - let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX); - if offer_expiry > duration_since_epoch.saturating_add(PATHS_EXPIRY_BUFFER) { - return - } + if !offer_cache.should_refresh_offer(duration_since_epoch) { + return } const MAX_ATTEMPTS: u8 = 3; @@ -12647,6 +12656,101 @@ 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 = { + let invoice_res = self.create_static_invoice_builder( + &offer, offer_nonce, Some(offer_and_invoice_absolute_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 = { + // We expect the static invoice server to respond quickly, but add some buffer for no-std + // users that rely on block timestamps. + const PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60); + + 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(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 cd62d3a8cb2113d3565afa7f16eae490b975a2a2 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 14:52:45 -0500 Subject: [PATCH 10/11] 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 a503c32b41e..a01ce490e0c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -12760,7 +12760,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 5cf358563130175a4d224843815a706fc82033a3 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 15:14:47 -0500 Subject: [PATCH 11/11] 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 a01ce490e0c..eb638733e45 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -10317,9 +10317,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, erroring otherwise. + #[cfg(async_payments)] + pub fn get_cached_async_receive_offer(&self) -> Result { + let mut offer_cache = self.async_receive_offer_cache.lock().unwrap(); + offer_cache.check_expire_offer(self.duration_since_epoch()); + + offer_cache.offer.clone().ok_or(()) + } + /// 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 @@ -10353,6 +10367,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