diff --git a/README.md b/README.md index 4078ce67b..7bcfb6f78 100644 --- a/README.md +++ b/README.md @@ -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; @@ -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); diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt index b629793cd..e2bcd4c89 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt @@ -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") diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1c9497264..6663604a2 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -8,6 +8,7 @@ dictionary Config { string? log_dir_path; Network network; sequence? listening_addresses; + NodeAlias? node_alias; u64 onchain_wallet_sync_interval_secs; u64 wallet_sync_interval_secs; u64 fee_rate_cache_update_interval_secs; @@ -40,6 +41,8 @@ interface Builder { [Throws=BuildError] void set_listening_addresses(sequence listening_addresses); [Throws=BuildError] + void set_node_alias(string node_alias); + [Throws=BuildError] Node build(); [Throws=BuildError] Node build_with_fs_store(); @@ -59,6 +62,7 @@ interface Node { void event_handled(); PublicKey node_id(); sequence? listening_addresses(); + NodeAlias? node_alias(); Bolt11Payment bolt11_payment(); Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); @@ -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] @@ -202,6 +208,7 @@ enum NodeError { "InvalidNetwork", "InvalidUri", "InvalidQuantity", + "InvalidNodeAlias", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -232,6 +239,7 @@ enum BuildError { "InvalidSystemTime", "InvalidChannelMonitor", "InvalidListeningAddresses", + "InvalidNodeAlias", "ReadFailed", "WriteFailed", "StoragePathAccessFailed", @@ -529,3 +537,6 @@ typedef string Mnemonic; [Custom] typedef string UntrustedString; + +[Custom] +typedef string NodeAlias; diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 92c4bf2d1..4f2931440 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -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) diff --git a/src/builder.rs b/src/builder.rs index fc9f839b0..d2ceb5f22 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -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, @@ -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, @@ -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 @@ -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."), } } } @@ -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; @@ -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); @@ -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 { + 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); + } +} diff --git a/src/config.rs b/src/config.rs index fac25b562..574789ac6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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; @@ -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 | @@ -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>, + /// 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, /// The time in-between background sync attempts of the onchain wallet, in seconds. /// /// **Note:** A minimum of 10 seconds is always enforced. @@ -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, } } } @@ -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); + } +} diff --git a/src/error.rs b/src/error.rs index 660c2036e..807e1ca54 100644 --- a/src/error.rs +++ b/src/error.rs @@ -98,6 +98,8 @@ pub enum Error { InvalidUri, /// The given quantity is invalid. InvalidQuantity, + /// The given node alias is invalid. + InvalidNodeAlias, /// A payment with the given hash has already been initiated. DuplicatePayment, /// The provided offer was denonminated in an unsupported currency. @@ -163,6 +165,7 @@ impl fmt::Display for Error { Self::InvalidNetwork => write!(f, "The given network is invalid."), Self::InvalidUri => write!(f, "The given URI is invalid."), Self::InvalidQuantity => write!(f, "The given quantity is invalid."), + Self::InvalidNodeAlias => write!(f, "The given node alias is invalid."), Self::DuplicatePayment => { write!(f, "A payment with the given hash has already been initiated.") }, diff --git a/src/lib.rs b/src/lib.rs index 0148cf8d4..48750f74e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! //! The primary abstraction of the library is the [`Node`], which can be retrieved by setting up //! and configuring a [`Builder`] to your liking and calling [`build`]. `Node` can then be -//! controlled via commands such as [`start`], [`stop`], [`connect_open_channel`], +//! controlled via commands such as [`start`], [`stop`], [`open_channel`], [`open_announced_channel`] //! [`send`], etc.: //! //! ```no_run @@ -47,7 +47,7 @@ //! //! 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); @@ -63,7 +63,8 @@ //! [`build`]: Builder::build //! [`start`]: Node::start //! [`stop`]: Node::stop -//! [`connect_open_channel`]: Node::connect_open_channel +//! [`open_channel`]: Node::open_channel +//! [`open_announced_channel`]: Node::open_announced_channel //! [`send`]: Bolt11Payment::send //! #![cfg_attr(not(feature = "uniffi"), deny(missing_docs))] @@ -99,6 +100,7 @@ mod wallet; pub use bip39; pub use bitcoin; pub use lightning; +use lightning::routing::gossip::NodeAlias; pub use lightning_invoice; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; @@ -121,8 +123,9 @@ pub use builder::BuildError; pub use builder::NodeBuilder as Builder; use config::{ - default_user_config, LDK_WALLET_SYNC_TIMEOUT_SECS, NODE_ANN_BCAST_INTERVAL, - PEER_RECONNECTION_INTERVAL, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, RGS_SYNC_INTERVAL, + can_announce_channel, default_user_config, ChannelAnnouncementStatus, + LDK_WALLET_SYNC_TIMEOUT_SECS, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, + RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, RGS_SYNC_INTERVAL, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use connection::ConnectionManager; @@ -601,6 +604,8 @@ impl Node { let bcast_logger = Arc::clone(&self.logger); let bcast_ann_timestamp = Arc::clone(&self.latest_node_announcement_broadcast_timestamp); let mut stop_bcast = self.stop_sender.subscribe(); + let node_alias = self.config.node_alias.clone(); + let can_announce_channel = can_announce_channel(&self.config); runtime.spawn(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. #[cfg(not(test))] @@ -643,15 +648,22 @@ impl Node { continue; } - let addresses = bcast_config.listening_addresses.clone().unwrap_or(Vec::new()); - - if addresses.is_empty() { - // Skip if we are not listening on any addresses. - continue; + match can_announce_channel { + ChannelAnnouncementStatus::Unannounceable(_) => { + // Skip if we are not listening on any addresses or if the node alias is not set. + continue; + } + ChannelAnnouncementStatus::Announceable => { + // Broadcast node announcement. + let addresses = bcast_config.listening_addresses.clone().unwrap_or(Vec::new()); + if let Some(node_alias) = node_alias.as_ref() { + bcast_pm.broadcast_node_announcement([0; 3], node_alias.0, addresses); + } else { + continue + } + } } - bcast_pm.broadcast_node_announcement([0; 3], [0; 32], addresses); - let unix_time_secs_opt = SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); *bcast_ann_timestamp.write().unwrap() = unix_time_secs_opt; @@ -961,6 +973,11 @@ impl Node { self.config.listening_addresses.clone() } + /// Returns our node alias. + pub fn node_alias(&self) -> Option { + self.config.node_alias + } + /// Returns a payment handler allowing to create and pay [BOLT 11] invoices. /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md @@ -1170,20 +1187,11 @@ impl Node { Ok(()) } - /// Connect to a node and open a new channel. Disconnects and re-connects are handled automatically - /// - /// Disconnects and reconnects are handled automatically. - /// - /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the - /// channel counterparty on channel open. This can be useful to start out with the balance not - /// entirely shifted to one side, therefore allowing to receive payments from the getgo. - /// - /// If Anchor channels are enabled, this will ensure the configured - /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before - /// opening the channel. + /// Connect to a node and open a new channel. /// - /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. - pub fn connect_open_channel( + /// See [`Node::open_channel`] or [`Node::open_announced_channel`] for more information about + /// parameters. + fn open_channel_inner( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, push_to_counterparty_msat: Option, channel_config: Option, announce_channel: bool, @@ -1286,6 +1294,81 @@ impl Node { } } + /// Connect to a node and open a new channel. + /// + /// Disconnects and reconnects are handled automatically. + /// + /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the + /// channel counterparty on channel open. This can be useful to start out with the balance not + /// entirely shifted to one side, therefore allowing to receive payments from the getgo. + /// + /// If Anchor channels are enabled, this will ensure the configured + /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before + /// opening the channel. + /// + /// Calls `Node::open_channel_inner` with `announce_channel` set to `false`. + /// + /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. + pub fn open_channel( + &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, + push_to_counterparty_msat: Option, channel_config: Option, + ) -> Result { + self.open_channel_inner( + node_id, + address, + channel_amount_sats, + push_to_counterparty_msat, + channel_config, + false, + ) + } + + /// Connect to a node and open a new announced channel. + /// + /// Disconnects and reconnects are handled automatically. + /// + /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the + /// channel counterparty on channel open. This can be useful to start out with the balance not + /// entirely shifted to one side, therefore allowing to receive payments from the getgo. + /// + /// If Anchor channels are enabled, this will ensure the configured + /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before + /// opening the channel. + /// + /// Note that, regardless of the value of `announce_channel` passed, this function + /// checks that a node is configured to announce the channel to be openned and returns + /// an error if the configuration is wrong. Otherwise, calls `Node::open_channel_inner` + /// with `announced_channel` equals to `true`. + /// See `config::can_announce_channel` for more details. + /// + /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. + pub fn open_announced_channel( + &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, + push_to_counterparty_msat: Option, channel_config: Option, + ) -> Result { + match can_announce_channel(&self.config) { + config::ChannelAnnouncementStatus::Announceable => self.open_channel_inner( + node_id, + address, + channel_amount_sats, + push_to_counterparty_msat, + channel_config, + true, + ), + config::ChannelAnnouncementStatus::Unannounceable(reason) => match reason { + config::ChannelAnnouncementBlocker::MissingNodeAlias => { + return Err(Error::InvalidNodeAlias) + }, + config::ChannelAnnouncementBlocker::MissingListeningAddresses => { + return Err(Error::InvalidSocketAddress) + }, + config::ChannelAnnouncementBlocker::EmptyListeningAddresses => { + return Err(Error::InvalidSocketAddress) + }, + }, + } + } + /// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate /// cache. /// diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 17e5713d9..2a6ac8da3 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -19,7 +19,7 @@ pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning::offers::invoice::Bolt12Invoice; pub use lightning::offers::offer::{Offer, OfferId}; pub use lightning::offers::refund::Refund; -pub use lightning::routing::gossip::{NodeId, RoutingFees}; +pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; @@ -30,6 +30,7 @@ pub use bip39::Mnemonic; use crate::UniffiCustomTypeConverter; +use crate::builder::sanitize_alias; use crate::error::Error; use crate::hex_utils; use crate::{SocketAddress, UserChannelId}; @@ -324,3 +325,15 @@ impl UniffiCustomTypeConverter for UntrustedString { obj.to_string() } } + +impl UniffiCustomTypeConverter for NodeAlias { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(sanitize_alias(&val).map_err(|_| Error::InvalidNodeAlias)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 09c17dcf2..4dcbfd999 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -16,6 +16,7 @@ use ldk_node::{ use lightning::ln::msgs::SocketAddress; use lightning::ln::{PaymentHash, PaymentPreimage}; +use lightning::routing::gossip::NodeAlias; use lightning::util::persist::KVStore; use lightning::util::test_utils::TestStore; use lightning_persister::fs_store::FilesystemStore; @@ -200,13 +201,39 @@ pub(crate) fn random_listening_addresses() -> Vec { listening_addresses } -pub(crate) fn random_config(anchor_channels: bool) -> Config { +pub(crate) fn random_node_alias() -> Option { + let mut rng = thread_rng(); + let ranged_val = rng.gen_range(0..10); + + let alias = format!("ldk-node-{}", ranged_val); + let mut bytes = [0u8; 32]; + bytes[..alias.as_bytes().len()].copy_from_slice(alias.as_bytes()); + + Some(NodeAlias(bytes)) +} + +pub(crate) fn random_announce_channel() -> bool { + let mut rng = thread_rng(); + let ranged_val = rng.gen_range(0..=1); + match ranged_val { + 0 => false, + _ => true, + } +} + +pub(crate) fn random_config(anchor_channels: bool, announce_channel: bool) -> Config { let mut config = Config::default(); if !anchor_channels { config.anchor_channels_config = None; } + if announce_channel { + let alias = random_node_alias(); + println!("Setting random LDK node alias: {:?}", alias); + config.node_alias = alias; + } + config.network = Network::Regtest; config.onchain_wallet_sync_interval_secs = 100000; config.wallet_sync_interval_secs = 100000; @@ -242,14 +269,15 @@ macro_rules! setup_builder { pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( - electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool, anchors_trusted_no_reserve: bool, + electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool, + anchors_trusted_no_reserve: bool, announce_channel: bool, ) -> (TestNode, TestNode) { println!("== Node A =="); - let config_a = random_config(anchor_channels); + let config_a = random_config(anchor_channels, announce_channel); let node_a = setup_node(electrsd, config_a); println!("\n== Node B =="); - let mut config_b = random_config(anchor_channels); + let mut config_b = random_config(anchor_channels, announce_channel); if allow_0conf { config_b.trusted_peers_0conf.push(node_a.node_id()); } @@ -385,19 +413,30 @@ pub(crate) fn premine_and_distribute_funds( } pub fn open_channel( - node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, announce: bool, - electrsd: &ElectrsD, + node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, electrsd: &ElectrsD, ) { - node_a - .connect_open_channel( - node_b.node_id(), - node_b.listening_addresses().unwrap().first().unwrap().clone(), - funding_amount_sat, - None, - None, - announce, - ) - .unwrap(); + if node_a.config().node_alias.is_some() { + node_a + .open_announced_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + funding_amount_sat, + None, + None, + ) + .unwrap(); + } else { + node_a + .open_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + funding_amount_sat, + None, + None, + ) + .unwrap(); + } + assert!(node_a.list_peers().iter().find(|c| { c.node_id == node_b.node_id() }).is_some()); let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id()); @@ -430,19 +469,31 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.next_event(), None); assert_eq!(node_b.next_event(), None); - println!("\nA -- connect_open_channel -> B"); + println!("\nA -- open_channel -> B"); let funding_amount_sat = 2_080_000; let push_msat = (funding_amount_sat / 2) * 1000; // balance the channel - node_a - .connect_open_channel( - node_b.node_id(), - node_b.listening_addresses().unwrap().first().unwrap().clone(), - funding_amount_sat, - Some(push_msat), - None, - true, - ) - .unwrap(); + + if node_a.config().node_alias.is_some() { + node_a + .open_announced_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + funding_amount_sat, + Some(push_msat), + None, + ) + .unwrap(); + } else { + node_a + .open_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + funding_amount_sat, + Some(push_msat), + None, + ) + .unwrap(); + } assert_eq!(node_a.list_peers().first().unwrap().node_id, node_b.node_id()); assert!(node_a.list_peers().first().unwrap().is_persisted); diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index bcb84833f..11065bfe6 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -9,6 +9,7 @@ mod common; +use common::random_announce_channel; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Amount; use ldk_node::lightning::ln::msgs::SocketAddress; @@ -43,7 +44,7 @@ fn test_cln() { common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1); // Setup LDK Node - let config = common::random_config(true); + let config = common::random_config(true, random_announce_channel()); let mut builder = Builder::from_config(config); builder.set_esplora_server("http://127.0.0.1:3002".to_string()); @@ -82,15 +83,19 @@ fn test_cln() { // Open the channel let funding_amount_sat = 1_000_000; - node.connect_open_channel( - cln_node_id, - cln_address, - funding_amount_sat, - Some(500_000_000), - None, - false, - ) - .unwrap(); + if node.config().node_alias.is_none() { + node.open_channel(cln_node_id, cln_address, funding_amount_sat, Some(500_000_000), None) + .unwrap(); + } else { + node.open_announced_channel( + cln_node_id, + cln_address, + funding_amount_sat, + Some(500_000_000), + None, + ) + .unwrap(); + } let funding_txo = common::expect_channel_pending_event!(node, cln_node_id); common::wait_for_tx(&electrs_client, funding_txo.txid); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 6b5b405dd..9c244e90a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -10,8 +10,9 @@ mod common; use common::{ do_channel_full_cycle, expect_channel_ready_event, expect_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, open_channel, - premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestSyncStore, + premine_and_distribute_funds, random_announce_channel, random_config, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, + TestSyncStore, }; use ldk_node::payment::{PaymentKind, QrPaymentResult, SendingParameters}; @@ -27,42 +28,46 @@ use std::sync::Arc; #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } #[test] fn channel_full_cycle_force_close() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true); } #[test] fn channel_full_cycle_force_close_trusted_no_reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, true); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, true, random_announce_channel()); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true); } #[test] fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, true, true, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, true, true, false, random_announce_channel()); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) } #[test] fn channel_full_cycle_legacy_staticremotekey() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, false, false, random_announce_channel()); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false); } #[test] fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -80,17 +85,26 @@ fn channel_open_fails_when_funds_insufficient() { assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - println!("\nA -- connect_open_channel -> B"); + println!("\nA -- open_channel -> B"); assert_eq!( Err(NodeError::InsufficientFunds), - node_a.connect_open_channel( - node_b.node_id(), - node_b.listening_addresses().unwrap().first().unwrap().clone(), - 120000, - None, - None, - true - ) + if node_a.config().node_alias.is_some() { + node_a.open_announced_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + 120000, + None, + None, + ) + } else { + node_a.open_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + 120000, + None, + None, + ) + } ); } @@ -101,8 +115,9 @@ fn multi_hop_sending() { // Setup and fund 5 nodes let mut nodes = Vec::new(); + let announce_channel = random_announce_channel(); for _ in 0..5 { - let config = random_config(true); + let config = random_config(true, announce_channel); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); let node = builder.build().unwrap(); @@ -132,16 +147,16 @@ fn multi_hop_sending() { // \ / // (1M:0)- N3 -(1M:0) - open_channel(&nodes[0], &nodes[1], 100_000, true, &electrsd); - open_channel(&nodes[1], &nodes[2], 1_000_000, true, &electrsd); + open_channel(&nodes[0], &nodes[1], 100_000, &electrsd); + open_channel(&nodes[1], &nodes[2], 1_000_000, &electrsd); // We need to sync wallets in-between back-to-back channel opens from the same node so BDK // wallet picks up on the broadcast funding tx and doesn't double-spend itself. // // TODO: Remove once fixed in BDK. nodes[1].sync_wallets().unwrap(); - open_channel(&nodes[1], &nodes[3], 1_000_000, true, &electrsd); - open_channel(&nodes[2], &nodes[4], 1_000_000, true, &electrsd); - open_channel(&nodes[3], &nodes[4], 1_000_000, true, &electrsd); + open_channel(&nodes[1], &nodes[3], 1_000_000, &electrsd); + open_channel(&nodes[2], &nodes[4], 1_000_000, &electrsd); + open_channel(&nodes[3], &nodes[4], 1_000_000, &electrsd); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); @@ -171,16 +186,22 @@ fn multi_hop_sending() { }; let invoice = nodes[4].bolt11_payment().receive(2_500_000, &"asdf", 9217).unwrap(); - nodes[0].bolt11_payment().send(&invoice, Some(sending_params)).unwrap(); + let send_res = nodes[0].bolt11_payment().send(&invoice, Some(sending_params)); - let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); - let fee_paid_msat = Some(2000); - expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); + // N0 cannot find a route to N4 if node and channel is unannounced. + if nodes[0].config().node_alias.is_none() { + assert_eq!(send_res, Err(NodeError::PaymentSendingFailed)) + } else { + let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); + assert_eq!(send_res.unwrap(), payment_id.unwrap()); + let fee_paid_msat = Some(2000); + expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); + } } #[test] fn connect_to_public_testnet_esplora() { - let mut config = random_config(true); + let mut config = random_config(true, random_announce_channel()); config.network = Network::Testnet; setup_builder!(builder, config); builder.set_esplora_server("https://blockstream.info/testnet/api".to_string()); @@ -192,7 +213,7 @@ fn connect_to_public_testnet_esplora() { #[test] fn start_stop_reinit() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(true); + let config = random_config(true, random_announce_channel()); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); @@ -260,7 +281,8 @@ fn start_stop_reinit() { #[test] fn onchain_spend_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -308,7 +330,7 @@ fn onchain_spend_receive() { #[test] fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(true); + let config = random_config(true, random_announce_channel()); let node = setup_node(&electrsd, config); // Tests arbitrary message signing and later verification @@ -326,7 +348,8 @@ fn connection_restart_behavior() { fn do_connection_restart_behavior(persist: bool) { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, false, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, false, false, random_announce_channel()); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -377,7 +400,8 @@ fn do_connection_restart_behavior(persist: bool) { #[test] fn concurrent_connections_succeed() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -407,7 +431,8 @@ fn concurrent_connections_succeed() { #[test] fn simple_bolt12_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; @@ -419,7 +444,7 @@ fn simple_bolt12_send_receive() { ); node_a.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + open_channel(&node_a, &node_b, 4_000_000, &electrsd); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); @@ -429,191 +454,203 @@ fn simple_bolt12_send_receive() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); - // Sleep until we broadcasted a node announcement. - while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { - std::thread::sleep(std::time::Duration::from_millis(10)); + // For announced nodes, we make a trivial check that node_alias is set knowing that + // `random_listening_addresses()` always sets the listening_addresses. This check + // is important to prevent the test from looping endlessly. + if node_a.config().node_alias.is_some() && node_b.config().node_alias.is_some() { + // Sleep until we broadcasted a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); } - // Sleep one more sec to make sure the node announcement propagates. - std::thread::sleep(std::time::Duration::from_secs(1)); - let expected_amount_msat = 100_000_000; - let offer = node_b.bolt12_payment().receive(expected_amount_msat, "asdf", Some(1)).unwrap(); - let expected_quantity = Some(1); - let expected_payer_note = Some("Test".to_string()); - let payment_id = node_a - .bolt12_payment() - .send(&offer, expected_quantity, expected_payer_note.clone()) - .unwrap(); - - expect_payment_successful_event!(node_a, Some(payment_id), None); - let node_a_payments = node_a.list_payments(); - assert_eq!(node_a_payments.len(), 1); - match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { - hash, - preimage, - secret: _, - offer_id, - quantity: ref qty, - payer_note: ref note, - } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert_eq!(offer_id, offer.id()); - assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); - //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 - //API currently doesn't allow to do that. - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - expect_payment_received_event!(node_b, expected_amount_msat); - let node_b_payments = node_b.list_payments(); - assert_eq!(node_b_payments.len(), 1); - match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert!(secret.is_some()); - assert_eq!(offer_id, offer.id()); - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - // Test send_using_amount - let offer_amount_msat = 100_000_000; - let less_than_offer_amount = offer_amount_msat - 10_000; - let expected_amount_msat = offer_amount_msat + 10_000; - let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf", Some(1)).unwrap(); - let expected_quantity = Some(1); - let expected_payer_note = Some("Test".to_string()); - assert!(node_a - .bolt12_payment() - .send_using_amount(&offer, less_than_offer_amount, None, None) - .is_err()); - let payment_id = node_a - .bolt12_payment() - .send_using_amount( - &offer, - expected_amount_msat, - expected_quantity, - expected_payer_note.clone(), - ) - .unwrap(); - - expect_payment_successful_event!(node_a, Some(payment_id), None); - let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id); - assert_eq!(node_a_payments.len(), 1); - let payment_hash = match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { - hash, - preimage, - secret: _, - offer_id, - quantity: ref qty, - payer_note: ref note, - } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert_eq!(offer_id, offer.id()); - assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); - //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 - //API currently doesn't allow to do that. - hash.unwrap() - }, - _ => { - panic!("Unexpected payment kind"); - }, - }; - assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - expect_payment_received_event!(node_b, expected_amount_msat); - let node_b_payment_id = PaymentId(payment_hash.0); - let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); - assert_eq!(node_b_payments.len(), 1); - match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert!(secret.is_some()); - assert_eq!(offer_id, offer.id()); - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - // Now node_b refunds the amount node_a just overpaid. - let overpaid_amount = expected_amount_msat - offer_amount_msat; - let expected_quantity = Some(1); - let expected_payer_note = Some("Test".to_string()); - let refund = node_b - .bolt12_payment() - .initiate_refund(overpaid_amount, 3600, expected_quantity, expected_payer_note.clone()) - .unwrap(); - let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); - expect_payment_received_event!(node_a, overpaid_amount); - - let node_b_payment_id = node_b - .list_payments_with_filter(|p| p.amount_msat == Some(overpaid_amount)) - .first() - .unwrap() - .id; - expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); - - let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); - assert_eq!(node_b_payments.len(), 1); - match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Refund { - hash, - preimage, - secret: _, - quantity: ref qty, - payer_note: ref note, - } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) - //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 - //API currently doesn't allow to do that. - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); - - let node_a_payment_id = PaymentId(invoice.payment_hash().0); - let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id); - assert_eq!(node_a_payments.len(), 1); - match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert!(secret.is_some()); - }, - _ => { - panic!("Unexpected payment kind"); - }, + let offer_res = node_b.bolt12_payment().receive(expected_amount_msat, "asdf", Some(1)); + if node_a.config().node_alias.is_none() && node_b.config().node_alias.is_none() { + // Node must be announced if alternative one-hop `BlindedPath` is to be used. + assert_eq!(offer_res, Err(NodeError::OfferCreationFailed)) + } else { + let offer = offer_res.unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + let payment_id = node_a + .bolt12_payment() + .send(&offer, expected_quantity, expected_payer_note.clone()) + .unwrap(); + + expect_payment_successful_event!(node_a, Some(payment_id), None); + let node_a_payments = node_a.list_payments(); + assert_eq!(node_a_payments.len(), 1); + match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { + hash, + preimage, + secret: _, + offer_id, + quantity: ref qty, + payer_note: ref note, + } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(offer_id, offer.id()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + //API currently doesn't allow to do that. + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + expect_payment_received_event!(node_b, expected_amount_msat); + let node_b_payments = node_b.list_payments(); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + assert_eq!(offer_id, offer.id()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + // Test send_using_amount + let offer_amount_msat = 100_000_000; + let less_than_offer_amount = offer_amount_msat - 10_000; + let expected_amount_msat = offer_amount_msat + 10_000; + let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf", Some(1)).unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + assert!(node_a + .bolt12_payment() + .send_using_amount(&offer, less_than_offer_amount, None, None) + .is_err()); + let payment_id = node_a + .bolt12_payment() + .send_using_amount( + &offer, + expected_amount_msat, + expected_quantity, + expected_payer_note.clone(), + ) + .unwrap(); + + expect_payment_successful_event!(node_a, Some(payment_id), None); + let node_a_payments = node_a.list_payments_with_filter(|p| p.id == payment_id); + assert_eq!(node_a_payments.len(), 1); + let payment_hash = match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { + hash, + preimage, + secret: _, + offer_id, + quantity: ref qty, + payer_note: ref note, + } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(offer_id, offer.id()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + //API currently doesn't allow to do that. + hash.unwrap() + }, + _ => { + panic!("Unexpected payment kind"); + }, + }; + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + expect_payment_received_event!(node_b, expected_amount_msat); + let node_b_payment_id = PaymentId(payment_hash.0); + let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + assert_eq!(offer_id, offer.id()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + // Now node_b refunds the amount node_a just overpaid. + let overpaid_amount = expected_amount_msat - offer_amount_msat; + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + let refund = node_b + .bolt12_payment() + .initiate_refund(overpaid_amount, 3600, expected_quantity, expected_payer_note.clone()) + .unwrap(); + let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); + expect_payment_received_event!(node_a, overpaid_amount); + + let node_b_payment_id = node_b + .list_payments_with_filter(|p| p.amount_msat == Some(overpaid_amount)) + .first() + .unwrap() + .id; + expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); + + let node_b_payments = node_b.list_payments_with_filter(|p| p.id == node_b_payment_id); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Refund { + hash, + preimage, + secret: _, + quantity: ref qty, + payer_note: ref note, + } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) + //TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + //API currently doesn't allow to do that. + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); + + let node_a_payment_id = PaymentId(invoice.payment_hash().0); + let node_a_payments = node_a.list_payments_with_filter(|p| p.id == node_a_payment_id); + assert_eq!(node_a_payments.len(), 1); + match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } - assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } #[test] fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -626,7 +663,7 @@ fn generate_bip21_uri() { ); node_a.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + open_channel(&node_a, &node_b, 4_000_000, &electrsd); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); node_a.sync_wallets().unwrap(); @@ -640,21 +677,26 @@ fn generate_bip21_uri() { let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec); - match uqr_payment.clone() { - Ok(ref uri) => { - println!("Generated URI: {}", uri); - assert!(uri.contains("BITCOIN:")); - assert!(uri.contains("lightning=")); - assert!(uri.contains("lno=")); - }, - Err(e) => panic!("Failed to generate URI: {:?}", e), + if node_a.config().node_alias.is_none() && node_b.config().node_alias.is_none() { + assert_eq!(uqr_payment, Err(NodeError::OfferCreationFailed)); + } else { + match uqr_payment.clone() { + Ok(ref uri) => { + println!("Generated URI: {}", uri); + assert!(uri.contains("BITCOIN:")); + assert!(uri.contains("lightning=")); + assert!(uri.contains("lno=")); + }, + Err(e) => panic!("Failed to generate URI: {:?}", e), + } } } #[test] fn unified_qr_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + let (node_a, node_b) = + setup_two_nodes(&electrsd, false, true, false, random_announce_channel()); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -667,7 +709,7 @@ fn unified_qr_send_receive() { ); node_a.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + open_channel(&node_a, &node_b, 4_000_000, &electrsd); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); node_a.sync_wallets().unwrap(); @@ -676,88 +718,98 @@ fn unified_qr_send_receive() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); - // Sleep until we broadcast a node announcement. - while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { - std::thread::sleep(std::time::Duration::from_millis(10)); + // For announced nodes, we make a trivial check that node_alias is set knowing that + // `random_listening_addresses()` always sets the listening_addresses. This check + // is important to prevent the test from looping endlessly. + if node_a.config().node_alias.is_some() && node_b.config().node_alias.is_some() { + // Sleep until we broadcasted a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Sleep one more sec to make sure the node announcement propagates. + std::thread::sleep(std::time::Duration::from_secs(1)); } - // Sleep one more sec to make sure the node announcement propagates. - std::thread::sleep(std::time::Duration::from_secs(1)); - let expected_amount_sats = 100_000; let expiry_sec = 4_000; let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec); - let uri_str = uqr_payment.clone().unwrap(); - let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_str) { - Ok(QrPaymentResult::Bolt12 { payment_id }) => { - println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id - }, - Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { - panic!("Expected Bolt12 payment but got Bolt11"); - }, - Ok(QrPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt12 payment but get On-chain transaction"); - }, - Err(e) => { - panic!("Expected Bolt12 payment but got error: {:?}", e); - }, - }; - - expect_payment_successful_event!(node_a, Some(offer_payment_id), None); + if node_a.config().node_alias.is_none() && node_b.config().node_alias.is_none() { + // Node must be announced if alternative one-hop `BlindedPath` is to be used used. + assert_eq!(uqr_payment, Err(NodeError::OfferCreationFailed)) + } else { + let uri_str = uqr_payment.clone().unwrap(); + let offer_payment_id: PaymentId = match node_a.unified_qr_payment().send(&uri_str) { + Ok(QrPaymentResult::Bolt12 { payment_id }) => { + println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected Bolt12 payment but got Bolt11"); + }, + Ok(QrPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but get On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; - // Removed one character from the offer to fall back on to invoice. - // Still needs work - let uri_str_with_invalid_offer = &uri_str[..uri_str.len() - 1]; - let invoice_payment_id: PaymentId = - match node_a.unified_qr_payment().send(uri_str_with_invalid_offer) { + expect_payment_successful_event!(node_a, Some(offer_payment_id), None); + + // Removed one character from the offer to fall back on to invoice. + // Still needs work + let uri_str_with_invalid_offer = &uri_str[..uri_str.len() - 1]; + let invoice_payment_id: PaymentId = + match node_a.unified_qr_payment().send(uri_str_with_invalid_offer) { + Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { + panic!("Expected Bolt11 payment but got Bolt12"); + }, + Ok(QrPaymentResult::Bolt11 { payment_id }) => { + println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(QrPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt11 payment but got on-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt11 payment but got error: {:?}", e); + }, + }; + expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); + + let expect_onchain_amount_sats = 800_000; + let onchain_uqr_payment = + node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); + + // Removed a character from the offer, so it would move on to the other parameters. + let txid = match node_a + .unified_qr_payment() + .send(&onchain_uqr_payment.as_str()[..onchain_uqr_payment.len() - 1]) + { Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { - panic!("Expected Bolt11 payment but got Bolt12"); + panic!("Expected on-chain payment but got Bolt12") }, - Ok(QrPaymentResult::Bolt11 { payment_id }) => { - println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id + Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected on-chain payment but got Bolt11"); }, - Ok(QrPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt11 payment but got on-chain transaction"); + Ok(QrPaymentResult::Onchain { txid }) => { + println!("\nOn-chain transaction successful with Txid: {}", txid); + txid }, Err(e) => { - panic!("Expected Bolt11 payment but got error: {:?}", e); + panic!("Expected on-chain payment but got error: {:?}", e); }, }; - expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); - - let expect_onchain_amount_sats = 800_000; - let onchain_uqr_payment = - node_b.unified_qr_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); - - // Removed a character from the offer, so it would move on to the other parameters. - let txid = match node_a - .unified_qr_payment() - .send(&onchain_uqr_payment.as_str()[..onchain_uqr_payment.len() - 1]) - { - Ok(QrPaymentResult::Bolt12 { payment_id: _ }) => { - panic!("Expected on-chain payment but got Bolt12") - }, - Ok(QrPaymentResult::Bolt11 { payment_id: _ }) => { - panic!("Expected on-chain payment but got Bolt11"); - }, - Ok(QrPaymentResult::Onchain { txid }) => { - println!("\nOn-chain transaction successful with Txid: {}", txid); - txid - }, - Err(e) => { - panic!("Expected on-chain payment but got error: {:?}", e); - }, - }; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); - wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + wait_for_tx(&electrsd.client, txid); - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); - assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); + assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); + assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); + } } diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index c572fbcd8..b7fc8ba42 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -9,6 +9,7 @@ mod common; +use common::random_announce_channel; use ldk_node::Builder; #[test] @@ -16,7 +17,8 @@ fn channel_full_cycle_with_vss_store() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); println!("== Node A =="); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let config_a = common::random_config(true); + let announce_channel = random_announce_channel(); + let config_a = common::random_config(true, announce_channel); let mut builder_a = Builder::from_config(config_a); builder_a.set_esplora_server(esplora_url.clone()); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); @@ -25,7 +27,7 @@ fn channel_full_cycle_with_vss_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(true); + let config_b = common::random_config(true, announce_channel); let mut builder_b = Builder::from_config(config_b); builder_b.set_esplora_server(esplora_url); let node_b = builder_b.build_with_vss_store(vss_base_url, "node_2_store".to_string()).unwrap();