Skip to content

Commit ee7e69c

Browse files
committed
Add LSPS5 (bLIP-55) webhook notification support
Implement the bLIP-55 / LSPS5 webhook registration protocol on top of the multi-LSP liquidity module (src/liquidity/{client,service}). Client side, exposed via Node::liquidity().lsps5(): - set_webhook / list_webhooks / remove_webhook to manage webhook registrations with an LSP. - When no node_id is given, set_webhook and remove_webhook fan out to every LSPS5-capable LSP so a webhook can be configured once across all configured LSPs; set_webhook returns one result per LSP that accepted the registration and remove_webhook returns the LSPs it was removed from. Service side, enabled via Builder::enable_liquidity_provider_lsps5(): - Deliver outgoing webhook notifications over HTTP in response to LSPS5ServiceEvent::SendWebhookNotification. - Automatically send an onion-message-incoming notification when an intercepted onion message targets a client that is currently offline (wired from LdkEvent::OnionMessageIntercepted, gated on peer connectivity). Wires the feature through the UniFFI bindings and adds LSPS5-specific Error variants.
1 parent d9b8f7b commit ee7e69c

12 files changed

Lines changed: 1258 additions & 20 deletions

File tree

bindings/ldk_node.udl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ enum NodeError {
249249
"InvalidLnurl",
250250
"ChainSourceNotSupported",
251251
"InvalidPayerProof",
252+
"LiquiditySetWebhookFailed",
253+
"LiquidityRemoveWebhookFailed",
254+
"LiquidityListWebhooksFailed",
255+
"LiquidityNotifyWebhookFailed"
252256
};
253257

254258
typedef dictionary NodeStatus;

