Skip to content

Commit 5a8dc1a

Browse files
committed
Maintain and expose anchor reserve
1 parent 7e4abff commit 5a8dc1a

File tree

11 files changed

+236
-51
lines changed

11 files changed

+236
-51
lines changed

bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ class LibraryTest {
203203
val spendableBalance2AfterOpen = node2.listBalances().spendableOnchainBalanceSats
204204
println("Spendable balance 1 after open: $spendableBalance1AfterOpen")
205205
println("Spendable balance 2 after open: $spendableBalance2AfterOpen")
206-
assert(spendableBalance1AfterOpen > 49000u)
207-
assert(spendableBalance1AfterOpen < 50000u)
208-
assertEquals(100000uL, spendableBalance2AfterOpen)
206+
assert(spendableBalance1AfterOpen > 24000u)
207+
assert(spendableBalance1AfterOpen < 25000u)
208+
assertEquals(75000uL, spendableBalance2AfterOpen)
209209

210210
val channelReadyEvent1 = node1.waitNextEvent()
211211
println("Got event: $channelReadyEvent1")

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ interface PendingSweepBalance {
354354
dictionary BalanceDetails {
355355
u64 total_onchain_balance_sats;
356356
u64 spendable_onchain_balance_sats;
357+
u64 total_anchor_channels_reserve_sats;
357358
u64 total_lightning_balance_sats;
358359
sequence<LightningBalance> lightning_balances;
359360
sequence<PendingSweepBalance> pending_balances_from_channel_closures;

src/balance.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ pub struct BalanceDetails {
1515
/// The total balance of our on-chain wallet.
1616
pub total_onchain_balance_sats: u64,
1717
/// The currently spendable balance of our on-chain wallet.
18+
///
19+
/// This includes any sufficiently confirmed funds, minus
20+
/// [`total_anchor_channels_reserve_sats`].
21+
///
22+
/// [`total_anchor_channels_reserve_sats`]: Self::total_anchor_channels_reserve_sats
1823
pub spendable_onchain_balance_sats: u64,
24+
/// The share of our total balance that we retain as an emergency reserve to (hopefully) be
25+
/// able to spend the Anchor outputs when one of our channels is closed.
26+
pub total_anchor_channels_reserve_sats: u64,
1927
/// The total balance that we would be able to claim across all our Lightning channels.
2028
///
2129
/// Note this excludes balances that we are unsure if we are able to claim (e.g., as we are

src/event.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -820,9 +820,68 @@ where
820820
temporary_channel_id,
821821
counterparty_node_id,
822822
funding_satoshis,
823-
channel_type: _,
823+
channel_type,
824824
push_msat: _,
825825
} => {
826+
let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx();
827+
828+
if anchor_channel {
829+
if let Some(anchor_channels_config) =
830+
self.config.anchor_channels_config.as_ref()
831+
{
832+
let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats(
833+
&self.channel_manager,
834+
&self.config,
835+
);
836+
let spendable_amount_sats = self
837+
.wallet
838+
.get_balances(cur_anchor_reserve_sats)
839+
.map(|(_, s)| s)
840+
.unwrap_or(0);
841+
842+
let required_amount_sats = if anchor_channels_config
843+
.trusted_peers_no_reserve
844+
.contains(&counterparty_node_id)
845+
{
846+
0
847+
} else {
848+
anchor_channels_config.per_channel_reserve_sats
849+
};
850+
851+
if spendable_amount_sats < required_amount_sats {
852+
log_error!(
853+
self.logger,
854+
"Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves.",
855+
counterparty_node_id,
856+
);
857+
self.channel_manager
858+
.force_close_without_broadcasting_txn(
859+
&temporary_channel_id,
860+
&counterparty_node_id,
861+
)
862+
.unwrap_or_else(|e| {
863+
log_error!(self.logger, "Failed to reject channel: {:?}", e)
864+
});
865+
return;
866+
}
867+
} else {
868+
log_error!(
869+
self.logger,
870+
"Rejecting inbound channel from peer {} due to Anchor channels being disabled.",
871+
counterparty_node_id,
872+
);
873+
self.channel_manager
874+
.force_close_without_broadcasting_txn(
875+
&temporary_channel_id,
876+
&counterparty_node_id,
877+
)
878+
.unwrap_or_else(|e| {
879+
log_error!(self.logger, "Failed to reject channel: {:?}", e)
880+
});
881+
return;
882+
}
883+
}
884+
826885
let user_channel_id: u128 = rand::thread_rng().gen::<u128>();
827886
let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id);
828887
let res = if allow_0conf {
@@ -843,8 +902,9 @@ where
843902
Ok(()) => {
844903
log_info!(
845904
self.logger,
846-
"Accepting inbound{} channel of {}sats from{} peer {}",
905+
"Accepting inbound{}{} channel of {}sats from{} peer {}",
847906
if allow_0conf { " 0conf" } else { "" },
907+
if anchor_channel { " Anchor" } else { "" },
848908
funding_satoshis,
849909
if allow_0conf { " trusted" } else { "" },
850910
counterparty_node_id,
@@ -853,8 +913,9 @@ where
853913
Err(e) => {
854914
log_error!(
855915
self.logger,
856-
"Error while accepting inbound{} channel from{} peer {}: {:?}",
916+
"Error while accepting inbound{}{} channel from{} peer {}: {:?}",
857917
if allow_0conf { " 0conf" } else { "" },
918+
if anchor_channel { " Anchor" } else { "" },
858919
counterparty_node_id,
859920
if allow_0conf { " trusted" } else { "" },
860921
e,

src/lib.rs

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger};
142142

143143
use lightning::chain::{BestBlock, Confirm};
144144
use lightning::events::bump_transaction::Wallet as LdkWallet;
145-
use lightning::ln::channelmanager::PaymentId;
145+
use lightning::ln::channelmanager::{ChannelShutdownState, PaymentId};
146146
use lightning::ln::msgs::SocketAddress;
147147

148148
use lightning::util::config::{ChannelHandshakeConfig, UserConfig};
@@ -916,6 +916,8 @@ impl Node {
916916
OnchainPayment::new(
917917
Arc::clone(&self.runtime),
918918
Arc::clone(&self.wallet),
919+
Arc::clone(&self.channel_manager),
920+
Arc::clone(&self.config),
919921
Arc::clone(&self.logger),
920922
)
921923
}
@@ -926,6 +928,8 @@ impl Node {
926928
Arc::new(OnchainPayment::new(
927929
Arc::clone(&self.runtime),
928930
Arc::clone(&self.wallet),
931+
Arc::clone(&self.channel_manager),
932+
Arc::clone(&self.config),
929933
Arc::clone(&self.logger),
930934
))
931935
}
@@ -1001,6 +1005,10 @@ impl Node {
10011005
/// channel counterparty on channel open. This can be useful to start out with the balance not
10021006
/// entirely shifted to one side, therefore allowing to receive payments from the getgo.
10031007
///
1008+
/// If Anchor channels are enabled, this will ensure the configured
1009+
/// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before
1010+
/// opening the channel.
1011+
///
10041012
/// Returns a [`UserChannelId`] allowing to locally keep track of the channel.
10051013
pub fn connect_open_channel(
10061014
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64,
@@ -1013,18 +1021,26 @@ impl Node {
10131021
}
10141022
let runtime = rt_lock.as_ref().unwrap();
10151023

1016-
let cur_balance = self.wallet.get_balance()?;
1017-
if cur_balance.get_spendable() < channel_amount_sats {
1018-
log_error!(self.logger, "Unable to create channel due to insufficient funds.");
1019-
return Err(Error::InsufficientFunds);
1020-
}
1021-
10221024
let peer_info = PeerInfo { node_id, address };
10231025

10241026
let con_node_id = peer_info.node_id;
10251027
let con_addr = peer_info.address.clone();
10261028
let con_cm = Arc::clone(&self.connection_manager);
10271029

1030+
let cur_anchor_reserve_sats =
1031+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1032+
let spendable_amount_sats =
1033+
self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
1034+
1035+
// Fail early if we have less than the channel value available.
1036+
if spendable_amount_sats < channel_amount_sats {
1037+
log_error!(self.logger,
1038+
"Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats",
1039+
spendable_amount_sats, channel_amount_sats
1040+
);
1041+
return Err(Error::InsufficientFunds);
1042+
}
1043+
10281044
// We need to use our main runtime here as a local runtime might not be around to poll
10291045
// connection futures going forward.
10301046
tokio::task::block_in_place(move || {
@@ -1033,11 +1049,37 @@ impl Node {
10331049
})
10341050
})?;
10351051

1052+
// Fail if we have less than the channel value + anchor reserve available (if applicable).
1053+
let init_features = self
1054+
.peer_manager
1055+
.peer_by_node_id(&node_id)
1056+
.ok_or(Error::ConnectionFailed)?
1057+
.init_features;
1058+
let required_funds_sats = channel_amount_sats
1059+
+ self.config.anchor_channels_config.as_ref().map_or(0, |c| {
1060+
if init_features.requires_anchors_zero_fee_htlc_tx()
1061+
&& !c.trusted_peers_no_reserve.contains(&node_id)
1062+
{
1063+
c.per_channel_reserve_sats
1064+
} else {
1065+
0
1066+
}
1067+
});
1068+
1069+
if spendable_amount_sats < required_funds_sats {
1070+
log_error!(self.logger,
1071+
"Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats",
1072+
spendable_amount_sats, required_funds_sats
1073+
);
1074+
return Err(Error::InsufficientFunds);
1075+
}
1076+
10361077
let channel_config = (*(channel_config.unwrap_or_default())).clone().into();
10371078
let user_config = UserConfig {
10381079
channel_handshake_limits: Default::default(),
10391080
channel_handshake_config: ChannelHandshakeConfig {
10401081
announced_channel: announce_channel,
1082+
negotiate_anchors_zero_fee_htlc_tx: self.config.anchor_channels_config.is_some(),
10411083
..Default::default()
10421084
},
10431085
channel_config,
@@ -1196,11 +1238,13 @@ impl Node {
11961238

11971239
/// Retrieves an overview of all known balances.
11981240
pub fn list_balances(&self) -> BalanceDetails {
1199-
let (total_onchain_balance_sats, spendable_onchain_balance_sats) = self
1200-
.wallet
1201-
.get_balance()
1202-
.map(|bal| (bal.get_total(), bal.get_spendable()))
1203-
.unwrap_or((0, 0));
1241+
let cur_anchor_reserve_sats =
1242+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1243+
let (total_onchain_balance_sats, spendable_onchain_balance_sats) =
1244+
self.wallet.get_balances(cur_anchor_reserve_sats).unwrap_or((0, 0));
1245+
1246+
let total_anchor_channels_reserve_sats =
1247+
std::cmp::min(cur_anchor_reserve_sats, total_onchain_balance_sats);
12041248

12051249
let mut total_lightning_balance_sats = 0;
12061250
let mut lightning_balances = Vec::new();
@@ -1235,6 +1279,7 @@ impl Node {
12351279
BalanceDetails {
12361280
total_onchain_balance_sats,
12371281
spendable_onchain_balance_sats,
1282+
total_anchor_channels_reserve_sats,
12381283
total_lightning_balance_sats,
12391284
lightning_balances,
12401285
pending_balances_from_channel_closures,
@@ -1367,3 +1412,23 @@ pub struct NodeStatus {
13671412
/// Will be `None` if we have no public channels or we haven't broadcasted since the [`Node`] was initialized.
13681413
pub latest_node_announcement_broadcast_timestamp: Option<u64>,
13691414
}
1415+
1416+
pub(crate) fn total_anchor_channels_reserve_sats(
1417+
channel_manager: &ChannelManager, config: &Config,
1418+
) -> u64 {
1419+
config.anchor_channels_config.as_ref().map_or(0, |anchor_channels_config| {
1420+
channel_manager
1421+
.list_channels()
1422+
.into_iter()
1423+
.filter(|c| {
1424+
!anchor_channels_config.trusted_peers_no_reserve.contains(&c.counterparty.node_id)
1425+
&& c.channel_shutdown_state
1426+
.map_or(true, |s| s != ChannelShutdownState::ShutdownComplete)
1427+
&& c.channel_type
1428+
.as_ref()
1429+
.map_or(false, |t| t.requires_anchors_zero_fee_htlc_tx())
1430+
})
1431+
.count() as u64
1432+
* anchor_channels_config.per_channel_reserve_sats
1433+
})
1434+
}

src/payment/onchain.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Holds a payment handler allowing to send and receive on-chain payments.
22
3+
use crate::config::Config;
34
use crate::error::Error;
45
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
5-
use crate::types::Wallet;
6+
use crate::types::{ChannelManager, Wallet};
67

78
use bitcoin::{Address, Txid};
89

@@ -16,15 +17,17 @@ use std::sync::{Arc, RwLock};
1617
pub struct OnchainPayment {
1718
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
1819
wallet: Arc<Wallet>,
20+
channel_manager: Arc<ChannelManager>,
21+
config: Arc<Config>,
1922
logger: Arc<FilesystemLogger>,
2023
}
2124

2225
impl OnchainPayment {
2326
pub(crate) fn new(
2427
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>, wallet: Arc<Wallet>,
25-
logger: Arc<FilesystemLogger>,
28+
channel_manager: Arc<ChannelManager>, config: Arc<Config>, logger: Arc<FilesystemLogger>,
2629
) -> Self {
27-
Self { runtime, wallet, logger }
30+
Self { runtime, wallet, channel_manager, config, logger }
2831
}
2932

3033
/// Retrieve a new on-chain/funding address.
@@ -35,6 +38,11 @@ impl OnchainPayment {
3538
}
3639

3740
/// Send an on-chain payment to the given address.
41+
///
42+
/// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into
43+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`].
44+
///
45+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats
3846
pub fn send_to_address(
3947
&self, address: &bitcoin::Address, amount_sats: u64,
4048
) -> Result<Txid, Error> {
@@ -43,15 +51,29 @@ impl OnchainPayment {
4351
return Err(Error::NotRunning);
4452
}
4553

46-
let cur_balance = self.wallet.get_balance()?;
47-
if cur_balance.get_spendable() < amount_sats {
48-
log_error!(self.logger, "Unable to send payment due to insufficient funds.");
54+
let cur_anchor_reserve_sats =
55+
crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
56+
let spendable_amount_sats =
57+
self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
58+
59+
if spendable_amount_sats < amount_sats {
60+
log_error!(self.logger,
61+
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats",
62+
spendable_amount_sats, amount_sats
63+
);
4964
return Err(Error::InsufficientFunds);
5065
}
5166
self.wallet.send_to_address(address, Some(amount_sats))
5267
}
5368

5469
/// Send an on-chain payment to the given address, draining all the available funds.
70+
///
71+
/// This is useful if you have closed all channels and want to migrate funds to another
72+
/// on-chain wallet.
73+
///
74+
/// Please note that this will **not** retain any on-chain reserves, which might be potentially
75+
/// dangerous if you have open Anchor channels for which you can't trust the counterparty to
76+
/// spend the Anchor output after channel closure.
5577
pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result<Txid, Error> {
5678
let rt_lock = self.runtime.read().unwrap();
5779
if rt_lock.is_none() {

src/wallet.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,17 @@ where
166166
Ok(address_info.address)
167167
}
168168

169-
pub(crate) fn get_balance(&self) -> Result<bdk::Balance, Error> {
170-
Ok(self.inner.lock().unwrap().get_balance()?)
169+
pub(crate) fn get_balances(
170+
&self, total_anchor_channels_reserve_sats: u64,
171+
) -> Result<(u64, u64), Error> {
172+
let wallet_lock = self.inner.lock().unwrap();
173+
let (total, spendable) = wallet_lock.get_balance().map(|bal| {
174+
(
175+
bal.get_total(),
176+
bal.get_spendable().saturating_sub(total_anchor_channels_reserve_sats),
177+
)
178+
})?;
179+
Ok((total, spendable))
171180
}
172181

173182
/// Send funds to the given address.

0 commit comments

Comments
 (0)