Skip to content

Commit edb0560

Browse files
authored
Merge pull request #420 from tnull/2024-12-add-lsps2-service-support
Add bLIP-52 / LSPS2 service-side support
2 parents e9c435a + 1091952 commit edb0560

File tree

8 files changed

+785
-157
lines changed

8 files changed

+785
-157
lines changed

bindings/ldk_node.udl

+12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ dictionary EsploraSyncConfig {
2525
u64 fee_rate_cache_update_interval_secs;
2626
};
2727

28+
dictionary LSPS2ServiceConfig {
29+
string? require_token;
30+
boolean advertise_service;
31+
u32 channel_opening_fee_ppm;
32+
u32 channel_over_provisioning_ppm;
33+
u64 min_channel_opening_fee_msat;
34+
u32 min_channel_lifetime;
35+
u32 max_client_to_self_delay;
36+
u64 min_payment_size_msat;
37+
u64 max_payment_size_msat;
38+
};
39+
2840
enum LogLevel {
2941
"Gossip",
3042
"Trace",

src/builder.rs

+113-46
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use crate::gossip::GossipSource;
1818
use crate::io::sqlite_store::SqliteStore;
1919
use crate::io::utils::{read_node_metrics, write_node_metrics};
2020
use crate::io::vss_store::VssStore;
21-
use crate::liquidity::LiquiditySourceBuilder;
21+
use crate::liquidity::{
22+
LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder,
23+
};
2224
use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger};
2325
use crate::message_handler::NodeCustomMessageHandler;
2426
use crate::payment::store::PaymentStore;
@@ -75,6 +77,10 @@ use std::sync::{Arc, Mutex, RwLock};
7577
use std::time::SystemTime;
7678
use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider};
7779

80+
const VSS_HARDENED_CHILD_INDEX: u32 = 877;
81+
const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138;
82+
const LSPS_HARDENED_CHILD_INDEX: u32 = 577;
83+
7884
#[derive(Debug, Clone)]
7985
enum ChainDataSourceConfig {
8086
Esplora { server_url: String, sync_config: Option<EsploraSyncConfig> },
@@ -94,18 +100,14 @@ enum GossipSourceConfig {
94100
RapidGossipSync(String),
95101
}
96102

97-
#[derive(Debug, Clone)]
103+
#[derive(Debug, Clone, Default)]
98104
struct LiquiditySourceConfig {
99-
// LSPS1 service's (node_id, address, token)
100-
lsps1_service: Option<(PublicKey, SocketAddress, Option<String>)>,
101-
// LSPS2 service's (node_id, address, token)
102-
lsps2_service: Option<(PublicKey, SocketAddress, Option<String>)>,
103-
}
104-
105-
impl Default for LiquiditySourceConfig {
106-
fn default() -> Self {
107-
Self { lsps1_service: None, lsps2_service: None }
108-
}
105+
// Act as an LSPS1 client connecting to the given service.
106+
lsps1_client: Option<LSPS1ClientConfig>,
107+
// Act as an LSPS2 client connecting to the given service.
108+
lsps2_client: Option<LSPS2ClientConfig>,
109+
// Act as an LSPS2 service.
110+
lsps2_service: Option<LSPS2ServiceConfig>,
109111
}
110112

111113
#[derive(Clone)]
@@ -319,7 +321,8 @@ impl NodeBuilder {
319321

320322
let liquidity_source_config =
321323
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
322-
liquidity_source_config.lsps1_service = Some((node_id, address, token));
324+
let lsps1_client_config = LSPS1ClientConfig { node_id, address, token };
325+
liquidity_source_config.lsps1_client = Some(lsps1_client_config);
323326
self
324327
}
325328

@@ -339,7 +342,23 @@ impl NodeBuilder {
339342

340343
let liquidity_source_config =
341344
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
342-
liquidity_source_config.lsps2_service = Some((node_id, address, token));
345+
let lsps2_client_config = LSPS2ClientConfig { node_id, address, token };
346+
liquidity_source_config.lsps2_client = Some(lsps2_client_config);
347+
self
348+
}
349+
350+
/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
351+
/// channels to clients.
352+
///
353+
/// **Caution**: LSP service support is in **alpha** and is considered an experimental feature.
354+
///
355+
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
356+
pub fn set_liquidity_provider_lsps2(
357+
&mut self, service_config: LSPS2ServiceConfig,
358+
) -> &mut Self {
359+
let liquidity_source_config =
360+
self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default());
361+
liquidity_source_config.lsps2_service = Some(service_config);
343362
self
344363
}
345364

