Add serialization logic for LSPS1 `PeerState` types
What changed, and why it matters
This commit adds code to save and load LSPS1 peer state data to disk. It is a routine feature addition for persistence. There is no direct evidence in the commit that it fixes a security vulnerability; it is more likely a correctness/robustness improvement for a new feature.
Review as normal feature code. Validate that deserialization of Address and Bolt11Invoice handles malformed or malicious persisted data gracefully (e.g., does not panic), and that assume_checked() is acceptable because the data was previously validated or is not used for transaction construction without further checks. No urgent security action is indicated by the commit alone.
Security signals we found
New serialization/persistence code added for peer state
Address deserialization uses assume_checked() after from_str parsing
Bolt11Invoice deserialization parses string without network/context validation
No explicit security framing in commit message or diff
Evidence from the diff
The commit introduces Writeable/Readable implementations for LSPS1 message types (LSPS1OrderId, LSPS1OrderParams, payment and channel info structs, enums) and PeerState/ChannelOrder, plus serialization support for bitcoin::Address and Bolt11Invoice. It uses the project’s TLV-based serialization macros. The change enables persistent storage of in-flight LSPS1 channel orders. No bounds checks, panic paths, or unsafe code are visibly added, and the commit message frames this as a feature addition rather than a security fix.
Changed components
lightning-liquidity/src/lsps1/msgs.rslightning-liquidity/src/lsps1/peer_state.rslightning/src/util/ser.rsInspect captured patch +133 / −1
diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs
index 4f79a13..5bf1304 100644
--- a/lightning-liquidity/src/lsps1/msgs.rs
+++ b/lightning-liquidity/src/lsps1/msgs.rs
@@ -19,8 +19,9 @@ use crate::lsps0::ser::{
};
use bitcoin::{Address, FeeRate, OutPoint};
-
use lightning::offers::offer::Offer;
+use lightning::util::ser::{Readable, Writeable};
+use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum};
use lightning_invoice::Bolt11Invoice;
use serde::{Deserialize, Serialize};
@@ -39,6 +40,23 @@ pub(crate) const LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE: i32 = 101;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
pub struct LSPS1OrderId(pub String);
+impl Writeable for LSPS1OrderId {
+ fn write<W: lightning::util::ser::Writer>(
+ &self, writer: &mut W,
+ ) -> Result<(), lightning::io::Error> {
+ self.0.write(writer)
+ }
+}
+
+impl Readable for LSPS1OrderId {
+ fn read<R: bitcoin::io::Read>(
+ reader: &mut R,
+ ) -> Result<Self, lightning::ln::msgs::DecodeError> {
+ let inner = Readable::read(reader)?;
+ Ok(Self(inner))
+ }
+}
+
/// A request made to an LSP to retrieve the supported options.
///
/// Please refer to the [bLIP-51 / LSPS1
@@ -128,6 +146,16 @@ pub struct LSPS1OrderParams {
pub announce_channel: bool,
}
+impl_writeable_tlv_based!(LSPS1OrderParams, {
+ (0, lsp_balance_sat, required),
+ (2, client_balance_sat, required),
+ (4, required_channel_confirmations, required),
+ (6, funding_confirms_within_blocks, required),
+ (8, channel_expiry_blocks, required),
+ (10, token, option),
+ (12, announce_channel, required),
+});
+
/// A response to a [`LSPS1CreateOrderRequest`].
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS1CreateOrderResponse {
@@ -158,6 +186,12 @@ pub enum LSPS1OrderState {
Failed,
}
+impl_writeable_tlv_based_enum!(LSPS1OrderState,
+ (0, Created) => {},
+ (2, Completed) => {},
+ (4, Failed) => {}
+);
+
/// Details regarding how to pay for an order.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS1PaymentInfo {
@@ -169,6 +203,12 @@ pub struct LSPS1PaymentInfo {
pub onchain: Option<LSPS1OnchainPaymentInfo>,
}
+impl_writeable_tlv_based!(LSPS1PaymentInfo, {
+ (0, bolt11, option),
+ (2, bolt12, option),
+ (4, onchain, option),
+});
+
/// A Lightning payment using BOLT 11.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS1Bolt11PaymentInfo {
@@ -186,6 +226,14 @@ pub struct LSPS1Bolt11PaymentInfo {
pub invoice: Bolt11Invoice,
}
+impl_writeable_tlv_based!(LSPS1Bolt11PaymentInfo, {
+ (0, state, required),
+ (2, expires_at, required),
+ (4, fee_total_sat, required),
+ (6, order_total_sat, required),
+ (8, invoice, required),
+});
+
/// A Lightning payment using BOLT 12.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS1Bolt12PaymentInfo {
@@ -204,6 +252,14 @@ pub struct LSPS1Bolt12PaymentInfo {
pub offer: Offer,
}
+impl_writeable_tlv_based!(LSPS1Bolt12PaymentInfo, {
+ (0, state, required),
+ (2, expires_at, required),
+ (4, fee_total_sat, required),
+ (6, order_total_sat, required),
+ (8, offer, required),
+});
+
/// An onchain payment.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS1OnchainPaymentInfo {
@@ -235,6 +291,17 @@ pub struct LSPS1OnchainPaymentInfo {
pub refund_onchain_address: Option<Address>,
}
+impl_writeable_tlv_based!(LSPS1OnchainPaymentInfo, {
+ (0, state, required),
+ (2, expires_at, required),
+ (4, fee_total_sat, required),
+ (6, order_total_sat, required),
+ (8, address, required),
+ (10, min_onchain_payment_confirmations, option),
+ (12, min_fee_for_0conf, required),
+ (14, refund_onchain_address, option),
+});
+
/// The state of a payment.
///
/// *Note*: Previously, the spec also knew a `CANCELLED` state for BOLT11 payments, which has since
@@ -251,6 +318,12 @@ pub enum LSPS1PaymentState {
Refunded,
}
+impl_writeable_tlv_based_enum!(LSPS1PaymentState,
+ (0, ExpectPayment) => {},
+ (2, Paid) => {},
+ (4, Refunded) => {}
+);
+
/// Details regarding a detected on-chain payment.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS1OnchainPayment {
@@ -274,6 +347,12 @@ pub struct LSPS1ChannelInfo {
pub expires_at: LSPSDateTime,
}
+impl_writeable_tlv_based!(LSPS1ChannelInfo, {
+ (0, funded_at, required),
+ (2, funding_outpoint, required),
+ (4, expires_at, required),
+});
+
/// A request made to an LSP to retrieve information about an previously made order.
///
/// Please refer to the [bLIP-51 / LSPS1
diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs
index 31ea41d..5af7537 100644
--- a/lightning-liquidity/src/lsps1/peer_state.rs
+++ b/lightning-liquidity/src/lsps1/peer_state.rs
@@ -17,6 +17,9 @@ use super::msgs::{
use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId};
use crate::prelude::HashMap;
+use lightning::impl_writeable_tlv_based;
+use lightning::util::hash_tables::new_hash_map;
+
use core::fmt;
#[derive(Default)]
@@ -87,6 +90,11 @@ impl PeerState {
}
}
+impl_writeable_tlv_based!(PeerState, {
+ (0, outbound_channels_by_order_id, required),
+ (_unused, pending_requests, (static_value, new_hash_map())),
+});
+
#[derive(Debug, Copy, Clone)]
pub(super) enum PeerStateError {
UnknownRequestId,
@@ -112,3 +120,11 @@ pub(super) struct ChannelOrder {
pub(super) payment_details: LSPS1PaymentInfo,
pub(super) channel_details: Option<LSPS1ChannelInfo>,
}
+
+impl_writeable_tlv_based!(ChannelOrder, {
+ (0, order_params, required),
+ (2, order_state, required),
+ (4, created_at, required),
+ (6, payment_details, required),
+ (8, channel_details, option),
+});
diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs
index 2eace55..5066515 100644
--- a/lightning/src/util/ser.rs
+++ b/lightning/src/util/ser.rs
@@ -22,10 +22,12 @@ use crate::sync::{Mutex, RwLock};
use core::cmp;
use core::hash::Hash;
use core::ops::Deref;
+use core::str::FromStr;
use alloc::collections::BTreeMap;
use bitcoin::absolute::LockTime as AbsoluteLockTime;
+use bitcoin::address::Address;
use bitcoin::amount::{Amount, SignedAmount};
use bitcoin::consensus::Encodable;
use bitcoin::constants::ChainHash;
@@ -46,6 +48,8 @@ use bitcoin::{consensus, Sequence, TxIn, Weight, Witness};
use dnssec_prover::rr::Name;
+use lightning_invoice::Bolt11Invoice;
+
use crate::chain::ClaimId;
#[cfg(taproot)]
use crate::ln::msgs::PartialSignatureWithNonce;
@@ -1499,6 +1503,39 @@ impl Readable for OutPoint {
}
}
+impl Writeable for Address {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+ self.to_string().write(w)?;
+ Ok(())
+ }
+}
+
+impl Readable for Address {
+ fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
+ let addr_string: String = Readable::read(r)?;
+ let addr = Address::from_str(&addr_string)
+ .map_err(|_| DecodeError::InvalidValue)?
+ .assume_checked();
+ Ok(addr)
+ }
+}
+
+impl Writeable for Bolt11Invoice {
+ fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+ self.to_string().write(w)?;
+ Ok(())
+ }
+}
+
+impl Readable for Bolt11Invoice {
+ fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
+ let invoice_string: String = Readable::read(r)?;
+ let invoice =
+ Bolt11Invoice::from_str(&invoice_string).map_err(|_| DecodeError::InvalidValue)?;
+ Ok(invoice)
+ }
+}
+
macro_rules! impl_consensus_ser {
($bitcoin_type: ty) => {
impl Writeable for $bitcoin_type {
Why this scored 17/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.