Define anchor channel reserve requirements

This change defines anchor reserve requirements by calculating weights
and fees for the transactions that need to be confirmed on-chain in the
event of a unilateral closure. The calculation is given a set of
parameters as input, including the expected fee rate and number of
in-flight HTLCs.
This commit is contained in:
Willem Van Lint 2024-12-16 16:05:25 -08:00
parent ec19ba1db2
commit 7354ebee7a
3 changed files with 447 additions and 0 deletions

View file

@ -34,6 +34,7 @@ use bitcoin::secp256k1::{self, SecretKey, PublicKey, Secp256k1, ecdsa::Signature
use crate::ln::channel::INITIAL_COMMITMENT_NUMBER;
use crate::ln::types::ChannelId;
use crate::types::features::ChannelTypeFeatures;
use crate::types::payment::{PaymentHash, PaymentPreimage};
use crate::ln::msgs::DecodeError;
use crate::ln::channel_keys::{DelayedPaymentKey, DelayedPaymentBasepoint, HtlcBasepoint, HtlcKey, RevocationKey, RevocationBasepoint};
@ -1614,6 +1615,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
self.inner.lock().unwrap().channel_id()
}
/// Gets the channel type of the corresponding channel.
pub fn channel_type_features(&self) -> ChannelTypeFeatures {
self.inner.lock().unwrap().channel_type_features()
}
/// Gets a list of txids, with their output scripts (in the order they appear in the
/// transaction), which we must learn about spends of via block_connected().
pub fn get_outputs_to_watch(&self) -> Vec<(Txid, Vec<(u32, ScriptBuf)>)> {
@ -4809,6 +4815,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.onchain_events_awaiting_threshold_conf.push(entry);
}
}
fn channel_type_features(&self) -> ChannelTypeFeatures {
self.onchain_tx_handler.channel_type_features().clone()
}
}
impl<Signer: EcdsaChannelSigner, T: Deref, F: Deref, L: Deref> chain::Listen for (ChannelMonitor<Signer>, T, F, L)

View file

