Skip to content

Commit a6bdd44

Browse files
committed
WIP Maintain and expose anchor reserve
1 parent 210ea82 commit a6bdd44

9 files changed

+183
-39
lines changed

bindings/ldk_node.udl

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ interface PendingSweepBalance {
260260
dictionary BalanceDetails {
261261
u64 total_onchain_balance_sats;
262262
u64 spendable_onchain_balance_sats;
263+
u64 total_anchor_channels_reserve_sats;
263264
u64 total_lightning_balance_sats;
264265
sequence<LightningBalance> lightning_balances;
265266
sequence<PendingSweepBalance> pending_balances_from_channel_closures;

src/balance.rs

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

src/event.rs

+54-3
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,58 @@ where
626626
temporary_channel_id,
627627
counterparty_node_id,
628628
funding_satoshis,
629-
channel_type: _,
629+
channel_type,
630630
push_msat: _,
631631
} => {
632+
let anchor_channel = channel_type.supports_anchors_zero_fee_htlc_tx();
633+
634+
if anchor_channel {
635+
if let Some(anchor_channels_config) =
636+
self.config.anchor_channels_config.as_ref()
637+
{
638+
let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats(
639+
&self.channel_manager,
640+
&self.config,
641+
);
642+
let spendable_amount_sats = self
643+
.wallet
644+
.get_balances(cur_anchor_reserve_sats)
645+
.map(|(_, s)| s)
646+
.unwrap_or(0);
647+
if spendable_amount_sats < anchor_channels_config.per_channel_reserve_sats {
648+
log_error!(
649+
self.logger,
650+
"Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves.",
651+
counterparty_node_id,
652+
);
653+
self.channel_manager
654+
.force_close_without_broadcasting_txn(
655+
&temporary_channel_id,
656+
&counterparty_node_id,
657+
)
658+
.unwrap_or_else(|e| {
659+
log_error!(self.logger, "Failed to reject channel: {:?}", e)
660+
});
661+
return;
662+
}
663+
} else {
664+
log_error!(
665+
self.logger,
666+
"Rejecting inbound channel from peer {} due to Anchor channels being disabled.",
667+
counterparty_node_id,
668+
);
669+
self.channel_manager
670+
.force_close_without_broadcasting_txn(
671+
&temporary_channel_id,
672+
&counterparty_node_id,
673+
)
674+
.unwrap_or_else(|e| {
675+
log_error!(self.logger, "Failed to reject channel: {:?}", e)
676+
});
677+
return;
678+
}
679+
}
680+
632681
let user_channel_id: u128 = rand::thread_rng().gen::<u128>();
633682
let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id);
634683
let res = if allow_0conf {
@@ -649,8 +698,9 @@ where
649698
Ok(()) => {
650699
log_info!(
651700
self.logger,
652-
"Accepting inbound{} channel of {}sats from{} peer {}",
701+
"Accepting inbound{}{} channel of {}sats from{} peer {}",
653702
if allow_0conf { " 0conf" } else { "" },
703+
if anchor_channel { " Anchor" } else { "" },
654704
funding_satoshis,
655705
if allow_0conf { " trusted" } else { "" },
656706
counterparty_node_id,
@@ -659,8 +709,9 @@ where
659709
Err(e) => {
660710
log_error!(
661711
self.logger,
662-
"Error while accepting inbound{} channel from{} peer {}: {:?}",
712+
"Error while accepting inbound{}{} channel from{} peer {}: {:?}",
663713
if allow_0conf { " 0conf" } else { "" },
714+
if anchor_channel { " Anchor" } else { "" },
664715
counterparty_node_id,
665716
if allow_0conf { " trusted" } else { "" },
666717
e,

src/lib.rs

+70-11
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,9 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
756756
}
757757

758758
/// Send an on-chain payment to the given address.
759+
///
760+
/// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into
761+
/// [`BalanceDetails::total_anchor_channels_reserve_sats`].
759762
pub fn send_to_onchain_address(
760763
&self, address: &bitcoin::Address, amount_sats: u64,
761764
) -> Result<Txid, Error> {
@@ -764,15 +767,29 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
764767
return Err(Error::NotRunning);
765768
}
766769

767-
let cur_balance = self.wallet.get_balance()?;
768-
if cur_balance.get_spendable() < amount_sats {
769-
log_error!(self.logger, "Unable to send payment due to insufficient funds.");
770+
let cur_anchor_reserve_sats =
771+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
772+
let spendable_amount_sats =
773+
self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
774+
775+
if spendable_amount_sats < amount_sats {
776+
log_error!(self.logger,
777+
"Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats",
778+
spendable_amount_sats, amount_sats
779+
);
770780
return Err(Error::InsufficientFunds);
771781
}
772782
self.wallet.send_to_address(address, Some(amount_sats))
773783
}
774784

775785
/// Send an on-chain payment to the given address, draining all the available funds.
786+
///
787+
/// This is useful if you have closed all channels and want to migrate funds to another
788+
/// on-chain wallet.
789+
///
790+
/// Please note that this will **not** retain any on-chain reserves, which might be potentially
791+
/// dangerous if you have open Anchor channels for which you can't trust the counterparty to
792+
/// spend the Anchor output after channel closure.
776793
pub fn send_all_to_onchain_address(&self, address: &bitcoin::Address) -> Result<Txid, Error> {
777794
let rt_lock = self.runtime.read().unwrap();
778795
if rt_lock.is_none() {
@@ -854,6 +871,10 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
854871
/// channel counterparty on channel open. This can be useful to start out with the balance not
855872
/// entirely shifted to one side, therefore allowing to receive payments from the getgo.
856873
///
874+
/// If Anchor channels are enabled, this will ensure the configured
875+
/// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before
876+
/// opening the channel.
877+
///
857878
/// Returns a temporary channel id.
858879
pub fn connect_open_channel(
859880
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64,
@@ -866,9 +887,25 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
866887
}
867888
let runtime = rt_lock.as_ref().unwrap();
868889

869-
let cur_balance = self.wallet.get_balance()?;
870-
if cur_balance.get_spendable() < channel_amount_sats {
871-
log_error!(self.logger, "Unable to create channel due to insufficient funds.");
890+
let required_funds_sats = channel_amount_sats
891+
+ self.config.anchor_channels_config.as_ref().map_or(0, |c| {
892+
if c.trusted_peers_no_reserve.contains(&node_id) {
893+
0
894+
} else {
895+
c.per_channel_reserve_sats
896+
}
897+
});
898+
899+
let cur_anchor_reserve_sats =
900+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
901+
let spendable_amount_sats =
902+
self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0);
903+
904+
if spendable_amount_sats < required_funds_sats {
905+
log_error!(self.logger,
906+
"Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats",
907+
spendable_amount_sats, required_funds_sats
908+
);
872909
return Err(Error::InsufficientFunds);
873910
}
874911

@@ -892,6 +929,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
892929
channel_handshake_limits: Default::default(),
893930
channel_handshake_config: ChannelHandshakeConfig {
894931
announced_channel: announce_channel,
932+
negotiate_anchors_zero_fee_htlc_tx: self.config.anchor_channels_config.is_some(),
895933
..Default::default()
896934
},
897935
channel_config,
@@ -1450,11 +1488,13 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
14501488

14511489
/// Retrieves an overview of all known balances.
14521490
pub fn list_balances(&self) -> BalanceDetails {
1453-
let (total_onchain_balance_sats, spendable_onchain_balance_sats) = self
1454-
.wallet
1455-
.get_balance()
1456-
.map(|bal| (bal.get_total(), bal.get_spendable()))
1457-
.unwrap_or((0, 0));
1491+
let cur_anchor_reserve_sats =
1492+
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
1493+
let (total_onchain_balance_sats, spendable_onchain_balance_sats) =
1494+
self.wallet.get_balances(cur_anchor_reserve_sats).unwrap_or((0, 0));
1495+
1496+
let total_anchor_channels_reserve_sats =
1497+
std::cmp::min(cur_anchor_reserve_sats, total_onchain_balance_sats);
14581498

14591499
let mut total_lightning_balance_sats = 0;
14601500
let mut lightning_balances = Vec::new();
@@ -1487,6 +1527,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
14871527
BalanceDetails {
14881528
total_onchain_balance_sats,
14891529
spendable_onchain_balance_sats,
1530+
total_anchor_channels_reserve_sats,
14901531
total_lightning_balance_sats,
14911532
lightning_balances,
14921533
pending_balances_from_channel_closures,
@@ -1635,3 +1676,21 @@ async fn do_connect_peer<K: KVStore + Sync + Send + 'static>(
16351676
}
16361677
}
16371678
}
1679+
1680+
pub(crate) fn total_anchor_channels_reserve_sats<K: KVStore + Sync + Send + 'static>(
1681+
channel_manager: &ChannelManager<K>, config: &Config,
1682+
) -> u64 {
1683+
config.anchor_channels_config.as_ref().map_or(0, |anchor_channels_config| {
1684+
channel_manager
1685+
.list_channels()
1686+
.into_iter()
1687+
.filter(|c| {
1688+
!anchor_channels_config.trusted_peers_no_reserve.contains(&c.counterparty.node_id)
1689+
&& c.channel_type
1690+
.as_ref()
1691+
.map_or(false, |t| t.supports_anchors_zero_fee_htlc_tx())
1692+
})
1693+
.count() as u64
1694+
* anchor_channels_config.per_channel_reserve_sats
1695+
})
1696+
}

src/wallet.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,17 @@ where
169169
Ok(address_info.address)
170170
}
171171

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

176185
/// Send funds to the given address.

tests/common.rs

+18-9
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,13 @@ pub(crate) fn random_listening_addresses() -> Vec<SocketAddress> {
126126
listening_addresses
127127
}
128128

129-
pub(crate) fn random_config() -> Config {
129+
pub(crate) fn random_config(anchor_channels: bool) -> Config {
130130
let mut config = Config::default();
131131

132+
if !anchor_channels {
133+
config.anchor_channels_config = None;
134+
}
135+
132136
config.network = Network::Regtest;
133137
println!("Setting network: {}", config.network);
134138

@@ -162,14 +166,14 @@ macro_rules! setup_builder {
162166
pub(crate) use setup_builder;
163167

164168
pub(crate) fn setup_two_nodes(
165-
electrsd: &ElectrsD, allow_0conf: bool,
169+
electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool,
166170
) -> (TestNode<TestSyncStore>, TestNode<TestSyncStore>) {
167171
println!("== Node A ==");
168-
let config_a = random_config();
172+
let config_a = random_config(anchor_channels);
169173
let node_a = setup_node(electrsd, config_a);
170174

171175
println!("\n== Node B ==");
172-
let mut config_b = random_config();
176+
let mut config_b = random_config(anchor_channels);
173177
if allow_0conf {
174178
config_b.trusted_peers_0conf.push(node_a.node_id());
175179
}
@@ -318,12 +322,12 @@ pub fn open_channel<K: KVStore + Sync + Send>(
318322

319323
pub(crate) fn do_channel_full_cycle<K: KVStore + Sync + Send, E: ElectrumApi>(
320324
node_a: TestNode<K>, node_b: TestNode<K>, bitcoind: &BitcoindClient, electrsd: &E,
321-
allow_0conf: bool,
325+
allow_0conf: bool, expect_anchor_channel: bool,
322326
) {
323327
let addr_a = node_a.new_onchain_address().unwrap();
324328
let addr_b = node_b.new_onchain_address().unwrap();
325329

326-
let premine_amount_sat = 100_000;
330+
let premine_amount_sat = if expect_anchor_channel { 125_000 } else { 100_000 };
327331

328332
premine_and_distribute_funds(
329333
&bitcoind,
@@ -369,11 +373,16 @@ pub(crate) fn do_channel_full_cycle<K: KVStore + Sync + Send, E: ElectrumApi>(
369373
node_b.sync_wallets().unwrap();
370374

371375
let onchain_fee_buffer_sat = 1500;
372-
let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat;
373-
let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat;
376+
let anchor_reserve_sat = if expect_anchor_channel { 25_000 } else { 0 };
377+
let node_a_upper_bound_sat = premine_amount_sat - anchor_reserve_sat - funding_amount_sat;
378+
let node_a_lower_bound_sat =
379+
premine_amount_sat - anchor_reserve_sat - funding_amount_sat - onchain_fee_buffer_sat;
374380
assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat);
375381
assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat);
376-
assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
382+
assert_eq!(
383+
node_b.list_balances().spendable_onchain_balance_sats,
384+
premine_amount_sat - anchor_reserve_sat
385+
);
377386

378387
expect_channel_ready_event!(node_a, node_b.node_id());
379388

tests/integration_tests_cln.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ fn test_cln() {
3636
common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1);
3737

3838
// Setup LDK Node
39-
let config = common::random_config();
39+
let config = common::random_config(true);
4040
let mut builder = Builder::from_config(config);
4141
builder.set_esplora_server("http://127.0.0.1:3002".to_string());
4242

0 commit comments

Comments
 (0)