From a2691e76db25bf712bcc31f5cf6289e364d6fbb0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 6 Feb 2024 10:27:19 +0100 Subject: [PATCH 1/7] Introduce `AnchorChannelsConfig` .. allowing to configure the per-channel emergency reserve as well as some trusted peers for which we won't maintain any reserve. --- bindings/ldk_node.udl | 6 +++ src/config.rs | 93 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 328dcfdbd..762eb25c4 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -15,6 +15,12 @@ dictionary Config { sequence trusted_peers_0conf; u64 probing_liquidity_limit_multiplier; LogLevel log_level; + AnchorChannelsConfig? anchor_channels_config; +}; + +dictionary AnchorChannelsConfig { + sequence trusted_peers_no_reserve; + u64 per_channel_reserve_sats; }; interface Builder { diff --git a/src/config.rs b/src/config.rs index 945d712c9..aaa205f24 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ const DEFAULT_LDK_WALLET_SYNC_INTERVAL_SECS: u64 = 30; const DEFAULT_FEE_RATE_CACHE_UPDATE_INTERVAL_SECS: u64 = 60 * 10; const DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER: u64 = 3; const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Debug; +const DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS: u64 = 25_000; // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold // number of derivation indexes after which BDK stops looking for new scripts belonging to the wallet. @@ -62,6 +63,9 @@ pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64; /// | `trusted_peers_0conf` | [] | /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | +/// | `anchor_channels_config` | Some(..) | +/// +/// See [`AnchorChannelsConfig`] for more information on its respective default values. /// /// [`Node`]: crate::Node pub struct Config { @@ -104,6 +108,23 @@ pub struct Config { /// /// Any messages below this level will be excluded from the logs. pub log_level: LogLevel, + /// Configuration options pertaining to Anchor channels, i.e., channels for which the + /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. + /// + /// Please refer to [`AnchorChannelsConfig`] for further information on Anchor channels. + /// + /// If set to `Some`, we'll try to open new channels with Anchors enabled, i.e., new channels + /// will be negotiated with the `option_anchors_zero_fee_htlc_tx` channel type if supported by + /// the counterparty. Note that this won't prevent us from opening non-Anchor channels if the + /// counterparty doesn't support `option_anchors_zero_fee_htlc_tx`. If set to `None`, new + /// channels will be negotiated with the legacy `option_static_remotekey` channel type only. + /// + /// **Note:** If set to `None` *after* some Anchor channels have already been + /// opened, no dedicated emergency on-chain reserve will be maintained for these channels, + /// which can be dangerous if only insufficient funds are available at the time of channel + /// closure. We *will* however still try to get the Anchor spending transactions confirmed + /// on-chain with the funds available. + pub anchor_channels_config: Option, } impl Default for Config { @@ -120,6 +141,78 @@ impl Default for Config { trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, log_level: DEFAULT_LOG_LEVEL, + anchor_channels_config: Some(AnchorChannelsConfig::default()), + } + } +} + +/// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the +/// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. +/// +/// Prior to the introduction of Anchor channels, the on-chain fees paying for the transactions +/// issued on channel closure were pre-determined and locked-in at the time of the channel +/// opening. This required to estimate what fee rate would be sufficient to still have the +/// closing transactions be spendable on-chain (i.e., not be considered dust). This legacy +/// design of pre-anchor channels proved inadequate in the unpredictable, often turbulent, fee +/// markets we experience today. +/// +/// In contrast, Anchor channels allow to determine an adequate fee rate *at the time of channel +/// closure*, making them much more robust in the face of fee spikes. In turn, they require to +/// maintain a reserve of on-chain funds to have the channel closure transactions confirmed +/// on-chain, at least if the channel counterparty can't be trusted to do this for us. +/// +/// See [BOLT 3] for more technical details on Anchor channels. +/// +/// +/// ### Defaults +/// +/// | Parameter | Value | +/// |----------------------------|--------| +/// | `trusted_peers_no_reserve` | [] | +/// | `per_channel_reserve_sats` | 25000 | +/// +/// +/// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#htlc-timeout-and-htlc-success-transactions +#[derive(Debug, Clone)] +pub struct AnchorChannelsConfig { + /// A list of peers that we trust to get the required channel closing transactions confirmed + /// on-chain. + /// + /// Channels with these peers won't count towards the retained on-chain reserve and we won't + /// take any action to get the required transactions confirmed ourselves. + /// + /// **Note:** Trusting the channel counterparty to take the necessary actions to get the + /// required Anchor spending and HTLC transactions confirmed on-chain is potentially insecure + /// as the channel may not be closed if they refuse to do so, potentially leaving the user + /// funds stuck *or* even allow the counterparty to steal any in-flight funds after the + /// corresponding HTLCs time out. + pub trusted_peers_no_reserve: Vec, + /// The amount of satoshis per anchors-negotiated channel with an untrusted peer that we keep + /// as an emergency reserve in our on-chain wallet. + /// + /// This allows for having the required Anchor output spending and HTLC transactions confirmed + /// when the channel is closed. + /// + /// If the channel peer is not marked as trusted via + /// [`AnchorChannelsConfig::trusted_peers_no_reserve`], we will always try to spend the Anchor + /// outputs with *any* on-chain funds available, i.e., the total reserve value as well as any + /// spendable funds available in the on-chain wallet. Therefore, this per-channel multiplier is + /// really a emergencey reserve that we maintain at all time to reduce reduce the risk of + /// insufficient funds at time of a channel closure. To this end, we will refuse to open + /// outbound or accept inbound channels if we don't have sufficient on-chain funds availble to + /// cover the additional reserve requirement. + /// + /// **Note:** Depending on the fee market at the time of closure, this reserve amount might or + /// might not suffice to successfully spend the Anchor output and have the HTLC transactions + /// confirmed on-chain, i.e., you may want to adjust this value accordingly. + pub per_channel_reserve_sats: u64, +} + +impl Default for AnchorChannelsConfig { + fn default() -> Self { + Self { + trusted_peers_no_reserve: Vec::new(), + per_channel_reserve_sats: DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS, } } } diff --git a/src/lib.rs b/src/lib.rs index 9a634c12d..2c66fd154 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ pub use lightning; pub use lightning_invoice; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; -pub use config::{default_config, Config}; +pub use config::{default_config, AnchorChannelsConfig, Config}; pub use error::Error as NodeError; use error::Error; From db1b3732af87e96091c53ed813b0933fc58acfcb Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 27 Jun 2023 12:34:01 -0400 Subject: [PATCH 2/7] Implement Anchor channel event handling When Anchor outputs need to be spent LDK will generate `BumpTransactionEvent`s. Here, we add the corresponding event-handling and PSBT-signing support. --- src/builder.rs | 11 ++--- src/event.rs | 49 +++++++++++++++--- src/lib.rs | 13 ++++- src/types.rs | 8 +++ src/wallet.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 192 insertions(+), 20 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 386deb418..2a361396d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -693,12 +693,11 @@ fn build_with_store_internal( // for inbound channels. let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; - - if !config.trusted_peers_0conf.is_empty() { - // Manually accept inbound channels if we expect 0conf channel requests, avoid - // generating the events otherwise. - user_config.manually_accept_inbound_channels = true; - } + user_config.manually_accept_inbound_channels = true; + // Note the channel_handshake_config will be overwritten in `connect_open_channel`, but we + // still set a default here. + user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = + config.anchor_channels_config.is_some(); if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll diff --git a/src/event.rs b/src/event.rs index b49fc96e8..5aeb8f346 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,7 +1,8 @@ use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ - hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, + hex_utils, BumpTransactionEventHandler, ChannelManager, Config, Error, NetworkGraph, PeerInfo, + PeerStore, UserChannelId, }; use crate::connection::ConnectionManager; @@ -15,9 +16,10 @@ use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; -use crate::logger::{log_error, log_info, Logger}; +use crate::logger::{log_debug, log_error, log_info, Logger}; use lightning::chain::chaininterface::ConfirmationTarget; +use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ClosureReason, PaymentPurpose}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; use lightning::impl_writeable_tlv_based_enum; @@ -317,6 +319,7 @@ where { event_queue: Arc>, wallet: Arc, + bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, @@ -333,15 +336,17 @@ where L::Target: Logger, { pub fn new( - event_queue: Arc>, wallet: Arc, channel_manager: Arc, - connection_manager: Arc>, output_sweeper: Arc, - network_graph: Arc, payment_store: Arc>, - peer_store: Arc>, runtime: Arc>>, - logger: L, config: Arc, + event_queue: Arc>, wallet: Arc, + bump_tx_event_handler: Arc, + channel_manager: Arc, connection_manager: Arc>, + output_sweeper: Arc, network_graph: Arc, + payment_store: Arc>, peer_store: Arc>, + runtime: Arc>>, logger: L, config: Arc, ) -> Self { Self { event_queue, wallet, + bump_tx_event_handler, channel_manager, connection_manager, output_sweeper, @@ -1018,7 +1023,6 @@ where }, LdkEvent::DiscardFunding { .. } => {}, LdkEvent::HTLCIntercepted { .. } => {}, - LdkEvent::BumpTransaction(_) => {}, LdkEvent::InvoiceRequestFailed { payment_id } => { log_error!( self.logger, @@ -1062,6 +1066,35 @@ where }); } }, + LdkEvent::BumpTransaction(bte) => { + let (channel_id, counterparty_node_id) = match bte { + BumpTransactionEvent::ChannelClose { + ref channel_id, + ref counterparty_node_id, + .. + } => (channel_id, counterparty_node_id), + BumpTransactionEvent::HTLCResolution { + ref channel_id, + ref counterparty_node_id, + .. + } => (channel_id, counterparty_node_id), + }; + + if let Some(anchor_channels_config) = self.config.anchor_channels_config.as_ref() { + if anchor_channels_config + .trusted_peers_no_reserve + .contains(counterparty_node_id) + { + log_debug!(self.logger, + "Ignoring BumpTransactionEvent for channel {} due to trusted counterparty {}", + channel_id, counterparty_node_id + ); + return; + } + } + + self.bump_tx_event_handler.handle_event(&bte); + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 2c66fd154..190cdfd44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,14 +133,15 @@ use payment::store::PaymentStore; use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; use peer_store::{PeerInfo, PeerStore}; use types::{ - Broadcaster, ChainMonitor, ChannelManager, DynStore, FeeEstimator, KeysManager, NetworkGraph, - PeerManager, Router, Scorer, Sweeper, Wallet, + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator, + KeysManager, NetworkGraph, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, PeerDetails, UserChannelId}; use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; use lightning::chain::{BestBlock, Confirm}; +use lightning::events::bump_transaction::Wallet as LdkWallet; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; @@ -620,9 +621,17 @@ impl Node { } }); + let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( + Arc::clone(&self.tx_broadcaster), + Arc::new(LdkWallet::new(Arc::clone(&self.wallet), Arc::clone(&self.logger))), + Arc::clone(&self.keys_manager), + Arc::clone(&self.logger), + )); + let event_handler = Arc::new(EventHandler::new( Arc::clone(&self.event_queue), Arc::clone(&self.wallet), + bump_tx_event_handler, Arc::clone(&self.channel_manager), Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), diff --git a/src/types.rs b/src/types.rs index 14d8adf76..b7cf4ad44 100644 --- a/src/types.rs +++ b/src/types.rs @@ -135,6 +135,14 @@ pub(crate) type Sweeper = OutputSweeper< Arc, >; +pub(crate) type BumpTransactionEventHandler = + lightning::events::bump_transaction::BumpTransactionEventHandler< + Arc, + Arc, Arc>>, + Arc, + Arc, + >; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. diff --git a/src/wallet.rs b/src/wallet.rs index 674cb6786..df9656d48 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -4,6 +4,7 @@ use crate::Error; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; use lightning::ln::script::ShutdownScript; use lightning::sign::{ @@ -19,8 +20,14 @@ use bdk::wallet::AddressIndex; use bdk::FeeRate; use bdk::{SignOptions, SyncOptions}; +use bitcoin::address::{Payload, WitnessVersion}; use bitcoin::bech32::u5; +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::hash_types::WPubkeyHash; +use bitcoin::hashes::Hash; +use bitcoin::key::XOnlyPublicKey; +use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; @@ -242,6 +249,123 @@ where } } +impl WalletSource for Wallet +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + fn list_confirmed_utxos(&self) -> Result, ()> { + let locked_wallet = self.inner.lock().unwrap(); + let mut utxos = Vec::new(); + let confirmed_txs: Vec = locked_wallet + .list_transactions(false) + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve transactions from wallet: {}", e); + })? + .into_iter() + .filter(|t| t.confirmation_time.is_some()) + .collect(); + let unspent_confirmed_utxos = locked_wallet + .list_unspent() + .map_err(|e| { + log_error!( + self.logger, + "Failed to retrieve unspent transactions from wallet: {}", + e + ); + })? + .into_iter() + .filter(|u| confirmed_txs.iter().find(|t| t.txid == u.outpoint.txid).is_some()); + + for u in unspent_confirmed_utxos { + let payload = Payload::from_script(&u.txout.script_pubkey).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + + match payload { + Payload::WitnessProgram(program) => match program.version() { + WitnessVersion::V0 if program.program().len() == 20 => { + let wpkh = + WPubkeyHash::from_slice(program.program().as_bytes()).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + let utxo = Utxo::new_v0_p2wpkh(u.outpoint, u.txout.value, &wpkh); + utxos.push(utxo); + }, + WitnessVersion::V1 => { + XOnlyPublicKey::from_slice(program.program().as_bytes()).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + + let utxo = Utxo { + outpoint: u.outpoint, + output: TxOut { + value: u.txout.value, + script_pubkey: ScriptBuf::new_witness_program(&program), + }, + satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 + + 1 /* witness items */ + 1 /* schnorr sig len */ + 64, /* schnorr sig */ + }; + utxos.push(utxo); + }, + _ => { + log_error!( + self.logger, + "Unexpected witness version or length. Version: {}, Length: {}", + program.version(), + program.program().len() + ); + }, + }, + _ => { + log_error!( + self.logger, + "Tried to use a non-witness script. This must never happen." + ); + panic!("Tried to use a non-witness script. This must never happen."); + }, + } + } + + Ok(utxos) + } + + fn get_change_script(&self) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let address_info = locked_wallet.get_address(AddressIndex::LastUnused).map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; + + Ok(address_info.address.script_pubkey()) + } + + fn sign_psbt(&self, mut psbt: PartiallySignedTransaction) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + + // While BDK populates both `witness_utxo` and `non_witness_utxo` fields, LDK does not. As + // BDK by default doesn't trust the witness UTXO to account for the Segwit bug, we must + // disable it here as otherwise we fail to sign. + let mut sign_options = SignOptions::default(); + sign_options.trust_witness_utxo = true; + + match locked_wallet.sign(&mut psbt, sign_options) { + Ok(_finalized) => { + // BDK will fail to finalize for all LDK-provided inputs of the PSBT. Unfortunately + // we can't check more fine grained if it succeeded for all the other inputs here, + // so we just ignore the returned `finalized` bool. + }, + Err(err) => { + log_error!(self.logger, "Failed to sign transaction: {}", err); + return Err(()); + }, + } + + Ok(psbt.extract_tx()) + } +} + /// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are /// directly spendable by the BDK wallet. pub struct WalletKeysManager @@ -407,11 +531,10 @@ where })?; match address.payload { - bitcoin::address::Payload::WitnessProgram(program) => { - ShutdownScript::new_witness_program(&program).map_err(|e| { + Payload::WitnessProgram(program) => ShutdownScript::new_witness_program(&program) + .map_err(|e| { log_error!(self.logger, "Invalid shutdown script: {:?}", e); - }) - }, + }), _ => { log_error!( self.logger, From 1952802c4929021538929b917bef0262fb7f0c5f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 19 Feb 2024 12:07:50 +0100 Subject: [PATCH 3/7] Maintain and expose anchor reserve --- .../lightningdevkit/ldknode/LibraryTest.kt | 6 +- bindings/ldk_node.udl | 1 + src/balance.rs | 8 ++ src/event.rs | 66 +++++++++++++- src/lib.rs | 89 ++++++++++++++++--- src/payment/onchain.rs | 34 +++++-- src/wallet.rs | 19 +++- tests/common/mod.rs | 28 ++++-- tests/integration_tests_cln.rs | 2 +- tests/integration_tests_rust.rs | 33 ++++--- tests/integration_tests_vss.rs | 6 +- 11 files changed, 241 insertions(+), 51 deletions(-) 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 b6b86da9c..6f863e637 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 @@ -203,9 +203,9 @@ class LibraryTest { val spendableBalance2AfterOpen = node2.listBalances().spendableOnchainBalanceSats println("Spendable balance 1 after open: $spendableBalance1AfterOpen") println("Spendable balance 2 after open: $spendableBalance2AfterOpen") - assert(spendableBalance1AfterOpen > 49000u) - assert(spendableBalance1AfterOpen < 50000u) - assertEquals(100000uL, spendableBalance2AfterOpen) + assert(spendableBalance1AfterOpen > 24000u) + assert(spendableBalance1AfterOpen < 25000u) + assertEquals(75000uL, spendableBalance2AfterOpen) val channelReadyEvent1 = node1.waitNextEvent() println("Got event: $channelReadyEvent1") diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 762eb25c4..7b1b3047e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -354,6 +354,7 @@ interface PendingSweepBalance { dictionary BalanceDetails { u64 total_onchain_balance_sats; u64 spendable_onchain_balance_sats; + u64 total_anchor_channels_reserve_sats; u64 total_lightning_balance_sats; sequence lightning_balances; sequence pending_balances_from_channel_closures; diff --git a/src/balance.rs b/src/balance.rs index bad2d1d5f..f1c95dcbe 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -15,7 +15,15 @@ pub struct BalanceDetails { /// The total balance of our on-chain wallet. pub total_onchain_balance_sats: u64, /// The currently spendable balance of our on-chain wallet. + /// + /// This includes any sufficiently confirmed funds, minus + /// [`total_anchor_channels_reserve_sats`]. + /// + /// [`total_anchor_channels_reserve_sats`]: Self::total_anchor_channels_reserve_sats pub spendable_onchain_balance_sats: u64, + /// The share of our total balance that we retain as an emergency reserve to (hopefully) be + /// able to spend the Anchor outputs when one of our channels is closed. + pub total_anchor_channels_reserve_sats: u64, /// The total balance that we would be able to claim across all our Lightning channels. /// /// Note this excludes balances that we are unsure if we are able to claim (e.g., as we are diff --git a/src/event.rs b/src/event.rs index 5aeb8f346..36769c0ee 100644 --- a/src/event.rs +++ b/src/event.rs @@ -820,9 +820,67 @@ where temporary_channel_id, counterparty_node_id, funding_satoshis, - channel_type: _, + channel_type, push_msat: _, } => { + let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); + + if anchor_channel { + if let Some(anchor_channels_config) = + self.config.anchor_channels_config.as_ref() + { + let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats( + &self.channel_manager, + &self.config, + ); + let spendable_amount_sats = self + .wallet + .get_spendable_amount_sats(cur_anchor_reserve_sats) + .unwrap_or(0); + + let required_amount_sats = if anchor_channels_config + .trusted_peers_no_reserve + .contains(&counterparty_node_id) + { + 0 + } else { + anchor_channels_config.per_channel_reserve_sats + }; + + if spendable_amount_sats < required_amount_sats { + log_error!( + self.logger, + "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves.", + counterparty_node_id, + ); + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return; + } + } else { + log_error!( + self.logger, + "Rejecting inbound channel from peer {} due to Anchor channels being disabled.", + counterparty_node_id, + ); + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return; + } + } + let user_channel_id: u128 = rand::thread_rng().gen::(); let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); let res = if allow_0conf { @@ -843,8 +901,9 @@ where Ok(()) => { log_info!( self.logger, - "Accepting inbound{} channel of {}sats from{} peer {}", + "Accepting inbound{}{} channel of {}sats from{} peer {}", if allow_0conf { " 0conf" } else { "" }, + if anchor_channel { " Anchor" } else { "" }, funding_satoshis, if allow_0conf { " trusted" } else { "" }, counterparty_node_id, @@ -853,8 +912,9 @@ where Err(e) => { log_error!( self.logger, - "Error while accepting inbound{} channel from{} peer {}: {:?}", + "Error while accepting inbound{}{} channel from{} peer {}: {:?}", if allow_0conf { " 0conf" } else { "" }, + if anchor_channel { " Anchor" } else { "" }, counterparty_node_id, if allow_0conf { " trusted" } else { "" }, e, diff --git a/src/lib.rs b/src/lib.rs index 190cdfd44..7b2ccaa83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,7 +142,7 @@ use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; use lightning::chain::{BestBlock, Confirm}; use lightning::events::bump_transaction::Wallet as LdkWallet; -use lightning::ln::channelmanager::PaymentId; +use lightning::ln::channelmanager::{ChannelShutdownState, PaymentId}; use lightning::ln::msgs::SocketAddress; use lightning::util::config::{ChannelHandshakeConfig, UserConfig}; @@ -916,6 +916,8 @@ impl Node { OnchainPayment::new( Arc::clone(&self.runtime), Arc::clone(&self.wallet), + Arc::clone(&self.channel_manager), + Arc::clone(&self.config), Arc::clone(&self.logger), ) } @@ -926,6 +928,8 @@ impl Node { Arc::new(OnchainPayment::new( Arc::clone(&self.runtime), Arc::clone(&self.wallet), + Arc::clone(&self.channel_manager), + Arc::clone(&self.config), Arc::clone(&self.logger), )) } @@ -1001,6 +1005,10 @@ impl Node { /// 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. + /// /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. pub fn connect_open_channel( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, @@ -1013,18 +1021,26 @@ impl Node { } let runtime = rt_lock.as_ref().unwrap(); - let cur_balance = self.wallet.get_balance()?; - if cur_balance.get_spendable() < channel_amount_sats { - log_error!(self.logger, "Unable to create channel due to insufficient funds."); - return Err(Error::InsufficientFunds); - } - let peer_info = PeerInfo { node_id, address }; let con_node_id = peer_info.node_id; let con_addr = peer_info.address.clone(); let con_cm = Arc::clone(&self.connection_manager); + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + + // Fail early if we have less than the channel value available. + if spendable_amount_sats < channel_amount_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + return Err(Error::InsufficientFunds); + } + // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. tokio::task::block_in_place(move || { @@ -1033,11 +1049,37 @@ impl Node { }) })?; + // Fail if we have less than the channel value + anchor reserve available (if applicable). + let init_features = self + .peer_manager + .peer_by_node_id(&node_id) + .ok_or(Error::ConnectionFailed)? + .init_features; + let required_funds_sats = channel_amount_sats + + self.config.anchor_channels_config.as_ref().map_or(0, |c| { + if init_features.requires_anchors_zero_fee_htlc_tx() + && !c.trusted_peers_no_reserve.contains(&node_id) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, required_funds_sats + ); + return Err(Error::InsufficientFunds); + } + let channel_config = (*(channel_config.unwrap_or_default())).clone().into(); let user_config = UserConfig { channel_handshake_limits: Default::default(), channel_handshake_config: ChannelHandshakeConfig { announced_channel: announce_channel, + negotiate_anchors_zero_fee_htlc_tx: self.config.anchor_channels_config.is_some(), ..Default::default() }, channel_config, @@ -1196,11 +1238,13 @@ impl Node { /// Retrieves an overview of all known balances. pub fn list_balances(&self) -> BalanceDetails { - let (total_onchain_balance_sats, spendable_onchain_balance_sats) = self - .wallet - .get_balance() - .map(|bal| (bal.get_total(), bal.get_spendable())) - .unwrap_or((0, 0)); + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let (total_onchain_balance_sats, spendable_onchain_balance_sats) = + self.wallet.get_balances(cur_anchor_reserve_sats).unwrap_or((0, 0)); + + let total_anchor_channels_reserve_sats = + std::cmp::min(cur_anchor_reserve_sats, total_onchain_balance_sats); let mut total_lightning_balance_sats = 0; let mut lightning_balances = Vec::new(); @@ -1235,6 +1279,7 @@ impl Node { BalanceDetails { total_onchain_balance_sats, spendable_onchain_balance_sats, + total_anchor_channels_reserve_sats, total_lightning_balance_sats, lightning_balances, pending_balances_from_channel_closures, @@ -1367,3 +1412,23 @@ pub struct NodeStatus { /// Will be `None` if we have no public channels or we haven't broadcasted since the [`Node`] was initialized. pub latest_node_announcement_broadcast_timestamp: Option, } + +pub(crate) fn total_anchor_channels_reserve_sats( + channel_manager: &ChannelManager, config: &Config, +) -> u64 { + config.anchor_channels_config.as_ref().map_or(0, |anchor_channels_config| { + channel_manager + .list_channels() + .into_iter() + .filter(|c| { + !anchor_channels_config.trusted_peers_no_reserve.contains(&c.counterparty.node_id) + && c.channel_shutdown_state + .map_or(true, |s| s != ChannelShutdownState::ShutdownComplete) + && c.channel_type + .as_ref() + .map_or(false, |t| t.requires_anchors_zero_fee_htlc_tx()) + }) + .count() as u64 + * anchor_channels_config.per_channel_reserve_sats + }) +} diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index c178e95a0..8a879ae8c 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -1,8 +1,9 @@ //! Holds a payment handler allowing to send and receive on-chain payments. +use crate::config::Config; use crate::error::Error; use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; -use crate::types::Wallet; +use crate::types::{ChannelManager, Wallet}; use bitcoin::{Address, Txid}; @@ -16,15 +17,17 @@ use std::sync::{Arc, RwLock}; pub struct OnchainPayment { runtime: Arc>>, wallet: Arc, + channel_manager: Arc, + config: Arc, logger: Arc, } impl OnchainPayment { pub(crate) fn new( runtime: Arc>>, wallet: Arc, - logger: Arc, + channel_manager: Arc, config: Arc, logger: Arc, ) -> Self { - Self { runtime, wallet, logger } + Self { runtime, wallet, channel_manager, config, logger } } /// Retrieve a new on-chain/funding address. @@ -35,6 +38,11 @@ impl OnchainPayment { } /// Send an on-chain payment to the given address. + /// + /// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into + /// [`BalanceDetails::total_anchor_channels_reserve_sats`]. + /// + /// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats pub fn send_to_address( &self, address: &bitcoin::Address, amount_sats: u64, ) -> Result { @@ -43,15 +51,29 @@ impl OnchainPayment { return Err(Error::NotRunning); } - let cur_balance = self.wallet.get_balance()?; - if cur_balance.get_spendable() < amount_sats { - log_error!(self.logger, "Unable to send payment due to insufficient funds."); + let cur_anchor_reserve_sats = + crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + + if spendable_amount_sats < amount_sats { + log_error!(self.logger, + "Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, amount_sats + ); return Err(Error::InsufficientFunds); } self.wallet.send_to_address(address, Some(amount_sats)) } /// Send an on-chain payment to the given address, draining all the available funds. + /// + /// This is useful if you have closed all channels and want to migrate funds to another + /// on-chain wallet. + /// + /// Please note that this will **not** retain any on-chain reserves, which might be potentially + /// dangerous if you have open Anchor channels for which you can't trust the counterparty to + /// spend the Anchor output after channel closure. pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { diff --git a/src/wallet.rs b/src/wallet.rs index df9656d48..1e26bba62 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -166,8 +166,23 @@ where Ok(address_info.address) } - pub(crate) fn get_balance(&self) -> Result { - Ok(self.inner.lock().unwrap().get_balance()?) + pub(crate) fn get_balances( + &self, total_anchor_channels_reserve_sats: u64, + ) -> Result<(u64, u64), Error> { + let wallet_lock = self.inner.lock().unwrap(); + let (total, spendable) = wallet_lock.get_balance().map(|bal| { + ( + bal.get_total(), + bal.get_spendable().saturating_sub(total_anchor_channels_reserve_sats), + ) + })?; + Ok((total, spendable)) + } + + pub(crate) fn get_spendable_amount_sats( + &self, total_anchor_channels_reserve_sats: u64, + ) -> Result { + self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) } /// Send funds to the given address. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 44c8efde9..607bb1167 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -163,9 +163,13 @@ pub(crate) fn random_listening_addresses() -> Vec { listening_addresses } -pub(crate) fn random_config() -> Config { +pub(crate) fn random_config(anchor_channels: bool) -> Config { let mut config = Config::default(); + if !anchor_channels { + config.anchor_channels_config = None; + } + config.network = Network::Regtest; println!("Setting network: {}", config.network); @@ -198,13 +202,15 @@ macro_rules! setup_builder { pub(crate) use setup_builder; -pub(crate) fn setup_two_nodes(electrsd: &ElectrsD, allow_0conf: bool) -> (TestNode, TestNode) { +pub(crate) fn setup_two_nodes( + electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool, +) -> (TestNode, TestNode) { println!("== Node A =="); - let config_a = random_config(); + let config_a = random_config(anchor_channels); let node_a = setup_node(electrsd, config_a); println!("\n== Node B =="); - let mut config_b = random_config(); + let mut config_b = random_config(anchor_channels); if allow_0conf { config_b.trusted_peers_0conf.push(node_a.node_id()); } @@ -355,11 +361,12 @@ pub fn open_channel( pub(crate) fn do_channel_full_cycle( node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool, + expect_anchor_channel: bool, ) { let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 100_000; + let premine_amount_sat = if expect_anchor_channel { 125_000 } else { 100_000 }; premine_and_distribute_funds( &bitcoind, @@ -406,11 +413,16 @@ pub(crate) fn do_channel_full_cycle( node_b.sync_wallets().unwrap(); let onchain_fee_buffer_sat = 1500; - let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat; - let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat; + let anchor_reserve_sat = if expect_anchor_channel { 25_000 } else { 0 }; + let node_a_upper_bound_sat = premine_amount_sat - anchor_reserve_sat - funding_amount_sat; + let node_a_lower_bound_sat = + premine_amount_sat - anchor_reserve_sat - funding_amount_sat - onchain_fee_buffer_sat; assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat); assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!( + node_b.list_balances().spendable_onchain_balance_sats, + premine_amount_sat - anchor_reserve_sat + ); expect_channel_ready_event!(node_a, node_b.node_id()); diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index ec752f474..38143ef10 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -36,7 +36,7 @@ fn test_cln() { common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1); // Setup LDK Node - let config = common::random_config(); + let config = common::random_config(true); let mut builder = Builder::from_config(config); builder.set_esplora_server("http://127.0.0.1:3002".to_string()); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fa1b33b39..5f64b44e6 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -22,21 +22,28 @@ use crate::common::expect_channel_ready_event; #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true); } #[test] fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, true); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true) + let (node_a, node_b) = setup_two_nodes(&electrsd, true, true); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true) +} + +#[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); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -76,7 +83,7 @@ fn multi_hop_sending() { // Setup and fund 5 nodes let mut nodes = Vec::new(); for _ in 0..5 { - let config = random_config(); + let config = random_config(true); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); let node = builder.build().unwrap(); @@ -147,7 +154,7 @@ fn multi_hop_sending() { #[test] fn connect_to_public_testnet_esplora() { - let mut config = random_config(); + let mut config = random_config(true); config.network = Network::Testnet; setup_builder!(builder, config); builder.set_esplora_server("https://blockstream.info/testnet/api".to_string()); @@ -159,7 +166,7 @@ fn connect_to_public_testnet_esplora() { #[test] fn start_stop_reinit() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(); + let config = random_config(true); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); @@ -227,7 +234,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -275,7 +282,7 @@ fn onchain_spend_receive() { #[test] fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(); + let config = random_config(true); let node = setup_node(&electrsd, config); // Tests arbitrary message signing and later verification @@ -293,7 +300,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -344,7 +351,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -374,7 +381,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 26d0456d4..47bae6b94 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -9,7 +9,7 @@ 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(); + let config_a = common::random_config(true); 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(); @@ -18,11 +18,11 @@ fn channel_full_cycle_with_vss_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(); + let config_b = common::random_config(true); 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(); node_b.start().unwrap(); - common::do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false); + common::do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true); } From 5ba1b35e4ed38a58fad6f5cad38b744100ae21e4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 20 Feb 2024 10:06:03 +0100 Subject: [PATCH 4/7] Use Anchor channels in CLN integration test .. because they will be the new default. Note the upcoming CLN 24.02 release will make Anchors default, too, but for now we have to set the `experimental-anchors` config option. --- docker-compose-cln.yml | 1 + tests/integration_tests_cln.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose-cln.yml b/docker-compose-cln.yml index 6628636b9..5fb1f2dcd 100644 --- a/docker-compose-cln.yml +++ b/docker-compose-cln.yml @@ -63,6 +63,7 @@ services: "--bitcoin-rpcuser=user", "--bitcoin-rpcpassword=pass", "--regtest", + "--experimental-anchors", ] ports: - "19846:19846" diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index 38143ef10..7aea13620 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -88,6 +88,7 @@ fn test_cln() { let funding_txo = common::expect_channel_pending_event!(node, cln_node_id); common::wait_for_tx(&electrs_client, funding_txo.txid); common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 6); + node.sync_wallets().unwrap(); let user_channel_id = common::expect_channel_ready_event!(node, cln_node_id); // Send a payment to CLN From 1887af8c9220196317860e0dbbf06a2cbaff88ea Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 26 Feb 2024 15:04:09 +0100 Subject: [PATCH 5/7] Add `force_close` method to allow channel force-closure .. which we somehow so far ommitted exposing in the API. We now introduce a `force_close` method and broadcast if the counterparty is not trusted. --- bindings/ldk_node.udl | 2 ++ src/lib.rs | 82 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 7b1b3047e..a3023eca1 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -72,6 +72,8 @@ interface Node { [Throws=NodeError] void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id); [Throws=NodeError] + void force_close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id); + [Throws=NodeError] void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config); [Throws=NodeError] void sync_wallets(); diff --git a/src/lib.rs b/src/lib.rs index 7b2ccaa83..9c3c12342 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1176,30 +1176,84 @@ impl Node { } /// Close a previously opened channel. + /// + /// Will attempt to close a channel coopertively. If this fails, users might need to resort to + /// [`Node::force_close_channel`]. pub fn close_channel( &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, + ) -> Result<(), Error> { + self.close_channel_internal(user_channel_id, counterparty_node_id, false) + } + + /// Force-close a previously opened channel. + /// + /// Will force-close the channel, potentially broadcasting our latest state. Note that in + /// contrast to cooperative closure, force-closing will have the channel funds time-locked, + /// i.e., they will only be available after the counterparty had time to contest our claim. + /// Force-closing channels also more costly in terms of on-chain fees. So cooperative closure + /// should always be preferred (and tried first). + /// + /// Broadcasting the closing transactions will be omitted for Anchor channels if we trust the + /// counterparty to broadcast for us (see [`AnchorChannelsConfig::trusted_peers_no_reserve`] + /// for more information). + pub fn force_close_channel( + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, + ) -> Result<(), Error> { + self.close_channel_internal(user_channel_id, counterparty_node_id, true) + } + + fn close_channel_internal( + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, force: bool, ) -> Result<(), Error> { let open_channels = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { - match self - .channel_manager - .close_channel(&channel_details.channel_id, &counterparty_node_id) - { - Ok(_) => { - // Check if this was the last open channel, if so, forget the peer. - if open_channels.len() == 1 { - self.peer_store.remove_peer(&counterparty_node_id)?; - } - Ok(()) - }, - Err(_) => Err(Error::ChannelClosingFailed), + if force { + if self.config.anchor_channels_config.as_ref().map_or(false, |acc| { + acc.trusted_peers_no_reserve.contains(&counterparty_node_id) + }) { + self.channel_manager + .force_close_without_broadcasting_txn( + &channel_details.channel_id, + &counterparty_node_id, + ) + .map_err(|e| { + log_error!( + self.logger, + "Failed to force-close channel to trusted peer: {:?}", + e + ); + Error::ChannelClosingFailed + })?; + } else { + self.channel_manager + .force_close_broadcasting_latest_txn( + &channel_details.channel_id, + &counterparty_node_id, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to force-close channel: {:?}", e); + Error::ChannelClosingFailed + })?; + } + } else { + self.channel_manager + .close_channel(&channel_details.channel_id, &counterparty_node_id) + .map_err(|e| { + log_error!(self.logger, "Failed to close channel: {:?}", e); + Error::ChannelClosingFailed + })?; + } + + // Check if this was the last open channel, if so, forget the peer. + if open_channels.len() == 1 { + self.peer_store.remove_peer(&counterparty_node_id)?; } - } else { - Ok(()) } + + Ok(()) } /// Update the config for a previously opened channel. From 0aaa8f1cf4f81212973a0f99e08f7fa67c49ad8a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 17 May 2024 09:44:25 +0200 Subject: [PATCH 6/7] Test force-closure 'happy' case .. i.e., without bumping. --- tests/common/mod.rs | 158 ++++++++++++++++++++++++++++---- tests/integration_tests_rust.rs | 36 +++++--- tests/integration_tests_vss.rs | 10 +- 3 files changed, 174 insertions(+), 30 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 607bb1167..062d14f61 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -3,7 +3,9 @@ use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; -use ldk_node::{Builder, Config, Event, LogLevel, Node, NodeError}; +use ldk_node::{ + Builder, Config, Event, LightningBalance, LogLevel, Node, NodeError, PendingSweepBalance, +}; use lightning::ln::msgs::SocketAddress; use lightning::util::persist::KVStore; @@ -171,6 +173,8 @@ pub(crate) fn random_config(anchor_channels: bool) -> Config { } config.network = Network::Regtest; + config.onchain_wallet_sync_interval_secs = 100000; + config.wallet_sync_interval_secs = 100000; println!("Setting network: {}", config.network); let rand_dir = random_storage_path(); @@ -203,7 +207,7 @@ macro_rules! setup_builder { pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( - electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool, + electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool, anchors_trusted_no_reserve: bool, ) -> (TestNode, TestNode) { println!("== Node A =="); let config_a = random_config(anchor_channels); @@ -214,6 +218,14 @@ pub(crate) fn setup_two_nodes( if allow_0conf { config_b.trusted_peers_0conf.push(node_a.node_id()); } + if anchor_channels && anchors_trusted_no_reserve { + config_b + .anchor_channels_config + .as_mut() + .unwrap() + .trusted_peers_no_reserve + .push(node_a.node_id()); + } let node_b = setup_node(electrsd, config_b); (node_a, node_b) } @@ -361,7 +373,7 @@ pub fn open_channel( pub(crate) fn do_channel_full_cycle( node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool, - expect_anchor_channel: bool, + expect_anchor_channel: bool, force_close: bool, ) { let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -413,20 +425,40 @@ pub(crate) fn do_channel_full_cycle( node_b.sync_wallets().unwrap(); let onchain_fee_buffer_sat = 1500; - let anchor_reserve_sat = if expect_anchor_channel { 25_000 } else { 0 }; - let node_a_upper_bound_sat = premine_amount_sat - anchor_reserve_sat - funding_amount_sat; - let node_a_lower_bound_sat = - premine_amount_sat - anchor_reserve_sat - funding_amount_sat - onchain_fee_buffer_sat; + let node_a_anchor_reserve_sat = if expect_anchor_channel { 25_000 } else { 0 }; + let node_a_upper_bound_sat = + premine_amount_sat - node_a_anchor_reserve_sat - funding_amount_sat; + let node_a_lower_bound_sat = premine_amount_sat + - node_a_anchor_reserve_sat + - funding_amount_sat + - onchain_fee_buffer_sat; assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat); assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat); assert_eq!( - node_b.list_balances().spendable_onchain_balance_sats, - premine_amount_sat - anchor_reserve_sat + node_a.list_balances().total_anchor_channels_reserve_sats, + node_a_anchor_reserve_sat ); - expect_channel_ready_event!(node_a, node_b.node_id()); + let node_b_anchor_reserve_sat = if node_b + .config() + .anchor_channels_config + .map_or(true, |acc| acc.trusted_peers_no_reserve.contains(&node_a.node_id())) + { + 0 + } else { + 25_000 + }; + assert_eq!( + node_b.list_balances().spendable_onchain_balance_sats, + premine_amount_sat - node_b_anchor_reserve_sat + ); + assert_eq!( + node_b.list_balances().total_anchor_channels_reserve_sats, + node_b_anchor_reserve_sat + ); - let user_channel_id = expect_channel_ready_event!(node_b, node_a.node_id()); + let user_channel_id = expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); println!("\nB receive"); let invoice_amount_1_msat = 2500_000; @@ -582,8 +614,14 @@ pub(crate) fn do_channel_full_cycle( assert_eq!(node_a.list_payments().len(), 4); assert_eq!(node_b.list_payments().len(), 5); - println!("\nB close_channel"); - node_b.close_channel(&user_channel_id, node_a.node_id()).unwrap(); + println!("\nB close_channel (force: {})", force_close); + if force_close { + std::thread::sleep(Duration::from_secs(1)); + node_a.force_close_channel(&user_channel_id, node_b.node_id()).unwrap(); + } else { + node_a.close_channel(&user_channel_id, node_b.node_id()).unwrap(); + } + expect_event!(node_a, ChannelClosed); expect_event!(node_b, ChannelClosed); @@ -593,6 +631,87 @@ pub(crate) fn do_channel_full_cycle( node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + if force_close { + // Check node_b properly sees all balances and sweeps them. + assert_eq!(node_b.list_balances().lightning_balances.len(), 1); + match node_b.list_balances().lightning_balances[0] { + LightningBalance::ClaimableAwaitingConfirmations { + counterparty_node_id, + confirmation_height, + .. + } => { + assert_eq!(counterparty_node_id, node_a.node_id()); + let cur_height = node_b.status().current_best_block.height; + let blocks_to_go = confirmation_height - cur_height; + generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize); + node_b.sync_wallets().unwrap(); + node_a.sync_wallets().unwrap(); + }, + _ => panic!("Unexpected balance state!"), + } + + assert!(node_b.list_balances().lightning_balances.is_empty()); + assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1); + match node_b.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + generate_blocks_and_wait(&bitcoind, electrsd, 1); + node_b.sync_wallets().unwrap(); + node_a.sync_wallets().unwrap(); + + assert!(node_b.list_balances().lightning_balances.is_empty()); + assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1); + match node_b.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + generate_blocks_and_wait(&bitcoind, electrsd, 5); + node_b.sync_wallets().unwrap(); + node_a.sync_wallets().unwrap(); + + assert!(node_b.list_balances().lightning_balances.is_empty()); + assert!(node_b.list_balances().pending_balances_from_channel_closures.is_empty()); + + // Check node_a properly sees all balances and sweeps them. + assert_eq!(node_a.list_balances().lightning_balances.len(), 1); + match node_a.list_balances().lightning_balances[0] { + LightningBalance::ClaimableAwaitingConfirmations { + counterparty_node_id, + confirmation_height, + .. + } => { + assert_eq!(counterparty_node_id, node_b.node_id()); + let cur_height = node_a.status().current_best_block.height; + let blocks_to_go = confirmation_height - cur_height; + generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + }, + _ => panic!("Unexpected balance state!"), + } + + assert!(node_a.list_balances().lightning_balances.is_empty()); + assert_eq!(node_a.list_balances().pending_balances_from_channel_closures.len(), 1); + match node_a.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + generate_blocks_and_wait(&bitcoind, electrsd, 1); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + assert!(node_a.list_balances().lightning_balances.is_empty()); + assert_eq!(node_a.list_balances().pending_balances_from_channel_closures.len(), 1); + match node_a.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + generate_blocks_and_wait(&bitcoind, electrsd, 5); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + } + let sum_of_all_payments_sat = (push_msat + invoice_amount_1_msat + overpaid_amount_msat @@ -604,11 +723,14 @@ pub(crate) fn do_channel_full_cycle( let node_a_lower_bound_sat = node_a_upper_bound_sat - onchain_fee_buffer_sat; assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat); assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat); - let expected_final_amount_node_b_sat = premine_amount_sat + sum_of_all_payments_sat; - assert_eq!( - node_b.list_balances().spendable_onchain_balance_sats, - expected_final_amount_node_b_sat - ); + + let node_b_upper_bound_sat = premine_amount_sat + sum_of_all_payments_sat; + let node_b_lower_bound_sat = node_b_upper_bound_sat - onchain_fee_buffer_sat; + assert!(node_b.list_balances().spendable_onchain_balance_sats > node_b_lower_bound_sat); + assert!(node_b.list_balances().spendable_onchain_balance_sats <= node_b_upper_bound_sat); + + assert_eq!(node_a.list_balances().total_anchor_channels_reserve_sats, 0); + assert_eq!(node_b.list_balances().total_anchor_channels_reserve_sats, 0); // Check we handled all events assert_eq!(node_a.next_event(), None); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5f64b44e6..37ddeb9a7 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -22,28 +22,42 @@ use crate::common::expect_channel_ready_event; #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); + 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); + 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); + 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); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true) + let (node_a, node_b) = setup_two_nodes(&electrsd, true, true, false); + 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); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false, false); + 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -234,7 +248,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -300,7 +314,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -351,7 +365,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -381,7 +395,7 @@ 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); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 47bae6b94..2a57ccffc 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -24,5 +24,13 @@ fn channel_full_cycle_with_vss_store() { let node_b = builder_b.build_with_vss_store(vss_base_url, "node_2_store".to_string()).unwrap(); node_b.start().unwrap(); - common::do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true); + common::do_channel_full_cycle( + node_a, + node_b, + &bitcoind.client, + &electrsd.client, + false, + true, + false, + ); } From 436f2e429f4b0cb5d001b9812e61447970cfa8a6 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 17 May 2024 14:28:38 +0200 Subject: [PATCH 7/7] Use internal addresses/`LastUnused` for change scripts --- src/wallet.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/wallet.rs b/src/wallet.rs index 1e26bba62..d970b52a6 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -166,6 +166,12 @@ where Ok(address_info.address) } + fn get_new_internal_address(&self) -> Result { + let address_info = + self.inner.lock().unwrap().get_internal_address(AddressIndex::LastUnused)?; + Ok(address_info.address) + } + pub(crate) fn get_balances( &self, total_anchor_channels_reserve_sats: u64, ) -> Result<(u64, u64), Error> { @@ -349,9 +355,10 @@ where fn get_change_script(&self) -> Result { let locked_wallet = self.inner.lock().unwrap(); - let address_info = locked_wallet.get_address(AddressIndex::LastUnused).map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; + let address_info = + locked_wallet.get_internal_address(AddressIndex::LastUnused).map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; Ok(address_info.address.script_pubkey()) } @@ -569,7 +576,7 @@ where L::Target: Logger, { fn get_change_destination_script(&self) -> Result { - let address = self.wallet.get_new_address().map_err(|e| { + let address = self.wallet.get_new_internal_address().map_err(|e| { log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); })?; Ok(address.script_pubkey())