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 8a01a2e
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
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 8a01a2e

Please sign in to comment.