Skip to content

Commit d9157eb

Browse files
Add LSPS5 DOS protections.
When handling an incoming LSPS5 request, the manager will check if the counterparty is 'engaged' in some way before responding. `Engaged` meaning = active channel | LSPS2 active operation | LSPS1 active operation. Logic: `If not engaged then reject request;` A single test is added only checking for the active channel condition, because it's not super easy to get LSPS1-2 on the correct state to check this (yet). Other tangential work is happening that will make this easier and more tests will come in the near future
1 parent 3b939c0 commit d9157eb

File tree

5 files changed

+387
-6
lines changed

5 files changed

+387
-6
lines changed

lightning-liquidity/src/lsps1/service.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,17 @@ where
174174
&self.config
175175
}
176176

177+
pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool {
178+
let outer_state_lock = self.per_peer_state.read().unwrap();
179+
if let Some(inner_state_lock) = outer_state_lock.get(counterparty_node_id) {
180+
let peer_state = inner_state_lock.lock().unwrap();
181+
!(peer_state.pending_requests.is_empty()
182+
&& peer_state.outbound_channels_by_order_id.is_empty())
183+
} else {
184+
false
185+
}
186+
}
187+
177188
fn handle_get_info_request(
178189
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
179190
) -> Result<(), LightningError> {

lightning-liquidity/src/lsps2/service.rs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ struct ForwardPaymentAction(ChannelId, FeePayment);
108108
struct ForwardHTLCsAction(ChannelId, Vec<InterceptedHTLC>);
109109

110110
/// The different states a requested JIT channel can be in.
111-
#[derive(Debug)]
112-
enum OutboundJITChannelState {
111+
#[derive(Clone, Debug, PartialEq, Eq)]
112+
pub(crate) enum OutboundJITChannelState {
113113
/// The JIT channel SCID was created after a buy request, and we are awaiting an initial payment
114114
/// of sufficient size to open the channel.
115115
PendingInitialPayment { payment_queue: PaymentQueue },
@@ -134,6 +134,30 @@ enum OutboundJITChannelState {
134134
PaymentForwarded { channel_id: ChannelId },
135135
}
136136

137+
impl OutboundJITChannelState {
138+
fn ord_index(&self) -> u8 {
139+
match self {
140+
OutboundJITChannelState::PendingInitialPayment { .. } => 0,
141+
OutboundJITChannelState::PendingChannelOpen { .. } => 1,
142+
OutboundJITChannelState::PendingPaymentForward { .. } => 2,
143+
OutboundJITChannelState::PendingPayment { .. } => 3,
144+
OutboundJITChannelState::PaymentForwarded { .. } => 4,
145+
}
146+
}
147+
}
148+
149+
impl PartialOrd for OutboundJITChannelState {
150+
fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
151+
Some(self.cmp(other))
152+
}
153+
}
154+
155+
impl Ord for OutboundJITChannelState {
156+
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
157+
self.ord_index().cmp(&other.ord_index())
158+
}
159+
}
160+
137161
impl OutboundJITChannelState {
138162
fn new() -> Self {
139163
OutboundJITChannelState::PendingInitialPayment { payment_queue: PaymentQueue::new() }
@@ -572,6 +596,18 @@ where
572596
&self.config
573597
}
574598

599+
pub(crate) fn highest_state_for_peer(
600+
&self, counterparty_node_id: &PublicKey,
601+
) -> Option<OutboundJITChannelState> {
602+
let outer_state_lock = self.per_peer_state.read().unwrap();
603+
if let Some(inner_state_lock) = outer_state_lock.get(counterparty_node_id) {
604+
let peer_state = inner_state_lock.lock().unwrap();
605+
peer_state.outbound_channels_by_intercept_scid.values().map(|c| c.state.clone()).max()
606+
} else {
607+
None
608+
}
609+
}
610+
575611
/// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid.
576612
///
577613
/// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event.
@@ -1905,4 +1941,55 @@ mod tests {
19051941
);
19061942
}
19071943
}
1944+
1945+
#[test]
1946+
fn highest_state_for_peer_orders() {
1947+
let opening_fee_params = LSPS2OpeningFeeParams {
1948+
min_fee_msat: 0,
1949+
proportional: 0,
1950+
valid_until: LSPSDateTime::from_str("1970-01-01T00:00:00Z").unwrap(),
1951+
min_lifetime: 0,
1952+
max_client_to_self_delay: 0,
1953+
min_payment_size_msat: 0,
1954+
max_payment_size_msat: 0,
1955+
promise: String::new(),
1956+
};
1957+
1958+
let mut map = new_hash_map();
1959+
map.insert(
1960+
0,
1961+
OutboundJITChannel {
1962+
state: OutboundJITChannelState::PendingInitialPayment {
1963+
payment_queue: PaymentQueue::new(),
1964+
},
1965+
user_channel_id: 0,
1966+
opening_fee_params: opening_fee_params.clone(),
1967+
payment_size_msat: None,
1968+
},
1969+
);
1970+
map.insert(
1971+
1,
1972+
OutboundJITChannel {
1973+
state: OutboundJITChannelState::PendingChannelOpen {
1974+
payment_queue: PaymentQueue::new(),
1975+
opening_fee_msat: 0,
1976+
},
1977+
user_channel_id: 1,
1978+
opening_fee_params: opening_fee_params.clone(),
1979+
payment_size_msat: None,
1980+
},
1981+
);
1982+
map.insert(
1983+
2,
1984+
OutboundJITChannel {
1985+
state: OutboundJITChannelState::PaymentForwarded { channel_id: ChannelId([0; 32]) },
1986+
user_channel_id: 2,
1987+
opening_fee_params,
1988+
payment_size_msat: None,
1989+
},
1990+
);
1991+
1992+
let max_state = map.values().map(|c| c.state.clone()).max().unwrap();
1993+
assert!(matches!(max_state, OutboundJITChannelState::PaymentForwarded { .. }));
1994+
}
19081995
}

