Skip to content

Commit b272235

Browse files
committed
Limit pending requests and peers in LSPS1 service
Add per-peer and global rate limiting to `LSPS1ServiceHandler` to prevent resource exhaustion, mirroring the existing LSPS2 pattern. Introduce `MAX_PENDING_REQUESTS_PER_PEER` (10), `MAX_TOTAL_PENDING_REQUESTS` (1000), and `MAX_TOTAL_PEERS` (100000) constants and enforce them in `handle_create_order_request`. Rejected requests receive a `CreateOrderError` with `LSPS0_CLIENT_REJECTED_ERROR_CODE`. A `total_pending_requests` atomic counter tracks the global count, and a `verify_pending_request_counter` debug assertion ensures it stays in sync. Co-Authored-By: HAL 9000
1 parent 98f71f5 commit b272235

3 files changed

Lines changed: 158 additions & 16 deletions

File tree

lightning-liquidity/src/lsps1/peer_state.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum};
2222

2323
use core::fmt;
2424

25+
const MAX_PENDING_REQUESTS_PER_PEER: usize = 10;
26+
2527
/// Indicates which payment method was used for the order.
2628
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2729
pub enum PaymentMethod {
@@ -340,6 +342,9 @@ impl PeerState {
340342
pub(super) fn register_request(
341343
&mut self, request_id: LSPSRequestId, request: LSPS1Request,
342344
) -> Result<(), PeerStateError> {
345+
if self.pending_requests_and_unpaid_orders() >= MAX_PENDING_REQUESTS_PER_PEER {
346+
return Err(PeerStateError::TooManyPendingRequests);
347+
}
343348
if self.pending_requests.contains_key(&request_id) {
344349
return Err(PeerStateError::DuplicateRequestId);
345350
}
@@ -376,8 +381,10 @@ impl PeerState {
376381
self.pending_requests.is_empty() && self.outbound_channels_by_order_id.is_empty()
377382
}
378383

379-
pub(super) fn prune_pending_requests(&mut self) {
380-
self.pending_requests.clear()
384+
pub(super) fn prune_pending_requests(&mut self) -> usize {
385+
let num_pruned = self.pending_requests.len();
386+
self.pending_requests.clear();
387+
num_pruned
381388
}
382389

383390
pub(super) fn prune_expired_request_state(&mut self) {
@@ -389,6 +396,23 @@ impl PeerState {
389396
true
390397
});
391398
}
399+
400+
fn pending_requests_and_unpaid_orders(&self) -> usize {
401+
let pending_requests = self.pending_requests.len();
402+
// We exclude paid and completed orders.
403+
let unpaid_orders = self
404+
.outbound_channels_by_order_id
405+
.iter()
406+
.filter(|(_, v)| {
407+
!matches!(
408+
v.state,
409+
ChannelOrderState::OrderPaid { .. }
410+
| ChannelOrderState::CompletedAndChannelOpened { .. }
411+
)
412+
})
413+
.count();
414+
pending_requests + unpaid_orders
415+
}
392416
}
393417

394418
impl_writeable_tlv_based!(PeerState, {
@@ -403,6 +427,7 @@ pub(super) enum PeerStateError {
403427
DuplicateRequestId,
404428
UnknownOrderId,
405429
InvalidStateTransition(ChannelOrderStateError),
430+
TooManyPendingRequests,
406431
}
407432

408433
impl fmt::Display for PeerStateError {
@@ -412,6 +437,7 @@ impl fmt::Display for PeerStateError {
412437
Self::DuplicateRequestId => write!(f, "duplicate request id"),
413438
Self::UnknownOrderId => write!(f, "unknown order id"),
414439
Self::InvalidStateTransition(e) => write!(f, "{}", e),
440+
Self::TooManyPendingRequests => write!(f, "too many pending requests"),
415441
}
416442
}
417443
}

lightning-liquidity/src/lsps1/service.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use crate::message_queue::MessageQueue;
3434
use crate::events::EventQueue;
3535
use crate::lsps0::ser::{
3636
LSPSDateTime, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError,
37+
LSPS0_CLIENT_REJECTED_ERROR_CODE,
3738
};
3839
use crate::persist::{
3940
LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE,
@@ -62,6 +63,8 @@ pub struct LSPS1ServiceConfig {
6263
pub supported_options: LSPS1Options,
6364
}
6465

66+
const MAX_TOTAL_PEERS: usize = 100000;
67+
6568
/// The main object allowing to send and receive bLIP-51 / LSPS1 messages.
6669
pub struct LSPS1ServiceHandler<
6770
ES: EntropySource,
@@ -308,11 +311,30 @@ where
308311

309312
{
310313
let mut outer_state_lock = self.per_peer_state.write().unwrap();
314+
let num_peers = outer_state_lock.len();
311315

312-
let inner_state_lock = outer_state_lock
313-
.entry(*counterparty_node_id)
314-
.or_insert(Mutex::new(PeerState::default()));
315-
let mut peer_state_lock = inner_state_lock.lock().unwrap();
316+
let inner_state_entry = outer_state_lock.entry(*counterparty_node_id);
317+
318+
if matches!(inner_state_entry, Entry::Vacant(_)) && num_peers >= MAX_TOTAL_PEERS {
319+
let response = LSPS1Response::CreateOrderError(LSPSResponseError {
320+
code: LSPS0_CLIENT_REJECTED_ERROR_CODE,
321+
message: "Reached maximum number of pending requests. Please try again later."
322+
.to_string(),
323+
data: None,
324+
});
325+
let msg = LSPS1Message::Response(request_id, response).into();
326+
message_queue_notifier.enqueue(counterparty_node_id, msg);
327+
return Err(LightningError {
328+
err: format!(
329+
"Dropping request from peer {} due to reaching maximally allowed number of total peers: {}",
330+
counterparty_node_id, MAX_TOTAL_PEERS
331+
),
332+
action: ErrorAction::IgnoreAndLog(Level::Debug),
333+
});
334+
}
335+
336+
let mut peer_state_lock =
337+
inner_state_entry.or_insert(Mutex::new(PeerState::default())).lock().unwrap();
316338

317339
let request = LSPS1Request::CreateOrder(params.clone());
318340
peer_state_lock.register_request(request_id.clone(), request).map_err(|e| {
@@ -734,16 +756,19 @@ where
734756
&self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey,
735757
) -> Result<(), LightningError> {
736758
match message {
737-
LSPS1Message::Request(request_id, request) => match request {
738-
LSPS1Request::GetInfo(_) => {
739-
self.handle_get_info_request(request_id, counterparty_node_id)
740-
},
741-
LSPS1Request::CreateOrder(params) => {
742-
self.handle_create_order_request(request_id, counterparty_node_id, params)
743-
},
744-
LSPS1Request::GetOrder(params) => {
745-
self.handle_get_order_request(request_id, counterparty_node_id, params)
746-
},
759+
LSPS1Message::Request(request_id, request) => {
760+
let res = match request {
761+
LSPS1Request::GetInfo(_) => {
762+
self.handle_get_info_request(request_id, counterparty_node_id)
763+
},
764+
LSPS1Request::CreateOrder(params) => {
765+
self.handle_create_order_request(request_id, counterparty_node_id, params)
766+
},
767+
LSPS1Request::GetOrder(params) => {
768+
self.handle_get_order_request(request_id, counterparty_node_id, params)
769+
},
770+
};
771+
res
747772
},
748773
_ => {
749774
debug_assert!(

lightning-liquidity/tests/lsps1_integration_tests.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ use lightning::ln::functional_test_utils::{create_network, Node};
3434
use lightning_liquidity::lsps1::msgs::LSPS1OrderId;
3535
use lightning_liquidity::utils::time::TimeProvider;
3636

37+
const MAX_PENDING_REQUESTS_PER_PEER: usize = 10;
38+
3739
fn build_lsps1_configs(
3840
supported_options: LSPS1Options,
3941
) -> (LiquidityServiceConfig, LiquidityClientConfig) {
@@ -1139,3 +1141,92 @@ fn lsps1_expired_orders_are_pruned_and_not_persisted() {
11391141
}
11401142
}
11411143
}
1144+
1145+
#[test]
1146+
fn max_pending_requests_per_peer_rejected() {
1147+
let chanmon_cfgs = create_chanmon_cfgs(2);
1148+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1149+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1150+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1151+
1152+
let supported_options = LSPS1Options {
1153+
min_required_channel_confirmations: 0,
1154+
min_funding_confirms_within_blocks: 6,
1155+
supports_zero_channel_reserve: true,
1156+
max_channel_expiry_blocks: 144,
1157+
min_initial_client_balance_sat: 10_000_000,
1158+
max_initial_client_balance_sat: 100_000_000,
1159+
min_initial_lsp_balance_sat: 100_000,
1160+
max_initial_lsp_balance_sat: 100_000_000,
1161+
min_channel_balance_sat: 100_000,
1162+
max_channel_balance_sat: 100_000_000,
1163+
};
1164+
1165+
let LSPSNodes { service_node, client_node } =
1166+
setup_test_lsps1_nodes(nodes, supported_options.clone());
1167+
let service_node_id = service_node.inner.node.get_our_node_id();
1168+
let client_node_id = client_node.inner.node.get_our_node_id();
1169+
let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap();
1170+
1171+
let order_params = LSPS1OrderParams {
1172+
lsp_balance_sat: 100_000,
1173+
client_balance_sat: 10_000_000,
1174+
required_channel_confirmations: 0,
1175+
funding_confirms_within_blocks: 6,
1176+
channel_expiry_blocks: 144,
1177+
token: None,
1178+
announce_channel: true,
1179+
};
1180+
1181+
let refund_onchain_address =
1182+
Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr")
1183+
.unwrap()
1184+
.assume_checked();
1185+
1186+
// Send MAX_PENDING_REQUESTS_PER_PEER create_order requests, all should succeed.
1187+
for _ in 0..MAX_PENDING_REQUESTS_PER_PEER {
1188+
let _ = client_handler.create_order(
1189+
&service_node_id,
1190+
order_params.clone(),
1191+
Some(refund_onchain_address.clone()),
1192+
);
1193+
let req_msg = get_lsps_message!(client_node, service_node_id);
1194+
let result = service_node.liquidity_manager.handle_custom_message(req_msg, client_node_id);
1195+
assert!(result.is_ok());
1196+
let event = service_node.liquidity_manager.next_event().unwrap();
1197+
assert!(matches!(
1198+
event,
1199+
LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { .. })
1200+
));
1201+
}
1202+
1203+
// The next request should be rejected due to per-peer limit.
1204+
let rejected_req_id = client_handler.create_order(
1205+
&service_node_id,
1206+
order_params.clone(),
1207+
Some(refund_onchain_address),
1208+
);
1209+
let rejected_req_msg = get_lsps_message!(client_node, service_node_id);
1210+
let result =
1211+
service_node.liquidity_manager.handle_custom_message(rejected_req_msg, client_node_id);
1212+
assert!(result.is_err(), "We should have hit the per-peer limit");
1213+
1214+
let error_response = get_lsps_message!(service_node, client_node_id);
1215+
let result =
1216+
client_node.liquidity_manager.handle_custom_message(error_response, service_node_id);
1217+
assert!(result.is_err());
1218+
1219+
let event = client_node.liquidity_manager.next_event().unwrap();
1220+
if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderRequestFailed {
1221+
request_id,
1222+
counterparty_node_id,
1223+
error,
1224+
}) = event
1225+
{
1226+
assert_eq!(request_id, rejected_req_id);
1227+
assert_eq!(counterparty_node_id, service_node_id);
1228+
assert_eq!(error.code, 1); // LSPS0_CLIENT_REJECTED_ERROR_CODE
1229+
} else {
1230+
panic!("Expected LSPS1ClientEvent::OrderRequestFailed event");
1231+
}
1232+
}

0 commit comments

Comments
 (0)