@ -0,0 +1,436 @@
//! Defines anchor channel reserve requirements.
//!
//! The Lightning protocol advances the state of the channel based on commitment and HTLC
//! transactions, which allow each participant to unilaterally close the channel with the correct
//! state and resolve pending HTLCs on-chain. Originally, these transactions are signed by both
//! counterparties over the entire transaction and therefore contain a fixed fee, which can be
//! updated with the `update_fee` message by the funder. However, these fees can lead to
//! disagreements and can diverge from the prevailing fee rate if a party is disconnected.
//!
//! To address these issues, fees are provided exogenously for anchor output channels.
//! Anchor outputs are negotiated on channel opening to add outputs to each commitment transaction.
//! These outputs can be spent in a child transaction with additional fees to incentivize the
//! mining of the parent transaction, this technique is called Child Pays For Parent (CPFP).
//! Similarly, HTLC transactions will be signed with `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` so
//! additional inputs and outputs can be added to pay for fees.
//!
//! UTXO reserves will therefore be required to supply commitment transactions and HTLC
//! transactions with fees to be confirmed in a timely manner. If HTLCs are not resolved
//! appropriately, it can lead to loss of funds of the in-flight HLTCs as mentioned above. Only
//! partially satisfying UTXO requirements incurs the risk of not being able to resolve a subset of
//! HTLCs.
use crate::chain::chaininterface::BroadcasterInterface;
use crate::chain::chaininterface::FeeEstimator;
use crate::chain::chainmonitor::ChainMonitor;
use crate::chain::chainmonitor::Persist;
use crate::chain::Filter;
use crate::events::bump_transaction::Utxo;
use crate::ln::chan_utils::MAX_HTLCS;
use crate::ln::channelmanager::AChannelManager;
use crate::prelude::new_hash_set;
use crate::sign::ecdsa::EcdsaChannelSigner;
use crate::util::logger::Logger;
use bitcoin::constants::WITNESS_SCALE_FACTOR;
use bitcoin::Amount;
use bitcoin::FeeRate;
use bitcoin::Weight;
use core::cmp::min;
use core::ops::Deref;
// Transaction weights based on:
// https://github.com/lightning/bolts/blob/master/03-transactions.md#appendix-a-expected-weights
const COMMITMENT_TRANSACTION_BASE_WEIGHT: u64 = 900 + 224;
const COMMITMENT_TRANSACTION_PER_HTLC_WEIGHT: u64 = 172;
const PER_HTLC_TIMEOUT_WEIGHT: u64 = 666;
const PER_HTLC_SUCCESS_WEIGHT: u64 = 706;
// The transaction at least contains:
// - 4 bytes for the version
// - 4 bytes for the locktime
// - 1 byte for the number of inputs
// - 1 byte for the number of outputs
// - 2 bytes for the witness header
// - 1 byte for the flag
// - 1 byte for the marker
const TRANSACTION_BASE_WEIGHT: u64 = (4 + 4 + 1 + 1) * WITNESS_SCALE_FACTOR as u64 + 2;
// A P2WPKH input consists of:
// - 36 bytes for the previous outpoint:
// - 32 bytes transaction hash
// - 4 bytes index
// - 4 bytes for the sequence
// - 1 byte for the script sig length
// - the witness:
// - 1 byte for witness items count
// - 1 byte for the signature length
// - 72 bytes for the signature
// - 1 byte for the public key length
// - 33 bytes for the public key
const P2WPKH_INPUT_WEIGHT: u64 = (36 + 4 + 1) * WITNESS_SCALE_FACTOR as u64 + (1 + 1 + 72 + 1 + 33);
// A P2WPKH output consists of:
// - 8 bytes for the output amount
// - 1 byte for the script length
// - 22 bytes for the script (OP_0 OP_PUSH20 20 byte public key hash)
const P2WPKH_OUTPUT_WEIGHT: u64 = (8 + 1 + 22) * WITNESS_SCALE_FACTOR as u64;
// A P2TR key path input consists of:
// - 36 bytes for the previous outpoint:
// - 32 bytes transaction hash
// - 4 bytes index
// - 4 bytes for the sequence
// - 1 byte for the script sig length
// - the witness:
// - 1 byte for witness items count
// - 1 byte for the signature length
// - 64 bytes for the Schnorr signature
const P2TR_KEYPATH_INPUT_WEIGHT: u64 = (36 + 4 + 1) * WITNESS_SCALE_FACTOR as u64 + (1 + 1 + 64);
// A P2TR output consists of:
// - 8 bytes for the output amount
// - 1 byte for the script length
// - 34 bytes for the script (OP_1 OP_PUSH32 32 byte Schnorr public key)
const P2TR_OUTPUT_WEIGHT: u64 = (8 + 1 + 34) * WITNESS_SCALE_FACTOR as u64;
// An P2WSH anchor input consists of:
// - 36 bytes for the previous outpoint:
// - 32 bytes transaction hash
// - 4 bytes index
// - 4 bytes for the sequence
// - 1 byte for the script sig length
// - the witness:
// - 1 byte for witness item count
// - 1 byte for signature length
// - 72 bytes signature
// - 1 byte for script length
// - 40 byte script
// <pubkey> OP_CHECKSIG OP_IFDUP OP_NOTIF OP_16 OP_CHECKSEQUENCEVERIFY OP_ENDIF
// - 33 byte pubkey with 1 byte OP_PUSHBYTES_33.
// - 6 1-byte opcodes
const ANCHOR_INPUT_WEIGHT: u64 = (36 + 4 + 1) * WITNESS_SCALE_FACTOR as u64 + (1 + 1 + 72 + 1 + 40);
fn htlc_success_transaction_weight(context: &AnchorChannelReserveContext) -> u64 {
PER_HTLC_SUCCESS_WEIGHT
+ if context.taproot_wallet {
P2TR_KEYPATH_INPUT_WEIGHT + P2TR_OUTPUT_WEIGHT
} else {
P2WPKH_INPUT_WEIGHT + P2WPKH_OUTPUT_WEIGHT
}
}
fn htlc_timeout_transaction_weight(context: &AnchorChannelReserveContext) -> u64 {
PER_HTLC_TIMEOUT_WEIGHT
+ if context.taproot_wallet {
P2TR_KEYPATH_INPUT_WEIGHT + P2TR_OUTPUT_WEIGHT
} else {
P2WPKH_INPUT_WEIGHT + P2WPKH_OUTPUT_WEIGHT
}
}
fn anchor_output_spend_transaction_weight(
context: &AnchorChannelReserveContext, input_weight: Weight,
) -> u64 {
TRANSACTION_BASE_WEIGHT
+ ANCHOR_INPUT_WEIGHT
+ input_weight.to_wu()
+ if context.taproot_wallet { P2TR_OUTPUT_WEIGHT } else { P2WPKH_OUTPUT_WEIGHT }
}
/// Parameters defining the context around the anchor channel reserve requirement calculation.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnchorChannelReserveContext {
/// An upper bound fee rate estimate used to calculate the anchor channel reserve that is
/// sufficient to provide fees for all required transactions.
pub upper_bound_fee_rate: FeeRate,
/// The expected number of accepted in-flight HTLCs per channel.
///
/// Note that malicious counterparties can saturate the number of accepted in-flight HTLCs up to
/// the maximum prior to forcing a unilateral closure. This estimate can include that case as a
/// weighted average, assuming some percentage of channels are controlled by malicious peers and
/// have the maximum number of accepted in-flight HTLCs.
///
/// See [ChannelHandshakeConfig::our_max_accepted_htlcs] to configure the maximum number of
/// accepted in-flight HTLCs.
///
/// [ChannelHandshakeConfig::our_max_accepted_htlcs]: crate::util::config::ChannelHandshakeConfig::our_max_accepted_htlcs
pub expected_accepted_htlcs: u16,
/// Whether the wallet handling anchor channel reserves creates Taproot P2TR outputs for any new
/// outputs, or Segwit P2WPKH outputs otherwise.
pub taproot_wallet: bool,
}
/// A default for the [AnchorChannelReserveContext] parameters is provided as follows:
/// - The upper bound fee rate is set to the 99th percentile of the median block fee rate since 2019:
/// ~50 sats/vbyte.
/// - The number of accepted in-flight HTLCs per channel is set to 10, providing additional margin
/// above the number seen for a large routing node over a month (average <1, maximum 10
/// accepted in-flight HTLCS aggregated across all channels).
/// - The wallet is assumed to be a Segwit wallet.
impl Default for AnchorChannelReserveContext {
fn default() -> Self {
AnchorChannelReserveContext {
upper_bound_fee_rate: FeeRate::from_sat_per_kwu(50 * 250),
expected_accepted_htlcs: 10,
taproot_wallet: false,
}
}
}
fn get_reserve_per_channel_with_input(
context: &AnchorChannelReserveContext, initial_input_weight: Weight,
) -> Amount {
let expected_accepted_htlcs = min(context.expected_accepted_htlcs, MAX_HTLCS) as u64;
let weight = Weight::from_wu(
COMMITMENT_TRANSACTION_BASE_WEIGHT +
// Reserves are calculated in terms of accepted HTLCs, as their timeout defines the urgency of
// on-chain resolution. Each accepted HTLC is assumed to be forwarded to calculate an upper
// bound for the reserve, resulting in `expected_accepted_htlcs` inbound HTLCs and
// `expected_accepted_htlcs` outbound HTLCs per channel in aggregate.
2 * expected_accepted_htlcs * COMMITMENT_TRANSACTION_PER_HTLC_WEIGHT +
anchor_output_spend_transaction_weight(context, initial_input_weight) +
// As an upper bound, it is assumed that each HTLC is resolved in a separate transaction.
// However, they might be aggregated when possible depending on timelocks and expiries.
htlc_success_transaction_weight(context) * expected_accepted_htlcs +
htlc_timeout_transaction_weight(context) * expected_accepted_htlcs,
);
context.upper_bound_fee_rate.fee_wu(weight).unwrap_or(Amount::MAX)
}
/// Returns the amount that needs to be maintained as a reserve per anchor channel.
///
/// This reserve currently needs to be allocated as a disjoint set of at least 1 UTXO per channel,
/// as claims are not yet aggregated across channels.
///
/// To only require 1 UTXO per channel, it is assumed that, on average, transactions are able to
/// get confirmed within 1 block with [ConfirmationTarget::UrgentOnChainSweep], or that only a
/// portion of channels will go through unilateral closure at the same time, allowing UTXOs to be
/// shared. Otherwise, multiple UTXOs would be needed per channel:
/// - HTLC time-out transactions with different expiries cannot be aggregated. This could result in
/// many individual transactions that need to be confirmed starting from different, but potentially
/// sequential block heights.
/// - If each transaction takes N blocks to confirm, at least N UTXOs per channel are needed to
/// provide the necessary concurrency.
///
/// The returned amount includes the fee to spend a single UTXO of the type indicated by
/// [AnchorChannelReserveContext::taproot_wallet]. Larger sets of UTXOs with more complex witnesses
/// will need to include the corresponding fee required to spend them.
///
/// [ConfirmationTarget::UrgentOnChainSweep]: crate::chain::chaininterface::ConfirmationTarget::UrgentOnChainSweep
pub fn get_reserve_per_channel(context: &AnchorChannelReserveContext) -> Amount {
get_reserve_per_channel_with_input(
context,
if context.taproot_wallet {
Weight::from_wu(P2TR_KEYPATH_INPUT_WEIGHT)
} else {
Weight::from_wu(P2WPKH_INPUT_WEIGHT)
},
)
}
/// Calculates the number of anchor channels that can be supported by the reserve provided
/// by `utxos`.
pub fn get_supportable_anchor_channels(
context: &AnchorChannelReserveContext, utxos: &[Utxo],
) -> u64 {
// Get the reserve needed per channel, accounting for the actual satisfaction weight below.
let reserve_per_channel = get_reserve_per_channel_with_input(context, Weight::ZERO);
let mut total_fractional_amount = Amount::from_sat(0);
let mut num_whole_utxos = 0;
for utxo in utxos {
let satisfaction_fee = context
.upper_bound_fee_rate
.fee_wu(Weight::from_wu(utxo.satisfaction_weight))
.unwrap_or(Amount::MAX);
let amount = utxo.output.value.checked_sub(satisfaction_fee).unwrap_or(Amount::MIN);
if amount >= reserve_per_channel {
num_whole_utxos += 1;
} else {
total_fractional_amount =
total_fractional_amount.checked_add(amount).unwrap_or(Amount::MAX);
}
}
// We require disjoint sets of UTXOs for the reserve of each channel,
// as claims are currently only aggregated per channel.
//
// A worst-case coin selection is assumed for fractional UTXOs, selecting up to double the
// required amount.
num_whole_utxos + total_fractional_amount.to_sat() / reserve_per_channel.to_sat() / 2
}
/// Verifies whether the anchor channel reserve provided by `utxos` is sufficient to support
/// an additional anchor channel.
///
/// This should be verified:
/// - Before opening a new outbound anchor channel with [ChannelManager::create_channel].
/// - Before accepting a new inbound anchor channel while handling [Event::OpenChannelRequest].
///
/// [ChannelManager::create_channel]: crate::ln::channelmanager::ChannelManager::create_channel
/// [Event::OpenChannelRequest]: crate::events::Event::OpenChannelRequest
pub fn can_support_additional_anchor_channel<
AChannelManagerRef: Deref,
ChannelSigner: EcdsaChannelSigner,
FilterRef: Deref,
BroadcasterRef: Deref,
EstimatorRef: Deref,
LoggerRef: Deref,
PersistRef: Deref,
ChainMonitorRef: Deref<
Target = ChainMonitor<
ChannelSigner,
FilterRef,
BroadcasterRef,
EstimatorRef,
LoggerRef,
PersistRef,
>,
>,
>(
context: &AnchorChannelReserveContext, utxos: &[Utxo], a_channel_manager: &AChannelManagerRef,
chain_monitor: &ChainMonitorRef,
) -> bool
where
AChannelManagerRef::Target: AChannelManager,
FilterRef::Target: Filter,
BroadcasterRef::Target: BroadcasterInterface,
EstimatorRef::Target: FeeEstimator,
LoggerRef::Target: Logger,
PersistRef::Target: Persist<ChannelSigner>,
{
let mut anchor_channels = new_hash_set();
// Calculate the number of in-progress anchor channels by inspecting ChannelMonitors with balance.
// This includes channels that are in the process of being resolved on-chain.
for channel_id in chain_monitor.list_monitors() {
let channel_monitor = if let Ok(channel_monitor) = chain_monitor.get_monitor(channel_id) {
channel_monitor
} else {
continue;
};
if channel_monitor.channel_type_features().supports_anchors_zero_fee_htlc_tx()
&& !channel_monitor.get_claimable_balances().is_empty()
{
anchor_channels.insert(channel_id);
}
}
// Also include channels that are in the middle of negotiation or anchor channels that don't have
// a ChannelMonitor yet.
for channel in a_channel_manager.get_cm().list_channels() {
if channel.channel_type.map_or(true, |ct| ct.supports_anchors_zero_fee_htlc_tx()) {
anchor_channels.insert(channel.channel_id);
}
}
get_supportable_anchor_channels(context, utxos) > anchor_channels.len() as u64
}
#[cfg(test)]
mod test {
use super::*;
use bitcoin::{OutPoint, ScriptBuf, TxOut, Txid};
use std::str::FromStr;
#[test]
fn test_get_reserve_per_channel() {
// At a 1000 sats/kw, with 4 expected transactions at ~1kw (commitment transaction, anchor
// output spend transaction, 2 HTLC transactions), we expect the reserve to be around 4k sats.
assert_eq!(
get_reserve_per_channel(&AnchorChannelReserveContext {
upper_bound_fee_rate: FeeRate::from_sat_per_kwu(1000),
expected_accepted_htlcs: 1,
taproot_wallet: false,
}),
Amount::from_sat(4349)
);
}
fn make_p2wpkh_utxo(amount: Amount) -> Utxo {
Utxo {
outpoint: OutPoint {
txid: Txid::from_str(
"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
)
.unwrap(),
vout: 0,
},
output: TxOut { value: amount, script_pubkey: ScriptBuf::new() },
satisfaction_weight: 1 * 4 + (1 + 1 + 72 + 1 + 33),
}
}
#[test]
fn test_get_supportable_anchor_channels() {
let context = AnchorChannelReserveContext::default();
let reserve_per_channel = get_reserve_per_channel(&context);
// Only 3 disjoint sets with a value greater than the required reserve can be created.
let utxos = vec![
make_p2wpkh_utxo(reserve_per_channel * 3 / 2),
make_p2wpkh_utxo(reserve_per_channel),
make_p2wpkh_utxo(reserve_per_channel * 99 / 100),
make_p2wpkh_utxo(reserve_per_channel * 99 / 100),
make_p2wpkh_utxo(reserve_per_channel * 20 / 100),
];
assert_eq!(get_supportable_anchor_channels(&context, utxos.as_slice()), 3);
}
#[test]
fn test_anchor_output_spend_transaction_weight() {
// Example with smaller signatures:
// https://mempool.space/tx/188b0f9f26999a48611dba4e2a88507251eba31f3695d005023de3514cba34bd
// DER-encoded ECDSA signatures vary in size and can be 71-73 bytes.
assert_eq!(
anchor_output_spend_transaction_weight(
&AnchorChannelReserveContext { taproot_wallet: false, ..Default::default() },
Weight::from_wu(P2WPKH_INPUT_WEIGHT),
),
717
);
// Example:
// https://mempool.space/tx/9c493177e395ec77d9e725e1cfd465c5f06d4a5816dd0274c3a8c2442d854a85
assert_eq!(
anchor_output_spend_transaction_weight(
&AnchorChannelReserveContext { taproot_wallet: true, ..Default::default() },
Weight::from_wu(P2TR_KEYPATH_INPUT_WEIGHT),
),
723
);
}
#[test]
fn test_htlc_success_transaction_weight() {
assert_eq!(
htlc_success_transaction_weight(&AnchorChannelReserveContext {
taproot_wallet: false,
..Default::default()
}),
1102
);
assert_eq!(
htlc_success_transaction_weight(&AnchorChannelReserveContext {
taproot_wallet: true,
..Default::default()
}),
1108
);
}
#[test]
fn test_htlc_timeout_transaction_weight() {
// Example with smaller signatures:
// https://mempool.space/tx/37185342f9f088bd12376599b245dbc02eb0bb6c4b99568b75a8cd775ddfd1f4
assert_eq!(
htlc_timeout_transaction_weight(&AnchorChannelReserveContext {
taproot_wallet: false,
..Default::default()
}),
1062
);
assert_eq!(
htlc_timeout_transaction_weight(&AnchorChannelReserveContext {
taproot_wallet: true,
..Default::default()
}),
1068
);
}
}

View file

@ -15,6 +15,7 @@ pub(crate) mod fuzz_wrappers;
#[macro_use]
pub mod ser_macros;
pub mod anchor_channel_reserves;
#[cfg(fuzzing)]
pub mod base32;
#[cfg(not(fuzzing))]