src/builder.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ use crate::runtime::{Runtime, RuntimeSpawner};
8383
use crate::tx_broadcaster::TransactionBroadcaster;
8484
use crate::types::{
8585
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
86-
GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore,
87-
PeerManager, PendingPaymentStore,
86+
GossipSync, Graph, HRNResolver, KeysManager, LSPS5ServiceConfig, MessageRouter, OnionMessenger,
87+
PaymentStore, PeerManager, PendingPaymentStore,
8888
};
8989
use crate::wallet::persist::{read_address_pool, KVStoreWalletPersister};
9090
use crate::wallet::Wallet;
@@ -127,10 +127,12 @@ struct PathfindingScoresSyncConfig {
127127

128128
#[derive(Debug, Clone, Default)]
129129
struct LiquiditySourceConfig {
130-
// Acts for both LSPS1 and LSPS2 clients connecting to the given service.
130+
// Acts for LSPS1, LSPS2 and LSPS5 clients connecting to the given service.
131131
lsp_nodes: Vec<LspConfig>,
132132
// Act as an LSPS2 service.
133133
lsps2_service: Option<LSPS2ServiceConfig>,
134+
// Act as an LSPS5 service.
135+
lsps5_service: Option<LSPS5ServiceConfig>,
134136
}
135137

136138
#[derive(Clone)]
@@ -522,6 +524,21 @@ impl NodeBuilder {
522524
self
523525
}
524526

527+
/// Configures the [`Node`] instance to provide an [bLIP-55 / LSPS5] service, enabling clients
528+
/// to register webhooks for push notifications.
529+
///
530+
/// **Caution**: LSP service support is in **alpha** and is considered an experimental feature.
531+
///
532+
/// [bLIP-55 / LSPS5]: https://github.com/lightning/blips/blob/master/blip-0055.md
533+
pub fn enable_liquidity_provider_lsps5(
534+
&mut self, lsps5_service_config: LSPS5ServiceConfig,
535+
) -> &mut Self {
536+
let liquidity_source_config =
537+
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
538+
liquidity_source_config.lsps5_service = Some(lsps5_service_config);
539+
self
540+
}
541+
525542
/// Sets the used storage directory path.
526543
pub fn set_storage_dir_path(&mut self, storage_dir_path: String) -> &mut Self {
527544
self.config.storage_dir_path = storage_dir_path;
@@ -1122,6 +1139,16 @@ impl ArcedNodeBuilder {
11221139
self.inner.write().expect("lock").enable_liquidity_provider(lsps2_service_config);
11231140
}
11241141

1142+
/// Configures the [`Node`] instance to provide an [bLIP-55 / LSPS5] service, enabling clients
1143+
/// to register webhooks for push notifications.
1144+
///
1145+
/// **Caution**: LSP service support is in **alpha** and is considered an experimental feature.
1146+
///
1147+
/// [bLIP-55 / LSPS5]: https://github.com/lightning/blips/blob/master/blip-0055.md
1148+
pub fn enable_liquidity_provider_lsps5(&self, lsps5_service_config: LSPS5ServiceConfig) {
1149+
self.inner.write().expect("lock").enable_liquidity_provider_lsps5(lsps5_service_config);
1150+
}
1151+
11251152
/// Sets the used storage directory path.
11261153
pub fn set_storage_dir_path(&self, storage_dir_path: String) {
11271154
self.inner.write().expect("lock").set_storage_dir_path(storage_dir_path);
@@ -2148,6 +2175,7 @@ fn build_with_store_internal(
21482175
Arc::clone(&tx_broadcaster),
21492176
Arc::clone(&kv_store),
21502177
Arc::clone(&config),
2178+
Arc::clone(&runtime),
21512179
Arc::clone(&logger),
21522180
);
21532181

@@ -2166,6 +2194,10 @@ fn build_with_store_internal(
21662194
lsc.lsps2_service.as_ref().map(|config| {
21672195
liquidity_source_builder.lsps2_service(promise_secret, config.clone())
21682196
});
2197+
2198+
lsc.lsps5_service
2199+
.as_ref()
2200+
.map(|config| liquidity_source_builder.lsps5_service(config.clone()));
21692201
}
21702202

21712203
let liquidity_source = runtime
@@ -2225,6 +2257,8 @@ fn build_with_store_internal(
22252257

22262258
liquidity_source.lsps2_service().set_peer_manager(Arc::downgrade(&peer_manager));
22272259

2260+
liquidity_source.lsps5_service().set_peer_manager(Arc::downgrade(&peer_manager));
2261+
22282262
let connection_manager = Arc::new(ConnectionManager::new(
22292263
Arc::clone(&peer_manager),
22302264
config.tor_config.clone(),

src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY: Duration = Duration::f
152152
// thereafter until every configured LSP has been discovered.
153153
pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_secs(60 * 60);
154154

155+
// The timeout after which we abort a LSPS5 webhook notification operation.
156+
pub(crate) const LSPS5_WEBHOOK_TIMEOUT_SECS: u64 = 30;
157+
158+
// The maximum size of a response body we'll accept when delivering an LSPS5 webhook notification.
159+
pub(crate) const LSPS5_WEBHOOK_MAX_RESPONSE_SIZE: usize = 64 * 1024;
160+
155161
#[derive(Debug, Clone)]
156162
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
157163
/// Represents the configuration of an [`Node`] instance.

src/error.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ pub enum Error {
143143
ChainSourceNotSupported,
144144
/// The provided payer proof is invalid.
145145
InvalidPayerProof,
146+
/// Failed to set a webhook with the LSP.
147+
LiquiditySetWebhookFailed,
148+
/// Failed to remove a webhook with the LSP.
149+
LiquidityRemoveWebhookFailed,
150+
/// Failed to list webhooks with the LSP.
151+
LiquidityListWebhooksFailed,
152+
/// Failed to send a webhook notification to a client.
153+
LiquidityNotifyWebhookFailed,
146154
}
147155

148156
impl fmt::Display for Error {
@@ -233,6 +241,18 @@ impl fmt::Display for Error {
233241
write!(f, "The configured chain source is not supported.")
234242
},
235243
Self::InvalidPayerProof => write!(f, "The provided payer proof is invalid."),
244+
Self::LiquiditySetWebhookFailed => {
245+
write!(f, "Failed to set a webhook with the LSP.")
246+
},
247+
Self::LiquidityRemoveWebhookFailed => {
248+
write!(f, "Failed to remove a webhook with the LSP.")
249+
},
250+
Self::LiquidityListWebhooksFailed => {
251+
write!(f, "Failed to list webhooks with the LSP.")
252+
},
253+
Self::LiquidityNotifyWebhookFailed => {
254+
write!(f, "Failed to send a webhook notification to a client.")
255+
},
236256
}
237257
}
238258
}

src/event.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ use lightning::events::bump_transaction::BumpTransactionEvent;
1919
#[cfg(not(feature = "uniffi"))]
2020
use lightning::events::PaidBolt12Invoice;
2121
use lightning::events::{
22-
ClosureReason, Event as LdkEvent, FundingInfo, HTLCLocator as LdkHtlcLocator,
23-
PaymentFailureReason, PaymentPurpose, ReplayEvent,
22+
ClosureReason, Event as LdkEvent, FundingInfo, HTLCHandlingFailureReason,
23+
HTLCHandlingFailureType, HTLCLocator as LdkHtlcLocator, PaymentFailureReason, PaymentPurpose,
24+
ReplayEvent,
2425
};
2526
use lightning::ln::channelmanager::{PaymentId, TrustedChannelFeatures};
27+
use lightning::ln::onion_utils::LocalHTLCFailureReason;
2628
use lightning::ln::types::ChannelId;
2729
use lightning::routing::gossip::NodeId;
2830
use lightning::sign::EntropySource;
@@ -1534,11 +1536,29 @@ where
15341536
prober.handle_background_probe_failed(&path, payment_id);
15351537
}
15361538
},
1537-
LdkEvent::HTLCHandlingFailed { failure_type, .. } => {
1539+
LdkEvent::HTLCHandlingFailed { failure_type, failure_reason, .. } => {
1540+
// Capture the client's node id before `failure_type` is consumed below. A forward
1541+
// that failed only because the next-hop peer was offline is our cue to wake an
1542+
// LSPS5 client. The HTLC is failed back as `temporary_channel_failure`, which is
1543+
// not permanent, so the sender can retry once the client is online.
1544+
let offline_node_id = match (&failure_type, &failure_reason) {
1545+
(
1546+
HTLCHandlingFailureType::Forward { node_id: Some(node_id), .. },
1547+
Some(HTLCHandlingFailureReason::Local {
1548+
reason: LocalHTLCFailureReason::PeerOffline,
1549+
}),
1550+
) => Some(*node_id),
1551+
_ => None,
1552+
};
1553+
15381554
self.liquidity_source
15391555
.lsps2_service()
15401556
.handle_htlc_handling_failed(failure_type)
15411557
.await;
1558+
1559+
if let Some(node_id) = offline_node_id {
1560+
self.liquidity_source.lsps5_service().notify_payment_incoming(node_id);
1561+
}
15421562
},
15431563
LdkEvent::SpendableOutputs { outputs, channel_id, counterparty_node_id } => {
15441564
match self
@@ -2022,6 +2042,8 @@ where
20222042
debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted.");
20232043
},
20242044
LdkEvent::ConnectionNeeded { node_id, addresses } => {
2045+
self.liquidity_source.lsps5_service().notify_onion_message_incoming(node_id);
2046+
20252047
let spawn_logger = self.logger.clone();
20262048
let spawn_cm = Arc::clone(&self.connection_manager);
20272049
let future = async move {
@@ -2080,6 +2102,9 @@ where
20802102
"Onion message intercepted, but no onion message mailbox available"
20812103
);
20822104
}
2105+
self.liquidity_source
2106+
.lsps5_service()
2107+
.notify_onion_message_incoming(peer_node_id);
20832108
} else {
20842109
log_error!(self.logger, "Onion message intercepted for unknown SCID");
20852110
}

0 commit comments

Comments
 (0)