Implement Holder HTLC claim chunking for 0FC channels
What changed, and why it matters
This commit fixes a design limitation in Lightning Dev Kit's handling of zero-fee commitment (0FC) channels. In those channels, on-chain transactions must follow Bitcoin's version-3 (TRUC) rules, which impose strict size limits: a parent commitment transaction can be at most 10,000 vB, and its child anchor/HTLC transactions can be at most 1,000 vB. Previously, LDK could try to claim too many HTLCs in a single transaction or select too many wallet inputs for an anchor transaction, producing a transaction that the network would reject. The patch splits large HTLC claims into multiple smaller transactions and teaches the coin-selection algorithm to respect a maximum transaction weight. It also updates the public API for wallet coin selection to accept a new max_tx_weight parameter. This is a correctness/robustness improvement rather than a remote-exploitable vulnerability.
Users implementing CoinSelectionSource must update their select_confirmed_utxos signature to accept the new max_tx_weight parameter and ensure their coin selection respects it, returning an error when the constraint cannot be satisfied. Node operators using 0FC channels should upgrade so that force-closure and HTLC claims produce validly-sized TRUC transactions. Review downstream wallet integrations for compatibility with the API change.
Security signals we found
BIP 431 / TRUC transaction weight limits enforced for V3 transactions
HTLC claim chunking prevents producing a single oversized claim transaction
Coin selection now bounded by max_tx_weight to avoid oversized anchor/HTLC transactions
New public API parameter max_tx_weight added to CoinSelectionSource::select_confirmed_utxos
Debug assertions verify commitment, anchor, and HTLC transaction weights stay within limits
Tests added for HTLC claim chunking and anchor transaction weight capping
Evidence from the diff
The change implements ‘Holder HTLC claim chunking’ for 0FC channels and adds a max_tx_weight constraint to CoinSelectionSource::select_confirmed_utxos. Key technical points: (1) New constants TRUC_MAX_WEIGHT (10,000 vB) and TRUC_CHILD_MAX_WEIGHT (1,000 vB) are added in ln/chan_utils.rs. (2) The bump_transaction logic now loops over htlc_descriptors, building batches whose input/output weight stays under max_tx_weight minus a budget for user coins; if coin selection fails, it reduces the batch size and retries. (3) The default coin-selection algorithm now sorts UTXOs by value-after-fee, skips UTXOs whose satisfaction weight alone exceeds the budget, and evicts the smallest-value UTXOs when adding a new one would exceed the weight cap. (4) Anchor transactions for 0FC channels are capped at TRUC_CHILD_MAX_WEIGHT plus the commitment transaction weight (because the commitment is treated as an input to the package). (5) Debug assertions enforce the weight limits. (6) Tests verify chunking produces multiple HTLC claim transactions and that oversized anchor bumps fail gracefully until sufficient funds are provided.
Changed components
lightning/src/chain/mod.rslightning/src/chain/onchaintx.rslightning/src/events/bump_transaction/mod.rslightning/src/events/bump_transaction/sync.rslightning/src/events/mod.rslightning/src/ln/chan_utils.rslightning/src/ln/channel.rslightning/src/ln/functional_test_utils.rslightning/src/ln/funding.rslightning/src/ln/interactivetxs.rslightning/src/ln/zero_fee_commitment_tests.rslightning/src/util/test_utils.rsInspect captured patch +775 / −180
diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs
index 2a6d3d2..b4cc6a3 100644
--- a/lightning/src/chain/mod.rs
+++ b/lightning/src/chain/mod.rs
@@ -12,6 +12,8 @@
use bitcoin::block::{Block, Header};
use bitcoin::constants::genesis_block;
use bitcoin::hash_types::{BlockHash, Txid};
+use bitcoin::hashes::sha256::Hash as Sha256;
+use bitcoin::hashes::{Hash, HashEngine};
use bitcoin::network::Network;
use bitcoin::script::{Script, ScriptBuf};
use bitcoin::secp256k1::PublicKey;
@@ -21,6 +23,7 @@ use crate::chain::transaction::{OutPoint, TransactionData};
use crate::impl_writeable_tlv_based;
use crate::ln::types::ChannelId;
use crate::sign::ecdsa::EcdsaChannelSigner;
+use crate::sign::HTLCDescriptor;
#[allow(unused_imports)]
use crate::prelude::*;
@@ -442,3 +445,20 @@ where
/// This is not exported to bindings users as we just use [u8; 32] directly.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub struct ClaimId(pub [u8; 32]);
+
+impl ClaimId {
+ pub(crate) fn from_htlcs(htlcs: &[HTLCDescriptor]) -> ClaimId {
+ let mut engine = Sha256::engine();
+ for htlc in htlcs {
+ engine.input(&htlc.commitment_txid.to_byte_array());
+ engine.input(&htlc.htlc.transaction_output_index.unwrap().to_be_bytes());
+ }
+ ClaimId(Sha256::from_engine(engine).to_byte_array())
+ }
+ pub(crate) fn step_with_bytes(&self, bytes: &[u8]) -> ClaimId {
+ let mut engine = Sha256::engine();
+ engine.input(&self.0);
+ engine.input(bytes);
+ ClaimId(Sha256::from_engine(engine).to_byte_array())
+ }
+}
diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs
index 0d70f9d..b52cdb7 100644
--- a/lightning/src/chain/onchaintx.rs
+++ b/lightning/src/chain/onchaintx.rs
@@ -14,8 +14,7 @@
use bitcoin::amount::Amount;
use bitcoin::hash_types::{BlockHash, Txid};
-use bitcoin::hashes::sha256::Hash as Sha256;
-use bitcoin::hashes::{Hash, HashEngine};
+use bitcoin::hashes::Hash;
use bitcoin::locktime::absolute::LockTime;
use bitcoin::script::{Script, ScriptBuf};
use bitcoin::secp256k1;
@@ -882,12 +881,7 @@ impl<ChannelSigner: EcdsaChannelSigner> OnchainTxHandler<ChannelSigner> {
// claim, which will always be unique per request. Once a claim ID
// is generated, it is assigned and remains unchanged, even if the
// underlying set of HTLCs changes.
- let mut engine = Sha256::engine();
- for htlc in htlcs {
- engine.input(&htlc.commitment_txid.to_byte_array());
- engine.input(&htlc.htlc.transaction_output_index.unwrap().to_be_bytes());
- }
- ClaimId(Sha256::from_engine(engine).to_byte_array())
+ ClaimId::from_htlcs(htlcs)
},
};
debug_assert!(self.pending_claim_requests.get(&claim_id).is_none());
diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs
index 4d6220b..92fcea5 100644
--- a/lightning/src/events/bump_transaction/mod.rs
+++ b/lightning/src/events/bump_transaction/mod.rs
@@ -24,9 +24,10 @@ use crate::io_extras::sink;
use crate::ln::chan_utils;
use crate::ln::chan_utils::{
shared_anchor_script_pubkey, HTLCOutputInCommitment, ANCHOR_INPUT_WITNESS_WEIGHT,
+ BASE_INPUT_WEIGHT, BASE_TX_SIZE, EMPTY_SCRIPT_SIG_WEIGHT, EMPTY_WITNESS_WEIGHT,
HTLC_SUCCESS_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT,
HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT,
- P2A_ANCHOR_INPUT_WITNESS_WEIGHT,
+ P2WSH_TXOUT_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, TRUC_CHILD_MAX_WEIGHT, TRUC_MAX_WEIGHT,
};
use crate::ln::types::ChannelId;
use crate::prelude::*;
@@ -42,6 +43,7 @@ use bitcoin::amount::Amount;
use bitcoin::consensus::Encodable;
use bitcoin::constants::WITNESS_SCALE_FACTOR;
use bitcoin::locktime::absolute::LockTime;
+use bitcoin::policy::MAX_STANDARD_TX_WEIGHT;
use bitcoin::secp256k1;
use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::{PublicKey, Secp256k1};
@@ -50,13 +52,6 @@ use bitcoin::{
OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness,
};
-pub(crate) const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
- 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64;
-
-const BASE_INPUT_SIZE: u64 = 32 /* txid */ + 4 /* vout */ + 4 /* sequence */;
-
-pub(crate) const BASE_INPUT_WEIGHT: u64 = BASE_INPUT_SIZE * WITNESS_SCALE_FACTOR as u64;
-
/// A descriptor used to sign for a commitment transaction's anchor output.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnchorDescriptor {
@@ -132,12 +127,21 @@ pub enum BumpTransactionEvent {
/// feerate of the commitment transaction is already sufficient, in which case the child anchor
/// transaction is not needed and only the commitment transaction should be broadcast.
///
+ /// In zero-fee commitment channels, the commitment transaction and the anchor transaction
+ /// form a 1-parent-1-child package that conforms to BIP 431 (known as TRUC transactions).
+ /// The anchor transaction must be version 3, and its size must be no more than 1000 vB.
+ /// The anchor transaction is usually needed to bump the fee of the commitment transaction
+ /// as the commitment transaction is not explicitly assigned any fees. In those cases the
+ /// anchor transaction must be broadcast together with the commitment transaction as a
+ /// `child-with-parents` package (usually using the Bitcoin Core `submitpackage` RPC).
+ ///
/// The consumer should be able to sign for any of the additional inputs included within the
- /// child anchor transaction. To sign its anchor input, an [`EcdsaChannelSigner`] should be
- /// re-derived through [`SignerProvider::derive_channel_signer`]. The anchor input signature
+ /// child anchor transaction. To sign its keyed-anchor input, an [`EcdsaChannelSigner`] should
+ /// be re-derived through [`SignerProvider::derive_channel_signer`]. The anchor input signature
/// can be computed with [`EcdsaChannelSigner::sign_holder_keyed_anchor_input`], which can then
/// be provided to [`build_keyed_anchor_input_witness`] along with the `funding_pubkey` to
- /// obtain the full witness required to spend.
+ /// obtain the full witness required to spend. Note that no signature or witness data is
+ /// required to spend the keyless anchor used in zero-fee commitment channels.
///
/// It is possible to receive more than one instance of this event if a valid child anchor
/// transaction is never broadcast or is but not with a sufficient fee to be mined. Care should
@@ -188,14 +192,25 @@ pub enum BumpTransactionEvent {
pending_htlcs: Vec<HTLCOutputInCommitment>,
},
/// Indicates that a channel featuring anchor outputs has unilaterally closed on-chain by a
- /// holder commitment transaction and its HTLC(s) need to be resolved on-chain. With the
- /// zero-HTLC-transaction-fee variant of anchor outputs, the pre-signed HTLC
- /// transactions have a zero fee, thus requiring additional inputs and/or outputs to be attached
- /// for a timely confirmation within the chain. These additional inputs and/or outputs must be
- /// appended to the resulting HTLC transaction to meet the target feerate. Failure to meet the
- /// target feerate decreases the confirmation odds of the transaction, possibly resulting in a
- /// loss of funds. Once the transaction meets the target feerate, it must be signed for and
- /// broadcast by the consumer of the event.
+ /// holder commitment transaction and its HTLC(s) need to be resolved on-chain. In all such
+ /// channels, the pre-signed HTLC transactions have a zero fee, thus requiring additional
+ /// inputs and/or outputs to be attached for a timely confirmation within the chain. These
+ /// additional inputs and/or outputs must be appended to the resulting HTLC transaction to
+ /// meet the target feerate. Failure to meet the target feerate decreases the confirmation
+ /// odds of the transaction, possibly resulting in a loss of funds. Once the transaction
+ /// meets the target feerate, it must be signed for and broadcast by the consumer of the
+ /// event.
+ ///
+ /// In zero-fee commitment channels, you must set the version of the HTLC claim transaction
+ /// to version 3 as the counterparty's signature commits to the version of
+ /// the transaction. You must also make sure that this claim transaction does not grow
+ /// bigger than 10,000 vB, the maximum vsize of any TRUC transaction as specified in
+ /// BIP 431. It is possible for [`htlc_descriptors`] to be long enough such
+ /// that claiming all the HTLCs therein in a single transaction would exceed this limit.
+ /// In this case, you must claim all the HTLCs in [`htlc_descriptors`] using multiple
+ /// transactions. Finally, note that while HTLCs in zero-fee commitment channels no
+ /// longer have the 1 CSV lock, LDK will still emit this event only after the commitment
+ /// transaction has 1 confirmation.
///
/// The consumer should be able to sign for any of the non-HTLC inputs added to the resulting
/// HTLC transaction. To sign HTLC inputs, an [`EcdsaChannelSigner`] should be re-derived
@@ -216,6 +231,7 @@ pub enum BumpTransactionEvent {
///
/// [`EcdsaChannelSigner`]: crate::sign::ecdsa::EcdsaChannelSigner
/// [`EcdsaChannelSigner::sign_holder_htlc_transaction`]: crate::sign::ecdsa::EcdsaChannelSigner::sign_holder_htlc_transaction
+ /// [`htlc_descriptors`]: `BumpTransactionEvent::HTLCResolution::htlc_descriptors`
HTLCResolution {
/// The `channel_id` of the channel which has been closed.
channel_id: ChannelId,
@@ -351,6 +367,12 @@ pub trait CoinSelectionSource {
/// provided, in which case a zero-value empty OP_RETURN output can be used instead.
/// 3. Enough inputs must be selected/contributed for the resulting transaction (including the
/// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`.
+ /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this
+ /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC
+ /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for
+ /// anchor transactions, we will try your coin selection again with the same input-output
+ /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions
+ /// cannot be downsized.
///
/// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of
/// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require
@@ -364,9 +386,11 @@ pub trait CoinSelectionSource {
/// other claims, implementations must be willing to double spend their UTXOs. The choice of
/// which UTXOs to double spend is left to the implementation, but it must strive to keep the
/// set of other claims being double spent to a minimum.
+ ///
+ /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims
fn select_confirmed_utxos<'a>(
&'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
- target_feerate_sat_per_1000_weight: u32,
+ target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> AsyncResult<'a, CoinSelection>;
/// Signs and provides the full witness for all inputs within the transaction known to the
/// trait (i.e., any provided via [`CoinSelectionSource::select_confirmed_utxos`]).
@@ -436,7 +460,18 @@ where
&self, utxos: &[Utxo], claim_id: ClaimId, force_conflicting_utxo_spend: bool,
tolerate_high_network_feerates: bool, target_feerate_sat_per_1000_weight: u32,
preexisting_tx_weight: u64, input_amount_sat: Amount, target_amount_sat: Amount,
+ max_tx_weight: u64,
) -> Result<CoinSelection, ()> {
+ // P2WSH and P2TR outputs are both the heaviest-weight standard outputs at 34 bytes
+ let max_coin_selection_weight = max_tx_weight
+ .checked_sub(preexisting_tx_weight + P2WSH_TXOUT_WEIGHT)
+ .ok_or_else(|| {
+ log_debug!(
+ self.logger,
+ "max_tx_weight is too small to accommodate the preexisting tx weight plus a P2WSH/P2TR output"
+ );
+ })?;
+
let mut selected_amount;
let mut total_fees;
let mut selected_utxos;
@@ -476,31 +511,67 @@ where
}
})
.collect::<Vec<_>>();
- eligible_utxos.sort_unstable_by_key(|(utxo, _)| utxo.output.value);
+ eligible_utxos.sort_unstable_by_key(|(utxo, fee_to_spend_utxo)| {
+ utxo.output.value - *fee_to_spend_utxo
+ });
selected_amount = input_amount_sat;
total_fees = Amount::from_sat(fee_for_weight(
target_feerate_sat_per_1000_weight,
preexisting_tx_weight,
));
- selected_utxos = Vec::new();
+ selected_utxos = VecDeque::new();
+ // Invariant: `selected_utxos_weight` is never greater than `max_coin_selection_weight`
+ let mut selected_utxos_weight = 0;
for (utxo, fee_to_spend_utxo) in eligible_utxos {
if selected_amount >= target_amount_sat + total_fees {
break;
}
+ // First skip any UTXOs with prohibitive satisfaction weights
+ if BASE_INPUT_WEIGHT + utxo.satisfaction_weight > max_coin_selection_weight {
+ continue;
+ }
+ // If adding this UTXO to `selected_utxos` would push us over the
+ // `max_coin_selection_weight`, remove UTXOs from the front to make room
+ // for this new UTXO.
+ while selected_utxos_weight + BASE_INPUT_WEIGHT + utxo.satisfaction_weight
+ > max_coin_selection_weight
+ && !selected_utxos.is_empty()
+ {
+ let (smallest_value_after_spend_utxo, fee_to_spend_utxo): (Utxo, Amount) =
+ selected_utxos.pop_front().unwrap();
+ selected_amount -= smallest_value_after_spend_utxo.output.value;
+ total_fees -= fee_to_spend_utxo;
+ selected_utxos_weight -=
+ BASE_INPUT_WEIGHT + smallest_value_after_spend_utxo.satisfaction_weight;
+ }
selected_amount += utxo.output.value;
total_fees += fee_to_spend_utxo;
- selected_utxos.push(utxo.clone());
+ selected_utxos_weight += BASE_INPUT_WEIGHT + utxo.satisfaction_weight;
+ selected_utxos.push_back((utxo.clone(), fee_to_spend_utxo));
}
if selected_amount < target_amount_sat + total_fees {
log_debug!(
self.logger,
- "Insufficient funds to meet target feerate {} sat/kW",
- target_feerate_sat_per_1000_weight
+ "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU",
+ target_feerate_sat_per_1000_weight,
+ max_coin_selection_weight,
);
return Err(());
}
- for utxo in &selected_utxos {
+ // Once we've selected enough UTXOs to cover `target_amount_sat + total_fees`,
+ // we may be able to remove some small-value ones while still covering
+ // `target_amount_sat + total_fees`.
+ while !selected_utxos.is_empty()
+ && selected_amount - selected_utxos.front().unwrap().0.output.value
+ >= target_amount_sat + total_fees - selected_utxos.front().unwrap().1
+ {
+ let (smallest_value_after_spend_utxo, fee_to_spend_utxo) =
+ selected_utxos.pop_front().unwrap();
+ selected_amount -= smallest_value_after_spend_utxo.output.value;
+ total_fees -= fee_to_spend_utxo;
+ }
+ for (utxo, _) in &selected_utxos {
locked_utxos.insert(utxo.outpoint, claim_id);
}
}
@@ -521,7 +592,10 @@ where
Some(TxOut { script_pubkey: change_script, value: change_output_amount })
};
- Ok(CoinSelection { confirmed_utxos: selected_utxos, change_output })
+ Ok(CoinSelection {
+ confirmed_utxos: selected_utxos.into_iter().map(|(utxo, _)| utxo).collect(),
+ change_output,
+ })
}
}
@@ -533,12 +607,11 @@ where
{
fn select_confirmed_utxos<'a>(
&'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
- target_feerate_sat_per_1000_weight: u32,
+ target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> AsyncResult<'a, CoinSelection> {
Box::pin(async move {
let utxos = self.source.list_confirmed_utxos().await?;
// TODO: Use fee estimation utils when we upgrade to bitcoin v0.30.0.
- const BASE_TX_SIZE: u64 = 4 /* version */ + 1 /* input count */ + 1 /* output count */ + 4 /* locktime */;
let total_output_size: u64 = must_pay_to
.iter()
.map(
@@ -550,8 +623,9 @@ where
let total_input_weight =
(BASE_INPUT_WEIGHT * must_spend.len() as u64) + total_satisfaction_weight;
- let preexisting_tx_weight = 2 /* segwit marker & flag */ + total_input_weight +
- ((BASE_TX_SIZE + total_output_size) * WITNESS_SCALE_FACTOR as u64);
+ let preexisting_tx_weight = SEGWIT_MARKER_FLAG_WEIGHT
+ + total_input_weight
+ + ((BASE_TX_SIZE + total_output_size) * WITNESS_SCALE_FACTOR as u64);
let input_amount_sat = must_spend.iter().map(|input| input.previous_utxo.value).sum();
let target_amount_sat = must_pay_to.iter().map(|output| output.value).sum();
@@ -574,6 +648,7 @@ where
preexisting_tx_weight,
input_amount_sat,
target_amount_sat,
+ max_tx_weight,
)
.await;
if attempt.is_ok() {
@@ -676,7 +751,7 @@ where
.transaction_parameters
.channel_type_features;
let anchor_input_witness_weight = if channel_type.supports_anchor_zero_fee_commitments() {
- P2A_ANCHOR_INPUT_WITNESS_WEIGHT
+ EMPTY_WITNESS_WEIGHT
} else {
ANCHOR_INPUT_WITNESS_WEIGHT
};
@@ -699,9 +774,10 @@ where
// the anchor input.
let mut anchor_utxo = anchor_descriptor.previous_utxo();
let commitment_tx_fee_sat = Amount::from_sat(commitment_tx_fee_sat);
+ let commitment_tx_weight = commitment_tx.weight().to_wu();
anchor_utxo.value += commitment_tx_fee_sat;
let starting_package_and_fixed_input_satisfaction_weight =
- commitment_tx.weight().to_wu() + anchor_input_witness_weight + EMPTY_SCRIPT_SIG_WEIGHT;
+ commitment_tx_weight + anchor_input_witness_weight + EMPTY_SCRIPT_SIG_WEIGHT;
let mut package_and_fixed_input_satisfaction_weight =
starting_package_and_fixed_input_satisfaction_weight;
@@ -723,6 +799,14 @@ where
must_spend,
&[],
package_target_feerate_sat_per_1000_weight,
+ if channel_type.supports_anchor_zero_fee_commitments() {
+ TRUC_CHILD_MAX_WEIGHT
+ } else {
+ MAX_STANDARD_TX_WEIGHT as u64
+ }
+ // We added the commitment tx weight to the input satisfaction weight above, so
+ // increase the max_tx_weight by the same delta here.
+ + commitment_tx_weight,
)
.await?;
@@ -833,6 +917,15 @@ where
assert!(package_fee >= expected_package_fee);
}
+ #[cfg(debug_assertions)]
+ if channel_type.supports_anchor_zero_fee_commitments() {
+ assert!(commitment_tx.weight().to_wu() < TRUC_MAX_WEIGHT);
+ assert!(anchor_tx.weight().to_wu() < TRUC_CHILD_MAX_WEIGHT);
+ } else {
+ assert!(commitment_tx.weight().to_wu() < MAX_STANDARD_TX_WEIGHT as u64);
+ assert!(anchor_tx.weight().to_wu() < MAX_STANDARD_TX_WEIGHT as u64);
+ }
+
log_info!(
self.logger,
"Broadcasting anchor transaction {} to bump channel close with txid {}",
@@ -854,17 +947,6 @@ where
.channel_derivation_parameters
.transaction_parameters
.channel_type_features;
- let mut htlc_tx = Transaction {
- version: if channel_type.supports_anchor_zero_fee_commitments() {
- Version::non_standard(3)
- } else {
- Version::TWO
- },
- lock_time: tx_lock_time,
- input: vec![],
- output: vec![],
- };
- let mut must_spend = Vec::with_capacity(htlc_descriptors.len());
let (htlc_success_witness_weight, htlc_timeout_witness_weight) =
if channel_type.supports_anchor_zero_fee_commitments() {
(
@@ -879,123 +961,213 @@ where
} else {
panic!("channel type should be either zero-fee HTLCs, or zero-fee commitments");
};
- for htlc_descriptor in htlc_descriptors {
- let htlc_input = htlc_descriptor.unsigned_tx_input();
- must_spend.push(Input {
- outpoint: htlc_input.previous_output.clone(),
- previous_utxo: htlc_descriptor.previous_utxo(&self.secp),
- satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT
- + if htlc_descriptor.preimage.is_some() {
- htlc_success_witness_weight
- } else {
- htlc_timeout_witness_weight
- },
- });
- htlc_tx.input.push(htlc_input);
- let htlc_output = htlc_descriptor.tx_output(&self.secp);
- htlc_tx.output.push(htlc_output);
- }
- log_debug!(
- self.logger,
- "Performing coin selection for HTLC transaction targeting {} sat/kW",
- target_feerate_sat_per_1000_weight
- );
+ let max_tx_weight = if channel_type.supports_anchor_zero_fee_commitments() {
+ // Cap the size of transactions claiming `HolderHTLCOutput` in 0FC channels.
+ // Otherwise, we could hit the max 10_000vB size limit on V3 transactions
+ // (BIP 431 rule 4).
+ TRUC_MAX_WEIGHT
+ } else {
+ // We should never hit this because HTLC-timeout transactions have a signed
+ // locktime, HTLC-success transactions do not, and we never aggregate
+ // packages with a signed locktime with packages that do not have a signed
+ // locktime.
+ // Hence in the worst case, we aggregate 483 success HTLC transactions,
+ // and 483 * 705 ~= 341_000, and 341_000 < 400_000.
+ MAX_STANDARD_TX_WEIGHT as u64
+ };
+ // A 1-input 1-output transaction, both p2wpkh is 438 WU.
+ // This is just an initial budget, we increase it further below in case the user can't satisfy it.
+ const USER_COINS_WEIGHT_BUDGET: u64 = 1000;
+
+ let mut broadcasted_htlcs = 0;
+ let mut batch_size = htlc_descriptors.len() - broadcasted_htlcs;
+ let mut utxo_id = claim_id;
+
+ while broadcasted_htlcs < htlc_descriptors.len() {
+ let mut htlc_tx = Transaction {
+ version: if channel_type.supports_anchor_zero_fee_commitments() {
+ Version::non_standard(3)
+ } else {
+ Version::TWO
+ },
+ lock_time: tx_lock_time,
+ input: vec![],
+ output: vec![],
+ };
+ let mut must_spend = Vec::with_capacity(htlc_descriptors.len() - broadcasted_htlcs);
+ let mut htlc_weight_sum = 0;
+ for htlc_descriptor in
+ &htlc_descriptors[broadcasted_htlcs..broadcasted_htlcs + batch_size]
+ {
+ let input_output_weight = if htlc_descriptor.preimage.is_some() {
+ chan_utils::aggregated_htlc_success_input_output_pair_weight(channel_type)
+ } else {
+ chan_utils::aggregated_htlc_timeout_input_output_pair_weight(channel_type)
+ };
+ if htlc_weight_sum + input_output_weight >= max_tx_weight - USER_COINS_WEIGHT_BUDGET
+ {
+ break;
+ }
+ htlc_weight_sum += input_output_weight;
+ let htlc_input = htlc_descriptor.unsigned_tx_input();
+ must_spend.push(Input {
+ outpoint: htlc_input.previous_output.clone(),
+ previous_utxo: htlc_descriptor.previous_utxo(&self.secp),
+ satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT
+ + if htlc_descriptor.preimage.is_some() {
+ htlc_success_witness_weight
+ } else {
+ htlc_timeout_witness_weight
+ },
+ });
+ htlc_tx.input.push(htlc_input);
+ let htlc_output = htlc_descriptor.tx_output(&self.secp);
+ htlc_tx.output.push(htlc_output);
+ }
+ batch_size = htlc_tx.input.len();
+ let selected_htlcs =
+ &htlc_descriptors[broadcasted_htlcs..broadcasted_htlcs + batch_size];
- #[cfg(debug_assertions)]
- let must_spend_satisfaction_weight =
- must_spend.iter().map(|input| input.satisfaction_weight).sum::<u64>();
- #[cfg(debug_assertions)]
- let must_spend_amount =
- must_spend.iter().map(|input| input.previous_utxo.value.to_sat()).sum::<u64>();
+ log_info!(
+ self.logger,
+ "Batch transaction assigned to UTXO id {} contains {} HTLCs: {}",
+ log_bytes!(utxo_id.0),
+ batch_size,
+ log_iter!(selected_htlcs.iter().map(|d| d.outpoint()))
+ );
- let coin_selection: CoinSelection = self
- .utxo_source
- .select_confirmed_utxos(
- claim_id,
- must_spend,
- &htlc_tx.output,
- target_feerate_sat_per_1000_weight,
- )
- .await?;
-
- #[cfg(debug_assertions)]
- let input_satisfaction_weight: u64 =
- coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum();
- #[cfg(debug_assertions)]
- let total_satisfaction_weight = must_spend_satisfaction_weight + input_satisfaction_weight;
- #[cfg(debug_assertions)]
- let input_value: u64 =
- coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value.to_sat()).sum();
- #[cfg(debug_assertions)]
- let total_input_amount = must_spend_amount + input_value;
-
- self.process_coin_selection(&mut htlc_tx, &coin_selection);
-
- // construct psbt
- let mut htlc_psbt = Psbt::from_unsigned_tx(htlc_tx).unwrap();
- // add witness_utxo to htlc inputs
- for (i, htlc_descriptor) in htlc_descriptors.iter().enumerate() {
- debug_assert_eq!(
- htlc_psbt.unsigned_tx.input[i].previous_output,
- htlc_descriptor.outpoint()
+ log_debug!(
+ self.logger,
+ "Performing coin selection for HTLC transaction targeting {} sat/kW",
+ target_feerate_sat_per_1000_weight
);
- htlc_psbt.inputs[i].witness_utxo = Some(htlc_descriptor.previous_utxo(&self.secp));
- }
- // add witness_utxo to remaining inputs
- for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() {
- // offset to skip the htlc inputs
- let index = idx + htlc_descriptors.len();
- debug_assert_eq!(htlc_psbt.unsigned_tx.input[index].previous_output, utxo.outpoint);
- if utxo.output.script_pubkey.is_witness_program() {
- htlc_psbt.inputs[index].witness_utxo = Some(utxo.output);
+
+ #[cfg(debug_assertions)]
+ let must_spend_satisfaction_weight =
+ must_spend.iter().map(|input| input.satisfaction_weight).sum::<u64>();
+ #[cfg(debug_assertions)]
+ let must_spend_amount =
+ must_spend.iter().map(|input| input.previous_utxo.value.to_sat()).sum::<u64>();
+
+ let coin_selection: CoinSelection = match self
+ .utxo_source
+ .select_confirmed_utxos(
+ utxo_id,
+ must_spend,
+ &htlc_tx.output,
+ target_feerate_sat_per_1000_weight,
+ max_tx_weight,
+ )
+ .await
+ {
+ Ok(selection) => selection,
+ Err(()) => {
+ let htlcs_to_remove = USER_COINS_WEIGHT_BUDGET.div_ceil(
+ chan_utils::aggregated_htlc_timeout_input_output_pair_weight(channel_type),
+ );
+ batch_size = batch_size.checked_sub(htlcs_to_remove as usize).ok_or(())?;
+ if batch_size == 0 {
+ return Err(());
+ }
+ continue;
+ },
+ };
+ broadcasted_htlcs += batch_size;
+ batch_size = htlc_descriptors.len() - broadcasted_htlcs;
+ utxo_id = claim_id.step_with_bytes(&broadcasted_htlcs.to_be_bytes());
+
+ #[cfg(debug_assertions)]
+ let input_satisfaction_weight: u64 =
+ coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum();
+ #[cfg(debug_assertions)]
+ let total_satisfaction_weight = must_spend_satisfaction_weight + input_satisfaction_weight;
+ #[cfg(debug_assertions)]
+ let input_value: u64 =
+ coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value.to_sat()).sum();
+ #[cfg(debug_assertions)]
+ let total_input_amount = must_spend_amount + input_value;
+
+ self.process_coin_selection(&mut htlc_tx, &coin_selection);
+
+ // construct psbt
+ let mut htlc_psbt = Psbt::from_unsigned_tx(htlc_tx).unwrap();
+ // add witness_utxo to htlc inputs
+ for (i, htlc_descriptor) in selected_htlcs.iter().enumerate() {
+ debug_assert_eq!(
+ htlc_psbt.unsigned_tx.input[i].previous_output,
+ htlc_descriptor.outpoint()
+ );
+ htlc_psbt.inputs[i].witness_utxo = Some(htlc_descriptor.previous_utxo(&self.secp));
}
- }
- #[cfg(debug_assertions)]
- let unsigned_tx_weight = htlc_psbt.unsigned_tx.weight().to_wu()
- - (htlc_psbt.unsigned_tx.input.len() as u64 * EMPTY_SCRIPT_SIG_WEIGHT);
+ // add witness_utxo to remaining inputs
+ for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() {
+ // offset to skip the htlc inputs
+ let index = idx + selected_htlcs.len();
+ debug_assert_eq!(htlc_psbt.unsigned_tx.input[index].previous_output, utxo.outpoint);
+ if utxo.output.script_pubkey.is_witness_program() {
+ htlc_psbt.inputs[index].witness_utxo = Some(utxo.output);
+ }
+ }
- log_debug!(
- self.logger,
- "Signing HTLC transaction {}",
- htlc_psbt.unsigned_tx.compute_txid()
- );
- htlc_tx = self.utxo_source.sign_psbt(htlc_psbt).await?;
-
- let mut signers = BTreeMap::new();
- for (idx, htlc_descriptor) in htlc_descriptors.iter().enumerate() {
- let keys_id = htlc_descriptor.channel_derivation_parameters.keys_id;
- let signer = signers
- .entry(keys_id)
- .or_insert_with(|| self.signer_provider.derive_channel_signer(keys_id));
- let htlc_sig =
- signer.sign_holder_htlc_transaction(&htlc_tx, idx, htlc_descriptor, &self.secp)?;
- let witness_script = htlc_descriptor.witness_script(&self.secp);
- htlc_tx.input[idx].witness =
- htlc_descriptor.tx_input_witness(&htlc_sig, &witness_script);
- }
+ #[cfg(debug_assertions)]
+ let unsigned_tx_weight = htlc_psbt.unsigned_tx.weight().to_wu()
+ - (htlc_psbt.unsigned_tx.input.len() as u64 * EMPTY_SCRIPT_SIG_WEIGHT);
- #[cfg(debug_assertions)]
- {
- let signed_tx_weight = htlc_tx.weight().to_wu();
- let expected_signed_tx_weight = unsigned_tx_weight + total_satisfaction_weight;
- // Our estimate should be within a 1% error margin of the actual weight and we should
- // never underestimate.
- assert!(expected_signed_tx_weight >= signed_tx_weight);
- assert!(expected_signed_tx_weight * 99 / 100 <= signed_tx_weight);
-
- let expected_signed_tx_fee =
- fee_for_weight(target_feerate_sat_per_1000_weight, signed_tx_weight);
- let signed_tx_fee = total_input_amount
- - htlc_tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
- // Our feerate should always be at least what we were seeking. It may overshoot if
- // the coin selector burned funds to an OP_RETURN without a change output.
- assert!(signed_tx_fee >= expected_signed_tx_fee);
+ log_debug!(
+ self.logger,
+ "Signing HTLC transaction {}",
+ htlc_psbt.unsigned_tx.compute_txid()
+ );
+ htlc_tx = self.utxo_source.sign_psbt(htlc_psbt).await?;
+
+ let mut signers = BTreeMap::new();
+ for (idx, htlc_descriptor) in selected_htlcs.iter().enumerate() {
+ let keys_id = htlc_descriptor.channel_derivation_parameters.keys_id;
+ let signer = signers
+ .entry(keys_id)
+ .or_insert_with(|| self.signer_provider.derive_channel_signer(keys_id));
+ let htlc_sig = signer.sign_holder_htlc_transaction(
+ &htlc_tx,
+ idx,
+ htlc_descriptor,
+ &self.secp,
+ )?;
+ let witness_script = htlc_descriptor.witness_script(&self.secp);
+ htlc_tx.input[idx].witness =
+ htlc_descriptor.tx_input_witness(&htlc_sig, &witness_script);
+ }
+
+ #[cfg(debug_assertions)]
+ {
+ let signed_tx_weight = htlc_tx.weight().to_wu();
+ let expected_signed_tx_weight = unsigned_tx_weight + total_satisfaction_weight;
+ // Our estimate should be within a 2% error margin of the actual weight and we should
+ // never underestimate.
+ assert!(expected_signed_tx_weight >= signed_tx_weight);
+ assert!(expected_signed_tx_weight * 98 / 100 <= signed_tx_weight);
+
+ let expected_signed_tx_fee =
+ fee_for_weight(target_feerate_sat_per_1000_weight, signed_tx_weight);
+ let signed_tx_fee = total_input_amount
+ - htlc_tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
+ // Our feerate should always be at least what we were seeking. It may overshoot if
+ // the coin selector burned funds to an OP_RETURN without a change output.
+ assert!(signed_tx_fee >= expected_signed_tx_fee);
+ }
+
+ #[cfg(debug_assertions)]
+ if channel_type.supports_anchor_zero_fee_commitments() {
+ assert!(htlc_tx.weight().to_wu() < TRUC_MAX_WEIGHT);
+ } else {
+ assert!(htlc_tx.weight().to_wu() < MAX_STANDARD_TX_WEIGHT as u64);
+ }
+
+ log_info!(self.logger, "Broadcasting {}", log_tx!(htlc_tx));
+ self.broadcaster.broadcast_transactions(&[&htlc_tx]);
}
- log_info!(self.logger, "Broadcasting {}", log_tx!(htlc_tx));
- self.broadcaster.broadcast_transactions(&[&htlc_tx]);
Ok(())
}
@@ -1090,7 +1262,7 @@ mod tests {
impl CoinSelectionSourceSync for TestCoinSelectionSource {
fn select_confirmed_utxos(
&self, _claim_id: ClaimId, must_spend: Vec<Input>, _must_pay_to: &[TxOut],
- target_feerate_sat_per_1000_weight: u32,
+ target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64,
) -> Result<CoinSelection, ()> {
let mut expected_selects = self.expected_selects.lock().unwrap();
let (weight, value, feerate, res) = expected_selects.remove(0);
diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs
index 7d407f4..50e8a8a 100644
--- a/lightning/src/events/bump_transaction/sync.rs
+++ b/lightning/src/events/bump_transaction/sync.rs
@@ -102,13 +102,14 @@ where
{
fn select_confirmed_utxos(
&self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &[TxOut],
- target_feerate_sat_per_1000_weight: u32,
+ target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> Result<CoinSelection, ()> {
let mut fut = self.wallet.select_confirmed_utxos(
claim_id,
must_spend,
must_pay_to,
target_feerate_sat_per_1000_weight,
+ max_tx_weight,
);
let mut waker = dummy_waker();
let mut ctx = task::Context::from_waker(&mut waker);
@@ -140,7 +141,7 @@ pub trait CoinSelectionSourceSync {
/// A synchronous version of [`CoinSelectionSource::select_confirmed_utxos`].
fn select_confirmed_utxos(
&self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &[TxOut],
- target_feerate_sat_per_1000_weight: u32,
+ target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> Result<CoinSelection, ()>;
/// A synchronous version of [`CoinSelectionSource::sign_psbt`].
@@ -169,13 +170,14 @@ where
{
fn select_confirmed_utxos<'a>(
&'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
- target_feerate_sat_per_1000_weight: u32,
+ target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64,
) -> AsyncResult<'a, CoinSelection> {
let coins = self.0.select_confirmed_utxos(
claim_id,
must_spend,
must_pay_to,
target_feerate_sat_per_1000_weight,
+ max_tx_weight,
);
Box::pin(async move { coins })
}
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index d7a13d5..cccbabb 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -1604,8 +1604,10 @@ pub enum Event {
/// Indicates that a transaction originating from LDK needs to have its fee bumped. This event
/// requires confirmed external funds to be readily available to spend.
///
- /// LDK does not currently generate this event unless the
- /// [`ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx`] config flag is set to true.
+ /// LDK does not currently generate this event unless either the
+ /// [`ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx`] or the
+ /// [`ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments`] config flags are set to
+ /// true.
/// It is limited to the scope of channels with anchor outputs.
///
/// # Failure Behavior and Persistence
@@ -1613,6 +1615,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx`]: crate::util::config::ChannelHandshakeConfig::negotiate_anchors_zero_fee_htlc_tx
+ /// [`ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments`]: crate::util::config::ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments
BumpTransaction(BumpTransactionEvent),
/// We received an onion message that is intended to be forwarded to a peer
/// that is currently offline. This event will only be generated if the
diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs
index 373d069..4549731 100644
--- a/lightning/src/ln/chan_utils.rs
+++ b/lightning/src/ln/chan_utils.rs
@@ -11,6 +11,7 @@
//! largely of interest for those implementing the traits on [`crate::sign`] by hand.
use bitcoin::amount::Amount;
+use bitcoin::constants::WITNESS_SCALE_FACTOR;
use bitcoin::opcodes;
use bitcoin::script::{Builder, Script, ScriptBuf};
use bitcoin::sighash;
@@ -89,12 +90,18 @@ pub const ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 114;
#[cfg(not(feature = "grind_signatures"))]
pub const ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 115;
-/// The weight of a P2A anchor witness.
-pub const P2A_ANCHOR_INPUT_WITNESS_WEIGHT: u64 = 1;
+/// The weight of an empty witness; used to spend a P2A output.
+pub const EMPTY_WITNESS_WEIGHT: u64 = 1;
/// The maximum value of a P2A anchor.
pub const P2A_MAX_VALUE: u64 = 240;
+/// The maximum weight of a TRUC transaction, see BIP431.
+pub const TRUC_MAX_WEIGHT: u64 = 10_000 * WITNESS_SCALE_FACTOR as u64;
+
+/// The maximum weight of a TRUC transaction with an unconfirmed TRUC ancestor, see BIP431.
+pub const TRUC_CHILD_MAX_WEIGHT: u64 = 1000 * WITNESS_SCALE_FACTOR as u64;
+
/// The upper bound weight of an HTLC timeout input from a commitment transaction with keyed anchor outputs.
pub const HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT: u64 = 288;
/// The upper bound weight of an HTLC timeout input from a commitment transaction with a p2a anchor output.
@@ -125,6 +132,15 @@ pub const FUNDING_TRANSACTION_WITNESS_WEIGHT: u64 = 1 + // number_of_witness_ele
1 + // witness_script_length
MULTISIG_SCRIPT_SIZE;
+pub(crate) const BASE_TX_SIZE: u64 = 4 /* version */ + 1 /* input count */ + 1 /* output count */ + 4 /* locktime */;
+pub(crate) const SEGWIT_MARKER_FLAG_WEIGHT: u64 = 2;
+pub(crate) const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
+ 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64;
+pub(crate) const BASE_INPUT_SIZE: u64 = 32 /* txid */ + 4 /* vout */ + 4 /* sequence */;
+pub(crate) const BASE_INPUT_WEIGHT: u64 = BASE_INPUT_SIZE * WITNESS_SCALE_FACTOR as u64;
+pub(crate) const P2WSH_TXOUT_WEIGHT: u64 =
+ (8 /* value */ + 1 /* var_int */ + 34/* p2wsh spk */) * WITNESS_SCALE_FACTOR as u64;
+
/// Gets the weight for an HTLC-Success transaction.
#[inline]
#[rustfmt::skip]
@@ -134,6 +150,18 @@ pub fn htlc_success_tx_weight(channel_type_features: &ChannelTypeFeatures) -> u6
if channel_type_features.supports_anchors_zero_fee_htlc_tx() { HTLC_SUCCESS_ANCHOR_TX_WEIGHT } else { HTLC_SUCCESS_TX_WEIGHT }
}
+/// Gets the weight of a single input-output pair in externally funded HTLC-success transactions
+pub fn aggregated_htlc_success_input_output_pair_weight(
+ channel_type_features: &ChannelTypeFeatures,
+) -> u64 {
+ let satisfaction_weight = if channel_type_features.supports_anchors_zero_fee_htlc_tx() {
+ EMPTY_SCRIPT_SIG_WEIGHT + HTLC_SUCCESS_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT
+ } else {
+ EMPTY_SCRIPT_SIG_WEIGHT + HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT
+ };
+ BASE_INPUT_WEIGHT + P2WSH_TXOUT_WEIGHT + satisfaction_weight
+}
+
/// Gets the weight for an HTLC-Timeout transaction.
#[inline]
#[rustfmt::skip]
@@ -143,6 +171,18 @@ pub fn htlc_timeout_tx_weight(channel_type_features: &ChannelTypeFeatures) -> u6
if channel_type_features.supports_anchors_zero_fee_htlc_tx() { HTLC_TIMEOUT_ANCHOR_TX_WEIGHT } else { HTLC_TIMEOUT_TX_WEIGHT }
}
+/// Gets the weight of a single input-output pair in externally funded HTLC-timeout transactions
+pub fn aggregated_htlc_timeout_input_output_pair_weight(
+ channel_type_features: &ChannelTypeFeatures,
+) -> u64 {
+ let satisfaction_weight = if channel_type_features.supports_anchors_zero_fee_htlc_tx() {
+ EMPTY_SCRIPT_SIG_WEIGHT + HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT
+ } else {
+ EMPTY_SCRIPT_SIG_WEIGHT + HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT
+ };
+ BASE_INPUT_WEIGHT + P2WSH_TXOUT_WEIGHT + satisfaction_weight
+}
+
/// Describes the type of HTLC claim as determined by analyzing the witness.
#[derive(PartialEq, Eq)]
pub enum HTLCClaim {
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 5344818..e4634bd 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -36,7 +36,6 @@ use crate::chain::channelmonitor::{
};
use crate::chain::transaction::{OutPoint, TransactionData};
use crate::chain::BestBlock;
-use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT};
use crate::events::{ClosureReason, FundingInfo};
use crate::ln::chan_utils;
use crate::ln::chan_utils::{
@@ -44,7 +43,7 @@ use crate::ln::chan_utils::{
selected_commitment_sat_per_1000_weight, ChannelPublicKeys, ChannelTransactionParameters,
ClosingTransaction, CommitmentTransaction, CounterpartyChannelTransactionParameters,
CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HolderCommitmentTransaction,
- FUNDING_TRANSACTION_WITNESS_WEIGHT,
+ BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT,
};
use crate::ln::channel_state::{
ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails,
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 6bde99b..884e717 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -21,7 +21,9 @@ use crate::events::{
ClaimedHTLC, ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PathFailure,
PaymentFailureReason, PaymentPurpose,
};
-use crate::ln::chan_utils::{commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC};
+use crate::ln::chan_utils::{
+ commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, TRUC_MAX_WEIGHT,
+};
use crate::ln::channelmanager::{
AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId,
RAACommitmentOrder, RecipientOnionFields, MIN_CLTV_EXPIRY_DELTA,
@@ -58,6 +60,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash as _;
use bitcoin::locktime::absolute::{LockTime, LOCK_TIME_THRESHOLD};
use bitcoin::network::Network;
+use bitcoin::policy::MAX_STANDARD_TX_WEIGHT;
use bitcoin::pow::CompactTarget;
use bitcoin::script::ScriptBuf;
use bitcoin::secp256k1::{PublicKey, SecretKey};
@@ -400,12 +403,18 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>(
}
pub fn provide_anchor_reserves<'a, 'b, 'c>(nodes: &[Node<'a, 'b, 'c>]) -> Transaction {
+ provide_anchor_utxo_reserves(nodes, 1, Amount::ONE_BTC)
+}
+
+pub fn provide_anchor_utxo_reserves<'a, 'b, 'c>(
+ nodes: &[Node<'a, 'b, 'c>], utxos: usize, amount: Amount,
+) -> Transaction {
let mut output = Vec::with_capacity(nodes.len());
for node in nodes {
- output.push(TxOut {
- value: Amount::ONE_BTC,
- script_pubkey: node.wallet_source.get_change_script().unwrap(),
- });
+ let script_pubkey = node.wallet_source.get_change_script().unwrap();
+ for _ in 0..utxos {
+ output.push(TxOut { value: amount, script_pubkey: script_pubkey.clone() });
+ }
}
let tx = Transaction {
version: TxVersion::TWO,
@@ -1941,6 +1950,11 @@ pub fn update_nodes_with_chan_announce<'a, 'b, 'c, 'd>(
pub fn do_check_spends<F: Fn(&bitcoin::transaction::OutPoint) -> Option<TxOut>>(
tx: &Transaction, get_output: F,
) {
+ if tx.version == TxVersion::non_standard(3) {
+ assert!(tx.weight().to_wu() <= TRUC_MAX_WEIGHT);
+ } else {
+ assert!(tx.weight().to_wu() <= MAX_STANDARD_TX_WEIGHT as u64);
+ }
let mut p2a_output_below_dust = false;
let mut has_p2a_output = false;
for outp in tx.output.iter() {
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index cc90e10..1c00b8a 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -12,7 +12,8 @@
use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut};
use bitcoin::{Script, Sequence, Transaction, Weight};
-use crate::events::bump_transaction::{Utxo, EMPTY_SCRIPT_SIG_WEIGHT};
+use crate::events::bump_transaction::Utxo;
+use crate::ln::chan_utils::EMPTY_SCRIPT_SIG_WEIGHT;
use crate::prelude::Vec;
use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT};
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index 3c683fc..164fabb 100644
--- a/lightning/src/ln/interactivetxs.rs
+++ b/lightning/src/ln/interactivetxs.rs
@@ -26,8 +26,9 @@ use bitcoin::{
};
use crate::chain::chaininterface::fee_for_weight;
-use crate::events::bump_transaction::{BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT};
-use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
+use crate::ln::chan_utils::{
+ BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT,
+};
use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS};
use crate::ln::funding::FundingTxInput;
use crate::ln::msgs;
diff --git a/lightning/src/ln/zero_fee_commitment_tests.rs b/lightning/src/ln/zero_fee_commitment_tests.rs
index 9d91578..b4fd9e0 100644
--- a/lightning/src/ln/zero_fee_commitment_tests.rs
+++ b/lightning/src/ln/zero_fee_commitment_tests.rs
@@ -1,5 +1,15 @@
-use crate::ln::chan_utils::shared_anchor_script_pubkey;
+use crate::events::{ClosureReason, Event};
+use crate::ln::chan_utils;
+use crate::ln::chan_utils::{
+ BASE_INPUT_WEIGHT, BASE_TX_SIZE, EMPTY_SCRIPT_SIG_WEIGHT, EMPTY_WITNESS_WEIGHT,
+ P2WSH_TXOUT_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, TRUC_CHILD_MAX_WEIGHT,
+};
use crate::ln::functional_test_utils::*;
+use crate::ln::msgs::BaseMessageHandler;
+use crate::prelude::*;
+
+use bitcoin::constants::WITNESS_SCALE_FACTOR;
+use bitcoin::Amount;
#[test]
fn test_p2a_anchor_values_under_trims_and_rounds() {
@@ -46,10 +56,10 @@ fn test_p2a_anchor_values_under_trims_and_rounds() {
)*
let txn = get_local_commitment_txn!(nodes[0], chan_id);
assert_eq!(txn.len(), 1);
- assert_eq!(txn[0].output.iter().find(|output| output.script_pubkey == shared_anchor_script_pubkey()).unwrap().value.to_sat(), $expected_p2a_value_sat);
+ assert_eq!(txn[0].output.iter().find(|output| output.script_pubkey == chan_utils::shared_anchor_script_pubkey()).unwrap().value.to_sat(), $expected_p2a_value_sat);
let txn = get_local_commitment_txn!(nodes[1], chan_id);
assert_eq!(txn.len(), 1);
- assert_eq!(txn[0].output.iter().find(|output| output.script_pubkey == shared_anchor_script_pubkey()).unwrap().value.to_sat(), $expected_p2a_value_sat);
+ assert_eq!(txn[0].output.iter().find(|output| output.script_pubkey == chan_utils::shared_anchor_script_pubkey()).unwrap().value.to_sat(), $expected_p2a_value_sat);
for hash in node_0_1_hashes {
fail_payment(&nodes[0], &[&nodes[1]], hash);
}
@@ -92,3 +102,338 @@ fn test_p2a_anchor_values_under_trims_and_rounds() {
p2a_value_test!([353_000], [353_001], 240);
p2a_value_test!([353_001], [353_001], 240);
}
+
+#[test]
+fn test_htlc_claim_chunking() {
+ // Assert we split an overall HolderHTLCOutput claim into constituent
+ // HTLC claim transactions such that each transaction is under TRUC_MAX_WEIGHT.
+ // Assert we reduce the number of HTLCs in a batch transaction by 2 if the
+ // coin selection algorithm fails to meet the target weight.
+ // Assert the claim_id of the first batch transaction is the claim
+ // id assigned to the overall claim.
+ // Assert we give up bumping a HTLC transaction once the batch size is
+ // 0 or negative.
+ //
+ // Route a bunch of HTLCs, force close the channel, assert two HTLC transactions
+ // get broadcasted, confirm only one of them, assert a new one gets broadcasted
+ // to sweep the remaining HTLCs, confirm a block without that transaction while
+ // dropping all available coin selection utxos, and assert we give up creating
+ // another HTLC transaction when handling the third HTLC bump.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut user_cfg = test_default_channel_config();
+ user_cfg.channel_handshake_config.our_htlc_minimum_msat = 1;
+ user_cfg.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+ user_cfg.channel_handshake_config.our_max_accepted_htlcs = 114;
+ user_cfg.manually_accept_inbound_channels = true;
+
+ let configs = [Some(user_cfg.clone()), Some(user_cfg)];
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &configs);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let coinbase_tx = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500));
+
+ const CHAN_CAPACITY: u64 = 10_000_000;
+ let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 0,
+ 1,
+ CHAN_CAPACITY,
+ (CHAN_CAPACITY / 2) * 1000,
+ );
+
+ let mut node_1_preimages = Vec::new();
+ const NONDUST_HTLC_AMT_MSAT: u64 = 1_000_000;
+ for _ in 0..75 {
+ let (preimage, payment_hash, _, _) =
+ route_payment(&nodes[0], &[&nodes[1]], NONDUST_HTLC_AMT_MSAT);
+ node_1_preimages.push((preimage, payment_hash));
+ }
+ let node_0_commit_tx = get_local_commitment_txn!(nodes[0], chan_id);
+ assert_eq!(node_0_commit_tx.len(), 1);
+ assert_eq!(node_0_commit_tx[0].output.len(), 75 + 2 + 1);
+ let node_1_commit_tx = get_local_commitment_txn!(nodes[1], chan_id);
+ assert_eq!(node_1_commit_tx.len(), 1);
+ assert_eq!(node_1_commit_tx[0].output.len(), 75 + 2 + 1);
+
+ for (preimage, payment_hash) in node_1_preimages {
+ nodes[1].node.claim_funds(preimage);
+ check_added_monitors!(nodes[1], 1);
+ expect_payment_claimed!(nodes[1], payment_hash, NONDUST_HTLC_AMT_MSAT);
+ }
+ nodes[0].node.get_and_clear_pending_msg_events();
+ nodes[1].node.get_and_clear_pending_msg_events();
+
+ mine_transaction(&nodes[0], &node_1_commit_tx[0]);
+ mine_transaction(&nodes[1], &node_1_commit_tx[0]);
+
+ let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events.pop().unwrap() {
+ Event::BumpTransaction(bump_event) => {
+ nodes[1].bump_tx_handler.handle_event(&bump_event);
+ },
+ _ => panic!("Unexpected event"),
+ }
+
+ let htlc_claims = nodes[1].tx_broadcaster.txn_broadcast();
+ assert_eq!(htlc_claims.len(), 2);
+
+ check_spends!(htlc_claims[0], node_1_commit_tx[0], coinbase_tx);
+ check_spends!(htlc_claims[1], node_1_commit_tx[0], coinbase_tx);
+
+ assert_eq!(htlc_claims[0].input.len(), 71);
+ assert_eq!(htlc_claims[0].output.len(), 51);
+ assert_eq!(htlc_claims[1].input.len(), 34);
+ assert_eq!(htlc_claims[1].output.len(), 24);
+
+ check_closed_broadcast!(nodes[0], true);
+ check_added_monitors!(nodes[0], 1);
+ check_closed_event!(
+ nodes[0],
+ 1,
+ ClosureReason::CommitmentTxConfirmed,
+ [nodes[1].node.get_our_node_id()],
+ CHAN_CAPACITY
+ );
+ assert!(nodes[0].node.list_channels().is_empty());
+ check_closed_broadcast!(nodes[1], true);
+ check_added_monitors!(nodes[1], 1);
+ check_closed_event!(
+ nodes[1],
+ 1,
+ ClosureReason::CommitmentTxConfirmed,
+ [nodes[0].node.get_our_node_id()],
+ CHAN_CAPACITY
+ );
+ assert!(nodes[1].node.list_channels().is_empty());
+ assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
+ assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
+
+ mine_transaction(&nodes[1], &htlc_claims[0]);
+
+ let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events.pop().unwrap() {
+ Event::BumpTransaction(bump_event) => {
+ nodes[1].bump_tx_handler.handle_event(&bump_event);
+ },
+ _ => panic!("Unexpected event"),
+ }
+
+ let fresh_htlc_claims = nodes[1].tx_broadcaster.txn_broadcast();
+ assert_eq!(fresh_htlc_claims.len(), 1);
+ check_spends!(fresh_htlc_claims[0], node_1_commit_tx[0], coinbase_tx);
+ // We are targeting a higher feerate here,
+ // so we need more utxos here compared to `htlc_claims[1]` above.
+ assert_eq!(fresh_htlc_claims[0].input.len(), 37);
+ assert_eq!(fresh_htlc_claims[0].output.len(), 25);
+
+ let log_entries = nodes[1].logger.lines.lock().unwrap();
+ let batch_tx_id_assignments: Vec<_> = log_entries
+ .keys()
+ .map(|key| &key.1)
+ .filter(|log_msg| log_msg.contains("Batch transaction assigned to UTXO id"))
+ .collect();
+ assert_eq!(batch_tx_id_assignments.len(), 7);
+
+ let mut unique_claim_ids: Vec<(&str, u8)> = Vec::new();
+ for claim_id in batch_tx_id_assignments
+ .iter()
+ .map(|assignment| assignment.split_whitespace().nth(6).unwrap())
+ {
+ if let Some((_, count)) = unique_claim_ids.iter_mut().find(|(id, _count)| &claim_id == id) {
+ *count += 1;
+ } else {
+ unique_claim_ids.push((claim_id, 1));
+ }
+ }
+ unique_claim_ids.sort_unstable_by_key(|(_id, count)| *count);
+ assert_eq!(unique_claim_ids.len(), 2);
+ let (og_claim_id, og_claim_id_count) = unique_claim_ids.pop().unwrap();
+ assert_eq!(og_claim_id_count, 6);
+ assert_eq!(unique_claim_ids.pop().unwrap().1, 1);
+
+ let handling_htlc_bumps: Vec<_> = log_entries
+ .keys()
+ .map(|key| &key.1)
+ .filter(|log_msg| log_msg.contains("Handling HTLC bump"))
+ .map(|log_msg| {
+ log_msg
+ .split_whitespace()
+ .nth(5)
+ .unwrap()
+ .trim_matches(|c: char| c.is_ascii_punctuation())
+ })
+ .collect();
+ assert_eq!(handling_htlc_bumps.len(), 2);
+ assert_eq!(handling_htlc_bumps[0], og_claim_id);
+ assert_eq!(handling_htlc_bumps[1], og_claim_id);
+
+ let mut batch_sizes: Vec<u8> = batch_tx_id_assignments
+ .iter()
+ .map(|assignment| assignment.split_whitespace().nth(8).unwrap().parse().unwrap())
+ .collect();
+ batch_sizes.sort_unstable();
+ batch_sizes.reverse();
+ assert_eq!(batch_sizes.len(), 7);
+ assert_eq!(batch_sizes.pop().unwrap(), 24);
+ assert_eq!(batch_sizes.pop().unwrap(), 24);
+ for i in (51..=59).step_by(2) {
+ assert_eq!(batch_sizes.pop().unwrap(), i);
+ }
+ drop(log_entries);
+
+ nodes[1].wallet_source.clear_utxos();
+ nodes[1].chain_monitor.chain_monitor.rebroadcast_pending_claims();
+
+ let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events.pop().unwrap() {
+ Event::BumpTransaction(bump_event) => {
+ nodes[1].bump_tx_handler.handle_event(&bump_event);
+ },
+ _ => panic!("Unexpected event"),
+ }
+
+ nodes[1].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!(
+ "Failed bumping HTLC transaction fee for commitment {}",
+ node_1_commit_tx[0].compute_txid()
+ ),
+ 1,
+ );
+}
+
+#[test]
+fn test_anchor_tx_too_big() {
+ // Assert all V3 anchor tx transactions are below TRUC_CHILD_MAX_WEIGHT.
+ //
+ // Provide a bunch of small utxos, fail to bump the commitment,
+ // then provide a single big-value utxo, and successfully broadcast
+ // the commitment.
+ const FEERATE: u32 = 500;
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ {
+ let mut feerate_lock = chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap();
+ *feerate_lock = FEERATE;
+ }
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let mut user_cfg = test_default_channel_config();
+ user_cfg.channel_handshake_config.our_htlc_minimum_msat = 1;
+ user_cfg.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true;
+ user_cfg.channel_handshake_config.our_max_accepted_htlcs = 114;
+ user_cfg.manually_accept_inbound_channels = true;
+
+ let configs = [Some(user_cfg.clone()), Some(user_cfg)];
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &configs);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let node_a_id = nodes[0].node.get_our_node_id();
+
+ let _coinbase_tx_a = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500));
+
+ const CHAN_CAPACITY: u64 = 10_000_000;
+ let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value(
+ &nodes,
+ 0,
+ 1,
+ CHAN_CAPACITY,
+ (CHAN_CAPACITY / 2) * 1000,
+ );
+
+ let mut node_1_preimages = Vec::new();
+ const NONDUST_HTLC_AMT_MSAT: u64 = 1_000_000;
+ for _ in 0..50 {
+ let (preimage, payment_hash, _, _) =
+ route_payment(&nodes[0], &[&nodes[1]], NONDUST_HTLC_AMT_MSAT);
+ node_1_preimages.push((preimage, payment_hash));
+ }
+ let commitment_tx = get_local_commitment_txn!(nodes[1], chan_id).pop().unwrap();
+ let commitment_txid = commitment_tx.compute_txid();
+
+ let message = "Channel force-closed".to_owned();
+ nodes[1]
+ .node
+ .force_close_broadcasting_latest_txn(&chan_id, &node_a_id, message.clone())
+ .unwrap();
+ check_added_monitors(&nodes[1], 1);
+ check_closed_broadcast!(nodes[1], true);
+
+ let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message };
+ check_closed_event!(nodes[1], 1, reason, [node_a_id], CHAN_CAPACITY);
+
+ let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events.pop().unwrap() {
+ Event::BumpTransaction(bump_event) => {
+ nodes[1].bump_tx_handler.handle_event(&bump_event);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ assert!(nodes[1].tx_broadcaster.txn_broadcast().is_empty());
+ let max_coin_selection_weight = TRUC_CHILD_MAX_WEIGHT
+ - BASE_TX_SIZE * WITNESS_SCALE_FACTOR as u64
+ - SEGWIT_MARKER_FLAG_WEIGHT
+ - BASE_INPUT_WEIGHT
+ - EMPTY_SCRIPT_SIG_WEIGHT
+ - EMPTY_WITNESS_WEIGHT
+ - P2WSH_TXOUT_WEIGHT;
+ nodes[1].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!(
+ "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU",
+ FEERATE, max_coin_selection_weight
+ ),
+ 4,
+ );
+ nodes[1].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!("Failed bumping commitment transaction fee for {}", commitment_txid),
+ 1,
+ );
+
+ let coinbase_tx_b = provide_anchor_reserves(&nodes);
+
+ nodes[1].chain_monitor.chain_monitor.rebroadcast_pending_claims();
+
+ let mut events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events();
+ assert_eq!(events.len(), 1);
+ match events.pop().unwrap() {
+ Event::BumpTransaction(bump_event) => {
+ nodes[1].bump_tx_handler.handle_event(&bump_event);
+ },
+ _ => panic!("Unexpected event"),
+ }
+ let txns = nodes[1].tx_broadcaster.txn_broadcast();
+ assert_eq!(txns.len(), 2);
+ check_spends!(txns[1], txns[0], coinbase_tx_b);
+ assert!(txns[1].weight().to_wu() < TRUC_CHILD_MAX_WEIGHT);
+
+ assert_eq!(txns[0].compute_txid(), commitment_txid);
+ assert_eq!(txns[1].input.len(), 2);
+ assert_eq!(txns[1].output.len(), 1);
+ nodes[1].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!(
+ "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU",
+ FEERATE, max_coin_selection_weight
+ ),
+ 4,
+ );
+ nodes[1].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!("Failed bumping commitment transaction fee for {}", txns[0].compute_txid()),
+ 1,
+ );
+ nodes[1].logger.assert_log(
+ "lightning::events::bump_transaction",
+ format!(
+ "Broadcasting anchor transaction {} to bump channel close with txid {}",
+ txns[1].compute_txid(),
+ txns[0].compute_txid()
+ ),
+ 1,
+ );
+}
diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs
index aacc38e..f0f1b8a 100644
--- a/lightning/src/util/test_utils.rs
+++ b/lightning/src/util/test_utils.rs
@@ -2155,6 +2155,10 @@ impl TestWalletSource {
self.utxos.lock().unwrap().retain(|utxo| utxo.outpoint != outpoint);
}
+ pub fn clear_utxos(&self) {
+ self.utxos.lock().unwrap().clear();
+ }
+
pub fn sign_tx(
&self, mut tx: Transaction,
) -> Result<Transaction, bitcoin::sighash::P2wpkhError> {
Why this scored 58/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.