From f5a0eb0bc6206177cb463f9deae419f73257a424 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Mon, 9 Jun 2025 15:32:25 -0300 Subject: [PATCH 1/7] Support client_trusts_lsp on LSPS2 --- lightning-liquidity/src/lsps2/service.rs | 393 +++++++++++++++++++++-- lightning/src/ln/channelmanager.rs | 39 ++- 2 files changed, 410 insertions(+), 22 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 309d7ae1755..50552fd1158 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -11,6 +11,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; +use bitcoin::Transaction; use core::cmp::Ordering as CmpOrdering; use core::ops::Deref; @@ -107,6 +108,79 @@ struct ForwardPaymentAction(ChannelId, FeePayment); #[derive(Debug, PartialEq)] struct ForwardHTLCsAction(ChannelId, Vec); +#[derive(Debug, Clone)] +enum TrustModel { + ClientTrustsLsp { + funding_tx_broadcast_safe: bool, + htlc_claimed: bool, + funding_tx: Option, + }, + LspTrustsClient, +} + +impl TrustModel { + fn should_broadcast(&self) -> bool { + match self { + TrustModel::ClientTrustsLsp { funding_tx_broadcast_safe, htlc_claimed, funding_tx } => { + *funding_tx_broadcast_safe && *htlc_claimed && funding_tx.is_some() + }, + TrustModel::LspTrustsClient => false, + } + } + + fn new(client_trusts_lsp: bool) -> Self { + if client_trusts_lsp { + return TrustModel::ClientTrustsLsp { + funding_tx_broadcast_safe: false, + htlc_claimed: false, + funding_tx: None, + }; + } else { + return 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_htlc_claimed(&mut self, htlc_claimed: bool) { + match self { + TrustModel::ClientTrustsLsp { htlc_claimed: claimed, .. } => { + *claimed = htlc_claimed; + }, + TrustModel::LspTrustsClient => { + // No-op + }, + } + } + + fn get_funding_tx(&self) -> Option { + match self { + TrustModel::ClientTrustsLsp { funding_tx, .. } => funding_tx.clone(), + TrustModel::LspTrustsClient => None, + } + } +} + /// The different states a requested JIT channel can be in. #[derive(Debug)] enum OutboundJITChannelState { @@ -115,7 +189,11 @@ enum OutboundJITChannelState { PendingInitialPayment { payment_queue: PaymentQueue }, /// An initial payment of sufficient size was intercepted to the JIT channel SCID, triggering the /// opening of the channel. We are awaiting the completion of the channel establishment. - PendingChannelOpen { payment_queue: PaymentQueue, opening_fee_msat: u64 }, + PendingChannelOpen { + payment_queue: PaymentQueue, + opening_fee_msat: u64, + trust_model: TrustModel, + }, /// The channel is open and a payment was forwarded while skimming the JIT channel fee. /// No further payments can be forwarded until the pending payment succeeds or fails, as we need /// to know whether the JIT channel fee needs to be skimmed from a next payment or not. @@ -123,15 +201,21 @@ enum OutboundJITChannelState { payment_queue: PaymentQueue, opening_fee_msat: u64, channel_id: ChannelId, + trust_model: TrustModel, }, /// The channel is open, no payment is currently being forwarded, and the JIT channel fee still /// needs to be paid. This state can occur when the initial payment fails, e.g. due to a /// prepayment probe. We are awaiting a next payment of sufficient size to forward and skim the /// JIT channel fee. - PendingPayment { payment_queue: PaymentQueue, opening_fee_msat: u64, channel_id: ChannelId }, + PendingPayment { + payment_queue: PaymentQueue, + opening_fee_msat: u64, + channel_id: ChannelId, + trust_model: TrustModel, + }, /// The channel is open and a payment was successfully forwarded while skimming the JIT channel /// fee. Any subsequent HTLCs can be forwarded without additional logic. - PaymentForwarded { channel_id: ChannelId }, + PaymentForwarded { channel_id: ChannelId, trust_model: TrustModel }, } impl OutboundJITChannelState { @@ -141,7 +225,7 @@ impl OutboundJITChannelState { fn htlc_intercepted( &mut self, opening_fee_params: &LSPS2OpeningFeeParams, payment_size_msat: &Option, - htlc: InterceptedHTLC, + htlc: InterceptedHTLC, client_trusts_lsp: bool, ) -> Result, ChannelStateError> { match self { OutboundJITChannelState::PendingInitialPayment { payment_queue } => { @@ -193,6 +277,7 @@ impl OutboundJITChannelState { *self = OutboundJITChannelState::PendingChannelOpen { payment_queue: core::mem::take(payment_queue), opening_fee_msat, + trust_model: TrustModel::new(client_trusts_lsp), }; let open_channel = HTLCInterceptedAction::OpenChannel(OpenChannelParams { opening_fee_msat, @@ -212,12 +297,17 @@ impl OutboundJITChannelState { } } }, - OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat } => { + OutboundJITChannelState::PendingChannelOpen { + payment_queue, + opening_fee_msat, + trust_model, + } => { let mut payment_queue = core::mem::take(payment_queue); payment_queue.add_htlc(htlc); *self = OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat: *opening_fee_msat, + trust_model: trust_model.clone(), }; Ok(None) }, @@ -225,6 +315,7 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, + trust_model, } => { let mut payment_queue = core::mem::take(payment_queue); payment_queue.add_htlc(htlc); @@ -232,6 +323,7 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, + trust_model: trust_model.clone(), }; Ok(None) }, @@ -239,6 +331,7 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, + trust_model, } => { let mut payment_queue = core::mem::take(payment_queue); payment_queue.add_htlc(htlc); @@ -253,6 +346,7 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, + trust_model: trust_model.clone(), }; Ok(Some(forward_payment)) } else { @@ -260,13 +354,17 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, + trust_model: trust_model.clone(), }; Ok(None) } }, - OutboundJITChannelState::PaymentForwarded { channel_id } => { + OutboundJITChannelState::PaymentForwarded { channel_id, trust_model } => { let forward = HTLCInterceptedAction::ForwardHTLC(*channel_id); - *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + *self = OutboundJITChannelState::PaymentForwarded { + channel_id: *channel_id, + trust_model: trust_model.clone(), + }; Ok(Some(forward)) }, } @@ -276,7 +374,11 @@ impl OutboundJITChannelState { &mut self, channel_id: ChannelId, ) -> Result { match self { - OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat } => { + OutboundJITChannelState::PendingChannelOpen { + payment_queue, + opening_fee_msat, + trust_model, + } => { if let Some((_payment_hash, htlcs)) = payment_queue.pop_greater_than_msat(*opening_fee_msat) { @@ -284,10 +386,12 @@ 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, channel_id, + trust_model: trust_model.clone(), }; Ok(forward_payment) } else { @@ -310,6 +414,7 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, + trust_model, } => { if let Some((_payment_hash, htlcs)) = payment_queue.pop_greater_than_msat(*opening_fee_msat) @@ -322,6 +427,7 @@ impl OutboundJITChannelState { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, + trust_model: trust_model.clone(), }; Ok(Some(forward_payment)) } else { @@ -329,6 +435,7 @@ impl OutboundJITChannelState { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, + trust_model: trust_model.clone(), }; Ok(None) } @@ -337,16 +444,21 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, + trust_model, } => { *self = OutboundJITChannelState::PendingPayment { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, + trust_model: trust_model.clone(), }; Ok(None) }, - OutboundJITChannelState::PaymentForwarded { channel_id } => { - *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + OutboundJITChannelState::PaymentForwarded { channel_id, trust_model } => { + *self = OutboundJITChannelState::PaymentForwarded { + channel_id: *channel_id, + trust_model: trust_model.clone(), + }; Ok(None) }, state => Err(ChannelStateError(format!( @@ -359,15 +471,26 @@ impl OutboundJITChannelState { fn payment_forwarded(&mut self) -> Result, ChannelStateError> { match self { OutboundJITChannelState::PendingPaymentForward { - payment_queue, channel_id, .. + payment_queue, + channel_id, + trust_model, + .. } => { let mut payment_queue = core::mem::take(payment_queue); let forward_htlcs = ForwardHTLCsAction(*channel_id, payment_queue.clear()); - *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + trust_model.set_htlc_claimed(true); + *self = OutboundJITChannelState::PaymentForwarded { + channel_id: *channel_id, + trust_model: trust_model.clone(), + }; Ok(Some(forward_htlcs)) }, - OutboundJITChannelState::PaymentForwarded { channel_id } => { - *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + OutboundJITChannelState::PaymentForwarded { channel_id, trust_model } => { + trust_model.set_htlc_claimed(true); + *self = OutboundJITChannelState::PaymentForwarded { + channel_id: *channel_id, + trust_model: trust_model.clone(), + }; Ok(None) }, state => Err(ChannelStateError(format!( @@ -376,6 +499,66 @@ impl OutboundJITChannelState { ))), } } + + fn store_funding_transaction( + &mut self, funding_tx: Transaction, + ) -> Result<(), ChannelStateError> { + match self { + OutboundJITChannelState::PendingChannelOpen { trust_model, .. } + | OutboundJITChannelState::PaymentForwarded { trust_model, .. } + | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } + | OutboundJITChannelState::PendingPayment { trust_model, .. } => { + trust_model.set_funding_tx(funding_tx); + + Ok(()) + }, + state => Err(ChannelStateError(format!( + "Store funding transaction when JIT Channel was in state: {:?}", + state + ))), + } + } + + fn store_funding_transaction_broadcast_safe( + &mut self, funding_tx_broadcast_safe: bool, + ) -> Result<(), ChannelStateError> { + match self { + OutboundJITChannelState::PendingChannelOpen { trust_model, .. } + | OutboundJITChannelState::PaymentForwarded { trust_model, .. } + | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } + | OutboundJITChannelState::PendingPayment { trust_model, .. } => { + trust_model.set_funding_tx_broadcast_safe(funding_tx_broadcast_safe); + + Ok(()) + }, + state => Err(ChannelStateError(format!( + "Store funding transaction broadcast safe when JIT Channel was in state: {:?}", + state + ))), + } + } + + fn should_broadcast_funding_transaction(&self) -> bool { + match self { + OutboundJITChannelState::PendingChannelOpen { trust_model, .. } + | OutboundJITChannelState::PaymentForwarded { trust_model, .. } + | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } + | OutboundJITChannelState::PendingPayment { trust_model, .. } => trust_model.should_broadcast(), + OutboundJITChannelState::PendingInitialPayment { .. } => false, + } + } + + fn client_trusts_lsp(&self) -> bool { + match self { + OutboundJITChannelState::PendingChannelOpen { trust_model, .. } + | OutboundJITChannelState::PaymentForwarded { trust_model, .. } + | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } + | OutboundJITChannelState::PendingPayment { trust_model, .. } => { + matches!(trust_model, TrustModel::ClientTrustsLsp { .. }) + }, + OutboundJITChannelState::PendingInitialPayment { .. } => false, + } + } } struct OutboundJITChannel { @@ -383,26 +566,32 @@ struct OutboundJITChannel { user_channel_id: u128, opening_fee_params: LSPS2OpeningFeeParams, payment_size_msat: Option, + client_trusts_lsp: bool, } 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, + client_trusts_lsp, } } fn htlc_intercepted( &mut self, htlc: InterceptedHTLC, ) -> Result, LightningError> { - let action = - self.state.htlc_intercepted(&self.opening_fee_params, &self.payment_size_msat, htlc)?; + let action = self.state.htlc_intercepted( + &self.opening_fee_params, + &self.payment_size_msat, + htlc, + self.client_trusts_lsp, + )?; Ok(action) } @@ -433,6 +622,38 @@ 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) -> Result<(), LightningError> { + self.state + .store_funding_transaction(funding_tx) + .map_err(|e| LightningError::from(ChannelStateError(e.0))) + } + + fn set_funding_tx_broadcast_safe( + &mut self, funding_tx_broadcast_safe: bool, + ) -> Result<(), LightningError> { + self.state + .store_funding_transaction_broadcast_safe(funding_tx_broadcast_safe) + .map_err(|e| LightningError::from(ChannelStateError(e.0))) + } + + fn should_broadcast_funding_transaction(&self) -> bool { + self.state.should_broadcast_funding_transaction() + } + + fn get_funding_tx(&self) -> Option { + match &self.state { + OutboundJITChannelState::PendingChannelOpen { trust_model, .. } + | OutboundJITChannelState::PaymentForwarded { trust_model, .. } + | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } + | OutboundJITChannelState::PendingPayment { trust_model, .. } => trust_model.get_funding_tx(), + _ => None, + } + } + + fn client_trusts_lsp(&self) -> bool { + self.state.client_trusts_lsp() + } } struct PeerState { @@ -698,6 +919,7 @@ where buy_request.payment_size_msat, buy_request.opening_fee_params, user_channel_id, + client_trusts_lsp, ); peer_state_lock @@ -926,12 +1148,14 @@ 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.broadcast_transaction_if_applies(&jit_channel); } } else { return Err(APIError::APIMisuseError { @@ -1418,6 +1642,125 @@ 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) + .map_err(|e| APIError::APIMisuseError { err: e.err.to_string() })?; + + self.broadcast_transaction_if_applies(jit_channel); + Ok(()) + } + + /// Called by ldk-node 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) + .map_err(|e| APIError::APIMisuseError { err: e.err.to_string() })?; + + self.broadcast_transaction_if_applies(jit_channel); + Ok(()) + } + + fn broadcast_transaction_if_applies(&self, jit_channel: &OutboundJITChannel) { + if jit_channel.should_broadcast_funding_transaction() { + let funding_tx = jit_channel.get_funding_tx(); + + if let Some(funding_tx) = funding_tx { + self.channel_manager.get_cm().unsafe_broadcast_transaction(&funding_tx); + } + } + } } impl LSPSProtocolMessageHandler for LSPS2ServiceHandler @@ -1612,6 +1955,7 @@ mod tests { expected_outbound_amount_msat: 200_000_000, payment_hash: PaymentHash([100; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingInitialPayment { .. })); @@ -1628,6 +1972,7 @@ mod tests { expected_outbound_amount_msat: 1_000_000, payment_hash: PaymentHash([101; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingInitialPayment { .. })); @@ -1645,6 +1990,7 @@ mod tests { expected_outbound_amount_msat: 300_000_000, payment_hash: PaymentHash([100; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingChannelOpen { .. })); @@ -1683,6 +2029,7 @@ mod tests { expected_outbound_amount_msat: 2_000_000, payment_hash: PaymentHash([102; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingPaymentForward { .. })); @@ -1706,6 +2053,7 @@ mod tests { expected_outbound_amount_msat: 500_000_000, payment_hash: PaymentHash([101; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingPaymentForward { .. })); @@ -1762,6 +2110,7 @@ mod tests { expected_outbound_amount_msat: 200_000_000, payment_hash: PaymentHash([103; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PaymentForwarded { .. })); @@ -1796,6 +2145,7 @@ mod tests { expected_outbound_amount_msat: 500_000_000, payment_hash: PaymentHash([100; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingChannelOpen { .. })); @@ -1812,6 +2162,7 @@ mod tests { expected_outbound_amount_msat: 600_000_000, payment_hash: PaymentHash([101; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingChannelOpen { .. })); @@ -1843,6 +2194,7 @@ mod tests { expected_outbound_amount_msat: 500_000_000, payment_hash: PaymentHash([102; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingPaymentForward { .. })); @@ -1897,6 +2249,7 @@ mod tests { expected_outbound_amount_msat: 200_000_000, payment_hash: PaymentHash([103; 32]), }, + false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PaymentForwarded { .. })); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 9d1c6292826..a2b3d5b953b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -995,6 +995,11 @@ enum FundingType { /// scenario could be when constructing the funding transaction as part of a Payjoin /// transaction. Unchecked(OutPoint), + /// 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), } impl FundingType { @@ -1002,6 +1007,7 @@ impl FundingType { match self { FundingType::Checked(tx) => tx.compute_txid(), FundingType::Unchecked(outp) => outp.txid, + FundingType::CheckedManualBroadcast(tx) => tx.compute_txid(), } } @@ -1014,6 +1020,7 @@ impl FundingType { input: Vec::new(), output: Vec::new(), }, + FundingType::CheckedManualBroadcast(tx) => tx.clone(), } } @@ -1021,6 +1028,7 @@ impl FundingType { match self { FundingType::Checked(_) => false, FundingType::Unchecked(_) => true, + FundingType::CheckedManualBroadcast(_) => true, } } } @@ -5779,6 +5787,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 @@ -5860,7 +5880,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() { @@ -8299,7 +8319,8 @@ where ComplFunc: FnOnce( Option, bool, - ) -> (Option, Option), + ) + -> (Option, Option), >( &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -11543,6 +11564,20 @@ 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). + /// + /// # Warning + /// Improper use of this method could lead to channel state inconsistencies. + /// Ensure the transaction being broadcast is valid and expected by LDK. + pub fn unsafe_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. From 9c5ca9ee92622870f2be3ce2839e862ddb387cf1 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 23 Jul 2025 09:26:38 -0300 Subject: [PATCH 2/7] fixup: Address comments. Mainly refactor to avoid all those clones --- lightning-liquidity/src/lsps2/service.rs | 256 +++++++---------------- lightning/src/ln/channelmanager.rs | 25 +-- lightning/src/routing/router.rs | 2 +- 3 files changed, 89 insertions(+), 194 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 50552fd1158..9f31c1df139 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -11,7 +11,6 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; -use bitcoin::Transaction; use core::cmp::Ordering as CmpOrdering; use core::ops::Deref; @@ -24,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, @@ -43,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; @@ -112,7 +111,7 @@ struct ForwardHTLCsAction(ChannelId, Vec); enum TrustModel { ClientTrustsLsp { funding_tx_broadcast_safe: bool, - htlc_claimed: bool, + payment_claimed: bool, funding_tx: Option, }, LspTrustsClient, @@ -121,23 +120,25 @@ enum TrustModel { impl TrustModel { fn should_broadcast(&self) -> bool { match self { - TrustModel::ClientTrustsLsp { funding_tx_broadcast_safe, htlc_claimed, funding_tx } => { - *funding_tx_broadcast_safe && *htlc_claimed && funding_tx.is_some() - }, - TrustModel::LspTrustsClient => false, + TrustModel::ClientTrustsLsp { + funding_tx_broadcast_safe, + payment_claimed, + funding_tx, + } => *funding_tx_broadcast_safe && *payment_claimed && funding_tx.is_some(), + TrustModel::LspTrustsClient => true, } } fn new(client_trusts_lsp: bool) -> Self { if client_trusts_lsp { - return TrustModel::ClientTrustsLsp { + TrustModel::ClientTrustsLsp { funding_tx_broadcast_safe: false, - htlc_claimed: false, + payment_claimed: false, funding_tx: None, - }; + } } else { - return TrustModel::LspTrustsClient; - }; + TrustModel::LspTrustsClient + } } fn set_funding_tx(&mut self, funding_tx: Transaction) { @@ -162,10 +163,10 @@ impl TrustModel { } } - fn set_htlc_claimed(&mut self, htlc_claimed: bool) { + fn set_payment_claimed(&mut self, payment_claimed: bool) { match self { - TrustModel::ClientTrustsLsp { htlc_claimed: claimed, .. } => { - *claimed = htlc_claimed; + TrustModel::ClientTrustsLsp { payment_claimed: claimed, .. } => { + *claimed = payment_claimed; }, TrustModel::LspTrustsClient => { // No-op @@ -189,11 +190,7 @@ enum OutboundJITChannelState { PendingInitialPayment { payment_queue: PaymentQueue }, /// An initial payment of sufficient size was intercepted to the JIT channel SCID, triggering the /// opening of the channel. We are awaiting the completion of the channel establishment. - PendingChannelOpen { - payment_queue: PaymentQueue, - opening_fee_msat: u64, - trust_model: TrustModel, - }, + PendingChannelOpen { payment_queue: PaymentQueue, opening_fee_msat: u64 }, /// The channel is open and a payment was forwarded while skimming the JIT channel fee. /// No further payments can be forwarded until the pending payment succeeds or fails, as we need /// to know whether the JIT channel fee needs to be skimmed from a next payment or not. @@ -201,21 +198,15 @@ enum OutboundJITChannelState { payment_queue: PaymentQueue, opening_fee_msat: u64, channel_id: ChannelId, - trust_model: TrustModel, }, /// The channel is open, no payment is currently being forwarded, and the JIT channel fee still /// needs to be paid. This state can occur when the initial payment fails, e.g. due to a /// prepayment probe. We are awaiting a next payment of sufficient size to forward and skim the /// JIT channel fee. - PendingPayment { - payment_queue: PaymentQueue, - opening_fee_msat: u64, - channel_id: ChannelId, - trust_model: TrustModel, - }, + PendingPayment { payment_queue: PaymentQueue, opening_fee_msat: u64, channel_id: ChannelId }, /// The channel is open and a payment was successfully forwarded while skimming the JIT channel /// fee. Any subsequent HTLCs can be forwarded without additional logic. - PaymentForwarded { channel_id: ChannelId, trust_model: TrustModel }, + PaymentForwarded { channel_id: ChannelId }, } impl OutboundJITChannelState { @@ -225,7 +216,7 @@ impl OutboundJITChannelState { fn htlc_intercepted( &mut self, opening_fee_params: &LSPS2OpeningFeeParams, payment_size_msat: &Option, - htlc: InterceptedHTLC, client_trusts_lsp: bool, + htlc: InterceptedHTLC, ) -> Result, ChannelStateError> { match self { OutboundJITChannelState::PendingInitialPayment { payment_queue } => { @@ -277,7 +268,6 @@ impl OutboundJITChannelState { *self = OutboundJITChannelState::PendingChannelOpen { payment_queue: core::mem::take(payment_queue), opening_fee_msat, - trust_model: TrustModel::new(client_trusts_lsp), }; let open_channel = HTLCInterceptedAction::OpenChannel(OpenChannelParams { opening_fee_msat, @@ -297,17 +287,12 @@ impl OutboundJITChannelState { } } }, - OutboundJITChannelState::PendingChannelOpen { - payment_queue, - opening_fee_msat, - trust_model, - } => { + OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat } => { let mut payment_queue = core::mem::take(payment_queue); payment_queue.add_htlc(htlc); *self = OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat: *opening_fee_msat, - trust_model: trust_model.clone(), }; Ok(None) }, @@ -315,7 +300,6 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, - trust_model, } => { let mut payment_queue = core::mem::take(payment_queue); payment_queue.add_htlc(htlc); @@ -323,7 +307,6 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, - trust_model: trust_model.clone(), }; Ok(None) }, @@ -331,7 +314,6 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, - trust_model, } => { let mut payment_queue = core::mem::take(payment_queue); payment_queue.add_htlc(htlc); @@ -346,7 +328,6 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, - trust_model: trust_model.clone(), }; Ok(Some(forward_payment)) } else { @@ -354,17 +335,13 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, - trust_model: trust_model.clone(), }; Ok(None) } }, - OutboundJITChannelState::PaymentForwarded { channel_id, trust_model } => { + OutboundJITChannelState::PaymentForwarded { channel_id } => { let forward = HTLCInterceptedAction::ForwardHTLC(*channel_id); - *self = OutboundJITChannelState::PaymentForwarded { - channel_id: *channel_id, - trust_model: trust_model.clone(), - }; + *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; Ok(Some(forward)) }, } @@ -374,11 +351,7 @@ impl OutboundJITChannelState { &mut self, channel_id: ChannelId, ) -> Result { match self { - OutboundJITChannelState::PendingChannelOpen { - payment_queue, - opening_fee_msat, - trust_model, - } => { + OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat } => { if let Some((_payment_hash, htlcs)) = payment_queue.pop_greater_than_msat(*opening_fee_msat) { @@ -391,7 +364,6 @@ impl OutboundJITChannelState { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id, - trust_model: trust_model.clone(), }; Ok(forward_payment) } else { @@ -414,7 +386,6 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, - trust_model, } => { if let Some((_payment_hash, htlcs)) = payment_queue.pop_greater_than_msat(*opening_fee_msat) @@ -427,7 +398,6 @@ impl OutboundJITChannelState { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, - trust_model: trust_model.clone(), }; Ok(Some(forward_payment)) } else { @@ -435,7 +405,6 @@ impl OutboundJITChannelState { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, - trust_model: trust_model.clone(), }; Ok(None) } @@ -444,21 +413,16 @@ impl OutboundJITChannelState { payment_queue, opening_fee_msat, channel_id, - trust_model, } => { *self = OutboundJITChannelState::PendingPayment { payment_queue: core::mem::take(payment_queue), opening_fee_msat: *opening_fee_msat, channel_id: *channel_id, - trust_model: trust_model.clone(), }; Ok(None) }, - OutboundJITChannelState::PaymentForwarded { channel_id, trust_model } => { - *self = OutboundJITChannelState::PaymentForwarded { - channel_id: *channel_id, - trust_model: trust_model.clone(), - }; + OutboundJITChannelState::PaymentForwarded { channel_id } => { + *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; Ok(None) }, state => Err(ChannelStateError(format!( @@ -471,26 +435,15 @@ impl OutboundJITChannelState { fn payment_forwarded(&mut self) -> Result, ChannelStateError> { match self { OutboundJITChannelState::PendingPaymentForward { - payment_queue, - channel_id, - trust_model, - .. + payment_queue, channel_id, .. } => { let mut payment_queue = core::mem::take(payment_queue); let forward_htlcs = ForwardHTLCsAction(*channel_id, payment_queue.clear()); - trust_model.set_htlc_claimed(true); - *self = OutboundJITChannelState::PaymentForwarded { - channel_id: *channel_id, - trust_model: trust_model.clone(), - }; + *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; Ok(Some(forward_htlcs)) }, - OutboundJITChannelState::PaymentForwarded { channel_id, trust_model } => { - trust_model.set_htlc_claimed(true); - *self = OutboundJITChannelState::PaymentForwarded { - channel_id: *channel_id, - trust_model: trust_model.clone(), - }; + OutboundJITChannelState::PaymentForwarded { channel_id } => { + *self = OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; Ok(None) }, state => Err(ChannelStateError(format!( @@ -499,66 +452,6 @@ impl OutboundJITChannelState { ))), } } - - fn store_funding_transaction( - &mut self, funding_tx: Transaction, - ) -> Result<(), ChannelStateError> { - match self { - OutboundJITChannelState::PendingChannelOpen { trust_model, .. } - | OutboundJITChannelState::PaymentForwarded { trust_model, .. } - | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } - | OutboundJITChannelState::PendingPayment { trust_model, .. } => { - trust_model.set_funding_tx(funding_tx); - - Ok(()) - }, - state => Err(ChannelStateError(format!( - "Store funding transaction when JIT Channel was in state: {:?}", - state - ))), - } - } - - fn store_funding_transaction_broadcast_safe( - &mut self, funding_tx_broadcast_safe: bool, - ) -> Result<(), ChannelStateError> { - match self { - OutboundJITChannelState::PendingChannelOpen { trust_model, .. } - | OutboundJITChannelState::PaymentForwarded { trust_model, .. } - | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } - | OutboundJITChannelState::PendingPayment { trust_model, .. } => { - trust_model.set_funding_tx_broadcast_safe(funding_tx_broadcast_safe); - - Ok(()) - }, - state => Err(ChannelStateError(format!( - "Store funding transaction broadcast safe when JIT Channel was in state: {:?}", - state - ))), - } - } - - fn should_broadcast_funding_transaction(&self) -> bool { - match self { - OutboundJITChannelState::PendingChannelOpen { trust_model, .. } - | OutboundJITChannelState::PaymentForwarded { trust_model, .. } - | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } - | OutboundJITChannelState::PendingPayment { trust_model, .. } => trust_model.should_broadcast(), - OutboundJITChannelState::PendingInitialPayment { .. } => false, - } - } - - fn client_trusts_lsp(&self) -> bool { - match self { - OutboundJITChannelState::PendingChannelOpen { trust_model, .. } - | OutboundJITChannelState::PaymentForwarded { trust_model, .. } - | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } - | OutboundJITChannelState::PendingPayment { trust_model, .. } => { - matches!(trust_model, TrustModel::ClientTrustsLsp { .. }) - }, - OutboundJITChannelState::PendingInitialPayment { .. } => false, - } - } } struct OutboundJITChannel { @@ -567,6 +460,7 @@ struct OutboundJITChannel { opening_fee_params: LSPS2OpeningFeeParams, payment_size_msat: Option, client_trusts_lsp: bool, + trust_model: Option, } impl OutboundJITChannel { @@ -580,18 +474,22 @@ impl OutboundJITChannel { opening_fee_params, payment_size_msat, client_trusts_lsp, + trust_model: None, } } fn htlc_intercepted( &mut self, htlc: InterceptedHTLC, ) -> Result, LightningError> { - let action = self.state.htlc_intercepted( - &self.opening_fee_params, - &self.payment_size_msat, - htlc, - self.client_trusts_lsp, - )?; + let was_initial = + matches!(self.state, OutboundJITChannelState::PendingInitialPayment { .. }); + let action = + self.state.htlc_intercepted(&self.opening_fee_params, &self.payment_size_msat, htlc)?; + if was_initial && self.trust_model.is_none() { + if !matches!(self.state, OutboundJITChannelState::PendingInitialPayment { .. }) { + self.trust_model = Some(TrustModel::new(self.client_trusts_lsp)); + } + } Ok(action) } @@ -609,6 +507,11 @@ impl OutboundJITChannel { fn payment_forwarded(&mut self) -> Result, LightningError> { let action = self.state.payment_forwarded()?; + if action.is_some() { + if let Some(tm) = &mut self.trust_model { + tm.set_payment_claimed(true); + } + } Ok(action) } @@ -624,35 +527,42 @@ impl OutboundJITChannel { } fn set_funding_tx(&mut self, funding_tx: Transaction) -> Result<(), LightningError> { - self.state - .store_funding_transaction(funding_tx) - .map_err(|e| LightningError::from(ChannelStateError(e.0))) + if let Some(tm) = &mut self.trust_model { + tm.set_funding_tx(funding_tx); + Ok(()) + } else { + Err(LightningError::from(ChannelStateError( + "Store funding transaction when JIT Channel was in invalid state".to_string(), + ))) + } } fn set_funding_tx_broadcast_safe( &mut self, funding_tx_broadcast_safe: bool, ) -> Result<(), LightningError> { - self.state - .store_funding_transaction_broadcast_safe(funding_tx_broadcast_safe) - .map_err(|e| LightningError::from(ChannelStateError(e.0))) + if let Some(tm) = &mut self.trust_model { + tm.set_funding_tx_broadcast_safe(funding_tx_broadcast_safe); + Ok(()) + } else { + Err(LightningError::from(ChannelStateError( + "Store funding transaction broadcast safe when JIT Channel was in invalid state" + .to_string(), + ))) + } } fn should_broadcast_funding_transaction(&self) -> bool { - self.state.should_broadcast_funding_transaction() + self.trust_model.as_ref().map_or(false, |tm| tm.should_broadcast()) } fn get_funding_tx(&self) -> Option { - match &self.state { - OutboundJITChannelState::PendingChannelOpen { trust_model, .. } - | OutboundJITChannelState::PaymentForwarded { trust_model, .. } - | OutboundJITChannelState::PendingPaymentForward { trust_model, .. } - | OutboundJITChannelState::PendingPayment { trust_model, .. } => trust_model.get_funding_tx(), - _ => None, - } + self.trust_model.as_ref().and_then(|tm| tm.get_funding_tx()) } fn client_trusts_lsp(&self) -> bool { - self.state.client_trusts_lsp() + self.trust_model + .as_ref() + .map_or(false, |tm| matches!(tm, TrustModel::ClientTrustsLsp { .. })) } } @@ -1714,7 +1624,7 @@ where Ok(()) } - /// Called by ldk-node when the funding transaction is safe to broadcast. + /// 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, @@ -1757,7 +1667,7 @@ where let funding_tx = jit_channel.get_funding_tx(); if let Some(funding_tx) = funding_tx { - self.channel_manager.get_cm().unsafe_broadcast_transaction(&funding_tx); + self.channel_manager.get_cm().broadcast_transaction(&funding_tx); } } } @@ -1955,7 +1865,6 @@ mod tests { expected_outbound_amount_msat: 200_000_000, payment_hash: PaymentHash([100; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingInitialPayment { .. })); @@ -1972,7 +1881,6 @@ mod tests { expected_outbound_amount_msat: 1_000_000, payment_hash: PaymentHash([101; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingInitialPayment { .. })); @@ -1990,7 +1898,6 @@ mod tests { expected_outbound_amount_msat: 300_000_000, payment_hash: PaymentHash([100; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingChannelOpen { .. })); @@ -2029,7 +1936,6 @@ mod tests { expected_outbound_amount_msat: 2_000_000, payment_hash: PaymentHash([102; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingPaymentForward { .. })); @@ -2053,7 +1959,6 @@ mod tests { expected_outbound_amount_msat: 500_000_000, payment_hash: PaymentHash([101; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingPaymentForward { .. })); @@ -2110,7 +2015,6 @@ mod tests { expected_outbound_amount_msat: 200_000_000, payment_hash: PaymentHash([103; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PaymentForwarded { .. })); @@ -2145,7 +2049,6 @@ mod tests { expected_outbound_amount_msat: 500_000_000, payment_hash: PaymentHash([100; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingChannelOpen { .. })); @@ -2162,7 +2065,6 @@ mod tests { expected_outbound_amount_msat: 600_000_000, payment_hash: PaymentHash([101; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingChannelOpen { .. })); @@ -2194,7 +2096,6 @@ mod tests { expected_outbound_amount_msat: 500_000_000, payment_hash: PaymentHash([102; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PendingPaymentForward { .. })); @@ -2249,7 +2150,6 @@ mod tests { expected_outbound_amount_msat: 200_000_000, payment_hash: PaymentHash([103; 32]), }, - false, ) .unwrap(); assert!(matches!(state, OutboundJITChannelState::PaymentForwarded { .. })); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a2b3d5b953b..27f41b51107 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -987,6 +987,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. /// @@ -995,40 +1000,35 @@ enum FundingType { /// scenario could be when constructing the funding transaction as part of a Payjoin /// transaction. Unchecked(OutPoint), - /// 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), } impl FundingType { fn txid(&self) -> Txid { match self { FundingType::Checked(tx) => tx.compute_txid(), - FundingType::Unchecked(outp) => outp.txid, FundingType::CheckedManualBroadcast(tx) => tx.compute_txid(), + FundingType::Unchecked(outp) => outp.txid, } } 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, input: Vec::new(), output: Vec::new(), }, - FundingType::CheckedManualBroadcast(tx) => tx.clone(), } } fn is_manual_broadcast(&self) -> bool { match self { FundingType::Checked(_) => false, - FundingType::Unchecked(_) => true, FundingType::CheckedManualBroadcast(_) => true, + FundingType::Unchecked(_) => true, } } } @@ -8319,8 +8319,7 @@ where ComplFunc: FnOnce( Option, bool, - ) - -> (Option, Option), + ) -> (Option, Option), >( &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option, attribution_data: Option, @@ -11568,11 +11567,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// /// This method should only be used in specific scenarios where manual control /// over transaction broadcast timing is required (e.g., LSPS2 workflows). - /// - /// # Warning - /// Improper use of this method could lead to channel state inconsistencies. - /// Ensure the transaction being broadcast is valid and expected by LDK. - pub fn unsafe_broadcast_transaction(&self, tx: &Transaction) { + 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]); diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 9d3093c5a90..f34e4578284 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. /// From d5d8f929cc92ff49cfddb29b3c1da337d7697d18 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 23 Jul 2025 10:46:54 -0300 Subject: [PATCH 3/7] tests wip --- lightning-liquidity/tests/common/mod.rs | 4 +- .../tests/lsps0_integration_tests.rs | 2 +- .../tests/lsps2_integration_tests.rs | 317 +++++++++++++++++- 3 files changed, 309 insertions(+), 14 deletions(-) diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index 013378f2cb0..9df48db5a3a 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -17,6 +17,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>( @@ -52,8 +53,9 @@ pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>( let mut iter = nodes.into_iter(); let service_node = LiquidityNode::new(iter.next().unwrap(), service_lm); let client_node = LiquidityNode::new(iter.next().unwrap(), client_lm); + let payer_node_optional = iter.next(); - LSPSNodes { service_node, client_node } + LSPSNodes { service_node, client_node, payer_node_optional } } 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 423d49785f2..29b9c6091ea 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -60,7 +60,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 } = create_service_and_client_nodes( + let LSPSNodes { service_node, client_node , ..} = create_service_and_client_nodes( nodes, service_config, client_config, diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 6ea42e17532..b10f6ada9cd 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -4,6 +4,19 @@ 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::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::do_commitment_signed_dance; +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_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::ser::LSPSDateTime; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; @@ -84,10 +97,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, @@ -118,11 +135,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] @@ -132,7 +154,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(); @@ -280,7 +302,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(); @@ -415,7 +437,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(); @@ -441,7 +463,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(); @@ -525,7 +547,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(); @@ -550,7 +572,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(); @@ -605,7 +627,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(); @@ -734,7 +756,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(); @@ -813,7 +835,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(); @@ -883,3 +905,274 @@ 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 node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[Some(service_node_config), None, 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 (_, _, _, channel_id, _) = create_chan_between_nodes_with_value( + &service_node.inner, + &client_node.inner, + *expected_outbound_amount_msat, + 0, + ); + + 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), + }; + + client_node.inner.node.claim_funds(preimage.unwrap()); + + 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); +} From f4c46e365f77433e5fd356de7d1542d7dd74864b Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 23 Jul 2025 15:35:53 -0300 Subject: [PATCH 4/7] fixup: refactor wip and e2e test wip --- lightning-liquidity/src/lsps2/event.rs | 12 ++ lightning-liquidity/src/lsps2/service.rs | 91 ++++------ lightning-liquidity/tests/common/mod.rs | 3 +- .../tests/lsps2_integration_tests.rs | 163 +++++++++++++++++- lightning/src/ln/functional_test_utils.rs | 3 +- 5 files changed, 209 insertions(+), 63 deletions(-) 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 9f31c1df139..cafe2568a50 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -118,14 +118,15 @@ enum TrustModel { } impl TrustModel { - fn should_broadcast(&self) -> bool { + 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(), - TrustModel::LspTrustsClient => true, + // in lsp-trusts-client, the broadcast is automatic, so we never need to manually broadcast. + TrustModel::LspTrustsClient => false, } } @@ -180,6 +181,13 @@ impl TrustModel { 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. @@ -459,8 +467,7 @@ struct OutboundJITChannel { user_channel_id: u128, opening_fee_params: LSPS2OpeningFeeParams, payment_size_msat: Option, - client_trusts_lsp: bool, - trust_model: Option, + trust_model: TrustModel, } impl OutboundJITChannel { @@ -473,23 +480,15 @@ impl OutboundJITChannel { state: OutboundJITChannelState::new(), opening_fee_params, payment_size_msat, - client_trusts_lsp, - trust_model: None, + trust_model: TrustModel::new(client_trusts_lsp), } } fn htlc_intercepted( &mut self, htlc: InterceptedHTLC, ) -> Result, LightningError> { - let was_initial = - matches!(self.state, OutboundJITChannelState::PendingInitialPayment { .. }); let action = self.state.htlc_intercepted(&self.opening_fee_params, &self.payment_size_msat, htlc)?; - if was_initial && self.trust_model.is_none() { - if !matches!(self.state, OutboundJITChannelState::PendingInitialPayment { .. }) { - self.trust_model = Some(TrustModel::new(self.client_trusts_lsp)); - } - } Ok(action) } @@ -508,9 +507,7 @@ impl OutboundJITChannel { fn payment_forwarded(&mut self) -> Result, LightningError> { let action = self.state.payment_forwarded()?; if action.is_some() { - if let Some(tm) = &mut self.trust_model { - tm.set_payment_claimed(true); - } + self.trust_model.set_payment_claimed(true); } Ok(action) } @@ -526,43 +523,24 @@ impl OutboundJITChannel { self.is_pending_initial_payment() && is_expired } - fn set_funding_tx(&mut self, funding_tx: Transaction) -> Result<(), LightningError> { - if let Some(tm) = &mut self.trust_model { - tm.set_funding_tx(funding_tx); - Ok(()) - } else { - Err(LightningError::from(ChannelStateError( - "Store funding transaction when JIT Channel was in invalid state".to_string(), - ))) - } + 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, - ) -> Result<(), LightningError> { - if let Some(tm) = &mut self.trust_model { - tm.set_funding_tx_broadcast_safe(funding_tx_broadcast_safe); - Ok(()) - } else { - Err(LightningError::from(ChannelStateError( - "Store funding transaction broadcast safe when JIT Channel was in invalid state" - .to_string(), - ))) - } + 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.as_ref().map_or(false, |tm| tm.should_broadcast()) + self.trust_model.should_manually_broadcast() } fn get_funding_tx(&self) -> Option { - self.trust_model.as_ref().and_then(|tm| tm.get_funding_tx()) + self.trust_model.get_funding_tx() } fn client_trusts_lsp(&self) -> bool { - self.trust_model - .as_ref() - .map_or(false, |tm| matches!(tm, TrustModel::ClientTrustsLsp { .. })) + self.trust_model.is_client_trusts_lsp() } } @@ -1065,7 +1043,10 @@ where }, } - self.broadcast_transaction_if_applies(&jit_channel); + self.emit_broadcast_funding_transaction_event_if_applies( + jit_channel, + counterparty_node_id, + ); } } else { return Err(APIError::APIMisuseError { @@ -1616,11 +1597,9 @@ where ), })?; - jit_channel - .set_funding_tx(funding_tx) - .map_err(|e| APIError::APIMisuseError { err: e.err.to_string() })?; + jit_channel.set_funding_tx(funding_tx); - self.broadcast_transaction_if_applies(jit_channel); + self.emit_broadcast_funding_transaction_event_if_applies(jit_channel, counterparty_node_id); Ok(()) } @@ -1654,20 +1633,26 @@ where ), })?; - jit_channel - .set_funding_tx_broadcast_safe(true) - .map_err(|e| APIError::APIMisuseError { err: e.err.to_string() })?; + jit_channel.set_funding_tx_broadcast_safe(true); - self.broadcast_transaction_if_applies(jit_channel); + self.emit_broadcast_funding_transaction_event_if_applies(jit_channel, counterparty_node_id); Ok(()) } - fn broadcast_transaction_if_applies(&self, jit_channel: &OutboundJITChannel) { + 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 { - self.channel_manager.get_cm().broadcast_transaction(&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); } } } diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index 9df48db5a3a..27d3f3f911f 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -53,9 +53,8 @@ pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>( let mut iter = nodes.into_iter(); let service_node = LiquidityNode::new(iter.next().unwrap(), service_lm); let client_node = LiquidityNode::new(iter.next().unwrap(), client_lm); - let payer_node_optional = iter.next(); - LSPSNodes { service_node, client_node, payer_node_optional } + LSPSNodes { service_node, client_node, payer_node_optional: iter.next() } } pub(crate) struct LiquidityNode<'a, 'b, 'c> { diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index b10f6ada9cd..7fc0dcfea95 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -6,10 +6,14 @@ use common::{create_service_and_client_nodes, get_lsps_message, LSPSNodes, Liqui 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; @@ -17,6 +21,8 @@ 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; @@ -912,8 +918,13 @@ fn full_lsps2_flow() { 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 node_chanmgrs = - create_node_chanmgrs(3, &node_cfgs, &[Some(service_node_config), None, None]); + 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; @@ -1123,11 +1134,17 @@ fn full_lsps2_flow() { other => panic!("Expected OpenChannel event, got: {:?}", other), }; - let (_, _, _, channel_id, _) = create_chan_between_nodes_with_value( - &service_node.inner, - &client_node.inner, - *expected_outbound_amount_msat, - 0, + 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(); @@ -1166,8 +1183,14 @@ fn full_lsps2_flow() { 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]]; @@ -1176,3 +1199,129 @@ fn full_lsps2_flow() { 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/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 88bdb6d1d08..d1c458fa714 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(); } @@ -3010,6 +3010,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, .. } => { From 7f6b00ede8bdca6a7598bf52a73c7b18f3a84d33 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 23 Jul 2025 16:14:44 -0300 Subject: [PATCH 5/7] wip --- .../tests/lsps2_integration_tests.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 7fc0dcfea95..20fd484e9fc 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -1183,20 +1183,32 @@ fn full_lsps2_flow() { other => panic!("Expected PaymentClaimable event on client, got: {:?}", other), }; + // Check that before the client claims, the service node has not broadcasted anything 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); + service_handler.payment_forwarded(channel_id).unwrap(); + + match service_node.liquidity_manager.next_event().unwrap() { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BroadcastFundingTransaction { + counterparty_node_id, + user_channel_id: uid, + .. + }) => { + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(uid, user_channel_id); + }, + other => panic!("Unexpected event: {:?}", other), + } + expect_payment_sent(&payer_node, preimage.unwrap(), Some(Some(total_fee_msat)), true, true); } From 49cc467e52a84fb08f56009494f5f4ce977f9012 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 23 Jul 2025 18:10:35 -0300 Subject: [PATCH 6/7] wip --- lightning-liquidity/tests/lsps2_integration_tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 20fd484e9fc..bdb663aec90 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -1188,7 +1188,7 @@ fn full_lsps2_flow() { assert!(events.is_empty(), "Expected no events from service node, got: {:?}", events); client_node.inner.node.claim_funds(preimage.unwrap()); - + /// TODO SIMPLIFY: put the payment_forwarded call inside a PaymentForwardedEvent let expected_paths: &[&[&lightning::ln::functional_test_utils::Node<'_, '_, '_>]] = &[&[&service_node.inner, &client_node.inner]]; @@ -1196,7 +1196,7 @@ fn full_lsps2_flow() { let total_fee_msat = pass_claimed_payment_along_route(args); service_handler.payment_forwarded(channel_id).unwrap(); - + /// match service_node.liquidity_manager.next_event().unwrap() { LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BroadcastFundingTransaction { counterparty_node_id, From 82a4bc11b0f2cb98ddbbd4a7d705dcdcc429f59c Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Tue, 12 Aug 2025 15:04:34 -0300 Subject: [PATCH 7/7] f: fix conflicts --- lightning-liquidity/src/lsps2/service.rs | 24 +++---- lightning-liquidity/tests/common/mod.rs | 43 ++++++++++-- .../tests/lsps0_integration_tests.rs | 2 +- .../tests/lsps2_integration_tests.rs | 68 +++++++++++-------- lightning/src/ln/functional_test_utils.rs | 3 +- lightning/src/routing/router.rs | 2 +- 6 files changed, 93 insertions(+), 49 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index cafe2568a50..ed56e4d3ac5 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -23,14 +23,6 @@ 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, @@ -52,6 +44,15 @@ use lightning_types::payment::PaymentHash; use bitcoin::secp256k1::PublicKey; use bitcoin::Transaction; +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, +}; + const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; const MAX_TOTAL_PENDING_REQUESTS: usize = 1000; const MAX_TOTAL_PEERS: usize = 100000; @@ -367,7 +368,6 @@ 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, @@ -1036,9 +1036,9 @@ 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 + ), }) }, } diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index 27d3f3f911f..1d2022850fd 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -17,13 +17,22 @@ 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>( - nodes: Vec>, service_config: LiquidityServiceConfig, +// this is ONLY used on LSPS2 so it says it's not used but it is +#[allow(dead_code)] +pub(crate) struct LSPSNodesWithPayer<'a, 'b, 'c> { + pub service_node: LiquidityNode<'a, 'b, 'c>, + pub client_node: LiquidityNode<'a, 'b, 'c>, + pub payer_node: Node<'a, 'b, 'c>, +} + +// Reusable helper: consumes a Vec, builds service + client LiquidityNodes, returns optional leftover node. +fn build_service_and_client<'a, 'b, 'c>( + mut nodes: Vec>, service_config: LiquidityServiceConfig, client_config: LiquidityClientConfig, time_provider: Arc, -) -> LSPSNodes<'a, 'b, 'c> { +) -> (LiquidityNode<'a, 'b, 'c>, LiquidityNode<'a, 'b, 'c>, Option>) { + assert!(nodes.len() >= 2, "Need at least two nodes (service, client)"); let chain_params = ChainParameters { network: Network::Testnet, best_block: BestBlock::from_network(Network::Testnet), @@ -50,11 +59,33 @@ pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>( time_provider, ); - let mut iter = nodes.into_iter(); + let mut iter = nodes.drain(..); let service_node = LiquidityNode::new(iter.next().unwrap(), service_lm); let client_node = LiquidityNode::new(iter.next().unwrap(), client_lm); + let leftover = iter.next(); // payer if present + (service_node, client_node, leftover) +} - LSPSNodes { service_node, client_node, payer_node_optional: iter.next() } +pub(crate) fn create_service_and_client_nodes<'a, 'b, 'c>( + nodes: Vec>, service_config: LiquidityServiceConfig, + client_config: LiquidityClientConfig, time_provider: Arc, +) -> LSPSNodes<'a, 'b, 'c> { + let (service_node, client_node, _extra) = + build_service_and_client(nodes, service_config, client_config, time_provider); + LSPSNodes { service_node, client_node } +} + +// this is ONLY used on LSPS2 so it says it's not used but it is +#[allow(dead_code)] +pub(crate) fn create_service_client_and_payer_nodes<'a, 'b, 'c>( + nodes: Vec>, service_config: LiquidityServiceConfig, + client_config: LiquidityClientConfig, time_provider: Arc, +) -> LSPSNodesWithPayer<'a, 'b, 'c> { + assert!(nodes.len() >= 3, "Need three nodes (service, client, payer)"); + let (service_node, client_node, payer_opt) = + build_service_and_client(nodes, service_config, client_config, time_provider); + let payer_node = payer_opt.expect("payer node missing"); + LSPSNodesWithPayer { service_node, client_node, payer_node } } 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 29b9c6091ea..423d49785f2 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -60,7 +60,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 , ..} = create_service_and_client_nodes( + let LSPSNodes { service_node, client_node } = create_service_and_client_nodes( nodes, service_config, client_config, diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index bdb663aec90..6d88434b629 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -2,7 +2,10 @@ mod common; -use common::{create_service_and_client_nodes, get_lsps_message, LSPSNodes, LiquidityNode}; +use common::{ + create_service_and_client_nodes, create_service_client_and_payer_nodes, get_lsps_message, + LSPSNodes, LSPSNodesWithPayer, LiquidityNode, +}; use lightning::check_added_monitors; use lightning::events::Event; @@ -61,9 +64,7 @@ use std::time::Duration; const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; const MAX_TOTAL_PENDING_REQUESTS: usize = 1000; -fn setup_test_lsps2_nodes<'a, 'b, 'c>( - nodes: Vec>, -) -> (LSPSNodes<'a, 'b, 'c>, [u8; 32]) { +fn build_lsps2_configs() -> ([u8; 32], LiquidityServiceConfig, LiquidityClientConfig) { let promise_secret = [42; 32]; let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; let service_config = LiquidityServiceConfig { @@ -73,20 +74,38 @@ fn setup_test_lsps2_nodes<'a, 'b, 'c>( lsps5_service_config: None, advertise_service: true, }; - let lsps2_client_config = LSPS2ClientConfig::default(); let client_config = LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config: Some(lsps2_client_config), lsps5_client_config: None, }; - let lsps_nodes = create_service_and_client_nodes( + (promise_secret, service_config, client_config) +} + +fn setup_test_lsps2_nodes_with_payer<'a, 'b, 'c>( + nodes: Vec>, +) -> (LSPSNodesWithPayer<'a, 'b, 'c>, [u8; 32]) { + let (promise_secret, service_config, client_config) = build_lsps2_configs(); + let lsps_nodes = create_service_client_and_payer_nodes( nodes, service_config, client_config, Arc::new(DefaultTimeProvider), ); + (lsps_nodes, promise_secret) +} +fn setup_test_lsps2_nodes<'a, 'b, 'c>( + nodes: Vec>, +) -> (LSPSNodes<'a, 'b, 'c>, [u8; 32]) { + let (promise_secret, service_config, client_config) = build_lsps2_configs(); + let lsps_nodes = create_service_and_client_nodes( + nodes, + service_config, + client_config, + Arc::new(DefaultTimeProvider), + ); (lsps_nodes, promise_secret) } @@ -103,10 +122,6 @@ 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, @@ -147,9 +162,6 @@ fn create_jit_invoice( }) })?; - // Add debugging to verify the invoice - println!("Created invoice with route hints: {:?}", invoice.route_hints()); - Ok(invoice) } @@ -160,7 +172,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(); @@ -308,7 +320,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(); @@ -443,7 +455,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(); @@ -469,7 +481,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(); @@ -553,7 +565,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(); @@ -578,7 +590,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(); @@ -633,7 +645,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(); @@ -762,7 +774,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(); @@ -841,7 +853,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(); @@ -926,9 +938,9 @@ fn full_lsps2_flow() { &[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 (lsps_nodes, promise_secret) = setup_test_lsps2_nodes_with_payer(nodes); + let LSPSNodesWithPayer { service_node, client_node, payer_node } = lsps_nodes; + 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(); @@ -1188,7 +1200,7 @@ fn full_lsps2_flow() { assert!(events.is_empty(), "Expected no events from service node, got: {:?}", events); client_node.inner.node.claim_funds(preimage.unwrap()); - /// TODO SIMPLIFY: put the payment_forwarded call inside a PaymentForwardedEvent + // TODO SIMPLIFY: put the payment_forwarded call inside a PaymentForwardedEvent let expected_paths: &[&[&lightning::ln::functional_test_utils::Node<'_, '_, '_>]] = &[&[&service_node.inner, &client_node.inner]]; @@ -1196,7 +1208,7 @@ fn full_lsps2_flow() { let total_fee_msat = pass_claimed_payment_along_route(args); service_handler.payment_forwarded(channel_id).unwrap(); - /// + match service_node.liquidity_manager.next_event().unwrap() { LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BroadcastFundingTransaction { counterparty_node_id, @@ -1205,6 +1217,8 @@ fn full_lsps2_flow() { }) => { assert_eq!(counterparty_node_id, client_node_id); assert_eq!(uid, user_channel_id); + + // TODO actually broadcast transaction }, other => panic!("Unexpected event: {:?}", other), } diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index d1c458fa714..88bdb6d1d08 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(); } @@ -3010,7 +3010,6 @@ 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 f34e4578284..9d3093c5a90 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)] +#[derive(Clone, Copy, Debug)] pub struct RouteParametersConfig { /// The maximum total fees, in millisatoshi, that may accrue during route finding. ///