diff --git a/lightning-liquidity/src/lsps2/event.rs b/lightning-liquidity/src/lsps2/event.rs index f738dc0d7bc..e192d5630dd 100644 --- a/lightning-liquidity/src/lsps2/event.rs +++ b/lightning-liquidity/src/lsps2/event.rs @@ -160,4 +160,16 @@ pub enum LSPS2ServiceEvent { /// The intercept short channel id to use in the route hint. intercept_scid: u64, }, + /// You should broadcast the funding transaction for the channel you opened. + /// + /// On a client_trusts_lsp context, the client has claimed the payment, so now + /// you must broadcast the funding transaction. + BroadcastFundingTransaction { + /// The node id of the counterparty. + counterparty_node_id: PublicKey, + /// The user channel id that was used to open the channel. + user_channel_id: u128, + /// The funding transaction to broadcast. + funding_tx: bitcoin::Transaction, + }, } diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index e1666b1d352..79482342c90 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -23,6 +23,14 @@ use crate::lsps0::ser::{ LSPS0_CLIENT_REJECTED_ERROR_CODE, }; use crate::lsps2::event::LSPS2ServiceEvent; +use crate::lsps2::msgs::{ + LSPS2BuyRequest, LSPS2BuyResponse, LSPS2GetInfoRequest, LSPS2GetInfoResponse, LSPS2Message, + LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams, LSPS2Request, LSPS2Response, + LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, + LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, + LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, +}; use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; use crate::lsps2::utils::{ compute_opening_fee, is_expired_opening_fee_params, is_valid_opening_fee_params, @@ -42,15 +50,7 @@ use lightning::util::logger::Level; use lightning_types::payment::PaymentHash; use bitcoin::secp256k1::PublicKey; - -use crate::lsps2::msgs::{ - LSPS2BuyRequest, LSPS2BuyResponse, LSPS2GetInfoRequest, LSPS2GetInfoResponse, LSPS2Message, - LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams, LSPS2Request, LSPS2Response, - LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, - LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, - LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, - LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, -}; +use bitcoin::Transaction; const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; const MAX_TOTAL_PENDING_REQUESTS: usize = 1000; @@ -107,6 +107,89 @@ struct ForwardPaymentAction(ChannelId, FeePayment); #[derive(Debug, PartialEq)] struct ForwardHTLCsAction(ChannelId, Vec); +#[derive(Debug, Clone)] +enum TrustModel { + ClientTrustsLsp { + funding_tx_broadcast_safe: bool, + payment_claimed: bool, + funding_tx: Option, + }, + LspTrustsClient, +} + +impl TrustModel { + fn should_manually_broadcast(&self) -> bool { + match self { + TrustModel::ClientTrustsLsp { + funding_tx_broadcast_safe, + payment_claimed, + funding_tx, + } => *funding_tx_broadcast_safe && *payment_claimed && funding_tx.is_some(), + // in lsp-trusts-client, the broadcast is automatic, so we never need to manually broadcast. + TrustModel::LspTrustsClient => false, + } + } + + fn new(client_trusts_lsp: bool) -> Self { + if client_trusts_lsp { + TrustModel::ClientTrustsLsp { + funding_tx_broadcast_safe: false, + payment_claimed: false, + funding_tx: None, + } + } else { + TrustModel::LspTrustsClient + } + } + + fn set_funding_tx(&mut self, funding_tx: Transaction) { + match self { + TrustModel::ClientTrustsLsp { funding_tx: tx, .. } => { + *tx = Some(funding_tx); + }, + TrustModel::LspTrustsClient => { + // No-op + }, + } + } + + fn set_funding_tx_broadcast_safe(&mut self, funding_tx_broadcast_safe: bool) { + match self { + TrustModel::ClientTrustsLsp { funding_tx_broadcast_safe: safe, .. } => { + *safe = funding_tx_broadcast_safe; + }, + TrustModel::LspTrustsClient => { + // No-op + }, + } + } + + fn set_payment_claimed(&mut self, payment_claimed: bool) { + match self { + TrustModel::ClientTrustsLsp { payment_claimed: claimed, .. } => { + *claimed = payment_claimed; + }, + TrustModel::LspTrustsClient => { + // No-op + }, + } + } + + fn get_funding_tx(&self) -> Option { + match self { + TrustModel::ClientTrustsLsp { funding_tx, .. } => funding_tx.clone(), + TrustModel::LspTrustsClient => None, + } + } + + fn is_client_trusts_lsp(&self) -> bool { + match self { + TrustModel::ClientTrustsLsp { .. } => true, + TrustModel::LspTrustsClient => false, + } + } +} + /// The different states a requested JIT channel can be in. #[derive(Debug)] enum OutboundJITChannelState { @@ -284,6 +367,7 @@ impl OutboundJITChannelState { channel_id, FeePayment { opening_fee_msat: *opening_fee_msat, htlcs }, ); + *self = OutboundJITChannelState::PendingPaymentForward { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, @@ -383,18 +467,20 @@ struct OutboundJITChannel { user_channel_id: u128, opening_fee_params: LSPS2OpeningFeeParams, payment_size_msat: Option, + trust_model: TrustModel, } impl OutboundJITChannel { fn new( payment_size_msat: Option, opening_fee_params: LSPS2OpeningFeeParams, - user_channel_id: u128, + user_channel_id: u128, client_trusts_lsp: bool, ) -> Self { Self { user_channel_id, state: OutboundJITChannelState::new(), opening_fee_params, payment_size_msat, + trust_model: TrustModel::new(client_trusts_lsp), } } @@ -420,6 +506,9 @@ impl OutboundJITChannel { fn payment_forwarded(&mut self) -> Result, LightningError> { let action = self.state.payment_forwarded()?; + if action.is_some() { + self.trust_model.set_payment_claimed(true); + } Ok(action) } @@ -433,6 +522,26 @@ impl OutboundJITChannel { let is_expired = is_expired_opening_fee_params(&self.opening_fee_params); self.is_pending_initial_payment() && is_expired } + + fn set_funding_tx(&mut self, funding_tx: Transaction) { + self.trust_model.set_funding_tx(funding_tx); + } + + fn set_funding_tx_broadcast_safe(&mut self, funding_tx_broadcast_safe: bool) { + self.trust_model.set_funding_tx_broadcast_safe(funding_tx_broadcast_safe); + } + + fn should_broadcast_funding_transaction(&self) -> bool { + self.trust_model.should_manually_broadcast() + } + + fn get_funding_tx(&self) -> Option { + self.trust_model.get_funding_tx() + } + + fn client_trusts_lsp(&self) -> bool { + self.trust_model.is_client_trusts_lsp() + } } struct PeerState { @@ -727,6 +836,7 @@ where buy_request.payment_size_msat, buy_request.opening_fee_params, user_channel_id, + client_trusts_lsp, ); peer_state_lock @@ -973,12 +1083,17 @@ where Err(e) => { return Err(APIError::APIMisuseError { err: format!( - "Forwarded payment was not applicable for JIT channel: {}", - e.err - ), + "Forwarded payment was not applicable for JIT channel: {}", + e.err + ), }) }, } + + self.emit_broadcast_funding_transaction_event_if_applies( + jit_channel, + counterparty_node_id, + ); } } else { return Err(APIError::APIMisuseError { @@ -1467,6 +1582,129 @@ where peer_state_lock.is_prunable() == false }); } + + /// Checks if the JIT channel with the given `user_channel_id` needs manual broadcast. + /// Will be true if client_trusts_lsp is set to true + pub fn channel_needs_manual_broadcast( + &self, user_channel_id: u128, counterparty_node_id: &PublicKey, + ) -> Result { + let outer_state_lock = self.per_peer_state.read().unwrap(); + let inner_state_lock = + outer_state_lock.get(counterparty_node_id).ok_or_else(|| APIError::APIMisuseError { + err: format!("No counterparty state for: {}", counterparty_node_id), + })?; + let peer_state = inner_state_lock.lock().unwrap(); + + let intercept_scid = peer_state + .intercept_scid_by_user_channel_id + .get(&user_channel_id) + .copied() + .ok_or_else(|| APIError::APIMisuseError { + err: format!("Could not find a channel with user_channel_id {}", user_channel_id), + })?; + + let jit_channel = peer_state + .outbound_channels_by_intercept_scid + .get(&intercept_scid) + .ok_or_else(|| APIError::APIMisuseError { + err: format!( + "Failed to map intercept_scid {} for user_channel_id {} to a channel.", + intercept_scid, user_channel_id, + ), + })?; + + Ok(jit_channel.client_trusts_lsp()) + } + + /// Called to store the funding transaction for a JIT channel. + /// This should be called when the funding transaction is created but before it's broadcast. + pub fn store_funding_transaction( + &self, user_channel_id: u128, counterparty_node_id: &PublicKey, funding_tx: Transaction, + ) -> Result<(), APIError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + let inner_state_lock = + outer_state_lock.get(counterparty_node_id).ok_or_else(|| APIError::APIMisuseError { + err: format!("No counterparty state for: {}", counterparty_node_id), + })?; + let mut peer_state = inner_state_lock.lock().unwrap(); + + let intercept_scid = peer_state + .intercept_scid_by_user_channel_id + .get(&user_channel_id) + .copied() + .ok_or_else(|| APIError::APIMisuseError { + err: format!("Could not find a channel with user_channel_id {}", user_channel_id), + })?; + + let jit_channel = peer_state + .outbound_channels_by_intercept_scid + .get_mut(&intercept_scid) + .ok_or_else(|| APIError::APIMisuseError { + err: format!( + "Failed to map intercept_scid {} for user_channel_id {} to a channel.", + intercept_scid, user_channel_id, + ), + })?; + + jit_channel.set_funding_tx(funding_tx); + + self.emit_broadcast_funding_transaction_event_if_applies(jit_channel, counterparty_node_id); + Ok(()) + } + + /// Called when the funding transaction is safe to broadcast. + /// This marks the funding_tx_broadcast_safe flag as true for the given user_channel_id. + pub fn funding_tx_broadcast_safe( + &self, user_channel_id: u128, counterparty_node_id: &PublicKey, + ) -> Result<(), APIError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + let inner_state_lock = + outer_state_lock.get(counterparty_node_id).ok_or_else(|| APIError::APIMisuseError { + err: format!("No counterparty state for: {}", counterparty_node_id), + })?; + let mut peer_state = inner_state_lock.lock().unwrap(); + + let intercept_scid = peer_state + .intercept_scid_by_user_channel_id + .get(&user_channel_id) + .copied() + .ok_or_else(|| APIError::APIMisuseError { + err: format!("Could not find a channel with user_channel_id {}", user_channel_id), + })?; + + let jit_channel = peer_state + .outbound_channels_by_intercept_scid + .get_mut(&intercept_scid) + .ok_or_else(|| APIError::APIMisuseError { + err: format!( + "Failed to map intercept_scid {} for user_channel_id {} to a channel.", + intercept_scid, user_channel_id, + ), + })?; + + jit_channel.set_funding_tx_broadcast_safe(true); + + self.emit_broadcast_funding_transaction_event_if_applies(jit_channel, counterparty_node_id); + Ok(()) + } + + fn emit_broadcast_funding_transaction_event_if_applies( + &self, jit_channel: &OutboundJITChannel, counterparty_node_id: &PublicKey, + ) { + if jit_channel.should_broadcast_funding_transaction() { + let funding_tx = jit_channel.get_funding_tx(); + + if let Some(funding_tx) = funding_tx { + let event_queue_notifier = self.pending_events.notifier(); + let event = LSPS2ServiceEvent::BroadcastFundingTransaction { + counterparty_node_id: *counterparty_node_id, + user_channel_id: jit_channel.user_channel_id, + funding_tx, + }; + event_queue_notifier.enqueue(event); + } + } + } } impl LSPSProtocolMessageHandler for LSPS2ServiceHandler diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index 3a31ab99ea7..a12d9059264 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -15,6 +15,7 @@ use std::sync::Arc; pub(crate) struct LSPSNodes<'a, 'b, 'c> { pub service_node: LiquidityNode<'a, 'b, 'c>, pub client_node: LiquidityNode<'a, 'b, 'c>, + pub payer_node_optional: Option>, } pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>( @@ -47,7 +48,7 @@ pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>( let service_node = LiquidityNode::new(iter.next().unwrap(), service_lm); let client_node = LiquidityNode::new(iter.next().unwrap(), client_lm); - LSPSNodes { service_node, client_node } + LSPSNodes { service_node, client_node, payer_node_optional: iter.next() } } pub(crate) struct LiquidityNode<'a, 'b, 'c> { diff --git a/lightning-liquidity/tests/lsps0_integration_tests.rs b/lightning-liquidity/tests/lsps0_integration_tests.rs index 465ab029d53..a841e2c5369 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -51,7 +51,7 @@ fn list_protocols_integration_test() { let service_node_id = nodes[0].node.get_our_node_id(); let client_node_id = nodes[1].node.get_our_node_id(); - let LSPSNodes { service_node, client_node } = + let LSPSNodes { service_node, client_node, .. } = create_service_and_client_nodes(nodes, service_config, client_config); let client_handler = client_node.liquidity_manager.lsps0_client_handler(); diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 02266807992..415ec1c57c5 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -4,6 +4,25 @@ mod common; use common::{create_service_and_client_nodes, get_lsps_message, LSPSNodes, LiquidityNode}; +use lightning::check_added_monitors; +use lightning::events::Event; +use lightning::get_event_msg; +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::channelmanager::Retry; +use lightning::ln::functional_test_utils::create_chan_between_nodes_with_value; +use lightning::ln::functional_test_utils::create_funding_transaction; +use lightning::ln::functional_test_utils::do_commitment_signed_dance; +use lightning::ln::functional_test_utils::expect_channel_pending_event; +use lightning::ln::functional_test_utils::expect_channel_ready_event; +use lightning::ln::functional_test_utils::expect_payment_sent; +use lightning::ln::functional_test_utils::pass_claimed_payment_along_route; +use lightning::ln::functional_test_utils::test_default_channel_config; +use lightning::ln::functional_test_utils::ClaimAlongRouteArgs; +use lightning::ln::functional_test_utils::SendEvent; +use lightning::ln::msgs::BaseMessageHandler; +use lightning::ln::msgs::ChannelMessageHandler; +use lightning::ln::msgs::MessageSendEvent; +use lightning::ln::types::ChannelId; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::ser::LSPSDateTime; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; @@ -75,10 +94,14 @@ fn create_jit_invoice( log_error!(node.logger, "Failed to register inbound payment: {:?}", e); })?; + // Add debugging here + println!("Creating route hint with intercept_scid: {}", intercept_scid); + println!("Service node ID: {}", service_node_id); + let route_hint = RouteHint(vec![RouteHintHop { src_node_id: service_node_id, short_channel_id: intercept_scid, - fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, + fees: RoutingFees { base_msat: 1000, proportional_millionths: 0 }, cltv_expiry_delta: cltv_expiry_delta as u16, htlc_minimum_msat: None, htlc_maximum_msat: None, @@ -109,11 +132,16 @@ fn create_jit_invoice( let sign_fn = node.inner.keys_manager.sign_invoice(&raw_invoice, lightning::sign::Recipient::Node); - raw_invoice.sign(|_| sign_fn).and_then(|signed_raw| { + let invoice = raw_invoice.sign(|_| sign_fn).and_then(|signed_raw| { Bolt11Invoice::from_signed(signed_raw).map_err(|e| { log_error!(node.inner.logger, "Failed to create invoice from signed raw: {:?}", e); }) - }) + })?; + + // Add debugging to verify the invoice + println!("Created invoice with route hints: {:?}", invoice.route_hints()); + + Ok(invoice) } #[test] @@ -123,7 +151,7 @@ fn invoice_generation_flow() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -271,7 +299,7 @@ fn channel_open_failed() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -406,7 +434,7 @@ fn channel_open_failed_nonexistent_channel() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let client_node_id = client_node.inner.node.get_our_node_id(); @@ -432,7 +460,7 @@ fn channel_open_abandoned() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -516,7 +544,7 @@ fn channel_open_abandoned_nonexistent_channel() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let client_node_id = client_node.inner.node.get_our_node_id(); let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); @@ -541,7 +569,7 @@ fn max_pending_requests_per_peer_rejected() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -596,7 +624,7 @@ fn max_total_requests_buy_rejected() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); @@ -725,7 +753,7 @@ fn invalid_token_flow() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -804,7 +832,7 @@ fn opening_fee_params_menu_is_sorted_by_spec() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let (lsps_nodes, _) = setup_test_lsps2_nodes(nodes); - let LSPSNodes { service_node, client_node } = lsps_nodes; + let LSPSNodes { service_node, client_node, .. } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -874,3 +902,417 @@ fn opening_fee_params_menu_is_sorted_by_spec() { panic!("Unexpected event"); } } + +#[test] +fn full_lsps2_flow() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut service_node_config = test_default_channel_config(); + service_node_config.accept_intercept_htlcs = true; + let mut client_node_config = test_default_channel_config(); + client_node_config.manually_accept_inbound_channels = true; + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(service_node_config), Some(client_node_config), None], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes(nodes); + let LSPSNodes { service_node, client_node, payer_node_optional } = lsps_nodes; + let payer_node = payer_node_optional.unwrap(); + let payer_node_id = payer_node.node.get_our_node_id(); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + + create_chan_between_nodes_with_value(&payer_node, &service_node.inner, 2000000, 100000); + + let get_info_request_id = client_handler.request_opening_params(service_node_id, None); + let get_info_request = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap(); + + let get_info_event = service_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) = get_info_event + { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(token, None); + } else { + panic!("Unexpected event"); + } + + let min_fee_msat = 1000; + let raw_opening_params = LSPS2RawOpeningFeeParams { + min_fee_msat, + proportional: 21, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 144, + max_client_to_self_delay: 128, + min_payment_size_msat: 1, + max_payment_size_msat: 100_000_000, + }; + + service_handler + .opening_fee_params_generated( + &client_node_id, + get_info_request_id.clone(), + vec![raw_opening_params], + ) + .unwrap(); + let get_info_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(get_info_response, service_node_id) + .unwrap(); + + let opening_params_event = client_node.liquidity_manager.next_event().unwrap(); + let opening_fee_params = match opening_params_event { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { + request_id, + counterparty_node_id, + opening_fee_params_menu, + }) => { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, service_node_id); + let opening_fee_params = opening_fee_params_menu.first().unwrap().clone(); + assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + opening_fee_params + }, + _ => panic!("Unexpected event"), + }; + + let payment_size_msat = Some(1_000_000); + let buy_request_id = client_handler + .select_opening_params(service_node_id, payment_size_msat, opening_fee_params.clone()) + .unwrap(); + + let buy_request = get_lsps_message!(client_node, service_node_id); + service_node.liquidity_manager.handle_custom_message(buy_request, client_node_id).unwrap(); + + let buy_event = service_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: ofp, + payment_size_msat: psm, + }) = buy_event + { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(opening_fee_params, ofp); + assert_eq!(payment_size_msat, psm); + } else { + panic!("Unexpected event"); + } + + let user_channel_id = 42; + let cltv_expiry_delta = 144; + let intercept_scid = service_node.node.get_intercept_scid(); + let client_trusts_lsp = true; + + service_handler + .invoice_parameters_generated( + &client_node_id, + buy_request_id.clone(), + intercept_scid, + cltv_expiry_delta, + client_trusts_lsp, + user_channel_id, + ) + .unwrap(); + + let buy_response = get_lsps_message!(service_node, client_node_id); + client_node.liquidity_manager.handle_custom_message(buy_response, service_node_id).unwrap(); + + let invoice_params_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { + request_id, + counterparty_node_id, + intercept_scid: iscid, + cltv_expiry_delta: ced, + payment_size_msat: psm, + }) = invoice_params_event + { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(intercept_scid, iscid); + assert_eq!(cltv_expiry_delta, ced); + assert_eq!(payment_size_msat, psm); + } else { + panic!("Unexpected event"); + } + + let description = "asdf"; + let expiry_secs = 3600; + let invoice = create_jit_invoice( + &client_node, + service_node_id, + intercept_scid, + cltv_expiry_delta, + payment_size_msat, + description, + expiry_secs, + ) + .unwrap(); + + payer_node + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(invoice.payment_hash().to_byte_array()), + None, + Default::default(), + Retry::Attempts(3), + ) + .unwrap(); + + check_added_monitors!(payer_node, 1); + let events = payer_node.node.get_and_clear_pending_msg_events(); + let ev = SendEvent::from_event(events[0].clone()); + service_node.inner.node.handle_update_add_htlc(payer_node_id, &ev.msgs[0]); + do_commitment_signed_dance(&service_node.inner, &payer_node, &ev.commitment_msg, false, true); + service_node.inner.node.process_pending_htlc_forwards(); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (payment_hash, expected_outbound_amount_msat) = match &events[0] { + Event::HTLCIntercepted { + intercept_id, + requested_next_hop_scid, + payment_hash, + inbound_amount_msat, + expected_outbound_amount_msat, + } => { + assert_eq!(*requested_next_hop_scid, intercept_scid); + + service_handler + .htlc_intercepted( + *requested_next_hop_scid, + *intercept_id, + *inbound_amount_msat, + *payment_hash, + ) + .unwrap(); + (*payment_hash, expected_outbound_amount_msat) + }, + other => panic!("Expected HTLCIntercepted event, got: {:?}", other), + }; + + let open_channel_event = service_node.liquidity_manager.next_event().unwrap(); + + match open_channel_event { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat, + user_channel_id, + intercept_scid: iscd, + }) => { + assert_eq!(their_network_key, client_node_id); + assert_eq!(amt_to_forward_msat, payment_size_msat.unwrap() - min_fee_msat); + assert_eq!(opening_fee_msat, min_fee_msat); + assert_eq!(user_channel_id, 42); + assert_eq!(iscd, intercept_scid); + }, + other => panic!("Expected OpenChannel event, got: {:?}", other), + }; + + let result = + service_handler.channel_needs_manual_broadcast(user_channel_id, &client_node_id).unwrap(); + assert!(result, "Channel should require manual broadcast"); + + let channel_id = create_channel_with_manual_broadcast( + &service_node_id, + &client_node_id, + &service_node, + &client_node, + user_channel_id, + expected_outbound_amount_msat, + ); + + service_handler.channel_ready(user_channel_id, &channel_id, &client_node_id).unwrap(); + + service_node.inner.node.process_pending_htlc_forwards(); + + let pay_event = { + { + let mut added_monitors = + service_node.inner.chain_monitor.added_monitors.lock().unwrap(); + assert_eq!(added_monitors.len(), 1); + added_monitors.clear(); + } + let mut events = service_node.inner.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + SendEvent::from_event(events.remove(0)) + }; + + client_node.inner.node.handle_update_add_htlc(service_node_id, &pay_event.msgs[0]); + do_commitment_signed_dance( + &client_node.inner, + &service_node.inner, + &pay_event.commitment_msg, + false, + true, + ); + client_node.inner.node.process_pending_htlc_forwards(); + + let client_events = client_node.inner.node.get_and_clear_pending_events(); + assert_eq!(client_events.len(), 1); + let preimage = match &client_events[0] { + Event::PaymentClaimable { payment_hash: ph, purpose, .. } => { + assert_eq!(*ph, payment_hash); + purpose.preimage() + }, + other => panic!("Expected PaymentClaimable event on client, got: {:?}", other), + }; + + let events = service_node.liquidity_manager.get_and_clear_pending_events(); + assert!(events.is_empty(), "Expected no events from service node, got: {:?}", events); + + client_node.inner.node.claim_funds(preimage.unwrap()); + + // TODO: Call service_manager payment_forwarded when service gets the payment forwarded event + // TODO: in here check that the service node got a BroadcastFundingTransaction event + + let expected_paths: &[&[&lightning::ln::functional_test_utils::Node<'_, '_, '_>]] = + &[&[&service_node.inner, &client_node.inner]]; + + let args = ClaimAlongRouteArgs::new(&payer_node, expected_paths, preimage.unwrap()); + let total_fee_msat = pass_claimed_payment_along_route(args); + + expect_payment_sent(&payer_node, preimage.unwrap(), Some(Some(total_fee_msat)), true, true); +} + +fn create_channel_with_manual_broadcast( + service_node_id: &PublicKey, client_node_id: &PublicKey, service_node: &LiquidityNode, + client_node: &LiquidityNode, user_channel_id: u128, expected_outbound_amount_msat: &u64, +) -> ChannelId { + assert!(service_node + .node + .create_channel( + *client_node_id, + *expected_outbound_amount_msat, + 0, + user_channel_id, + None, + None + ) + .is_ok()); + let open_channel = + get_event_msg!(service_node, MessageSendEvent::SendOpenChannel, *client_node_id); + + client_node.node.handle_open_channel(*service_node_id, &open_channel); + + let events = client_node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id, .. } => { + client_node + .node + .accept_inbound_channel_from_trusted_peer_0conf( + &temporary_channel_id, + &service_node_id, + user_channel_id, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + let accept_channel = + get_event_msg!(client_node, MessageSendEvent::SendAcceptChannel, *service_node_id); + assert_eq!(accept_channel.common_fields.minimum_depth, 0); + + service_node.node.handle_accept_channel(*client_node_id, &accept_channel); + let (temp_channel_id, tx, funding_outpoint) = create_funding_transaction( + &service_node, + &client_node_id, + *expected_outbound_amount_msat, + 42, + ); + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + service_handler + .store_funding_transaction(user_channel_id, &client_node_id, tx.clone()) + .unwrap(); + service_node + .node + .funding_transaction_generated_manual_broadcast(temp_channel_id, *client_node_id, tx) + .unwrap(); + + let funding_created = + get_event_msg!(service_node, MessageSendEvent::SendFundingCreated, *client_node_id); + client_node.node.handle_funding_created(*service_node_id, &funding_created); + check_added_monitors!(client_node.inner, 1); + + let bs_signed_locked = client_node.node.get_and_clear_pending_msg_events(); + assert_eq!(bs_signed_locked.len(), 2); + + let as_channel_ready; + match &bs_signed_locked[0] { + MessageSendEvent::SendFundingSigned { node_id, msg } => { + assert_eq!(*node_id, *service_node_id); + service_node.node.handle_funding_signed(*client_node_id, &msg); + let events = &service_node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + match &events[0] { + Event::FundingTxBroadcastSafe { + funding_txo, + user_channel_id, + counterparty_node_id, + .. + } => { + assert_eq!(funding_txo.txid, funding_outpoint.txid); + assert_eq!(funding_txo.vout, funding_outpoint.index as u32); + + service_handler + .funding_tx_broadcast_safe(*user_channel_id, counterparty_node_id) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + match &events[1] { + Event::ChannelPending { counterparty_node_id, .. } => { + assert_eq!(counterparty_node_id, client_node_id); + }, + _ => panic!("Unexpected event"), + } + expect_channel_pending_event(&client_node, &service_node_id); + check_added_monitors!(service_node.inner, 1); + + as_channel_ready = + get_event_msg!(service_node, MessageSendEvent::SendChannelReady, *client_node_id); + }, + _ => panic!("Unexpected event"), + } + + match &bs_signed_locked[1] { + MessageSendEvent::SendChannelReady { node_id, msg } => { + assert_eq!(*node_id, *service_node_id); + service_node.node.handle_channel_ready(*client_node_id, &msg); + expect_channel_ready_event(&service_node, &client_node_id); + }, + _ => panic!("Unexpected event"), + } + + client_node.node.handle_channel_ready(*service_node_id, &as_channel_ready); + expect_channel_ready_event(&client_node, &service_node_id); + + let as_channel_update = + get_event_msg!(service_node, MessageSendEvent::SendChannelUpdate, *client_node_id); + let bs_channel_update = + get_event_msg!(client_node, MessageSendEvent::SendChannelUpdate, *service_node_id); + + service_node.node.handle_channel_update(*client_node_id, &bs_channel_update); + client_node.node.handle_channel_update(*service_node_id, &as_channel_update); + + as_channel_ready.channel_id +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e8e1c474736..075bf237624 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -976,6 +976,11 @@ enum FundingType { /// /// This is the normal flow. Checked(Transaction), + /// This variant is useful when we want LDK to validate the funding transaction and + /// broadcast it manually. + /// + /// Used in LSPS2 on a client_trusts_lsp model + CheckedManualBroadcast(Transaction), /// This variant is useful when we want to loosen the validation checks and allow to /// manually broadcast the funding transaction, leaving the responsibility to the caller. /// @@ -990,6 +995,7 @@ impl FundingType { fn txid(&self) -> Txid { match self { FundingType::Checked(tx) => tx.compute_txid(), + FundingType::CheckedManualBroadcast(tx) => tx.compute_txid(), FundingType::Unchecked(outp) => outp.txid, } } @@ -997,6 +1003,7 @@ impl FundingType { fn transaction_or_dummy(&self) -> Transaction { match self { FundingType::Checked(tx) => tx.clone(), + FundingType::CheckedManualBroadcast(tx) => tx.clone(), FundingType::Unchecked(_) => Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, @@ -1009,6 +1016,7 @@ impl FundingType { fn is_manual_broadcast(&self) -> bool { match self { FundingType::Checked(_) => false, + FundingType::CheckedManualBroadcast(_) => true, FundingType::Unchecked(_) => true, } } @@ -5709,6 +5717,18 @@ where self.batch_funding_transaction_generated_intern(temporary_chans, funding_type) } + /// Same as batch_funding_transaction_generated but it does not automatically broadcast the funding transaction + pub fn funding_transaction_generated_manual_broadcast( + &self, temporary_channel_id: ChannelId, counterparty_node_id: PublicKey, + funding_transaction: Transaction, + ) -> Result<(), APIError> { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + self.batch_funding_transaction_generated_intern( + &[(&temporary_channel_id, &counterparty_node_id)], + FundingType::CheckedManualBroadcast(funding_transaction), + ) + } + /// Call this upon creation of a batch funding transaction for the given channels. /// /// Return values are identical to [`Self::funding_transaction_generated`], respective to @@ -5790,7 +5810,7 @@ where let mut output_index = None; let expected_spk = chan.funding.get_funding_redeemscript().to_p2wsh(); let outpoint = match &funding { - FundingType::Checked(tx) => { + FundingType::Checked(tx) | FundingType::CheckedManualBroadcast(tx) => { for (idx, outp) in tx.output.iter().enumerate() { if outp.script_pubkey == expected_spk && outp.value.to_sat() == chan.funding.get_value_satoshis() { if output_index.is_some() { @@ -11139,6 +11159,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + /// Manually broadcast a transaction using the internal transaction broadcaster. + /// + /// This method should only be used in specific scenarios where manual control + /// over transaction broadcast timing is required (e.g., LSPS2 workflows). + pub fn broadcast_transaction(&self, tx: &Transaction) { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + log_info!(self.logger, "Broadcasting transaction {}", log_tx!(tx)); + self.tx_broadcaster.broadcast_transactions(&[tx]); + } + /// Check whether any channels have finished removing all pending updates after a shutdown /// exchange and can now send a closing_signed. /// Returns whether any closing_signed messages were generated. diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index a46e7c0a381..a88908c38d7 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1952,7 +1952,7 @@ pub fn do_check_spends Option>( total_value_out += output.value.to_sat(); } let min_fee = (tx.weight().to_wu() as u64 + 3) / 4; // One sat per vbyte (ie per weight/4, rounded up) - // Input amount - output amount = fee, so check that out + min_fee is smaller than input + // Input amount - output amount = fee, so check that out + min_fee is smaller than input assert!(total_value_out + min_fee <= total_value_in); tx.verify(get_output).unwrap(); } @@ -2994,6 +2994,7 @@ pub fn expect_channel_pending_event<'a, 'b, 'c, 'd>( node: &'a Node<'b, 'c, 'd>, expected_counterparty_node_id: &PublicKey, ) -> ChannelId { let events = node.node.get_and_clear_pending_events(); + println!("Pending events: {:?}", events); assert_eq!(events.len(), 1); match &events[0] { crate::events::Event::ChannelPending { channel_id, counterparty_node_id, .. } => { diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 5ad5c3e7786..13ff6d49aa2 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -1142,7 +1142,7 @@ impl PaymentParameters { } /// A struct for configuring parameters for routing the payment. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy)] pub struct RouteParametersConfig { /// The maximum total fees, in millisatoshi, that may accrue during route finding. ///