Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bLIP-52 / LSPS2 service-side support #420

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ dictionary EsploraSyncConfig {
u64 fee_rate_cache_update_interval_secs;
};

dictionary LSPS2ServiceConfig {
string? require_token;
boolean advertise_service;
u32 channel_opening_fee_ppm;
u32 channel_over_provisioning_ppm;
u64 min_channel_opening_fee_msat;
u32 min_channel_lifetime;
u32 max_client_to_self_delay;
u64 min_payment_size_msat;
u64 max_payment_size_msat;
};

enum LogLevel {
"Gossip",
"Trace",
Expand Down
159 changes: 113 additions & 46 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use crate::gossip::GossipSource;
use crate::io::sqlite_store::SqliteStore;
use crate::io::utils::{read_node_metrics, write_node_metrics};
use crate::io::vss_store::VssStore;
use crate::liquidity::LiquiditySourceBuilder;
use crate::liquidity::{
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
};
use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger};
use crate::message_handler::NodeCustomMessageHandler;
use crate::payment::store::PaymentStore;
Expand Down Expand Up @@ -75,6 +77,10 @@ use std::sync::{Arc, Mutex, RwLock};
use std::time::SystemTime;
use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider};

const VSS_HARDENED_CHILD_INDEX: u32 = 877;
const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138;
const LSPS_HARDENED_CHILD_INDEX: u32 = 577;

#[derive(Debug, Clone)]
enum ChainDataSourceConfig {
Esplora { server_url: String, sync_config: Option<EsploraSyncConfig> },
Expand All @@ -94,18 +100,14 @@ enum GossipSourceConfig {
RapidGossipSync(String),
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
struct LiquiditySourceConfig {
// LSPS1 service's (node_id, address, token)
lsps1_service: Option<(PublicKey, SocketAddress, Option<String>)>,
// LSPS2 service's (node_id, address, token)
lsps2_service: Option<(PublicKey, SocketAddress, Option<String>)>,
}

impl Default for LiquiditySourceConfig {
fn default() -> Self {
Self { lsps1_service: None, lsps2_service: None }
}
// Act as an LSPS1 client connecting to the given service.
lsps1_client: Option<LSPS1ClientConfig>,
// Act as an LSPS2 client connecting to the given service.
lsps2_client: Option<LSPS2ClientConfig>,
// Act as an LSPS2 service.
lsps2_service: Option<LSPS2ServiceConfig>,
}

#[derive(Clone)]
Expand Down Expand Up @@ -317,7 +319,8 @@ impl NodeBuilder {

let liquidity_source_config =
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
liquidity_source_config.lsps1_service = Some((node_id, address, token));
let lsps1_client_config = LSPS1ClientConfig { node_id, address, token };
liquidity_source_config.lsps1_client = Some(lsps1_client_config);
self
}

Expand All @@ -337,7 +340,23 @@ impl NodeBuilder {

let liquidity_source_config =
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
liquidity_source_config.lsps2_service = Some((node_id, address, token));
let lsps2_client_config = LSPS2ClientConfig { node_id, address, token };
liquidity_source_config.lsps2_client = Some(lsps2_client_config);
self
}

/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
/// channels to clients.
///
/// **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 set_liquidity_provider_lsps2(
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we have probably used setX in names because it is builder but it is getting slightly confusing with lsps client and service.
Personally, I was slightly confused due to the fact "we need to set liquidity provider to act as lsps-client".

no strong opinion, but imo, these methods could be renamed to enable_lsps2_service, enable_lsps2_client and so on.
what do you think?

alternatively, we can add another layer here similar to .bolt11() and .bolt12().

Copy link
Collaborator Author

@tnull tnull Feb 14, 2025

Choose a reason for hiding this comment

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

Hmm, thanks for bringing this up. I see you point in that theset_liquidity_provider case that could be a bit confusing, but still think I'd like to keep the set_ pattern everywhere for now. For one, it follows the Rust API guidelines for naming setters. Also note that once we break the pattern, we might need to revisit all of the builder methods, which a) would be a larger refactor out-of-scope of this PR and b) would make the API less predicatable, IMO. But noted.

&mut self, service_config: LSPS2ServiceConfig,
) -> &mut Self {
let liquidity_source_config =
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
liquidity_source_config.lsps2_service = Some(service_config);
self
}

Expand Down Expand Up @@ -464,10 +483,14 @@ impl NodeBuilder {

let config = Arc::new(self.config.clone());

let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?;
let vss_xprv =
derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?;

let lnurl_auth_xprv = vss_xprv
.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 138 }])
.derive_priv(
&Secp256k1::new(),
&[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }],
)
.map_err(|e| {
log_error!(logger, "Failed to derive VSS secret: {}", e);
BuildError::KVStoreSetupFailed
Expand Down Expand Up @@ -529,7 +552,12 @@ impl NodeBuilder {

let config = Arc::new(self.config.clone());

let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?;
let vss_xprv = derive_xprv(
config.clone(),
&seed_bytes,
VSS_HARDENED_CHILD_INDEX,
Arc::clone(&logger),
)?;

let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes();

Expand Down Expand Up @@ -684,6 +712,16 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token);
}

/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
/// channels to clients.
///
/// **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 set_liquidity_provider_lsps2(&self, service_config: LSPS2ServiceConfig) {
self.inner.write().unwrap().set_liquidity_provider_lsps2(service_config);
}

