Read persisted LSPS1ServiceHandler state on startup
What changed, and why it matters
This commit fixes a startup bug in the Lightning Dev Kit's liquidity service module. Previously, the LSPS1 service handler was created with an empty set of peer states, ignoring any previously saved data. Now it reads saved peer state from the key-value store during startup, matching how LSPS2 and LSPS5 already worked. This is a reliability/consistency fix rather than an active security vulnerability, but ignoring persisted state could previously lead to duplicate orders, lost channel state, or protocol confusion after a restart.
Treat as a normal reliability fix. Reviewers should verify that `LSPS1ServicePeerState::read` handles malformed/truncated data safely and that the KV store key format is stable, since a bad stored key now causes startup failure. No urgent security response is indicated.
Security signals we found
State persistence not loaded on startup (data-loss / state inconsistency)
New deserialization path for persisted peer state introduced
PublicKey parsed from stored key string without additional sanitization
Mirrors existing LSPS2/LSPS5 persistence patterns
No explicit security framing in commit message or diff
Evidence from the diff
The change adds read_lsps1_service_peer_states in lightning-liquidity/src/persist.rs, exposes lsps1::peer_state as pub(crate), and wires it into LiquidityManager::new so that LSPS1ServiceHandler::new receives the deserialized HashMap<PublicKey, Mutex<PeerState>> from the KV store. Before this patch, the handler always started with new_hash_map(), discarding persisted state. The deserialization uses LSPS1ServicePeerState::read and PublicKey::from_str. The patch mirrors existing LSPS2/LSPS5 persistence logic. No input validation beyond deserialization is added, and the commit message frames this as a missing startup read, not a security fix.
Changed components
lightning-liquidity/src/lsps1/peer_state.rslightning-liquidity/src/lsps1/service.rslightning-liquidity/src/manager.rslightning-liquidity/src/persist.rsInspect captured patch +73 / −19
diff --git a/lightning-liquidity/src/lsps1/mod.rs b/lightning-liquidity/src/lsps1/mod.rs
index bdfc404..2270abe 100644
--- a/lightning-liquidity/src/lsps1/mod.rs
+++ b/lightning-liquidity/src/lsps1/mod.rs
@@ -13,6 +13,6 @@ pub mod client;
pub mod event;
pub mod msgs;
#[cfg(lsps1_service)]
-mod peer_state;
+pub(crate) mod peer_state;
#[cfg(lsps1_service)]
pub mod service;
diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs
index a4c477f..2b94f76 100644
--- a/lightning-liquidity/src/lsps1/peer_state.rs
+++ b/lightning-liquidity/src/lsps1/peer_state.rs
@@ -23,7 +23,7 @@ use lightning::util::hash_tables::new_hash_map;
use core::fmt;
#[derive(Default)]
-pub(super) struct PeerState {
+pub(crate) struct PeerState {
outbound_channels_by_order_id: HashMap<LSPS1OrderId, ChannelOrder>,
pending_requests: HashMap<LSPSRequestId, LSPS1Request>,
needs_persist: bool,
diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs
index 71587ae..459406e 100644
--- a/lightning-liquidity/src/lsps1/service.rs
+++ b/lightning-liquidity/src/lsps1/service.rs
@@ -37,7 +37,7 @@ use crate::persist::{
LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE,
};
use crate::prelude::hash_map::Entry;
-use crate::prelude::{new_hash_map, HashMap};
+use crate::prelude::HashMap;
use crate::sync::{Arc, Mutex, RwLock};
use crate::utils;
use crate::utils::async_poll::dummy_waker;
@@ -91,9 +91,9 @@ where
{
/// Constructs a `LSPS1ServiceHandler`.
pub(crate) fn new(
- entropy_source: ES, pending_messages: Arc<MessageQueue>,
- pending_events: Arc<EventQueue<K>>, channel_manager: CM, kv_store: K, time_provider: TP,
- config: LSPS1ServiceConfig,
+ per_peer_state: HashMap<PublicKey, Mutex<PeerState>>, entropy_source: ES,
+ pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue<K>>,
+ channel_manager: CM, kv_store: K, time_provider: TP, config: LSPS1ServiceConfig,
) -> Self {
Self {
entropy_source,
@@ -101,7 +101,7 @@ where
kv_store,
pending_messages,
pending_events,
- per_peer_state: RwLock::new(new_hash_map()),
+ per_peer_state: RwLock::new(per_peer_state),
persistence_in_flight: AtomicUsize::new(0),
time_provider,
config,
diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs
index da87b4c..99a0c8f 100644
--- a/lightning-liquidity/src/manager.rs
+++ b/lightning-liquidity/src/manager.rs
@@ -23,6 +23,8 @@ use crate::lsps5::client::{LSPS5ClientConfig, LSPS5ClientHandler};
use crate::lsps5::msgs::LSPS5Message;
use crate::lsps5::service::{LSPS5ServiceConfig, LSPS5ServiceHandler};
use crate::message_queue::MessageQueue;
+#[cfg(lsps1_service)]
+use crate::persist::read_lsps1_service_peer_states;
use crate::persist::{
read_event_queue, read_lsps2_service_peer_states, read_lsps5_service_peer_states,
};
@@ -450,24 +452,32 @@ where
});
#[cfg(lsps1_service)]
- let lsps1_service_handler = service_config.as_ref().and_then(|config| {
- if let Some(number) =
- <LSPS1ServiceHandler<ES, CM, K, TP> as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER
- {
- supported_protocols.push(number);
- }
- config.lsps1_service_config.as_ref().map(|config| {
- LSPS1ServiceHandler::new(
+ let lsps1_service_handler = if let Some(service_config) = service_config.as_ref() {
+ if let Some(lsps1_service_config) = service_config.lsps1_service_config.as_ref() {
+ if let Some(number) =
+ <LSPS1ServiceHandler<ES, CM, K, TP> as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER
+ {
+ supported_protocols.push(number);
+ }
+
+ let peer_states = read_lsps1_service_peer_states(kv_store.clone()).await?;
+
+ Some(LSPS1ServiceHandler::new(
+ peer_states,
entropy_source.clone(),
Arc::clone(&pending_messages),
Arc::clone(&pending_events),
channel_manager.clone(),
kv_store.clone(),
time_provider,
- config.clone(),
- )
- })
- });
+ lsps1_service_config.clone(),
+ ))
+ } else {
+ None
+ }
+ } else {
+ None
+ };
let lsps0_client_handler = LSPS0ClientHandler::new(
entropy_source.clone(),
diff --git a/lightning-liquidity/src/persist.rs b/lightning-liquidity/src/persist.rs
index 9518b40..13afdab 100644
--- a/lightning-liquidity/src/persist.rs
+++ b/lightning-liquidity/src/persist.rs
@@ -10,6 +10,8 @@
//! Types and utils for persistence.
use crate::events::{EventQueueDeserWrapper, LiquidityEvent};
+#[cfg(lsps1_service)]
+use crate::lsps1::peer_state::PeerState as LSPS1ServicePeerState;
use crate::lsps2::service::PeerState as LSPS2ServicePeerState;
use crate::lsps5::service::PeerState as LSPS5ServicePeerState;
use crate::prelude::{new_hash_map, HashMap};
@@ -86,6 +88,48 @@ pub(crate) async fn read_event_queue<K: KVStore>(
Ok(Some(queue.0))
}
+#[cfg(lsps1_service)]
+pub(crate) async fn read_lsps1_service_peer_states<K: KVStore>(
+ kv_store: K,
+) -> Result<HashMap<PublicKey, Mutex<LSPS1ServicePeerState>>, lightning::io::Error> {
+ let mut res = new_hash_map();
+
+ for stored_key in kv_store
+ .list(
+ LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
+ LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE,
+ )
+ .await?
+ {
+ let mut reader = Cursor::new(
+ kv_store
+ .read(
+ LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
+ LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE,
+ &stored_key,
+ )
+ .await?,
+ );
+
+ let peer_state = LSPS1ServicePeerState::read(&mut reader).map_err(|_| {
+ lightning::io::Error::new(
+ lightning::io::ErrorKind::InvalidData,
+ "Failed to deserialize LSPS1 peer state",
+ )
+ })?;
+
+ let key = PublicKey::from_str(&stored_key).map_err(|_| {
+ lightning::io::Error::new(
+ lightning::io::ErrorKind::InvalidData,
+ "Failed to deserialize stored key entry",
+ )
+ })?;
+
+ res.insert(key, Mutex::new(peer_state));
+ }
+ Ok(res)
+}
+
pub(crate) async fn read_lsps2_service_peer_states<K: KVStore>(
kv_store: K,
) -> Result<HashMap<PublicKey, Mutex<LSPS2ServicePeerState>>, lightning::io::Error> {
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.