lightning-liquidity/src/lsps5/service.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::prelude::*;
2222
use crate::sync::{Arc, Mutex};
2323
use crate::utils::time::TimeProvider;
2424

25+
use crate::lsps2::service::OutboundJITChannelState;
2526
use bitcoin::secp256k1::PublicKey;
2627

2728
use lightning::ln::channelmanager::AChannelManager;
@@ -150,6 +151,28 @@ where
150151
}
151152
}
152153

154+
/// Returns whether a request from the given client should be accepted.
155+
///
156+
/// Prior activity includes an existing open channel, an active LSPS1 flow,
157+
/// or an LSPS2 flow that has progressed to at least
158+
/// [`OutboundJITChannelState::PendingChannelOpen`].
159+
pub(crate) fn can_accept_request(
160+
&self, client_id: &PublicKey, lsps2_max_state: Option<OutboundJITChannelState>,
161+
lsps1_has_activity: bool,
162+
) -> bool {
163+
self.client_has_open_channel(client_id)
164+
|| lsps1_has_activity
165+
|| lsps2_max_state.map_or(false, |s| {
166+
matches!(
167+
s,
168+
OutboundJITChannelState::PendingChannelOpen { .. }
169+
| OutboundJITChannelState::PendingPaymentForward { .. }
170+
| OutboundJITChannelState::PendingPayment { .. }
171+
| OutboundJITChannelState::PaymentForwarded { .. }
172+
)
173+
})
174+
}
175+
153176
fn check_prune_stale_webhooks(&self) {
154177
let now =
155178
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch());
@@ -515,7 +538,7 @@ where
515538
*last_pruning = Some(now);
516539
}
517540

518-
fn client_has_open_channel(&self, client_id: &PublicKey) -> bool {
541+
pub(crate) fn client_has_open_channel(&self, client_id: &PublicKey) -> bool {
519542
self.channel_manager
520543
.get_cm()
521544
.list_channels()

lightning-liquidity/src/manager.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,32 @@ where
559559
LSPSMessage::LSPS5(msg @ LSPS5Message::Request(..)) => {
560560
match &self.lsps5_service_handler {
561561
Some(lsps5_service_handler) => {
562+
let lsps2_max_state = self
563+
.lsps2_service_handler
564+
.as_ref()
565+
.and_then(|h| h.highest_state_for_peer(sender_node_id));
566+
#[cfg(lsps1_service)]
567+
let lsps1_has_active_requests = self
568+
.lsps1_service_handler
569+
.as_ref()
570+
.map_or(false, |h| h.has_active_requests(sender_node_id));
571+
#[cfg(not(lsps1_service))]
572+
let lsps1_has_active_requests = false;
573+
574+
if !lsps5_service_handler.can_accept_request(
575+
sender_node_id,
576+
lsps2_max_state,
577+
lsps1_has_active_requests,
578+
) {
579+
return Err(LightningError {
580+
err: format!(
581+
"Rejecting LSPS5 request from {:?} without prior activity (requires open channel or active LSPS1 or LSPS2 flow)",
582+
sender_node_id
583+
),
584+
action: ErrorAction::IgnoreAndLog(Level::Debug),
585+
});
586+
}
587+
562588
lsps5_service_handler.handle_message(msg, sender_node_id)?;
563589
},
564590
None => {

0 commit comments

Comments
 (0)