Skip to content

Commit

Permalink
WIP Maintain and expose anchor reserve
Browse files Browse the repository at this point in the history
  • Loading branch information
tnull committed Feb 19, 2024
1 parent 3962f31 commit 394625d
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 24 deletions.
1 change: 1 addition & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,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<LightningBalance> lightning_balances;
sequence<PendingSweepBalance> pending_balances_from_channel_closures;
Expand Down
9 changes: 9 additions & 0 deletions src/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ 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_emergency_reserve_sats`].
///
/// [`total_anchor_channels_emergency_reserve_sats`]:
/// Self::total_anchor_channels_emergency_reserve_sats
pub spendable_onchain_balance_sats: u64,
/// The share of our total balance which we retain es 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
Expand Down
57 changes: 54 additions & 3 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,9 +626,58 @@ where
temporary_channel_id,
counterparty_node_id,
funding_satoshis,
channel_type: _,
channel_type,
push_msat: _,
} => {
let anchor_channel = channel_type.supports_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_balances(cur_anchor_reserve_sats)
.map(|(_, s)| s)
.unwrap_or(0);
if spendable_amount_sats < anchor_channels_config.per_channel_reserve_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::<u128>();
let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id);
let res = if allow_0conf {
Expand All @@ -649,8 +698,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,
Expand All @@ -659,8 +709,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,
Expand Down
83 changes: 72 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,9 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
}

/// 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`].
pub fn send_to_onchain_address(
&self, address: &bitcoin::Address, amount_sats: u64,
) -> Result<Txid, Error> {
Expand All @@ -933,15 +936,29 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
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 =
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
let spendable_amount_sats =
self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).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_onchain_address(&self, address: &bitcoin::Address) -> Result<Txid, Error> {
let rt_lock = self.runtime.read().unwrap();
if rt_lock.is_none() {
Expand Down Expand Up @@ -1023,6 +1040,10 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
/// 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 temporary channel id.
pub fn connect_open_channel(
&self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64,
Expand All @@ -1035,9 +1056,25 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
}
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.");
let required_funds_sats = channel_amount_sats
+ self.config.anchor_channels_config.as_ref().map_or(0, |c| {
if c.trusted_peers_no_reserve.contains(&node_id) {
0
} else {
c.per_channel_reserve_sats
}
});

let cur_anchor_reserve_sats =
total_anchor_channels_reserve_sats(&self.channel_manager, &self.config);
let spendable_amount_sats =
self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(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);
}

Expand All @@ -1061,6 +1098,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
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,
Expand Down Expand Up @@ -1619,11 +1657,13 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {

/// 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();
Expand Down Expand Up @@ -1656,6 +1696,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
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,
Expand Down Expand Up @@ -1804,3 +1845,23 @@ async fn do_connect_peer<K: KVStore + Sync + Send + 'static>(
}
}
}

pub(crate) fn total_anchor_channels_reserve_sats<K: KVStore + Sync + Send + 'static>(
channel_manager: &ChannelManager<K>, config: &Config,
) -> u64 {
if let Some(anchor_channels_config) = config.anchor_channels_config.as_ref() {
channel_manager
.list_channels()
.into_iter()
.filter(|c| {
!anchor_channels_config.trusted_peers_no_reserve.contains(&c.counterparty.node_id)
&& c.channel_type
.as_ref()
.map_or(false, |t| t.supports_anchors_zero_fee_htlc_tx())
})
.count() as u64
* anchor_channels_config.per_channel_reserve_sats
} else {
0
}
}
13 changes: 11 additions & 2 deletions src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,17 @@ where
Ok(address_info.address)
}

pub(crate) fn get_balance(&self) -> Result<bdk::Balance, Error> {
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))
}

/// Send funds to the given address.
Expand Down
15 changes: 10 additions & 5 deletions tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,12 @@ pub fn open_channel<K: KVStore + Sync + Send>(

pub(crate) fn do_channel_full_cycle<K: KVStore + Sync + Send, E: ElectrumApi>(
node_a: TestNode<K>, node_b: TestNode<K>, bitcoind: &BitcoindClient, electrsd: &E,
allow_0conf: bool,
allow_0conf: bool, expect_anchor_channel: bool,
) {
let addr_a = node_a.new_onchain_address().unwrap();
let addr_b = node_b.new_onchain_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,
Expand Down Expand Up @@ -369,11 +369,16 @@ pub(crate) fn do_channel_full_cycle<K: KVStore + Sync + Send, E: ElectrumApi>(
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());

Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ use std::sync::Arc;
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);
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)
do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true)
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests_vss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ 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);
common::do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true);
}

0 comments on commit 394625d

Please sign in to comment.