Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ enum NodeError {
"InvalidLnurl",
"ChainSourceNotSupported",
"InvalidPayerProof",
"LiquiditySetWebhookFailed",
"LiquidityRemoveWebhookFailed",
"LiquidityListWebhooksFailed",
"LiquidityNotifyWebhookFailed",
"LiquidityNotifyWebhookRateLimited",
"LiquidityWebhookLimitExceeded",
"LiquidityWebhookNoPriorActivity",
"LiquidityWebhookAppNameNotFound",
"LiquidityWebhookInvalid"
};

typedef dictionary NodeStatus;
Expand Down
69 changes: 56 additions & 13 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ use crate::runtime::{Runtime, RuntimeSpawner};
use crate::tx_broadcaster::TransactionBroadcaster;
use crate::types::{
AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper,
GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager,
PendingPaymentStore,
GossipSync, Graph, KeysManager, LSPS5ServiceConfig, MessageRouter, OnionMessenger,
PaymentStore, PeerManager, PendingPaymentStore,
};
use crate::wallet::persist::{read_address_pool, KVStoreWalletPersister};
use crate::wallet::Wallet;
Expand Down Expand Up @@ -149,10 +149,14 @@ struct PathfindingScoresSyncConfig {

#[derive(Debug, Clone, Default)]
struct LiquiditySourceConfig {
// Acts for both LSPS1 and LSPS2 clients connecting to the given service.
// Acts for LSPS1, LSPS2 and LSPS5 clients connecting to the given service.
lsp_nodes: Vec<LspConfig>,
// Act as an LSPS2 service.
lsps2_service: Option<LSPS2ServiceConfig>,
// Act as an LSPS5 service.
lsps5_service: Option<LSPS5ServiceConfig>,
// Indicates whether the LSPS service will be announced via the gossip network.
advertise_service: bool,
}

#[derive(Clone)]
Expand Down Expand Up @@ -545,18 +549,31 @@ impl NodeBuilder {
self
}

/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
/// channels to clients.
/// Configures the [`Node`] instance to provide [bLIP-52 / LSPS2] and/or [bLIP-55 / LSPS5]
/// services to clients.
///
/// [bLIP-52 / LSPS2] issues just-in-time channels to clients, [bLIP-55 / LSPS5] allows clients
/// to register webhooks for push notifications.
///
/// Passing `None` leaves the respective service disabled.
///
/// `advertise_service` indicates whether we'll announce LSPS support via the gossip network.
/// The announced feature bit is shared across all LSPS services, so it applies to every
/// service enabled here.
///
/// **Caution**: LSP service support is in **alpha** and is considered an experimental feature.
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md
/// [bLIP-55 / LSPS5]: https://github.com/lightning/blips/blob/master/blip-0055.md
pub fn enable_liquidity_provider(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this method now takes two Option configs plus a trailing bool, and every call site (7 in the integration tests) has had to add arguments as it grew. I also see the builder has separate methods per chain source (set_chain_source_esplora, set_chain_source_electrum, etc.) rather than one method with growing options.

Would splitting into enable_lsps2_service(cfg: LSPS2ServiceConfig, advertise: bool) and enable_lsps5_service(cfg: LSPS5ServiceConfig, advertise: bool) (or a shared set_advertise_service(bool) alongside two single-purpose enable calls) fit better? Non-breaking for a future third protocol, and each call site says exactly one thing again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The single method was a decision made during the liquidity refactor #792 (comment), so the one method with an options shape is deliberate, and LSPS1 service slots in as a third Option rather than a third method.

On the bool, advertise isn't per protocol. The LSPS feature bit is set at the liquidity level rather than per service: when a service is configured and advertise_service is true. That's why it moved out of LSPS2ServiceConfig here in the first place.

enable_lsps2_service(cfg, advertise) plus enable_lsps5_service(cfg, advertise) would put two bools behind one bit, and we'd be back to the duplication.

The docs should say the advertise part more plainly though, so I'll expand that paragraph to name the shared feature.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense misseed that advertise_service is a single shared bit rather than per-protocol state. Thanks for pointing to #792 for context too. The docs clarification sounds good.

&mut self, lsps2_service_config: LSPS2ServiceConfig,
&mut self, lsps2_service_config: Option<LSPS2ServiceConfig>,
lsps5_service_config: Option<LSPS5ServiceConfig>, advertise_service: bool,
) -> &mut Self {
let liquidity_source_config =
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
liquidity_source_config.lsps2_service = Some(lsps2_service_config);
liquidity_source_config.lsps2_service = lsps2_service_config;
liquidity_source_config.lsps5_service = lsps5_service_config;
liquidity_source_config.advertise_service = advertise_service;
self
}

Expand Down Expand Up @@ -1194,14 +1211,31 @@ impl Builder {

#[cfg(feature = "uniffi")]
impl ArcedNodeBuilder {
/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
/// channels to clients.
/// Configures the [`Node`] instance to provide [bLIP-52 / LSPS2] and/or [bLIP-55 / LSPS5]
/// services to clients.
///
/// [bLIP-52 / LSPS2] issues just-in-time channels to clients, [bLIP-55 / LSPS5] allows clients
/// to register webhooks for push notifications.
///
/// Passing `None` leaves the respective service disabled.
///
/// `advertise_service` indicates whether we'll announce LSPS support via the gossip network.
/// The announced feature bit is shared across all LSPS services, so it applies to every
/// service enabled here.
///
/// **Caution**: LSP service support is in **alpha** and is considered an experimental feature.
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
pub fn enable_liquidity_provider(&self, lsps2_service_config: LSPS2ServiceConfig) {
self.inner.write().expect("lock").enable_liquidity_provider(lsps2_service_config);
/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md
/// [bLIP-55 / LSPS5]: https://github.com/lightning/blips/blob/master/blip-0055.md
pub fn enable_liquidity_provider(
&self, lsps2_service_config: Option<LSPS2ServiceConfig>,
lsps5_service_config: Option<LSPS5ServiceConfig>, advertise_service: bool,
) {
self.inner.write().expect("lock").enable_liquidity_provider(
lsps2_service_config,
lsps5_service_config,
advertise_service,
);
}
}

Expand Down Expand Up @@ -2311,6 +2345,7 @@ fn build_with_store_internal(
Arc::clone(&tx_broadcaster),
Arc::clone(&kv_store),
Arc::clone(&config),
Arc::clone(&runtime),
Arc::clone(&logger),
);

Expand All @@ -2329,6 +2364,12 @@ fn build_with_store_internal(
lsc.lsps2_service.as_ref().map(|config| {
liquidity_source_builder.lsps2_service(promise_secret, config.clone())
});

lsc.lsps5_service
.as_ref()
.map(|config| liquidity_source_builder.lsps5_service(config.clone()));

liquidity_source_builder.set_advertise_service(lsc.advertise_service);
}

let liquidity_source = runtime
Expand Down Expand Up @@ -2389,6 +2430,8 @@ fn build_with_store_internal(

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

liquidity_source.lsps5_service().set_peer_manager(Arc::downgrade(&peer_manager));

let connection_manager = Arc::new(ConnectionManager::new(
Arc::clone(&peer_manager),
config.tor_config.clone(),
Expand Down
23 changes: 23 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::time::Duration;

use bitcoin::secp256k1::PublicKey;
use bitcoin::Network;
use lightning::chain::channelmonitor::HTLC_FAIL_BACK_BUFFER;
use lightning::ln::msgs::SocketAddress;
use lightning::routing::gossip::NodeAlias;
use lightning::routing::router::RouteParametersConfig;
Expand Down Expand Up @@ -169,6 +170,28 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY: Duration = Duration::f
// thereafter until every configured LSP has been discovered.
pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_secs(60 * 60);

// The timeout after which we abort a LSPS5 webhook notification operation.
pub(crate) const LSPS5_WEBHOOK_TIMEOUT_SECS: u64 = 30;

// The maximum size of a response body we'll accept when delivering an LSPS5 webhook notification.
pub(crate) const LSPS5_WEBHOOK_MAX_RESPONSE_SIZE: usize = 64 * 1024;

// The time in-between checks for HTLCs approaching expiry on LSPS5 clients' channels.
pub(crate) const LSPS5_EXPIRY_CHECK_INTERVAL: Duration = Duration::from_secs(60);

// The number of blocks we wait before notifying a client about the same expiring HTLCs again.
pub(crate) const LSPS5_EXPIRY_RENOTIFY_INTERVAL_BLOCKS: u32 = 6;

// The number of blocks before an outbound HTLC's expiry at which we start notifying offline
// LSPS5 clients.
//
// A client that doesn't come online and settle before `cltv_expiry` loses the payment, and LDK
// force-closes the channel shortly after (`cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS`). We anchor
// the lead time on `HTLC_FAIL_BACK_BUFFER`, the margin LDK itself treats as too close to expiry to
// safely handle an HTLC, and double it to leave the client room to receive the notification and
// act on it.
pub(crate) const LSPS5_EXPIRY_NOTIFICATION_THRESHOLD_BLOCKS: u32 = HTLC_FAIL_BACK_BUFFER * 2;

/// The mode used for tracking forwarded payments.
///
/// In either mode, a forward is tracked only when it has exactly one incoming HTLC and one outgoing
Expand Down
59 changes: 59 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,35 @@ pub enum Error {
ChainSourceNotSupported,
/// The provided payer proof is invalid.
InvalidPayerProof,
/// Failed to set a webhook with the LSP.
LiquiditySetWebhookFailed,
/// Failed to remove a webhook with the LSP.
LiquidityRemoveWebhookFailed,
/// Failed to list webhooks with the LSP.
LiquidityListWebhooksFailed,
/// Failed to send a webhook notification to a client.
LiquidityNotifyWebhookFailed,
/// The webhook notification was not sent because a notification of the same kind was sent to
/// this client too recently.
///
/// Notifications are rate limited per client, so the call may succeed when retried after the
/// LSP's cooldown period has elapsed.
LiquidityNotifyWebhookRateLimited,
/// The LSP rejected a webhook registration because the client has reached the maximum number
/// of webhooks the LSP allows.
LiquidityWebhookLimitExceeded,
/// The LSP rejected a webhook registration because we have no prior activity with it.
///
/// LSPs typically require an open channel, or an in-flight LSPS1 or LSPS2 flow, before
/// accepting webhook registrations.
LiquidityWebhookNoPriorActivity,
/// No webhook is registered under the given `app_name` with the LSP.
LiquidityWebhookAppNameNotFound,
/// The `app_name` or webhook URL is invalid.
///
/// The `app_name` may exceed 64 bytes, or the URL may exceed 1024 bytes, fail to parse, or
/// not use the `https` scheme.
LiquidityWebhookInvalid,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -249,6 +278,36 @@ impl fmt::Display for Error {
write!(f, "The configured chain source is not supported.")
},
Self::InvalidPayerProof => write!(f, "The provided payer proof is invalid."),
Self::LiquiditySetWebhookFailed => {
write!(f, "Failed to set a webhook with the LSP.")
},
Self::LiquidityRemoveWebhookFailed => {
write!(f, "Failed to remove a webhook with the LSP.")
},
Self::LiquidityListWebhooksFailed => {
write!(f, "Failed to list webhooks with the LSP.")
},
Self::LiquidityNotifyWebhookFailed => {
write!(f, "Failed to send a webhook notification to a client.")
},
Self::LiquidityNotifyWebhookRateLimited => {
write!(f, "The webhook notification was rate limited and was not sent.")
},
Self::LiquidityWebhookLimitExceeded => {
write!(
f,
"The LSP's maximum number of webhooks for this client is already reached."
)
},
Self::LiquidityWebhookNoPriorActivity => {
write!(f, "The LSP rejected the webhook registration due to no prior activity.")
},
Self::LiquidityWebhookAppNameNotFound => {
write!(f, "No webhook is registered under the given app name with this LSP.")
},
Self::LiquidityWebhookInvalid => {
write!(f, "The given app name or webhook URL is invalid.")
},
}
}
}
Expand Down
29 changes: 27 additions & 2 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ use lightning::events::bump_transaction::BumpTransactionEvent;
#[cfg(not(feature = "uniffi"))]
use lightning::events::PaidBolt12Invoice;
use lightning::events::{
ClosureReason, Event as LdkEvent, FundingInfo, InboundHTLCLocator as LdkInboundHtlcLocator,
ClosureReason, Event as LdkEvent, FundingInfo, HTLCHandlingFailureReason,
HTLCHandlingFailureType, InboundHTLCLocator as LdkInboundHtlcLocator,
OutboundHTLCLocator as LdkOutboundHtlcLocator, PaymentFailureReason, PaymentPurpose,
ReplayEvent,
};
use lightning::ln::channelmanager::{PaymentId, TrustedChannelFeatures};
use lightning::ln::onion_utils::LocalHTLCFailureReason;
use lightning::ln::types::ChannelId;
use lightning::routing::gossip::NodeId;
use lightning::sign::EntropySource;
Expand Down Expand Up @@ -1538,11 +1540,29 @@ where
prober.handle_background_probe_failed(&path, payment_id);
}
},
LdkEvent::HTLCHandlingFailed { failure_type, .. } => {
LdkEvent::HTLCHandlingFailed { failure_type, failure_reason, .. } => {
// Capture the client's node id before `failure_type` is consumed below. A forward
// that failed only because the next-hop peer was offline is our cue to wake an
// LSPS5 client. The HTLC is failed back as `temporary_channel_failure`, which is
// not permanent, so the sender can retry once the client is online.
let offline_node_id = match (&failure_type, &failure_reason) {
(
HTLCHandlingFailureType::Forward { node_id: Some(node_id), .. },
Some(HTLCHandlingFailureReason::Local {
reason: LocalHTLCFailureReason::PeerOffline,
}),
) => Some(*node_id),
_ => None,
};

self.liquidity_source
.lsps2_service()
.handle_htlc_handling_failed(failure_type)
.await;

if let Some(node_id) = offline_node_id {
self.liquidity_source.lsps5_service().notify_payment_incoming(node_id);
}
},
LdkEvent::SpendableOutputs { outputs, channel_id, counterparty_node_id } => {
match self
Expand Down Expand Up @@ -2038,6 +2058,8 @@ where
debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted.");
},
LdkEvent::ConnectionNeeded { node_id, addresses } => {
self.liquidity_source.lsps5_service().notify_onion_message_incoming(node_id);

let spawn_logger = self.logger.clone();
let spawn_cm = Arc::clone(&self.connection_manager);
let future = async move {
Expand Down Expand Up @@ -2096,6 +2118,9 @@ where
"Onion message intercepted, but no onion message mailbox available"
);
}
self.liquidity_source
.lsps5_service()
.notify_onion_message_incoming(peer_node_id);
} else {
log_error!(self.logger, "Onion message intercepted for unknown SCID");
}
Expand Down
27 changes: 26 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ pub use types::{
#[cfg(feature = "storage-vss")]
pub use vss_client;

use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY};
use crate::config::{
LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY,
LSPS5_EXPIRY_CHECK_INTERVAL,
};
use crate::ffi::{maybe_deref, maybe_wrap};
use crate::liquidity::Liquidity;
use crate::scoring::setup_background_pathfinding_scores_sync;
Expand Down Expand Up @@ -849,6 +852,28 @@ impl Node {
}
});

// Regularly notify offline LSPS5 clients about HTLCs approaching their expiry.
if self.liquidity_source.liquidity_manager().lsps5_service_handler().is_some() {
let expiry_liquidity_source = Arc::clone(&self.liquidity_source);
let expiry_liquidy_logger = Arc::clone(&self.logger);
let mut stop_expiry = self.stop_sender.subscribe();
self.runtime.spawn_cancellable_background_task(async move {
let mut interval = tokio::time::interval(LSPS5_EXPIRY_CHECK_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = stop_expiry.changed() => {
log_debug!(expiry_liquidy_logger, "Stopping LSPS5 HTLC expiry checks.");
return;
}
_ = interval.tick() => {
expiry_liquidity_source.lsps5_service().check_expiring_htlcs();
}
}
}
});
}

log_info!(self.logger, "Startup complete.");
*is_running_lock = true;
Ok(())
Expand Down
Loading
Loading