Implement serialization for LSPS5 state types
What changed, and why it matters
This commit adds serialization support for LSPS5 (webhook-related) state types so they can be saved to disk and restored. It is a routine feature patch: it makes app names, webhook URLs, webhook records, and peer state persistable. There is no direct evidence in the commit that this fixes an active security bug, but adding serialization can indirectly improve reliability and reduce state-loss risks.
No immediate security action required. Review the TLV serialization definitions for forward/backward compatibility and ensure that persisted state is stored with appropriate filesystem permissions. Consider whether webhook URLs deserialized from disk should be re-validated on load.
Security signals we found
Serialization trait implementations added for state persistence
No changes to input validation, authentication, or authorization
No cryptographic operations modified
No memory-unsafe code introduced
Refactor of LSPSUrl struct shape only, not parsing rules
Evidence from the diff
The diff implements the Writeable and Readable traits for LSPS5AppName, LSPS5WebhookUrl, and LSPSUrl, and uses impl_writeable_tlv_based! for Webhook and PeerState. It also refactors LSPSUrl from a named-field struct to a tuple struct to match the serialization pattern used elsewhere. No validation logic, URL parsing, or access controls are changed.
Changed components
lightning-liquidity/src/lsps5/msgs.rslightning-liquidity/src/lsps5/service.rslightning-liquidity/src/lsps5/url_utils.rsInspect captured patch +64 / −7
diff --git a/lightning-liquidity/src/lsps5/msgs.rs b/lightning-liquidity/src/lsps5/msgs.rs
index c45e188..f1ef06d 100644
--- a/lightning-liquidity/src/lsps5/msgs.rs
+++ b/lightning-liquidity/src/lsps5/msgs.rs
@@ -16,6 +16,8 @@ use crate::lsps0::ser::LSPSResponseError;
use super::url_utils::LSPSUrl;
+use lightning::ln::msgs::DecodeError;
+use lightning::util::ser::{Readable, Writeable};
use lightning_types::string::UntrustedString;
use serde::de::{self, Deserializer, MapAccess, Visitor};
@@ -288,6 +290,20 @@ impl From<LSPS5Error> for LSPSResponseError {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LSPS5AppName(UntrustedString);
+impl Writeable for LSPS5AppName {
+ fn write<W: lightning::util::ser::Writer>(
+ &self, writer: &mut W,
+ ) -> Result<(), lightning::io::Error> {
+ self.0.write(writer)
+ }
+}
+
+impl Readable for LSPS5AppName {
+ fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+ Ok(Self(Readable::read(reader)?))
+ }
+}
+
impl LSPS5AppName {
/// Create a new LSPS5 app name.
pub fn new(app_name: String) -> Result<Self, LSPS5Error> {
@@ -430,6 +446,20 @@ impl From<LSPS5WebhookUrl> for String {
}
}
+impl Writeable for LSPS5WebhookUrl {
+ fn write<W: lightning::util::ser::Writer>(
+ &self, writer: &mut W,
+ ) -> Result<(), lightning::io::Error> {
+ self.0.write(writer)
+ }
+}
+
+impl Readable for LSPS5WebhookUrl {
+ fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+ Ok(Self(Readable::read(reader)?))
+ }
+}
+
/// Parameters for `lsps5.set_webhook` request.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SetWebhookRequest {
diff --git a/lightning-liquidity/src/lsps5/service.rs b/lightning-liquidity/src/lsps5/service.rs
index 9f0a802..e9a8eff 100644
--- a/lightning-liquidity/src/lsps5/service.rs
+++ b/lightning-liquidity/src/lsps5/service.rs
@@ -23,6 +23,7 @@ use crate::utils::time::TimeProvider;
use bitcoin::secp256k1::PublicKey;
+use lightning::impl_writeable_tlv_based;
use lightning::ln::channelmanager::AChannelManager;
use lightning::ln::msgs::{ErrorAction, LightningError};
use lightning::sign::NodeSigner;
@@ -58,6 +59,14 @@ struct Webhook {
last_notification_sent: Option<LSPSDateTime>,
}
+impl_writeable_tlv_based!(Webhook, {
+ (0, _app_name, required),
+ (2, url, required),
+ (4, _counterparty_node_id, required),
+ (6, last_used, required),
+ (8, last_notification_sent, option),
+});
+
/// Server-side configuration options for LSPS5 Webhook Registration.
#[derive(Clone, Debug)]
pub struct LSPS5ServiceConfig {
@@ -647,3 +656,7 @@ impl PeerState {
self.webhooks.is_empty()
}
}
+
+impl_writeable_tlv_based!(PeerState, {
+ (0, webhooks, required_vec),
+});
diff --git a/lightning-liquidity/src/lsps5/url_utils.rs b/lightning-liquidity/src/lsps5/url_utils.rs
index 139b5e2..c9d5f9e 100644
--- a/lightning-liquidity/src/lsps5/url_utils.rs
+++ b/lightning-liquidity/src/lsps5/url_utils.rs
@@ -11,15 +11,15 @@
use super::msgs::LSPS5ProtocolError;
+use lightning::ln::msgs::DecodeError;
+use lightning::util::ser::{Readable, Writeable};
use lightning_types::string::UntrustedString;
use alloc::string::String;
/// Represents a parsed URL for LSPS5 webhook notifications.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct LSPSUrl {
- url: UntrustedString,
-}
+pub struct LSPSUrl(UntrustedString);
impl LSPSUrl {
/// Parses a URL string into a URL instance.
@@ -66,17 +66,17 @@ impl LSPSUrl {
None => {},
};
- Ok(LSPSUrl { url: UntrustedString(url_str) })
+ Ok(LSPSUrl(UntrustedString(url_str)))
}
/// Returns URL length.
pub fn url_length(&self) -> usize {
- self.url.0.chars().count()
+ self.0 .0.chars().count()
}
/// Returns the full URL string.
pub fn url(&self) -> &str {
- self.url.0.as_str()
+ self.0 .0.as_str()
}
fn is_valid_url_char(c: char) -> bool {
@@ -89,6 +89,20 @@ impl LSPSUrl {
}
}
+impl Writeable for LSPSUrl {
+ fn write<W: lightning::util::ser::Writer>(
+ &self, writer: &mut W,
+ ) -> Result<(), lightning::io::Error> {
+ self.0.write(writer)
+ }
+}
+
+impl Readable for LSPSUrl {
+ fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+ Ok(Self(Readable::read(reader)?))
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -104,7 +118,7 @@ mod tests {
assert!(result.is_ok());
let url = result.unwrap();
- assert_eq!(url.url.0.chars().count(), url_chars);
+ assert_eq!(url.0 .0.chars().count(), url_chars);
}
#[test]
Why this scored 20/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.