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
149 changes: 102 additions & 47 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ use crate::io::{
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
};
use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig};
use crate::liquidity::{LSPS2ServiceConfig, LSPS5ServiceConfig, LiquiditySourceBuilder, LspConfig};
use crate::lnurl_auth::LnurlAuth;
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
use crate::message_handler::NodeCustomMessageHandler;
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,30 @@ 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.
/// This signals that we act as an LSP, 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 +1210,30 @@ 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.
/// This signals that we act as an LSP, 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 @@ -2097,12 +2129,27 @@ fn build_with_store_internal(

let mut user_config = default_user_config(&config);

if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() {
let lsps2_service = liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref());
let lsps5_service = liquidity_source_config.and_then(|lsc| lsc.lsps5_service.as_ref());

if lsps2_service.is_some() || lsps5_service.is_some() {
let mut interception_flags = 0u8;

// If we act as an LSPS2 service, we need to be able to intercept HTLCs and forward the
// information to the service handler.
user_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs.into();
if lsps2_service.is_some() {
interception_flags |= HTLCInterceptionFlags::ToInterceptSCIDs as u8;
}

// As an LSPS5 service we intercept HTLCs destined for offline clients, so we can wake them
// and forward once they connect.
if lsps5_service.is_some() {
interception_flags |= HTLCInterceptionFlags::ToOfflinePrivateChannels as u8;
}

user_config.htlc_interception_flags = interception_flags;

// If we act as an LSPS2 service, we allow forwarding to unannounced channels.
// If we act as an LSPS2 or LSPS5 service, we allow forwarding to unannounced channels.
user_config.accept_forwards_to_priv_channels = true;
}

Expand Down Expand Up @@ -2247,33 +2294,36 @@ fn build_with_store_internal(
Arc::new(IgnoringMessageHandler {});

// Initialize the PeerManager
let onion_messenger: Arc<OnionMessenger> =
if let Some(AsyncPaymentsRole::Server) = async_payments_role {
Arc::new(OnionMessenger::new_with_offline_peer_interception(
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&logger),
Arc::clone(&channel_manager),
message_router,
Arc::clone(&channel_manager),
Arc::clone(&channel_manager),
Arc::clone(&om_resolver),
IgnoringMessageHandler {},
false,
))
} else {
Arc::new(OnionMessenger::new(
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&logger),
Arc::clone(&channel_manager),
message_router,
Arc::clone(&channel_manager),
Arc::clone(&channel_manager),
Arc::clone(&om_resolver),
IgnoringMessageHandler {},
))
};
// Async payments servers and LSPS5 services both hold onion messages for offline peers until
// they reconnect. LSPS5 services also wake the client up via their webhooks.
let intercept_offline_peer_messages =
matches!(async_payments_role, Some(AsyncPaymentsRole::Server)) || lsps5_service.is_some();
let onion_messenger: Arc<OnionMessenger> = if intercept_offline_peer_messages {
Arc::new(OnionMessenger::new_with_offline_peer_interception(
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&logger),
Arc::clone(&channel_manager),
message_router,
Arc::clone(&channel_manager),
Arc::clone(&channel_manager),
Arc::clone(&om_resolver),
IgnoringMessageHandler {},
false,
))
} else {
Arc::new(OnionMessenger::new(
Arc::clone(&keys_manager),
Arc::clone(&keys_manager),
Arc::clone(&logger),
Arc::clone(&channel_manager),
message_router,
Arc::clone(&channel_manager),
Arc::clone(&channel_manager),
Arc::clone(&om_resolver),
IgnoringMessageHandler {},
))
};
let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes();

// Initialize the GossipSource
Expand Down Expand Up @@ -2311,6 +2361,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 +2380,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 +2446,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 Expand Up @@ -2441,11 +2500,7 @@ fn build_with_store_internal(
},
};

let om_mailbox = if let Some(AsyncPaymentsRole::Server) = async_payments_role {
Some(Arc::new(OnionMessageMailbox::new()))
} else {
None
};
let om_mailbox = intercept_offline_peer_messages.then(|| Arc::new(OnionMessageMailbox::new()));

let lnurl_auth = Arc::new(LnurlAuth::new(xprv, Arc::clone(&logger)));

Expand Down
31 changes: 31 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,36 @@ 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 HTLC's deadline at which we start notifying offline LSPS5 clients.
//
// A client that doesn't come online and settle an HTLC before its deadline costs us the channel.
// 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;

// How long we hold an HTLC intercepted for an offline LSPS5 client while waiting for them to come online.
//
// LDK requires intercepted HTLCs to be forwarded or failed within a few seconds, so this is a hard
// ceiling on the wake-up we can offer: a client that misses it has to be paid by a retry from the
// sender, which by then will find them online.
pub(crate) const LSPS5_INTERCEPT_HOLD_TIMEOUT: Duration = Duration::from_secs(10);

// How often we re-check whether a woken LSPS5 client's channel is ready to forward over.
pub(crate) const LSPS5_INTERCEPT_POLL_INTERVAL: Duration = Duration::from_millis(200);

/// 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
Loading
Loading