@@ -471,10 +490,14 @@ impl NodeBuilder {
471490

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

474-
let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?;
493+
let vss_xprv =
494+
derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?;
475495

476496
let lnurl_auth_xprv = vss_xprv
477-
.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 138 }])
497+
.derive_priv(
498+
&Secp256k1::new(),
499+
&[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }],
500+
)
478501
.map_err(|e| {
479502
log_error!(logger, "Failed to derive VSS secret: {}", e);
480503
BuildError::KVStoreSetupFailed
@@ -536,7 +559,12 @@ impl NodeBuilder {
536559

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

539-
let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?;
562+
let vss_xprv = derive_xprv(
563+
config.clone(),
564+
&seed_bytes,
565+
VSS_HARDENED_CHILD_INDEX,
566+
Arc::clone(&logger),
567+
)?;
540568

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

@@ -691,6 +719,16 @@ impl ArcedNodeBuilder {
691719
self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token);
692720
}
693721

722+
/// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time
723+
/// channels to clients.
724+
///
725+
/// **Caution**: LSP service support is in **alpha** and is considered an experimental feature.
726+
///
727+
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
728+
pub fn set_liquidity_provider_lsps2(&self, service_config: LSPS2ServiceConfig) {
729+
self.inner.write().unwrap().set_liquidity_provider_lsps2(service_config);
730+
}
731+
694732
/// Sets the used storage directory path.
695733
pub fn set_storage_dir_path(&self, storage_dir_path: String) {
696734
self.inner.write().unwrap().set_storage_dir_path(storage_dir_path);
@@ -1039,7 +1077,7 @@ fn build_with_store_internal(
10391077
};
10401078

10411079
let mut user_config = default_user_config(&config);
1042-
if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() {
1080+
if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() {
10431081
// Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll
10441082
// check that they don't take too much before claiming.
10451083
user_config.channel_config.accept_underpaying_htlcs = true;
@@ -1051,6 +1089,12 @@ fn build_with_store_internal(
10511089
100;
10521090
}
10531091

1092+
if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() {
1093+
// If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the
1094+
// information to the service handler.
1095+
user_config.accept_intercept_htlcs = true;
1096+
}
1097+
10541098
let message_router =
10551099
Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager)));
10561100

@@ -1171,31 +1215,53 @@ fn build_with_store_internal(
11711215
},
11721216
};
11731217

1174-
let liquidity_source = liquidity_source_config.as_ref().map(|lsc| {
1175-
let mut liquidity_source_builder = LiquiditySourceBuilder::new(
1176-
Arc::clone(&channel_manager),
1177-
Arc::clone(&keys_manager),
1178-
Arc::clone(&chain_source),
1179-
Arc::clone(&config),
1180-
Arc::clone(&logger),
1181-
);
1182-
1183-
lsc.lsps1_service.as_ref().map(|(node_id, address, token)| {
1184-
liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone())
1185-
});
1218+
let (liquidity_source, custom_message_handler) =
1219+
if let Some(lsc) = liquidity_source_config.as_ref() {
1220+
let mut liquidity_source_builder = LiquiditySourceBuilder::new(
1221+
Arc::clone(&wallet),
1222+
Arc::clone(&channel_manager),
1223+
Arc::clone(&keys_manager),
1224+
Arc::clone(&chain_source),
1225+
Arc::clone(&config),
1226+
Arc::clone(&logger),
1227+
);
11861228

1187-
lsc.lsps2_service.as_ref().map(|(node_id, address, token)| {
1188-
liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone())
1189-
});
1229+
lsc.lsps1_client.as_ref().map(|config| {
1230+
liquidity_source_builder.lsps1_client(
1231+
config.node_id,
1232+
config.address.clone(),
1233+
config.token.clone(),
1234+
)
1235+
});
11901236

1191-
Arc::new(liquidity_source_builder.build())
1192-
});
1237+
lsc.lsps2_client.as_ref().map(|config| {
1238+
liquidity_source_builder.lsps2_client(
1239+
config.node_id,
1240+
config.address.clone(),
1241+
config.token.clone(),
1242+
)
1243+
});
11931244

1194-
let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() {
1195-
Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source)))
1196-
} else {
1197-
Arc::new(NodeCustomMessageHandler::new_ignoring())
1198-
};
1245+
let promise_secret = {
1246+
let lsps_xpriv = derive_xprv(
1247+
Arc::clone(&config),
1248+
&seed_bytes,
1249+
LSPS_HARDENED_CHILD_INDEX,
1250+
Arc::clone(&logger),
1251+
)?;
1252+
lsps_xpriv.private_key.secret_bytes()
1253+
};
1254+
lsc.lsps2_service.as_ref().map(|config| {
1255+
liquidity_source_builder.lsps2_service(promise_secret, config.clone())
1256+
});
1257+
1258+
let liquidity_source = Arc::new(liquidity_source_builder.build());
1259+
let custom_message_handler =
1260+
Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source)));
1261+
(Some(liquidity_source), custom_message_handler)
1262+
} else {
1263+
(None, Arc::new(NodeCustomMessageHandler::new_ignoring()))
1264+
};
11991265

12001266
let msg_handler = match gossip_source.as_gossip_sync() {
12011267
GossipSync::P2P(p2p_gossip_sync) => MessageHandler {
@@ -1397,8 +1463,8 @@ fn seed_bytes_from_config(
13971463
}
13981464
}
13991465

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

@@ -1407,10 +1473,11 @@ fn derive_vss_xprv(
14071473
BuildError::InvalidSeedBytes
14081474
})?;
14091475

1410-
xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| {
1411-
log_error!(logger, "Failed to derive VSS secret: {}", e);
1412-
BuildError::KVStoreSetupFailed
1413-
})
1476+
xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }])
1477+
.map_err(|e| {
1478+
log_error!(logger, "Failed to derive hardened child secret: {}", e);
1479+
BuildError::InvalidSeedBytes
1480+
})
14141481
}
14151482

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

0 commit comments

Comments
 (0)