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 210ea82 commit a6bdd44
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 39 deletions.
1 change: 1 addition & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,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
8 changes: 8 additions & 0 deletions src/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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 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
81 changes: 70 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,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 @@ -764,15 +767,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 @@ -854,6 +871,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 @@ -866,9 +887,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 @@ -892,6 +929,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 @@ -1450,11 +1488,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 @@ -1487,6 +1527,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 @@ -1635,3 +1676,21 @@ 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 {
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_type
.as_ref()
.map_or(false, |t| t.supports_anchors_zero_fee_htlc_tx())
})
.count() as u64
* anchor_channels_config.per_channel_reserve_sats
})
}
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
27 changes: 18 additions & 9 deletions tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ pub(crate) fn random_listening_addresses() -> Vec<SocketAddress> {
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);

Expand Down Expand Up @@ -162,14 +166,14 @@ macro_rules! setup_builder {
pub(crate) use setup_builder;

pub(crate) fn setup_two_nodes(
electrsd: &ElectrsD, allow_0conf: bool,
electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool,
) -> (TestNode<TestSyncStore>, TestNode<TestSyncStore>) {
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());
}
Expand Down Expand Up @@ -318,12 +322,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 +373,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
2 changes: 1 addition & 1 deletion tests/integration_tests_cln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Loading

0 comments on commit a6bdd44

Please sign in to comment.