Implement `LSPS1ServiceHandler` persistence and state pruning
What changed, and why it matters
This commit adds the ability for an LSPS1 (a Lightning service protocol) server to save its per-peer state to disk and to clean up old, expired state. It mirrors the persistence and pruning patterns already used for LSPS2/LSPS5. The change is defensive: it reduces the risk of losing order state across restarts and limits unbounded state growth. There is no direct evidence in the commit of a security vulnerability being fixed; it is a reliability and operational-hygiene improvement.
Treat as a normal feature/reliability commit. Reviewers should verify that the `persistence_in_flight` atomic guard correctly serializes overlapping `persist()` calls, that the write-lock ordering between `per_peer_state` and the peer mutex cannot deadlock, and that the no-std TODO is tracked so stale state does not accumulate indefinitely in no-std deployments.
Security signals we found
New persistence path for LSPS1 service peer state via KVStore write/remove
Addition of `needs_persist` dirty-bit and `persistence_in_flight` concurrency guard
Pruning of expired order/request state to limit state accumulation
Async conversion of `send_payment_details` and `update_order_status` with sync wrapper
TODO noting no-std builds cannot yet check expiry times, disabling pruning there
Evidence from the diff
The patch introduces needs_persist tracking in PeerState, a persist() loop in LSPS1ServiceHandler, KVStore write/remove calls under a new lsps1_service namespace, and pruning of expired request/order state on peer disconnect and during persistence. Public methods send_payment_details and update_order_status are made async and now trigger per-peer persistence; a synchronous wrapper LSPS1ServiceHandlerSync is exposed for callers that cannot use async. The LiquidityManager now includes LSPS1 service persistence in its own persist() path and invokes peer_disconnected() cleanup. A TODO remains for no-std expiry checks, where prunability is conservatively set to false.
Changed components
lightning-liquidity/src/lsps1/peer_state.rslightning-liquidity/src/lsps1/service.rslightning-liquidity/src/manager.rslightning-liquidity/src/persist.rslightning-liquidity/tests/lsps1_integration_tests.rsInspect captured patch +365 / −28
diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs
index 5af7537..a4c477f 100644
--- a/lightning-liquidity/src/lsps1/peer_state.rs
+++ b/lightning-liquidity/src/lsps1/peer_state.rs
@@ -26,6 +26,7 @@ use core::fmt;
pub(super) struct PeerState {
outbound_channels_by_order_id: HashMap<LSPS1OrderId, ChannelOrder>,
pending_requests: HashMap<LSPSRequestId, LSPS1Request>,
+ needs_persist: bool,
}
impl PeerState {
@@ -43,6 +44,7 @@ impl PeerState {
channel_details,
};
self.outbound_channels_by_order_id.insert(order_id, channel_order.clone());
+ self.needs_persist |= true;
channel_order
}
@@ -66,6 +68,7 @@ impl PeerState {
.ok_or(PeerStateError::UnknownOrderId)?;
order.order_state = order_state;
order.channel_details = channel_details;
+ self.needs_persist |= true;
Ok(())
}
@@ -88,11 +91,39 @@ impl PeerState {
pub(super) fn has_active_orders(&self) -> bool {
!self.outbound_channels_by_order_id.is_empty()
}
+
+ pub(super) fn needs_persist(&self) -> bool {
+ self.needs_persist
+ }
+
+ pub(super) fn set_needs_persist(&mut self, needs_persist: bool) {
+ self.needs_persist = needs_persist;
+ }
+
+ pub(super) fn is_prunable(&self) -> bool {
+ // Return whether the entire state is empty.
+ self.pending_requests.is_empty() && self.outbound_channels_by_order_id.is_empty()
+ }
+
+ pub(super) fn prune_pending_requests(&mut self) {
+ self.pending_requests.clear()
+ }
+
+ pub(super) fn prune_expired_request_state(&mut self) {
+ self.outbound_channels_by_order_id.retain(|_order_id, entry| {
+ if entry.is_prunable() {
+ self.needs_persist |= true;
+ return false;
+ }
+ true
+ });
+ }
}
impl_writeable_tlv_based!(PeerState, {
(0, outbound_channels_by_order_id, required),
(_unused, pending_requests, (static_value, new_hash_map())),
+ (_unused, needs_persist, (static_value, false)),
});
#[derive(Debug, Copy, Clone)]
@@ -121,6 +152,30 @@ pub(super) struct ChannelOrder {
pub(super) channel_details: Option<LSPS1ChannelInfo>,
}
+impl ChannelOrder {
+ fn is_prunable(&self) -> bool {
+ let all_payment_details_expired;
+ #[cfg(feature = "time")]
+ {
+ let details = &self.payment_details;
+ all_payment_details_expired =
+ details.bolt11.as_ref().map_or(true, |d| d.expires_at.is_past())
+ && details.bolt12.as_ref().map_or(true, |d| d.expires_at.is_past())
+ && details.onchain.as_ref().map_or(true, |d| d.expires_at.is_past());
+ }
+ #[cfg(not(feature = "time"))]
+ {
+ // TODO: We need to find a way to check expiry times in no-std builds.
+ all_payment_details_expired = false;
+ }
+
+ let created_or_failed =
+ matches!(self.order_state, LSPS1OrderState::Created | LSPS1OrderState::Failed);
+
+ all_payment_details_expired && created_or_failed
+ }
+}
+
impl_writeable_tlv_based!(ChannelOrder, {
(0, order_params, required),
(2, order_state, required),
diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs
index f4e1c1d..71587ae 100644
--- a/lightning-liquidity/src/lsps1/service.rs
+++ b/lightning-liquidity/src/lsps1/service.rs
@@ -9,9 +9,14 @@
//! Contains the main bLIP-51 / LSPS1 server object, [`LSPS1ServiceHandler`].
-use alloc::string::String;
+use alloc::string::{String, ToString};
+use alloc::vec::Vec;
+use core::future::Future as StdFuture;
use core::ops::Deref;
+use core::pin::pin;
+use core::sync::atomic::{AtomicUsize, Ordering};
+use core::task;
use super::event::LSPS1ServiceEvent;
use super::msgs::{
@@ -28,9 +33,14 @@ use crate::events::EventQueue;
use crate::lsps0::ser::{
LSPSDateTime, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError,
};
+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::sync::{Arc, Mutex, RwLock};
use crate::utils;
+use crate::utils::async_poll::dummy_waker;
use crate::utils::time::TimeProvider;
use lightning::ln::channelmanager::AChannelManager;
@@ -39,6 +49,7 @@ use lightning::sign::EntropySource;
use lightning::util::errors::APIError;
use lightning::util::logger::Level;
use lightning::util::persist::KVStore;
+use lightning::util::ser::Writeable;
use bitcoin::secp256k1::PublicKey;
@@ -63,9 +74,11 @@ pub struct LSPS1ServiceHandler<
{
entropy_source: ES,
_channel_manager: CM,
+ kv_store: K,
pending_messages: Arc<MessageQueue>,
pending_events: Arc<EventQueue<K>>,
per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>,
+ persistence_in_flight: AtomicUsize,
time_provider: TP,
config: LSPS1ServiceConfig,
}
@@ -79,15 +92,17 @@ where
/// Constructs a `LSPS1ServiceHandler`.
pub(crate) fn new(
entropy_source: ES, pending_messages: Arc<MessageQueue>,
- pending_events: Arc<EventQueue<K>>, channel_manager: CM, time_provider: TP,
+ pending_events: Arc<EventQueue<K>>, channel_manager: CM, kv_store: K, time_provider: TP,
config: LSPS1ServiceConfig,
) -> Self {
Self {
entropy_source,
_channel_manager: channel_manager,
+ kv_store,
pending_messages,
pending_events,
per_peer_state: RwLock::new(new_hash_map()),
+ persistence_in_flight: AtomicUsize::new(0),
time_provider,
config,
}
@@ -106,12 +121,153 @@ where
/// Pending requests that are still awaiting our response are deliberately NOT counted.
pub(crate) fn has_active_orders(&self, counterparty_node_id: &PublicKey) -> bool {
let outer_state_lock = self.per_peer_state.read().unwrap();
- outer_state_lock.get(counterparty_node_id).map_or(false, |inner| {
+ outer_state_lock.get(counterparty_node_id).is_some_and(|inner| {
let peer_state = inner.lock().unwrap();
peer_state.has_active_orders()
})
}
+ pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) {
+ let outer_state_lock = self.per_peer_state.write().unwrap();
+ if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) {
+ let mut peer_state_lock = inner_state_lock.lock().unwrap();
+ // We clean up the peer state, but leave removing the peer entry to the prune logic in
+ // `persist` which removes it from the store.
+ peer_state_lock.prune_pending_requests();
+ peer_state_lock.prune_expired_request_state();
+ }
+ }
+
+ pub(crate) async fn persist(&self) -> Result<bool, lightning::io::Error> {
+ // TODO: We should eventually persist in parallel, however, when we do, we probably want to
+ // introduce some batching to upper-bound the number of requests inflight at any given
+ // time.
+ let mut did_persist = false;
+
+ if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 {
+ // If we're not the first event processor to get here, just return early, the increment
+ // we just did will be treated as "go around again" at the end.
+ return Ok(did_persist);
+ }
+
+ loop {
+ let mut need_remove = Vec::new();
+ let mut need_persist = Vec::new();
+
+ {
+ // First build a list of peers to persist and prune with the read lock. This allows
+ // us to avoid the write lock unless we actually need to remove a node.
+ let outer_state_lock = self.per_peer_state.read().unwrap();
+ for (counterparty_node_id, inner_state_lock) in outer_state_lock.iter() {
+ let mut peer_state_lock = inner_state_lock.lock().unwrap();
+ peer_state_lock.prune_expired_request_state();
+ let is_prunable = peer_state_lock.is_prunable();
+ if is_prunable {
+ need_remove.push(*counterparty_node_id);
+ } else if peer_state_lock.needs_persist() {
+ need_persist.push(*counterparty_node_id);
+ }
+ }
+ }
+
+ for counterparty_node_id in need_persist.into_iter() {
+ debug_assert!(!need_remove.contains(&counterparty_node_id));
+ self.persist_peer_state(counterparty_node_id).await?;
+ did_persist = true;
+ }
+
+ for counterparty_node_id in need_remove {
+ let mut future_opt = None;
+ {
+ // We need to take the `per_peer_state` write lock to remove an entry, but also
+ // have to hold it until after the `remove` call returns (but not through
+ // future completion) to ensure that writes for the peer's state are
+ // well-ordered with other `persist_peer_state` calls even across the removal
+ // itself.
+ let mut per_peer_state = self.per_peer_state.write().unwrap();
+ if let Entry::Occupied(mut entry) = per_peer_state.entry(counterparty_node_id) {
+ let state = entry.get_mut().get_mut().unwrap();
+ if state.is_prunable() {
+ entry.remove();
+ let key = counterparty_node_id.to_string();
+ future_opt = Some(self.kv_store.remove(
+ LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
+ LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE,
+ &key,
+ true,
+ ));
+ } else {
+ // If the peer got new state, force a re-persist of the current state.
+ state.set_needs_persist(true);
+ }
+ } else {
+ // This should never happen, we can only have one `persist` call
+ // in-progress at once and map entries are only removed by it.
+ debug_assert!(false);
+ }
+ }
+ if let Some(future) = future_opt {
+ future.await?;
+ did_persist = true;
+ } else {
+ self.persist_peer_state(counterparty_node_id).await?;
+ }
+ }
+
+ if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 {
+ // If another thread incremented the state while we were running we should go
+ // around again, but only once.
+ self.persistence_in_flight.store(1, Ordering::Release);
+ continue;
+ }
+ break;
+ }
+
+ Ok(did_persist)
+ }
+
+ async fn persist_peer_state(
+ &self, counterparty_node_id: PublicKey,
+ ) -> Result<(), lightning::io::Error> {
+ let fut = {
+ let outer_state_lock = self.per_peer_state.read().unwrap();
+ match outer_state_lock.get(&counterparty_node_id) {
+ None => {
+ // We dropped the peer state by now.
+ return Ok(());
+ },
+ Some(entry) => {
+ let mut peer_state_lock = entry.lock().unwrap();
+ if !peer_state_lock.needs_persist() {
+ // We already have persisted otherwise by now.
+ return Ok(());
+ } else {
+ peer_state_lock.set_needs_persist(false);
+ let key = counterparty_node_id.to_string();
+ let encoded = peer_state_lock.encode();
+ // Begin the write with the entry lock held. This avoids racing with
+ // potentially-in-flight `persist` calls writing state for the same peer.
+ self.kv_store.write(
+ LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
+ LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE,
+ &key,
+ encoded,
+ )
+ }
+ },
+ }
+ };
+
+ fut.await.map_err(|e| {
+ self.per_peer_state
+ .read()
+ .unwrap()
+ .get(&counterparty_node_id)
+ .map(|p| p.lock().unwrap().set_needs_persist(true));
+ e
+ })
+ }
+
fn handle_get_info_request(
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
) -> Result<(), LightningError> {
@@ -180,18 +336,17 @@ where
/// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] event.
///
/// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails
- pub fn send_payment_details(
- &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey,
+ pub async fn send_payment_details(
+ &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey,
payment_details: LSPS1PaymentInfo,
) -> Result<(), APIError> {
let mut message_queue_notifier = self.pending_messages.notifier();
+ let mut should_persist = false;
- let outer_state_lock = self.per_peer_state.read().unwrap();
- match outer_state_lock.get(counterparty_node_id) {
+ match self.per_peer_state.read().unwrap().get(&counterparty_node_id) {
Some(inner_state_lock) => {
let mut peer_state_lock = inner_state_lock.lock().unwrap();
let request = peer_state_lock.remove_request(&request_id).map_err(|e| {
- debug_assert!(false, "Failed to send response due to: {}", e);
let err = format!("Failed to send response due to: {}", e);
APIError::APIMisuseError { err }
})?;
@@ -208,6 +363,7 @@ where
created_at,
payment_details,
);
+ should_persist |= peer_state_lock.needs_persist();
let response = LSPS1Response::CreateOrder(LSPS1CreateOrderResponse {
order: order.order_params,
@@ -219,8 +375,7 @@ where
channel: order.channel_details,
});
let msg = LSPS1Message::Response(request_id, response).into();
- message_queue_notifier.enqueue(counterparty_node_id, msg);
- Ok(())
+ message_queue_notifier.enqueue(&counterparty_node_id, msg);
},
t => {
debug_assert!(
@@ -236,10 +391,25 @@ where
},
}
},
- None => Err(APIError::APIMisuseError {
- err: format!("No state for the counterparty exists: {}", counterparty_node_id),
- }),
+ None => {
+ return Err(APIError::APIMisuseError {
+ err: format!("No state for the counterparty exists: {}", counterparty_node_id),
+ });
+ },
+ }
+
+ if should_persist {
+ self.persist_peer_state(counterparty_node_id).await.map_err(|e| {
+ APIError::APIMisuseError {
+ err: format!(
+ "Failed to persist peer state for {}: {}",
+ counterparty_node_id, e
+ ),
+ }
+ })?;
}
+
+ Ok(())
}
fn handle_get_order_request(
@@ -300,13 +470,12 @@ where
///
/// The LSP continously polls for checking payment confirmation on-chain or Lightning
/// and then responds to client request.
- pub fn update_order_status(
+ pub async fn update_order_status(
&self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId,
order_state: LSPS1OrderState, channel_details: Option<LSPS1ChannelInfo>,
) -> Result<(), APIError> {
- let outer_state_lock = self.per_peer_state.read().unwrap();
-
- match outer_state_lock.get(&counterparty_node_id) {
+ let mut should_persist = false;
+ match self.per_peer_state.read().unwrap().get(&counterparty_node_id) {
Some(inner_state_lock) => {
let mut peer_state_lock = inner_state_lock.lock().unwrap();
peer_state_lock.update_order(&order_id, order_state, channel_details).map_err(
@@ -314,13 +483,27 @@ where
err: format!("Failed to update order: {:?}", e),
},
)?;
-
- Ok(())
+ should_persist |= peer_state_lock.needs_persist();
+ },
+ None => {
+ return Err(APIError::APIMisuseError {
+ err: format!("No existing state with counterparty {}", counterparty_node_id),
+ });
},
- None => Err(APIError::APIMisuseError {
- err: format!("No existing state with counterparty {}", counterparty_node_id),
- }),
}
+
+ if should_persist {
+ self.persist_peer_state(counterparty_node_id).await.map_err(|e| {
+ APIError::APIMisuseError {
+ err: format!(
+ "Failed to persist peer state for {}: {}",
+ counterparty_node_id, e
+ ),
+ }
+ })?;
+ }
+
+ Ok(())
}
fn generate_order_id(&self) -> LSPS1OrderId {
@@ -364,6 +547,88 @@ where
}
}
+/// A synchroneous wrapper around [`LSPS1ServiceHandler`] to be used in contexts where async is not
+/// available.
+pub struct LSPS1ServiceHandlerSync<
+ 'a,
+ ES: EntropySource,
+ CM: Deref + Clone,
+ K: KVStore + Clone,
+ TP: Deref + Clone,
+> where
+ CM::Target: AChannelManager,
+ TP::Target: TimeProvider,
+{
+ inner: &'a LSPS1ServiceHandler<ES, CM, K, TP>,
+}
+
+impl<'a, ES: EntropySource, CM: Deref + Clone, K: KVStore + Clone, TP: Deref + Clone>
+ LSPS1ServiceHandlerSync<'a, ES, CM, K, TP>
+where
+ CM::Target: AChannelManager,
+ TP::Target: TimeProvider,
+{
+ pub(crate) fn from_inner(inner: &'a LSPS1ServiceHandler<ES, CM, K, TP>) -> Self {
+ Self { inner }
+ }
+
+ /// Returns a reference to the used config.
+ ///
+ /// Wraps [`LSPS1ServiceHandler::config`].
+ pub fn config(&self) -> &LSPS1ServiceConfig {
+ &self.inner.config
+ }
+
+ /// Used by LSP to send response containing details regarding the channel fees and payment information.
+ ///
+ /// Wraps [`LSPS1ServiceHandler::send_payment_details`].
+ pub fn send_payment_details(
+ &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey,
+ payment_details: LSPS1PaymentInfo,
+ ) -> Result<(), APIError> {
+ let mut fut = pin!(self.inner.send_payment_details(
+ request_id,
+ counterparty_node_id,
+ payment_details
+ ));
+
+ let mut waker = dummy_waker();
+ let mut ctx = task::Context::from_waker(&mut waker);
+ match fut.as_mut().poll(&mut ctx) {
+ task::Poll::Ready(result) => result,
+ task::Poll::Pending => {
+ // In a sync context, we can't wait for the future to complete.
+ unreachable!("Should not be pending in a sync context");
+ },
+ }
+ }
+
+ /// Used by LSP to give details to client regarding the status of channel opening.
+ ///
+ /// Wraps [`LSPS1ServiceHandler::update_order_status`].
+ pub fn update_order_status(
+ &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId,
+ order_state: LSPS1OrderState, channel_details: Option<LSPS1ChannelInfo>,
+ ) -> Result<(), APIError> {
+ let mut fut = pin!(self.inner.update_order_status(
+ counterparty_node_id,
+ order_id,
+ order_state,
+ channel_details
+ ));
+
+ let mut waker = dummy_waker();
+ let mut ctx = task::Context::from_waker(&mut waker);
+ match fut.as_mut().poll(&mut ctx) {
+ task::Poll::Ready(result) => result,
+ task::Poll::Pending => {
+ // In a sync context, we can't wait for the future to complete.
+ unreachable!("Should not be pending in a sync context");
+ },
+ }
+ }
+}
+
fn check_range(min: u64, max: u64, value: u64) -> bool {
(value >= min) && (value <= max)
}
diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs
index 85c8ba3..da87b4c 100644
--- a/lightning-liquidity/src/manager.rs
+++ b/lightning-liquidity/src/manager.rs
@@ -30,7 +30,7 @@ use crate::persist::{
use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler};
use crate::lsps1::msgs::LSPS1Message;
#[cfg(lsps1_service)]
-use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler};
+use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler, LSPS1ServiceHandlerSync};
use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler};
use crate::lsps2::msgs::LSPS2Message;
@@ -462,6 +462,7 @@ where
Arc::clone(&pending_messages),
Arc::clone(&pending_events),
channel_manager.clone(),
+ kv_store.clone(),
time_provider,
config.clone(),
)
@@ -623,6 +624,11 @@ where
let mut did_persist = false;
did_persist |= self.pending_events.persist().await?;
+ #[cfg(lsps1_service)]
+ if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() {
+ did_persist |= lsps1_service_handler.persist().await?;
+ }
+
if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() {
did_persist |= lsps2_service_handler.persist().await?;
}
@@ -879,6 +885,11 @@ where
// If the peer was misbehaving, drop it from the ignored list to cleanup the kept state.
self.ignored_peers.write().unwrap().remove(&counterparty_node_id);
+ #[cfg(lsps1_service)]
+ if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() {
+ lsps1_service_handler.peer_disconnected(counterparty_node_id);
+ }
+
if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() {
lsps2_service_handler.peer_disconnected(counterparty_node_id);
}
@@ -1031,10 +1042,10 @@ where
///
/// Wraps [`LiquidityManager::lsps1_service_handler`].
#[cfg(lsps1_service)]
- pub fn lsps1_service_handler(
- &self,
- ) -> Option<&LSPS1ServiceHandler<ES, CM, KVStoreSyncWrapper<KS>, TP>> {
- self.inner.lsps1_service_handler()
+ pub fn lsps1_service_handler<'a>(
+ &'a self,
+ ) -> Option<LSPS1ServiceHandlerSync<'a, ES, CM, KVStoreSyncWrapper<KS>, TP>> {
+ self.inner.lsps1_service_handler.as_ref().map(|r| LSPS1ServiceHandlerSync::from_inner(r))
}
/// Returns a reference to the LSPS2 client-side handler.
diff --git a/lightning-liquidity/src/persist.rs b/lightning-liquidity/src/persist.rs
index d019944..9518b40 100644
--- a/lightning-liquidity/src/persist.rs
+++ b/lightning-liquidity/src/persist.rs
@@ -39,6 +39,12 @@ pub const LIQUIDITY_MANAGER_EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE: &str =
/// [`LiquidityManager`]: crate::LiquidityManager
pub const LIQUIDITY_MANAGER_EVENT_QUEUE_PERSISTENCE_KEY: &str = "event_queue";
+/// The secondary namespace under which the [`LSPS1ServiceHandler`] data will be persisted.
+///
+/// [`LSPS1ServiceHandler`]: crate::lsps1::service::LSPS1ServiceHandler
+#[cfg(lsps1_service)]
+pub const LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE: &str = "lsps1_service";
+
/// The secondary namespace under which the [`LSPS2ServiceHandler`] data will be persisted.
///
/// [`LSPS2ServiceHandler`]: crate::lsps2::service::LSPS2ServiceHandler
diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs
index ef210a3..0343116 100644
--- a/lightning-liquidity/tests/lsps1_integration_tests.rs
+++ b/lightning-liquidity/tests/lsps1_integration_tests.rs
@@ -174,7 +174,7 @@ fn lsps1_happy_path() {
serde_json::from_str(json_str).expect("Failed to parse JSON");
let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) };
service_handler
- .send_payment_details(_create_order_id.clone(), &client_node_id, payment_info.clone())
+ .send_payment_details(_create_order_id.clone(), client_node_id, payment_info.clone())
.unwrap();
let create_order_response = get_lsps_message!(service_node, client_node_id);
Why this scored 29/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.