Implement serialization for LSPS2 state types
What changed, and why it matters
This commit adds data-saving (serialization) support for internal LSPS2 state types in the lightning-liquidity crate. It does not change network behavior, fix a bug, or alter security checks. It is a routine feature addition that enables these objects to be written to disk or restored later.
No security action required. Review as normal feature code.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces Writeable/Readable implementations for LSPS2-related types (LSPSRequestId, LSPSDateTime, LSPS2OpeningFeeParams, PaymentQueue, OutboundJITChannelState, OutboundJITChannel, PeerState) using the lightning-macros macros impl_writeable_tlv_based and impl_writeable_tlv_based_enum. It also adds a Cargo.toml dependency on lightning-macros and a round-trip test for LSPSDateTime serialization. No deserialization bounds, input validation, cryptographic, or consensus logic is changed.
Changed components
lightning-liquidity/src/lsps0/ser.rslightning-liquidity/src/lsps2/msgs.rslightning-liquidity/src/lsps2/payment_queue.rslightning-liquidity/src/lsps2/service.rslightning-liquidity/Cargo.tomlInspect captured patch +117 / −1
diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml
index ff270a7..a29fc36 100644
--- a/lightning-liquidity/Cargo.toml
+++ b/lightning-liquidity/Cargo.toml
@@ -24,6 +24,7 @@ _test_utils = []
lightning = { version = "0.2.0", path = "../lightning", default-features = false }
lightning-types = { version = "0.3.0", path = "../lightning-types", default-features = false }
lightning-invoice = { version = "0.34.0", path = "../lightning-invoice", default-features = false, features = ["serde"] }
+lightning-macros = { version = "0.2", path = "../lightning-macros" }
bitcoin = { version = "0.32.2", default-features = false, features = ["serde"] }
diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs
index 213e276..70649fe 100644
--- a/lightning-liquidity/src/lsps0/ser.rs
+++ b/lightning-liquidity/src/lsps0/ser.rs
@@ -30,7 +30,7 @@ use crate::prelude::HashMap;
use lightning::ln::msgs::{DecodeError, LightningError};
use lightning::ln::wire;
-use lightning::util::ser::{LengthLimitedRead, LengthReadable, WithoutLength};
+use lightning::util::ser::{LengthLimitedRead, LengthReadable, Readable, WithoutLength, Writeable};
use bitcoin::secp256k1::PublicKey;
@@ -217,6 +217,22 @@ impl wire::Type for RawLSPSMessage {
#[serde(transparent)]
pub struct LSPSRequestId(pub String);
+impl Writeable for LSPSRequestId {
+ fn write<W: lightning::util::ser::Writer>(
+ &self, writer: &mut W,
+ ) -> Result<(), lightning::io::Error> {
+ self.0.write(writer)?;
+ Ok(())
+ }
+}
+
+impl Readable for LSPSRequestId {
+ fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+ let s: String = Readable::read(reader)?;
+ Ok(Self(s))
+ }
+}
+
/// An object representing datetimes as described in bLIP-50 / LSPS0.
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(transparent)]
@@ -266,6 +282,23 @@ impl Display for LSPSDateTime {
}
}
+impl Writeable for LSPSDateTime {
+ fn write<W: lightning::util::ser::Writer>(
+ &self, writer: &mut W,
+ ) -> Result<(), lightning::io::Error> {
+ self.to_rfc3339().write(writer)?;
+ Ok(())
+ }
+}
+
+impl Readable for LSPSDateTime {
+ fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+ let s: String = Readable::read(reader)?;
+ let val = Self::from_str(&s).map_err(|_| lightning::ln::msgs::DecodeError::InvalidValue)?;
+ Ok(val)
+ }
+}
+
/// An error returned in response to an JSON-RPC request.
///
/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#error_object) for
@@ -933,3 +966,19 @@ pub(crate) mod u32_fee_rate {
Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_kwu as u64))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use lightning::io::Cursor;
+
+ #[test]
+ fn datetime_serializaton() {
+ let expected_datetime = LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap();
+ let mut buf = Vec::new();
+ expected_datetime.write(&mut buf).unwrap();
+ let decoded_datetime: LSPSDateTime = Readable::read(&mut Cursor::new(buf)).unwrap();
+ assert_eq!(expected_datetime, decoded_datetime);
+ }
+}
diff --git a/lightning-liquidity/src/lsps2/msgs.rs b/lightning-liquidity/src/lsps2/msgs.rs
index 699e5a3..21e1af8 100644
--- a/lightning-liquidity/src/lsps2/msgs.rs
+++ b/lightning-liquidity/src/lsps2/msgs.rs
@@ -21,6 +21,7 @@ use bitcoin::secp256k1::PublicKey;
use serde::{Deserialize, Serialize};
+use lightning::impl_writeable_tlv_based;
use lightning::util::scid_utils;
use crate::lsps0::ser::{
@@ -122,6 +123,17 @@ pub struct LSPS2OpeningFeeParams {
pub promise: String,
}
+impl_writeable_tlv_based!(LSPS2OpeningFeeParams, {
+ (0, min_fee_msat, required),
+ (2, proportional, required),
+ (4, valid_until, required),
+ (6, min_lifetime, required),
+ (8, max_client_to_self_delay, required),
+ (10, min_payment_size_msat, required),
+ (12, max_payment_size_msat, required),
+ (14, promise, required),
+});
+
/// A response to a [`LSPS2GetInfoRequest`]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LSPS2GetInfoResponse {
diff --git a/lightning-liquidity/src/lsps2/payment_queue.rs b/lightning-liquidity/src/lsps2/payment_queue.rs
index d6474dc..003939d 100644
--- a/lightning-liquidity/src/lsps2/payment_queue.rs
+++ b/lightning-liquidity/src/lsps2/payment_queue.rs
@@ -9,6 +9,7 @@
use alloc::vec::Vec;
+use lightning::impl_writeable_tlv_based;
use lightning::ln::channelmanager::InterceptId;
use lightning_types::payment::PaymentHash;
@@ -62,12 +63,21 @@ impl PaymentQueue {
}
}
+impl_writeable_tlv_based!(PaymentQueue, {
+ (0, payments, optional_vec),
+});
+
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct PaymentQueueEntry {
pub(crate) payment_hash: PaymentHash,
pub(crate) htlcs: Vec<InterceptedHTLC>,
}
+impl_writeable_tlv_based!(PaymentQueueEntry, {
+ (0, payment_hash, required),
+ (2, htlcs, optional_vec),
+});
+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) struct InterceptedHTLC {
pub(crate) intercept_id: InterceptId,
@@ -75,6 +85,12 @@ pub(crate) struct InterceptedHTLC {
pub(crate) payment_hash: PaymentHash,
}
+impl_writeable_tlv_based!(InterceptedHTLC, {
+ (0, intercept_id, required),
+ (2, expected_outbound_amount_msat, required),
+ (4, payment_hash, required),
+});
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs
index 3aeca0e..d61ad70 100644
--- a/lightning-liquidity/src/lsps2/service.rs
+++ b/lightning-liquidity/src/lsps2/service.rs
@@ -38,6 +38,7 @@ use lightning::ln::msgs::{ErrorAction, LightningError};
use lightning::ln::types::ChannelId;
use lightning::util::errors::APIError;
use lightning::util::logger::Level;
+use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum};
use lightning_types::payment::PaymentHash;
@@ -372,6 +373,29 @@ impl OutboundJITChannelState {
}
}
+impl_writeable_tlv_based_enum!(OutboundJITChannelState,
+ (0, PendingInitialPayment) => {
+ (0, payment_queue, required),
+ },
+ (2, PendingChannelOpen) => {
+ (0, payment_queue, required),
+ (2, opening_fee_msat, required),
+ },
+ (4, PendingPaymentForward) => {
+ (0, payment_queue, required),
+ (2, opening_fee_msat, required),
+ (4, channel_id, required),
+ },
+ (6, PendingPayment) => {
+ (0, payment_queue, required),
+ (2, opening_fee_msat, required),
+ (4, channel_id, required),
+ },
+ (8, PaymentForwarded) => {
+ (0, channel_id, required),
+ },
+);
+
struct OutboundJITChannel {
state: OutboundJITChannelState,
user_channel_id: u128,
@@ -379,6 +403,13 @@ struct OutboundJITChannel {
payment_size_msat: Option<u64>,
}
+impl_writeable_tlv_based!(OutboundJITChannel, {
+ (0, state, required),
+ (2, user_channel_id, required),
+ (4, opening_fee_params, required),
+ (6, payment_size_msat, option),
+});
+
impl OutboundJITChannel {
fn new(
payment_size_msat: Option<u64>, opening_fee_params: LSPS2OpeningFeeParams,
@@ -492,6 +523,13 @@ impl PeerState {
}
}
+impl_writeable_tlv_based!(PeerState, {
+ (0, outbound_channels_by_intercept_scid, required),
+ (2, intercept_scid_by_user_channel_id, required),
+ (4, intercept_scid_by_channel_id, required),
+ (_unused, pending_requests, (static_value, new_hash_map())),
+});
+
macro_rules! get_or_insert_peer_state_entry {
($self: ident, $outer_state_lock: expr, $message_queue_notifier: expr, $counterparty_node_id: expr) => {{
// Return an internal error and abort if we hit the maximum allowed number of total peers.
Why this scored 15/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.