Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add anchor support #141

Merged
merged 7 commits into from
Jun 11, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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")
9 changes: 9 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
@@ -15,6 +15,12 @@ dictionary Config {
sequence<PublicKey> trusted_peers_0conf;
u64 probing_liquidity_limit_multiplier;
LogLevel log_level;
AnchorChannelsConfig? anchor_channels_config;
};

dictionary AnchorChannelsConfig {
sequence<PublicKey> trusted_peers_no_reserve;
u64 per_channel_reserve_sats;
};

interface Builder {
@@ -66,6 +72,8 @@ interface Node {
[Throws=NodeError]
void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
[Throws=NodeError]
void force_close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id);
[Throws=NodeError]
void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config);
[Throws=NodeError]
void sync_wallets();
@@ -348,6 +356,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;
1 change: 1 addition & 0 deletions docker-compose-cln.yml
Original file line number Diff line number Diff line change
@@ -63,6 +63,7 @@ services:
"--bitcoin-rpcuser=user",
"--bitcoin-rpcpassword=pass",
"--regtest",
"--experimental-anchors",
]
ports:
- "19846:19846"
8 changes: 8 additions & 0 deletions src/balance.rs
Original file line number Diff line number Diff line change
@@ -15,7 +15,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 that we retain as 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
11 changes: 5 additions & 6 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -693,12 +693,11 @@ fn build_with_store_internal(
// for inbound channels.
let mut user_config = UserConfig::default();
user_config.channel_handshake_limits.force_announced_channel_preference = false;

if !config.trusted_peers_0conf.is_empty() {
// Manually accept inbound channels if we expect 0conf channel requests, avoid
// generating the events otherwise.
user_config.manually_accept_inbound_channels = true;
}
user_config.manually_accept_inbound_channels = true;
// Note the channel_handshake_config will be overwritten in `connect_open_channel`, but we
// still set a default here.
user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx =
config.anchor_channels_config.is_some();

if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() {
// Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll
93 changes: 93 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ const DEFAULT_LDK_WALLET_SYNC_INTERVAL_SECS: u64 = 30;
const DEFAULT_FEE_RATE_CACHE_UPDATE_INTERVAL_SECS: u64 = 60 * 10;
const DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER: u64 = 3;
const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Debug;
const DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS: u64 = 25_000;

// The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold
// number of derivation indexes after which BDK stops looking for new scripts belonging to the wallet.
@@ -62,6 +63,9 @@ pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64;
/// | `trusted_peers_0conf` | [] |
/// | `probing_liquidity_limit_multiplier` | 3 |
/// | `log_level` | Debug |
/// | `anchor_channels_config` | Some(..) |
///
/// See [`AnchorChannelsConfig`] for more information on its respective default values.
///
/// [`Node`]: crate::Node
pub struct Config {
@@ -104,6 +108,23 @@ pub struct Config {
///
/// Any messages below this level will be excluded from the logs.
pub log_level: LogLevel,
/// Configuration options pertaining to Anchor channels, i.e., channels for which the
/// `option_anchors_zero_fee_htlc_tx` channel type is negotiated.
///
/// Please refer to [`AnchorChannelsConfig`] for further information on Anchor channels.
///
/// If set to `Some`, we'll try to open new channels with Anchors enabled, i.e., new channels
/// will be negotiated with the `option_anchors_zero_fee_htlc_tx` channel type if supported by
/// the counterparty. Note that this won't prevent us from opening non-Anchor channels if the
/// counterparty doesn't support `option_anchors_zero_fee_htlc_tx`. If set to `None`, new
/// channels will be negotiated with the legacy `option_static_remotekey` channel type only.
///
/// **Note:** If set to `None` *after* some Anchor channels have already been
/// opened, no dedicated emergency on-chain reserve will be maintained for these channels,
/// which can be dangerous if only insufficient funds are available at the time of channel
/// closure. We *will* however still try to get the Anchor spending transactions confirmed
/// on-chain with the funds available.
pub anchor_channels_config: Option<AnchorChannelsConfig>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider not starting up if anchor channels are opened and this is not set? If they wish to have this not set going forward, they'd have to wait for them to be closed and fully swept.

}

impl Default for Config {
@@ -120,6 +141,78 @@ impl Default for Config {
trusted_peers_0conf: Vec::new(),
probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER,
log_level: DEFAULT_LOG_LEVEL,
anchor_channels_config: Some(AnchorChannelsConfig::default()),
}
}
}

/// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the
/// `option_anchors_zero_fee_htlc_tx` channel type is negotiated.
///
/// Prior to the introduction of Anchor channels, the on-chain fees paying for the transactions
/// issued on channel closure were pre-determined and locked-in at the time of the channel
/// opening. This required to estimate what fee rate would be sufficient to still have the
/// closing transactions be spendable on-chain (i.e., not be considered dust). This legacy
/// design of pre-anchor channels proved inadequate in the unpredictable, often turbulent, fee
/// markets we experience today.
///
/// In contrast, Anchor channels allow to determine an adequate fee rate *at the time of channel
/// closure*, making them much more robust in the face of fee spikes. In turn, they require to
/// maintain a reserve of on-chain funds to have the channel closure transactions confirmed
/// on-chain, at least if the channel counterparty can't be trusted to do this for us.
///
/// See [BOLT 3] for more technical details on Anchor channels.
///
///
/// ### Defaults
///
/// | Parameter | Value |
/// |----------------------------|--------|
/// | `trusted_peers_no_reserve` | [] |
/// | `per_channel_reserve_sats` | 25000 |
///
///
/// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#htlc-timeout-and-htlc-success-transactions
#[derive(Debug, Clone)]
pub struct AnchorChannelsConfig {
/// A list of peers that we trust to get the required channel closing transactions confirmed
/// on-chain.
///
/// Channels with these peers won't count towards the retained on-chain reserve and we won't
/// take any action to get the required transactions confirmed ourselves.
///
/// **Note:** Trusting the channel counterparty to take the necessary actions to get the
/// required Anchor spending and HTLC transactions confirmed on-chain is potentially insecure
/// as the channel may not be closed if they refuse to do so, potentially leaving the user
/// funds stuck *or* even allow the counterparty to steal any in-flight funds after the
/// corresponding HTLCs time out.
pub trusted_peers_no_reserve: Vec<PublicKey>,
/// The amount of satoshis per anchors-negotiated channel with an untrusted peer that we keep
/// as an emergency reserve in our on-chain wallet.
///
/// This allows for having the required Anchor output spending and HTLC transactions confirmed
/// when the channel is closed.
///
/// If the channel peer is not marked as trusted via
/// [`AnchorChannelsConfig::trusted_peers_no_reserve`], we will always try to spend the Anchor
/// outputs with *any* on-chain funds available, i.e., the total reserve value as well as any
/// spendable funds available in the on-chain wallet. Therefore, this per-channel multiplier is
/// really a emergencey reserve that we maintain at all time to reduce reduce the risk of
/// insufficient funds at time of a channel closure. To this end, we will refuse to open
/// outbound or accept inbound channels if we don't have sufficient on-chain funds availble to
/// cover the additional reserve requirement.
///
/// **Note:** Depending on the fee market at the time of closure, this reserve amount might or
/// might not suffice to successfully spend the Anchor output and have the HTLC transactions
/// confirmed on-chain, i.e., you may want to adjust this value accordingly.
pub per_channel_reserve_sats: u64,
}

impl Default for AnchorChannelsConfig {
fn default() -> Self {
Self {
trusted_peers_no_reserve: Vec::new(),
per_channel_reserve_sats: DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS,
}
}
}
115 changes: 104 additions & 11 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::types::{DynStore, Sweeper, Wallet};

use crate::{
hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId,
hex_utils, BumpTransactionEventHandler, ChannelManager, Config, Error, NetworkGraph, PeerInfo,
PeerStore, UserChannelId,
};

use crate::connection::ConnectionManager;
@@ -15,9 +16,10 @@ use crate::io::{
EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE,
EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE,
};
use crate::logger::{log_error, log_info, Logger};
use crate::logger::{log_debug, log_error, log_info, Logger};

use lightning::chain::chaininterface::ConfirmationTarget;
use lightning::events::bump_transaction::BumpTransactionEvent;
use lightning::events::{ClosureReason, PaymentPurpose};
use lightning::events::{Event as LdkEvent, PaymentFailureReason};
use lightning::impl_writeable_tlv_based_enum;
@@ -317,6 +319,7 @@ where
{
event_queue: Arc<EventQueue<L>>,
wallet: Arc<Wallet>,
bump_tx_event_handler: Arc<BumpTransactionEventHandler>,
channel_manager: Arc<ChannelManager>,
connection_manager: Arc<ConnectionManager<L>>,
output_sweeper: Arc<Sweeper>,
@@ -333,15 +336,17 @@ where
L::Target: Logger,
{
pub fn new(
event_queue: Arc<EventQueue<L>>, wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
connection_manager: Arc<ConnectionManager<L>>, output_sweeper: Arc<Sweeper>,
network_graph: Arc<NetworkGraph>, payment_store: Arc<PaymentStore<L>>,
peer_store: Arc<PeerStore<L>>, runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
logger: L, config: Arc<Config>,
event_queue: Arc<EventQueue<L>>, wallet: Arc<Wallet>,
bump_tx_event_handler: Arc<BumpTransactionEventHandler>,
channel_manager: Arc<ChannelManager>, connection_manager: Arc<ConnectionManager<L>>,
output_sweeper: Arc<Sweeper>, network_graph: Arc<NetworkGraph>,
payment_store: Arc<PaymentStore<L>>, peer_store: Arc<PeerStore<L>>,
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>, logger: L, config: Arc<Config>,
) -> Self {
Self {
event_queue,
wallet,
bump_tx_event_handler,
channel_manager,
connection_manager,
output_sweeper,
@@ -815,9 +820,67 @@ where
temporary_channel_id,
counterparty_node_id,
funding_satoshis,
channel_type: _,
channel_type,
push_msat: _,
} => {
let anchor_channel = channel_type.requires_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_spendable_amount_sats(cur_anchor_reserve_sats)
.unwrap_or(0);

let required_amount_sats = if anchor_channels_config
.trusted_peers_no_reserve
.contains(&counterparty_node_id)
{
0
} else {
anchor_channels_config.per_channel_reserve_sats
};

if spendable_amount_sats < required_amount_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 {
@@ -838,8 +901,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,
@@ -848,8 +912,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,
@@ -1018,7 +1083,6 @@ where
},
LdkEvent::DiscardFunding { .. } => {},
LdkEvent::HTLCIntercepted { .. } => {},
LdkEvent::BumpTransaction(_) => {},
LdkEvent::InvoiceRequestFailed { payment_id } => {
log_error!(
self.logger,
@@ -1062,6 +1126,35 @@ where
});
}
},
LdkEvent::BumpTransaction(bte) => {
let (channel_id, counterparty_node_id) = match bte {
BumpTransactionEvent::ChannelClose {
ref channel_id,
ref counterparty_node_id,
..
} => (channel_id, counterparty_node_id),
BumpTransactionEvent::HTLCResolution {
ref channel_id,
ref counterparty_node_id,
..
} => (channel_id, counterparty_node_id),
};

if let Some(anchor_channels_config) = self.config.anchor_channels_config.as_ref() {
if anchor_channels_config
.trusted_peers_no_reserve
.contains(counterparty_node_id)
{
log_debug!(self.logger,
"Ignoring BumpTransactionEvent for channel {} due to trusted counterparty {}",
channel_id, counterparty_node_id
);
return;
}
}

self.bump_tx_event_handler.handle_event(&bte);
},
}
}
}
Loading