Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
if: matrix.msrv
run: |
cargo update -p home --precise "0.5.9" --verbose # home v0.5.11 requires rustc 1.81 or newer
cargo update -p idna_adapter --precise "1.1.0" --verbose # idna_adapter 1.2 switched to ICU4X, requiring 1.81 and newer
- name: Set RUSTFLAGS to deny warnings
if: "matrix.toolchain == 'stable'"
run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV"
Expand Down
14 changes: 13 additions & 1 deletion 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 Expand Up @@ -62,7 +74,7 @@ interface Builder {
void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token);
void set_storage_dir_path(string storage_dir_path);
void set_filesystem_logger(string? log_file_path, LogLevel? max_log_level);
void set_log_facade_logger(LogLevel? max_log_level);
void set_log_facade_logger();
void set_custom_logger(LogWriter log_writer);
void set_network(Network network);
[Throws=BuildError]
Expand Down
184 changes: 120 additions & 64 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,24 +100,20 @@ 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)]
enum LogWriterConfig {
File { log_file_path: Option<String>, max_log_level: Option<LogLevel> },
Log { max_log_level: Option<LogLevel> },
Log,
Custom(Arc<dyn LogWriter>),
}

Expand All @@ -123,9 +125,7 @@ impl std::fmt::Debug for LogWriterConfig {
.field("max_log_level", max_log_level)
.field("log_file_path", log_file_path)
.finish(),
LogWriterConfig::Log { max_log_level } => {
f.debug_tuple("Log").field(max_log_level).finish()
},
LogWriterConfig::Log => write!(f, "LogWriterConfig::Log"),
LogWriterConfig::Custom(_) => {
f.debug_tuple("Custom").field(&"<config internal to custom log writer>").finish()
},
Expand Down Expand Up @@ -319,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 @@ -339,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(
&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 All @@ -366,11 +383,8 @@ impl NodeBuilder {
}

/// Configures the [`Node`] instance to write logs to the [`log`](https://crates.io/crates/log) facade.
///
/// If set, the `max_log_level` sets the maximum log level. Otherwise, the latter defaults to
/// [`DEFAULT_LOG_LEVEL`].
pub fn set_log_facade_logger(&mut self, max_log_level: Option<LogLevel>) -> &mut Self {
self.log_writer_config = Some(LogWriterConfig::Log { max_log_level });
pub fn set_log_facade_logger(&mut self) -> &mut Self {
self.log_writer_config = Some(LogWriterConfig::Log);
self
}

Expand Down Expand Up @@ -471,10 +485,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 @@ -536,7 +554,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 @@ -691,6 +714,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 All @@ -712,11 +745,8 @@ impl ArcedNodeBuilder {
}

/// Configures the [`Node`] instance to write logs to the [`log`](https://crates.io/crates/log) facade.
///
/// If set, the `max_log_level` sets the maximum log level. Otherwise, the latter defaults to
/// [`DEFAULT_LOG_LEVEL`].
pub fn set_log_facade_logger(&self, log_level: Option<LogLevel>) {
self.inner.write().unwrap().set_log_facade_logger(log_level);
pub fn set_log_facade_logger(&self) {
self.inner.write().unwrap().set_log_facade_logger();
}

/// Configures the [`Node`] instance to write logs to the provided custom [`LogWriter`].
Expand Down Expand Up @@ -1039,7 +1069,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 @@ -1051,6 +1081,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 @@ -1171,31 +1207,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 @@ -1355,10 +1413,7 @@ fn setup_logger(
Logger::new_fs_writer(log_file_path, max_log_level)
.map_err(|_| BuildError::LoggerSetupFailed)?
},
Some(LogWriterConfig::Log { max_log_level }) => {
let max_log_level = max_log_level.unwrap_or_else(|| DEFAULT_LOG_LEVEL);
Logger::new_log_facade(max_log_level)
},
Some(LogWriterConfig::Log) => Logger::new_log_facade(),

Some(LogWriterConfig::Custom(custom_log_writer)) => {
Logger::new_custom_writer(Arc::clone(&custom_log_writer))
Expand Down Expand Up @@ -1397,8 +1452,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 @@ -1407,10 +1462,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
Loading