Skip to content

Commit

Permalink
Merge pull request #330 from enigbe/feat-set-node-alias
Browse files Browse the repository at this point in the history
Set node alias
  • Loading branch information
tnull authored Oct 7, 2024
2 parents 1a6a2ca + 4fd1cb8 commit c9ebeb6
Show file tree
Hide file tree
Showing 13 changed files with 775 additions and 363 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A ready-to-go Lightning node library built using [LDK][ldk] and [BDK][bdk].
LDK Node is a self-custodial Lightning node in library form. Its central goal is to provide a small, simple, and straightforward interface that enables users to easily set up and run a Lightning node with an integrated on-chain wallet. While minimalism is at its core, LDK Node aims to be sufficiently modular and configurable to be useful for a variety of use cases.

## Getting Started
The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `connect_open_channel`, `send`, etc.
The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `open_channel`, `open_announced_channel`, `send`, etc.

```rust
use ldk_node::Builder;
Expand All @@ -37,7 +37,7 @@ fn main() {

let node_id = PublicKey::from_str("NODE_ID").unwrap();
let node_addr = SocketAddress::from_str("IP_ADDR:PORT").unwrap();
node.connect_open_channel(node_id, node_addr, 10000, None, None, false).unwrap();
node.open_channel(node_id, node_addr, 10000, None, None).unwrap();

let event = node.wait_next_event();
println!("EVENT: {:?}", event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class LibraryTest {
assertEquals(100000uL, totalBalance1)
assertEquals(100000uL, totalBalance2)

node1.connectOpenChannel(nodeId2, listenAddress2, 50000u, null, null, true)
node1.openChannel(nodeId2, listenAddress2, 50000u, null, null)

val channelPendingEvent1 = node1.waitNextEvent()
println("Got event: $channelPendingEvent1")
Expand Down
13 changes: 12 additions & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dictionary Config {
string? log_dir_path;
Network network;
sequence<SocketAddress>? listening_addresses;
NodeAlias? node_alias;
u64 onchain_wallet_sync_interval_secs;
u64 wallet_sync_interval_secs;
u64 fee_rate_cache_update_interval_secs;
Expand Down Expand Up @@ -40,6 +41,8 @@ interface Builder {
[Throws=BuildError]
void set_listening_addresses(sequence<SocketAddress> listening_addresses);
[Throws=BuildError]
void set_node_alias(string node_alias);
[Throws=BuildError]
Node build();
[Throws=BuildError]
Node build_with_fs_store();
Expand All @@ -59,6 +62,7 @@ interface Node {
void event_handled();
PublicKey node_id();
sequence<SocketAddress>? listening_addresses();
NodeAlias? node_alias();
Bolt11Payment bolt11_payment();
Bolt12Payment bolt12_payment();
SpontaneousPayment spontaneous_payment();
Expand All @@ -69,7 +73,9 @@ interface Node {
[Throws=NodeError]
void disconnect(PublicKey node_id);
[Throws=NodeError]
UserChannelId connect_open_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config, boolean announce_channel);
UserChannelId open_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
[Throws=NodeError]
UserChannelId open_announced_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config);
[Throws=NodeError]
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
[Throws=NodeError]
Expand Down Expand Up @@ -202,6 +208,7 @@ enum NodeError {
"InvalidNetwork",
"InvalidUri",
"InvalidQuantity",
"InvalidNodeAlias",
"DuplicatePayment",
"UnsupportedCurrency",
"InsufficientFunds",
Expand Down Expand Up @@ -232,6 +239,7 @@ enum BuildError {
"InvalidSystemTime",
"InvalidChannelMonitor",
"InvalidListeningAddresses",
"InvalidNodeAlias",
"ReadFailed",
"WriteFailed",
"StoragePathAccessFailed",
Expand Down Expand Up @@ -529,3 +537,6 @@ typedef string Mnemonic;

[Custom]
typedef string UntrustedString;

[Custom]
typedef string NodeAlias;
2 changes: 1 addition & 1 deletion bindings/python/src/ldk_node/test_ldk_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def test_channel_full_cycle(self):
print("TOTAL 2:", total_balance_2)
self.assertEqual(total_balance_2, 100000)

node_1.connect_open_channel(node_id_2, listening_addresses_2[0], 50000, None, None, True)
node_1.open_channel(node_id_2, listening_addresses_2[0], 50000, None, None)

channel_pending_event_1 = node_1.wait_next_event()
assert isinstance(channel_pending_event_1, Event.CHANNEL_PENDING)
Expand Down
76 changes: 75 additions & 1 deletion src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use lightning::chain::{chainmonitor, BestBlock, Watch};
use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs};
use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress};
use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler};
use lightning::routing::gossip::NodeAlias;
use lightning::routing::router::DefaultRouter;
use lightning::routing::scoring::{
ProbabilisticScorer, ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters,
Expand Down Expand Up @@ -109,7 +110,7 @@ impl Default for LiquiditySourceConfig {
/// An error encountered during building a [`Node`].
///
/// [`Node`]: crate::Node
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum BuildError {
/// The given seed bytes are invalid, e.g., have invalid length.
InvalidSeedBytes,
Expand All @@ -121,6 +122,8 @@ pub enum BuildError {
InvalidChannelMonitor,
/// The given listening addresses are invalid, e.g. too many were passed.
InvalidListeningAddresses,
/// The provided alias is invalid.
InvalidNodeAlias,
/// We failed to read data from the [`KVStore`].
///
/// [`KVStore`]: lightning::util::persist::KVStore
Expand Down Expand Up @@ -159,6 +162,7 @@ impl fmt::Display for BuildError {
Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
Self::InvalidNodeAlias => write!(f, "Given node alias is invalid."),
}
}
}
Expand Down Expand Up @@ -303,6 +307,16 @@ impl NodeBuilder {
Ok(self)
}

/// Sets the alias the [`Node`] will use in its announcement.
///
/// The provided alias must be a valid UTF-8 string.
pub fn set_node_alias(&mut self, node_alias: String) -> Result<&mut Self, BuildError> {
let node_alias = sanitize_alias(&node_alias)?;

self.config.node_alias = Some(node_alias);
Ok(self)
}

/// Sets the level at which [`Node`] will log messages.
pub fn set_log_level(&mut self, level: LogLevel) -> &mut Self {
self.config.log_level = level;
Expand Down Expand Up @@ -501,6 +515,11 @@ impl ArcedNodeBuilder {
self.inner.write().unwrap().set_listening_addresses(listening_addresses).map(|_| ())
}

/// Sets the node alias.
pub fn set_node_alias(&self, node_alias: String) -> Result<(), BuildError> {
self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ())
}

/// Sets the level at which [`Node`] will log messages.
pub fn set_log_level(&self, level: LogLevel) {
self.inner.write().unwrap().set_log_level(level);
Expand Down Expand Up @@ -1050,3 +1069,58 @@ fn seed_bytes_from_config(
},
}
}

/// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string.
pub(crate) fn sanitize_alias(alias_str: &str) -> Result<NodeAlias, BuildError> {
let alias = alias_str.trim();

// Alias must be 32-bytes long or less.
if alias.as_bytes().len() > 32 {
return Err(BuildError::InvalidNodeAlias);
}

let mut bytes = [0u8; 32];
bytes[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes());
Ok(NodeAlias(bytes))
}

#[cfg(test)]
mod tests {
use super::{sanitize_alias, BuildError, NodeAlias};

#[test]
fn sanitize_empty_node_alias() {
// Empty node alias
let alias = "";
let mut buf = [0u8; 32];
buf[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes());

let expected_node_alias = NodeAlias([0; 32]);
let node_alias = sanitize_alias(alias).unwrap();
assert_eq!(node_alias, expected_node_alias);
}

#[test]
fn sanitize_alias_with_sandwiched_null() {
// Alias with emojis
let alias = "I\u{1F496}LDK-Node!";
let mut buf = [0u8; 32];
buf[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes());
let expected_alias = NodeAlias(buf);

let user_provided_alias = "I\u{1F496}LDK-Node!\0\u{26A1}";
let node_alias = sanitize_alias(user_provided_alias).unwrap();

let node_alias_display = format!("{}", node_alias);

assert_eq!(alias, &node_alias_display);
assert_ne!(expected_alias, node_alias);
}

#[test]
fn sanitize_alias_gt_32_bytes() {
let alias = "This is a string longer than thirty-two bytes!"; // 46 bytes
let node = sanitize_alias(alias);
assert_eq!(node.err().unwrap(), BuildError::InvalidNodeAlias);
}
}
124 changes: 121 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use crate::payment::SendingParameters;

use lightning::ln::msgs::SocketAddress;
use lightning::routing::gossip::NodeAlias;
use lightning::util::config::UserConfig;
use lightning::util::logger::Level as LogLevel;

Expand Down Expand Up @@ -86,6 +87,7 @@ pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64;
/// | `log_dir_path` | None |
/// | `network` | Bitcoin |
/// | `listening_addresses` | None |
/// | `node_alias` | None |
/// | `default_cltv_expiry_delta` | 144 |
/// | `onchain_wallet_sync_interval_secs` | 80 |
/// | `wallet_sync_interval_secs` | 30 |
Expand All @@ -110,7 +112,15 @@ pub struct Config {
/// The used Bitcoin network.
pub network: Network,
/// The addresses on which the node will listen for incoming connections.
///
/// **Note**: Node announcements will only be broadcast if the `node_alias` and the
/// `listening_addresses` are set.
pub listening_addresses: Option<Vec<SocketAddress>>,
/// The node alias to be used in announcements.
///
/// **Note**: Node announcements will only be broadcast if the `node_alias` and the
/// `listening_addresses` are set.
pub node_alias: Option<NodeAlias>,
/// The time in-between background sync attempts of the onchain wallet, in seconds.
///
/// **Note:** A minimum of 10 seconds is always enforced.
Expand Down Expand Up @@ -180,6 +190,7 @@ impl Default for Config {
log_level: DEFAULT_LOG_LEVEL,
anchor_channels_config: Some(AnchorChannelsConfig::default()),
sending_parameters: None,
node_alias: None,
}
}
}
Expand Down Expand Up @@ -265,17 +276,124 @@ pub fn default_config() -> Config {
Config::default()
}

/// Specifies reasons why a channel cannot be announced.
#[derive(Debug, PartialEq)]
pub(crate) enum ChannelAnnouncementBlocker {
/// The node alias is not set.
MissingNodeAlias,
/// The listening addresses are not set.
MissingListeningAddresses,
// This listening addresses is set but the vector is empty.
EmptyListeningAddresses,
}

/// Enumeration defining the announcement status of a channel.
#[derive(Debug, PartialEq)]
pub(crate) enum ChannelAnnouncementStatus {
/// The channel is announceable.
Announceable,
/// The channel is not announceable.
Unannounceable(ChannelAnnouncementBlocker),
}

/// Checks if a node is can announce a channel based on the configured values of both the node's
/// alias and its listening addresses.
///
/// If either of them is unset, the node cannot announce the channel. This ability to announce/
/// unannounce a channel is codified with `ChannelAnnouncementStatus`
pub(crate) fn can_announce_channel(config: &Config) -> ChannelAnnouncementStatus {
if config.node_alias.is_none() {
return ChannelAnnouncementStatus::Unannounceable(
ChannelAnnouncementBlocker::MissingNodeAlias,
);
}

match &config.listening_addresses {
None => ChannelAnnouncementStatus::Unannounceable(
ChannelAnnouncementBlocker::MissingListeningAddresses,
),
Some(addresses) if addresses.is_empty() => ChannelAnnouncementStatus::Unannounceable(
ChannelAnnouncementBlocker::EmptyListeningAddresses,
),
Some(_) => ChannelAnnouncementStatus::Announceable,
}
}

pub(crate) fn default_user_config(config: &Config) -> UserConfig {
// Initialize the default config values.
//
// Note that methods such as Node::connect_open_channel might override some of the values set
// here, e.g. the ChannelHandshakeConfig, meaning these default values will mostly be relevant
// for inbound channels.
// Note that methods such as Node::open_channel and Node::open_announced_channel might override
// some of the values set here, e.g. the ChannelHandshakeConfig, meaning these default values
// will mostly be relevant for inbound channels.
let mut user_config = UserConfig::default();
user_config.channel_handshake_limits.force_announced_channel_preference = false;
user_config.manually_accept_inbound_channels = true;
user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx =
config.anchor_channels_config.is_some();

match can_announce_channel(config) {
ChannelAnnouncementStatus::Announceable => (),
ChannelAnnouncementStatus::Unannounceable(_) => {
user_config.accept_forwards_to_priv_channels = false;
user_config.channel_handshake_config.announced_channel = false;
user_config.channel_handshake_limits.force_announced_channel_preference = true;
},
}

user_config
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use crate::config::ChannelAnnouncementStatus;

use super::can_announce_channel;
use super::Config;
use super::NodeAlias;
use super::SocketAddress;

#[test]
fn node_can_announce_channel() {
// Default configuration with node alias and listening addresses unset
let mut node_config = Config::default();
assert_eq!(
can_announce_channel(&node_config),
ChannelAnnouncementStatus::Unannounceable(
crate::config::ChannelAnnouncementBlocker::MissingNodeAlias
)
);

// Set node alias with listening addresses unset
let alias_frm_str = |alias: &str| {
let mut bytes = [0u8; 32];
bytes[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes());
NodeAlias(bytes)
};
node_config.node_alias = Some(alias_frm_str("LDK_Node"));
assert_eq!(
can_announce_channel(&node_config),
ChannelAnnouncementStatus::Unannounceable(
crate::config::ChannelAnnouncementBlocker::MissingListeningAddresses
)
);

// Set node alias with an empty list of listening addresses
node_config.listening_addresses = Some(vec![]);
assert_eq!(
can_announce_channel(&node_config),
ChannelAnnouncementStatus::Unannounceable(
crate::config::ChannelAnnouncementBlocker::EmptyListeningAddresses
)
);

// Set node alias with a non-empty list of listening addresses
let socket_address =
SocketAddress::from_str("localhost:8000").expect("Socket address conversion failed.");
if let Some(ref mut addresses) = node_config.listening_addresses {
addresses.push(socket_address);
}
assert_eq!(can_announce_channel(&node_config), ChannelAnnouncementStatus::Announceable);
}
}
Loading

0 comments on commit c9ebeb6

Please sign in to comment.