/// Sets the used storage directory path.
pub fn set_storage_dir_path(&self, storage_dir_path: String) {
self.inner.write().unwrap().set_storage_dir_path(storage_dir_path);
Expand Down Expand Up @@ -1027,7 +1065,7 @@ 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() {
if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() {
// Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll
// check that they don't take too much before claiming.
user_config.channel_config.accept_underpaying_htlcs = true;
Expand All @@ -1039,6 +1077,12 @@ fn build_with_store_internal(
100;
}

if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() {
// If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the
// information to the service handler.
user_config.accept_intercept_htlcs = true;
}

let message_router =
Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager)));

Expand Down Expand Up @@ -1159,31 +1203,53 @@ fn build_with_store_internal(
},
};

let liquidity_source = liquidity_source_config.as_ref().map(|lsc| {
let mut liquidity_source_builder = LiquiditySourceBuilder::new(
Arc::clone(&channel_manager),
Arc::clone(&keys_manager),
Arc::clone(&chain_source),
Arc::clone(&config),
Arc::clone(&logger),
);

lsc.lsps1_service.as_ref().map(|(node_id, address, token)| {
liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone())
});
let (liquidity_source, custom_message_handler) =
if let Some(lsc) = liquidity_source_config.as_ref() {
let mut liquidity_source_builder = LiquiditySourceBuilder::new(
Arc::clone(&wallet),
Arc::clone(&channel_manager),
Arc::clone(&keys_manager),
Arc::clone(&chain_source),
Arc::clone(&config),
Arc::clone(&logger),
);

lsc.lsps2_service.as_ref().map(|(node_id, address, token)| {
liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone())
});
lsc.lsps1_client.as_ref().map(|config| {
liquidity_source_builder.lsps1_client(
config.node_id,
config.address.clone(),
config.token.clone(),
)
});

Arc::new(liquidity_source_builder.build())
});
lsc.lsps2_client.as_ref().map(|config| {
liquidity_source_builder.lsps2_client(
config.node_id,
config.address.clone(),
config.token.clone(),
)
});

let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() {
Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source)))
} else {
Arc::new(NodeCustomMessageHandler::new_ignoring())
};
let promise_secret = {
let lsps_xpriv = derive_xprv(
Arc::clone(&config),
&seed_bytes,
LSPS_HARDENED_CHILD_INDEX,
Arc::clone(&logger),
)?;
lsps_xpriv.private_key.secret_bytes()
};
lsc.lsps2_service.as_ref().map(|config| {
liquidity_source_builder.lsps2_service(promise_secret, config.clone())
});

let liquidity_source = Arc::new(liquidity_source_builder.build());
let custom_message_handler =
Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source)));
(Some(liquidity_source), custom_message_handler)
} else {
(None, Arc::new(NodeCustomMessageHandler::new_ignoring()))
};

let msg_handler = match gossip_source.as_gossip_sync() {
GossipSync::P2P(p2p_gossip_sync) => MessageHandler {
Expand Down Expand Up @@ -1382,8 +1448,8 @@ fn seed_bytes_from_config(
}
}

fn derive_vss_xprv(
config: Arc<Config>, seed_bytes: &[u8; 64], logger: Arc<Logger>,
fn derive_xprv(
config: Arc<Config>, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc<Logger>,
) -> Result<Xpriv, BuildError> {
use bitcoin::key::Secp256k1;

Expand All @@ -1392,10 +1458,11 @@ fn derive_vss_xprv(
BuildError::InvalidSeedBytes
})?;

xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| {
log_error!(logger, "Failed to derive VSS secret: {}", e);
BuildError::KVStoreSetupFailed
})
xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }])
.map_err(|e| {
log_error!(logger, "Failed to derive hardened child secret: {}", e);
BuildError::InvalidSeedBytes
})
}

/// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string.
Expand Down
Loading