Consider currently confirmed FundingScope when claiming commitments
What changed, and why it matters
This commit fixes a bug in how the Lightning Dev Kit's on-chain channel monitor matches a confirmed commitment transaction to the correct funding state. Previously, the code always looked at the primary channel funding state (`self.funding`) when deciding whose commitment transaction was broadcast and which HTLCs/outputs to claim. After splicing-style channel upgrades, a commitment transaction can spend an alternative (pending) funding output. If the monitor used the wrong funding state, it could fail to recognize the commitment, miss HTLC claims, or use stale channel parameters—potentially leading to stuck funds or an inability to claim outputs during a force-close. The fix introduces a helper that selects the funding state actually confirmed on-chain.
Treat as a security-relevant correctness fix. Review related splicing/funding-scope logic for other places that still assume `self.funding` is authoritative. Add or extend tests covering alternative-funding commitment confirmation, revoked counterparty spends after splice, and holder commitment claims. Consider whether the new `expect`/`assert_eq!` calls are safe against maliciously crafted on-chain transactions; they appear guarded by prior funding-outpoint matching but should be fuzzed.
Security signals we found
Wrong-state lookup: monitor used primary `self.funding` instead of the confirmed alternative funding scope
Potential missed output claims during force-close/splicing
Potential use of stale channel parameters (e.g., channel_value_satoshis, channel_transaction_parameters) when generating claim transactions
New runtime assertions added to detect funding/commitment mismatches
Splicing-related code path (alternative_funding_confirmed / pending_funding)
TODO removed indicating previously incomplete handling
Evidence from the diff
The patch adds get_confirmed_funding_scope!, a macro that returns &FundingScope from pending_funding when alternative_funding_confirmed is set, otherwise falling back to self.funding. It then threads this funding_spent reference through commitment-spend detection, counterparty and holder commitment handling, HTLC resolution, and spendable-output generation. Key changes include: replacing direct self.funding accesses in check_spend_counterparty_transaction, check_spend_holder_transaction, is_resolving_htlc_output, get_spendable_outputs, and related helpers; adding assert_eq!(funding_spent.funding_txid(), funding_txid_spent) sanity checks; and updating the funding-output-spend detection loop to consider all pending funding outpoints. The removed TODO comment ‘Produce commitment claims for currently confirmed funding’ confirms this was an unimplemented splicing edge case.
Changed components
lightning/src/chain/channelmonitor.rsChannelMonitorImplcheck_spend_counterparty_transactioncheck_spend_holder_transactionget_counterparty_output_claim_infoget_spendable_outputsis_resolving_htlc_outputtransactions_confirmed / block_connected funding-spend detectionholder_commitment_htlcs macroInspect captured patch +167 / −123
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index e5f351e..04e81c9 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -1302,30 +1302,51 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> {
alternative_funding_confirmed: Option<(Txid, u32)>,
}
+// Returns a `&FundingScope` for the one we are currently observing/handling commitment transactions
+// for on the chain.
+macro_rules! get_confirmed_funding_scope {
+ ($self: expr) => {
+ $self
+ .alternative_funding_confirmed
+ .map(|(alternative_funding_txid, _)| {
+ $self
+ .pending_funding
+ .iter()
+ .find(|funding| funding.funding_txid() == alternative_funding_txid)
+ .expect("FundingScope for confirmed alternative funding must exist")
+ })
+ .unwrap_or(&$self.funding)
+ };
+}
+
// Macro helper to access holder commitment HTLC data (including both non-dust and dust) while
// holding mutable references to `self`. Unfortunately, if these were turned into helper functions,
// we'd be unable to mutate `self` while holding an immutable iterator (specifically, returned from
// a function) over `self`.
#[rustfmt::skip]
macro_rules! holder_commitment_htlcs {
- ($self: expr, CURRENT) => {
- $self.funding.current_holder_commitment_tx.nondust_htlcs().iter()
+ ($self: expr, CURRENT) => {{
+ let funding = get_confirmed_funding_scope!($self);
+ funding.current_holder_commitment_tx.nondust_htlcs().iter()
.chain($self.current_holder_htlc_data.dust_htlcs.iter().map(|(htlc, _)| htlc))
- };
+ }};
($self: expr, CURRENT_WITH_SOURCES) => {{
+ let funding = get_confirmed_funding_scope!($self);
holder_commitment_htlcs!(
- &$self.funding.current_holder_commitment_tx, &$self.current_holder_htlc_data
+ &funding.current_holder_commitment_tx, &$self.current_holder_htlc_data
)
}};
($self: expr, PREV) => {{
- $self.funding.prev_holder_commitment_tx.as_ref().map(|tx| {
+ let funding = get_confirmed_funding_scope!($self);
+ funding.prev_holder_commitment_tx.as_ref().map(|tx| {
let dust_htlcs = $self.prev_holder_htlc_data.as_ref().unwrap().dust_htlcs.iter()
.map(|(htlc, _)| htlc);
tx.nondust_htlcs().iter().chain(dust_htlcs)
})
}};
($self: expr, PREV_WITH_SOURCES) => {{
- $self.funding.prev_holder_commitment_tx.as_ref().map(|tx| {
+ let funding = get_confirmed_funding_scope!($self);
+ funding.prev_holder_commitment_tx.as_ref().map(|tx| {
holder_commitment_htlcs!(tx, $self.prev_holder_htlc_data.as_ref().unwrap())
})
}};
@@ -2408,7 +2429,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
pub fn get_spendable_outputs(&self, tx: &Transaction, confirmation_height: u32) -> Vec<SpendableOutputDescriptor> {
let inner = self.inner.lock().unwrap();
let current_height = inner.best_block.height;
- let mut spendable_outputs = inner.get_spendable_outputs(tx);
+ let funding = get_confirmed_funding_scope!(inner);
+ let mut spendable_outputs = inner.get_spendable_outputs(&funding, tx);
spendable_outputs.retain(|descriptor| {
let mut conf_threshold = current_height.saturating_sub(ANTI_REORG_DELAY) + 1;
if let SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) = descriptor {
@@ -3015,17 +3037,19 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
}
}
- let txid = confirmed_txid.unwrap();
- if Some(txid) == us.funding.current_counterparty_commitment_txid || Some(txid) == us.funding.prev_counterparty_commitment_txid {
- walk_htlcs!(false, us.funding.counterparty_claimable_outpoints.get(&txid).unwrap().iter().filter_map(|(a, b)| {
+ let commitment_txid = confirmed_txid.unwrap();
+ let funding_spent = get_confirmed_funding_scope!(us);
+
+ if Some(commitment_txid) == funding_spent.current_counterparty_commitment_txid || Some(commitment_txid) == funding_spent.prev_counterparty_commitment_txid {
+ walk_htlcs!(false, funding_spent.counterparty_claimable_outpoints.get(&commitment_txid).unwrap().iter().filter_map(|(a, b)| {
if let &Some(ref source) = b {
Some((a, Some(&**source)))
} else { None }
}));
- } else if txid == us.funding.current_holder_commitment_tx.trust().txid() {
+ } else if commitment_txid == funding_spent.current_holder_commitment_tx.trust().txid() {
walk_htlcs!(true, holder_commitment_htlcs!(us, CURRENT_WITH_SOURCES));
- } else if let Some(prev_commitment_tx) = &us.funding.prev_holder_commitment_tx {
- if txid == prev_commitment_tx.trust().txid() {
+ } else if let Some(prev_commitment_tx) = &funding_spent.prev_holder_commitment_tx {
+ if commitment_txid == prev_commitment_tx.trust().txid() {
walk_htlcs!(true, holder_commitment_htlcs!(us, PREV_WITH_SOURCES).unwrap());
}
}
@@ -3562,12 +3586,13 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
} else {
return;
};
+ let funding_spent = get_confirmed_funding_scope!(self);
// If the channel is force closed, try to claim the output from this preimage.
// First check if a counterparty commitment transaction has been broadcasted:
macro_rules! claim_htlcs {
($commitment_number: expr, $txid: expr, $htlcs: expr) => {
- let (htlc_claim_reqs, _) = self.get_counterparty_output_claim_info($commitment_number, $txid, None, $htlcs, confirmed_spend_height);
+ let (htlc_claim_reqs, _) = self.get_counterparty_output_claim_info(funding_spent, $commitment_number, $txid, None, $htlcs, confirmed_spend_height);
let conf_target = self.closure_conf_target();
self.onchain_tx_handler.update_claims_view_from_requests(
htlc_claim_reqs, self.best_block.height, self.best_block.height, broadcaster,
@@ -3575,10 +3600,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
);
}
}
- if let Some(txid) = self.funding.current_counterparty_commitment_txid {
+ if let Some(txid) = funding_spent.current_counterparty_commitment_txid {
if txid == confirmed_spend_txid {
if let Some(commitment_number) = self.counterparty_commitment_txn_on_chain.get(&txid) {
- claim_htlcs!(*commitment_number, txid, self.funding.counterparty_claimable_outpoints.get(&txid));
+ claim_htlcs!(*commitment_number, txid, funding_spent.counterparty_claimable_outpoints.get(&txid));
} else {
debug_assert!(false);
log_error!(logger, "Detected counterparty commitment tx on-chain without tracking commitment number");
@@ -3586,10 +3611,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
return;
}
}
- if let Some(txid) = self.funding.prev_counterparty_commitment_txid {
+ if let Some(txid) = funding_spent.prev_counterparty_commitment_txid {
if txid == confirmed_spend_txid {
if let Some(commitment_number) = self.counterparty_commitment_txn_on_chain.get(&txid) {
- claim_htlcs!(*commitment_number, txid, self.funding.counterparty_claimable_outpoints.get(&txid));
+ claim_htlcs!(*commitment_number, txid, funding_spent.counterparty_claimable_outpoints.get(&txid));
} else {
debug_assert!(false);
log_error!(logger, "Detected counterparty commitment tx on-chain without tracking commitment number");
@@ -3604,9 +3629,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
// *we* sign a holder commitment transaction, not when e.g. a watchtower broadcasts one of our
// holder commitment transactions.
if self.broadcasted_holder_revokable_script.is_some() {
- let holder_commitment_tx = if self.funding.current_holder_commitment_tx.trust().txid() == confirmed_spend_txid {
- Some(&self.funding.current_holder_commitment_tx)
- } else if let Some(prev_holder_commitment_tx) = &self.funding.prev_holder_commitment_tx {
+ let holder_commitment_tx = if funding_spent.current_holder_commitment_tx.trust().txid() == confirmed_spend_txid {
+ Some(&funding_spent.current_holder_commitment_tx)
+ } else if let Some(prev_holder_commitment_tx) = &funding_spent.prev_holder_commitment_tx {
if prev_holder_commitment_tx.trust().txid() == confirmed_spend_txid {
Some(prev_holder_commitment_tx)
} else {
@@ -3619,7 +3644,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
// Assume that the broadcasted commitment transaction confirmed in the current best
// block. Even if not, its a reasonable metric for the bump criteria on the HTLC
// transactions.
- let (claim_reqs, _) = self.get_broadcasted_holder_claims(&self.funding, holder_commitment_tx, self.best_block.height);
+ let (claim_reqs, _) = self.get_broadcasted_holder_claims(
+ funding_spent, holder_commitment_tx, self.best_block.height,
+ );
let conf_target = self.closure_conf_target();
self.onchain_tx_handler.update_claims_view_from_requests(
claim_reqs, self.best_block.height, self.best_block.height, broadcaster,
@@ -3633,14 +3660,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
fn generate_claimable_outpoints_and_watch_outputs(
&mut self, generate_monitor_event_with_reason: Option<ClosureReason>,
) -> (Vec<PackageTemplate>, Vec<TransactionOutputs>) {
- let funding = self.alternative_funding_confirmed
- .map(|(alternative_funding_txid, _)| {
- self.pending_funding
- .iter()
- .find(|funding| funding.funding_txid() == alternative_funding_txid)
- .expect("FundingScope for confirmed alternative funding must exist")
- })
- .unwrap_or(&self.funding);
+ let funding = get_confirmed_funding_scope!(self);
let holder_commitment_tx = &funding.current_holder_commitment_tx;
let funding_outp = HolderFundingOutput::build(
holder_commitment_tx.clone(),
@@ -4283,7 +4303,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
/// Returns packages to claim the revoked output(s) and general information about the output that
/// is to the counterparty in the commitment transaction.
#[rustfmt::skip]
- fn check_spend_counterparty_transaction<L: Deref>(&mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &L)
+ fn check_spend_counterparty_transaction<L: Deref>(&mut self, commitment_txid: Txid, commitment_tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &L)
-> (Vec<PackageTemplate>, CommitmentTxCounterpartyOutputInfo)
where L::Target: Logger {
// Most secp and related errors trying to create keys means we have no hope of constructing
@@ -4291,8 +4311,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
let mut claimable_outpoints = Vec::new();
let mut to_counterparty_output_info = None;
- let commitment_txid = tx.compute_txid(); //TODO: This is gonna be a performance bottleneck for watchtowers!
- let per_commitment_option = self.funding.counterparty_claimable_outpoints.get(&commitment_txid);
+ let funding_spent = get_confirmed_funding_scope!(self);
+ let per_commitment_option = funding_spent.counterparty_claimable_outpoints.get(&commitment_txid);
macro_rules! ignore_error {
( $thing : expr ) => {
@@ -4303,8 +4323,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
};
}
- let commitment_number = 0xffffffffffff - ((((tx.input[0].sequence.0 as u64 & 0xffffff) << 3*8) | (tx.lock_time.to_consensus_u32() as u64 & 0xffffff)) ^ self.commitment_transaction_number_obscure_factor);
+ let funding_txid_spent = commitment_tx.input[0].previous_output.txid;
+ let commitment_number = 0xffffffffffff - ((((commitment_tx.input[0].sequence.0 as u64 & 0xffffff) << 3*8) | (commitment_tx.lock_time.to_consensus_u32() as u64 & 0xffffff)) ^ self.commitment_transaction_number_obscure_factor);
if commitment_number >= self.get_min_seen_secret() {
+ assert_eq!(funding_spent.funding_txid(), funding_txid_spent);
+
let secret = self.get_secret(commitment_number).unwrap();
let per_commitment_key = ignore_error!(SecretKey::from_slice(&secret));
let per_commitment_point = PublicKey::from_secret_key(&self.onchain_tx_handler.secp_ctx, &per_commitment_key);
@@ -4315,13 +4338,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
let revokeable_p2wsh = revokeable_redeemscript.to_p2wsh();
// First, process non-htlc outputs (to_holder & to_counterparty)
- for (idx, outp) in tx.output.iter().enumerate() {
+ for (idx, outp) in commitment_tx.output.iter().enumerate() {
if outp.script_pubkey == revokeable_p2wsh {
let revk_outp = RevokedOutput::build(
per_commitment_point, per_commitment_key, outp.value,
- self.funding.channel_parameters.channel_type_features.supports_anchors_zero_fee_htlc_tx(),
- self.funding.channel_parameters.clone(),
- height,
+ funding_spent.channel_type_features().supports_anchors_zero_fee_htlc_tx(),
+ funding_spent.channel_parameters.clone(), height,
);
let justice_package = PackageTemplate::build_package(
commitment_txid, idx as u32,
@@ -4338,15 +4360,14 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
if let Some(per_commitment_claimable_data) = per_commitment_option {
for (htlc, _) in per_commitment_claimable_data {
if let Some(transaction_output_index) = htlc.transaction_output_index {
- if transaction_output_index as usize >= tx.output.len() ||
- tx.output[transaction_output_index as usize].value != htlc.to_bitcoin_amount() {
+ if transaction_output_index as usize >= commitment_tx.output.len() ||
+ commitment_tx.output[transaction_output_index as usize].value != htlc.to_bitcoin_amount() {
// per_commitment_data is corrupt or our commitment signing key leaked!
return (claimable_outpoints, to_counterparty_output_info);
}
let revk_htlc_outp = RevokedHTLCOutput::build(
per_commitment_point, per_commitment_key, htlc.clone(),
- self.funding.channel_parameters.clone(),
- height,
+ funding_spent.channel_parameters.clone(), height,
);
let counterparty_spendable_height = if htlc.offered {
htlc.cltv_expiry
@@ -4371,7 +4392,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.counterparty_commitment_txn_on_chain.insert(commitment_txid, commitment_number);
if let Some(per_commitment_claimable_data) = per_commitment_option {
- fail_unbroadcast_htlcs!(self, "revoked_counterparty", commitment_txid, tx, height,
+ fail_unbroadcast_htlcs!(self, "revoked_counterparty", commitment_txid, commitment_tx, height,
block_hash, per_commitment_claimable_data.iter().map(|(htlc, htlc_source)|
(htlc, htlc_source.as_ref().map(|htlc_source| htlc_source.as_ref()))
), logger);
@@ -4381,11 +4402,13 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
// commitment transactions. Thus, we can only debug-assert here when not
// fuzzing.
debug_assert!(cfg!(fuzzing), "We should have per-commitment option for any recognized old commitment txn");
- fail_unbroadcast_htlcs!(self, "revoked counterparty", commitment_txid, tx, height,
+ fail_unbroadcast_htlcs!(self, "revoked counterparty", commitment_txid, commitment_tx, height,
block_hash, [].iter().map(|reference| *reference), logger);
}
}
} else if let Some(per_commitment_claimable_data) = per_commitment_option {
+ assert_eq!(funding_spent.funding_txid(), funding_txid_spent);
+
// While this isn't useful yet, there is a potential race where if a counterparty
// revokes a state at the same time as the commitment transaction for that state is
// confirmed, and the watchtower receives the block before the user, the user could
@@ -4396,25 +4419,26 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
self.counterparty_commitment_txn_on_chain.insert(commitment_txid, commitment_number);
log_info!(logger, "Got broadcast of non-revoked counterparty commitment transaction {}", commitment_txid);
- fail_unbroadcast_htlcs!(self, "counterparty", commitment_txid, tx, height, block_hash,
+ fail_unbroadcast_htlcs!(self, "counterparty", commitment_txid, commitment_tx, height, block_hash,
per_commitment_claimable_data.iter().map(|(htlc, htlc_source)|
(htlc, htlc_source.as_ref().map(|htlc_source| htlc_source.as_ref()))
), logger);
let (htlc_claim_reqs, counterparty_output_info) =
- self.get_counterparty_output_claim_info(commitment_number, commitment_txid, Some(tx), per_commitment_option, Some(height));
+ self.get_counterparty_output_claim_info(funding_spent, commitment_number, commitment_txid, Some(commitment_tx), per_commitment_option, Some(height));
to_counterparty_output_info = counterparty_output_info;
for req in htlc_claim_reqs {
claimable_outpoints.push(req);
}
-
}
+
(claimable_outpoints, to_counterparty_output_info)
}
/// Returns the HTLC claim package templates and the counterparty output info
#[rustfmt::skip]
fn get_counterparty_output_claim_info(
- &self, commitment_number: u64, commitment_txid: Txid, tx: Option<&Transaction>,
+ &self, funding_spent: &FundingScope, commitment_number: u64, commitment_txid: Txid,
+ tx: Option<&Transaction>,
per_commitment_option: Option<&Vec<(HTLCOutputInCommitment, Option<Box<HTLCSource>>)>>,
confirmation_height: Option<u32>,
) -> (Vec<PackageTemplate>, CommitmentTxCounterpartyOutputInfo) {
@@ -4476,7 +4500,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
CounterpartyOfferedHTLCOutput::build(
*per_commitment_point, preimage.unwrap(),
htlc.clone(),
- self.funding.channel_parameters.clone(),
+ funding_spent.channel_parameters.clone(),
confirmation_height,
)
)
@@ -4485,7 +4509,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
CounterpartyReceivedHTLCOutput::build(
*per_commitment_point,
htlc.clone(),
- self.funding.channel_parameters.clone(),
+ funding_spent.channel_parameters.clone(),
confirmation_height,
)
)
@@ -4511,6 +4535,9 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
};
let per_commitment_point = PublicKey::from_secret_key(&self.onchain_tx_handler.secp_ctx, &per_commitment_key);
+ let funding_spent = get_confirmed_funding_scope!(self);
+ debug_assert!(funding_spent.counterparty_claimable_outpoints.contains_key(commitment_txid));
+
let htlc_txid = tx.compute_txid();
let mut claimable_outpoints = vec![];
let mut outputs_to_watch = None;
@@ -4529,7 +4556,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
log_error!(logger, "Got broadcast of revoked counterparty HTLC transaction, spending {}:{}", htlc_txid, idx);
let revk_outp = RevokedOutput::build(
per_commitment_point, per_commitment_key, tx.output[idx].value, false,
- self.funding.channel_parameters.clone(),
+ funding_spent.channel_parameters.clone(),
height,
);
let justice_package = PackageTemplate::build_package(
@@ -4643,66 +4670,65 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
/// Should not be used if check_spend_revoked_transaction succeeds.
/// Returns None unless the transaction is definitely one of our commitment transactions.
fn check_spend_holder_transaction<L: Deref>(
- &mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &L,
+ &mut self, commitment_txid: Txid, commitment_tx: &Transaction, height: u32,
+ block_hash: &BlockHash, logger: &L,
) -> Option<(Vec<PackageTemplate>, TransactionOutputs)>
where
L::Target: Logger,
{
- let commitment_txid = tx.compute_txid();
- let mut claim_requests = Vec::new();
- let mut watch_outputs = Vec::new();
-
- macro_rules! append_onchain_update {
- ($updates: expr, $to_watch: expr) => {
- claim_requests = $updates.0;
- self.broadcasted_holder_revokable_script = $updates.1;
- watch_outputs.append(&mut $to_watch);
- };
- }
+ let funding_spent = get_confirmed_funding_scope!(self);
// HTLCs set may differ between last and previous holder commitment txn, in case of one them hitting chain, ensure we cancel all HTLCs backward
- let mut is_holder_tx = false;
-
- if self.funding.current_holder_commitment_tx.trust().txid() == commitment_txid {
- is_holder_tx = true;
- log_info!(logger, "Got broadcast of latest holder commitment tx {}, searching for available HTLCs to claim", commitment_txid);
- let holder_commitment_tx = &self.funding.current_holder_commitment_tx;
- let res =
- self.get_broadcasted_holder_claims(&self.funding, holder_commitment_tx, height);
- let mut to_watch = self.get_broadcasted_holder_watch_outputs(holder_commitment_tx);
- append_onchain_update!(res, to_watch);
- fail_unbroadcast_htlcs!(
- self,
- "latest holder",
- commitment_txid,
- tx,
- height,
- block_hash,
- holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES),
- logger
- );
- } else if let Some(holder_commitment_tx) = &self.funding.prev_holder_commitment_tx {
- if holder_commitment_tx.trust().txid() == commitment_txid {
- is_holder_tx = true;
- log_info!(logger, "Got broadcast of previous holder commitment tx {}, searching for available HTLCs to claim", commitment_txid);
- let res =
- self.get_broadcasted_holder_claims(&self.funding, holder_commitment_tx, height);
- let mut to_watch = self.get_broadcasted_holder_watch_outputs(holder_commitment_tx);
- append_onchain_update!(res, to_watch);
+ let holder_commitment_tx = Some((&funding_spent.current_holder_commitment_tx, true))
+ .filter(|(current_holder_commitment_tx, _)| {
+ current_holder_commitment_tx.trust().txid() == commitment_txid
+ })
+ .or_else(|| {
+ funding_spent
+ .prev_holder_commitment_tx
+ .as_ref()
+ .map(|prev_holder_commitment_tx| (prev_holder_commitment_tx, false))
+ .filter(|(prev_holder_commitment_tx, _)| {
+ prev_holder_commitment_tx.trust().txid() == commitment_txid
+ })
+ });
+
+ if let Some((holder_commitment_tx, current)) = holder_commitment_tx {
+ let funding_txid_spent = commitment_tx.input[0].previous_output.txid;
+ assert_eq!(funding_spent.funding_txid(), funding_txid_spent);
+
+ let current_msg = if current { "latest holder" } else { "previous holder" };
+ log_info!(logger, "Got broadcast of {current_msg} commitment tx {commitment_txid}, searching for available HTLCs to claim");
+
+ let (claim_requests, broadcasted_holder_revokable_script) =
+ self.get_broadcasted_holder_claims(funding_spent, holder_commitment_tx, height);
+ self.broadcasted_holder_revokable_script = broadcasted_holder_revokable_script;
+ let watch_outputs = self.get_broadcasted_holder_watch_outputs(holder_commitment_tx);
+
+ if current {
fail_unbroadcast_htlcs!(
self,
- "previous holder",
+ current_msg,
commitment_txid,
- tx,
+ commitment_tx,
+ height,
+ block_hash,
+ holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES),
+ logger
+ );
+ } else {
+ fail_unbroadcast_htlcs!(
+ self,
+ current_msg,
+ commitment_txid,
+ commitment_tx,
height,
block_hash,
holder_commitment_htlcs!(self, PREV_WITH_SOURCES).unwrap(),
logger
);
}
- }
- if is_holder_tx {
Some((claim_requests, (commitment_txid, watch_outputs)))
} else {
None
@@ -5014,18 +5040,30 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
// commitment transactions and HTLC transactions will all only ever have one input
// (except for HTLC transactions for channels with anchor outputs), which is an easy
// way to filter out any potential non-matching txn for lazy filters.
- //
- // TODO(splicing): Produce commitment claims for currently confirmed funding.
- let prevout = &tx.input[0].previous_output;
- let funding_outpoint = self.get_funding_txo();
- if prevout.txid == funding_outpoint.txid && prevout.vout == funding_outpoint.index as u32 {
- let mut balance_spendable_csv = None;
- log_info!(logger, "Channel {} closed by funding output spend in txid {}.",
- &self.channel_id(), txid);
+ if let Some(funding_txid_spent) = core::iter::once(&self.funding)
+ .chain(self.pending_funding.iter())
+ .find(|funding| {
+ let funding_outpoint = funding.funding_outpoint().into_bitcoin_outpoint();
+ funding_outpoint == tx.input[0].previous_output
+ })
+ .map(|funding| funding.funding_txid())
+ {
+ assert_eq!(
+ funding_txid_spent,
+ self.alternative_funding_confirmed
+ .map(|(txid, _)| txid)
+ .unwrap_or_else(|| self.funding.funding_txid())
+ );
+ log_info!(logger, "Channel {} closed by funding output spend in txid {txid}",
+ self.channel_id());
self.funding_spend_seen = true;
+
+ let mut balance_spendable_csv = None;
let mut commitment_tx_to_counterparty_output = None;
+
+ // Is it a commitment transaction?
if (tx.input[0].sequence.0 >> 8*3) as u8 == 0x80 && (tx.lock_time.to_consensus_u32() >> 8*3) as u8 == 0x20 {
- if let Some((mut new_outpoints, new_outputs)) = self.check_spend_holder_transaction(&tx, height, &block_hash, &logger) {
+ if let Some((mut new_outpoints, new_outputs)) = self.check_spend_holder_transaction(txid, &tx, height, &block_hash, &logger) {
if !new_outputs.1.is_empty() {
watch_outputs.push(new_outputs);
}
@@ -5040,7 +5078,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
watch_outputs.push((txid, new_watch_outputs));
let (mut new_outpoints, counterparty_output_idx_sats) =
- self.check_spend_counterparty_transaction(&tx, height, &block_hash, &logger);
+ self.check_spend_counterparty_transaction(txid, &tx, height, &block_hash, &logger);
commitment_tx_to_counterparty_output = counterparty_output_idx_sats;
claimable_outpoints.append(&mut new_outpoints);
@@ -5053,6 +5091,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
should_broadcast_commitment = false;
}
}
+
self.onchain_events_awaiting_threshold_conf.push(OnchainEventEntry {
txid,
transaction: Some((*tx).clone()),
@@ -5063,6 +5102,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
commitment_tx_to_counterparty_output,
},
});
+
// Now that we've detected a confirmed commitment transaction, attempt to cancel
// pending claims for any commitments that were previously confirmed such that
// we don't continue claiming inputs that no longer exist.
@@ -5556,6 +5596,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
fn is_resolving_htlc_output<L: Deref>(
&mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &WithChannelMonitor<L>,
) where L::Target: Logger {
+ let funding_spent = get_confirmed_funding_scope!(self);
+
'outer_loop: for input in &tx.input {
let mut payment_data = None;
let htlc_claim = HTLCClaim::from_witness(&input.witness);
@@ -5617,7 +5659,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}
macro_rules! scan_commitment {
- ($htlcs: expr, $tx_info: expr, $holder_tx: expr) => {
+ ($funding_spent: expr, $htlcs: expr, $tx_info: expr, $holder_tx: expr) => {
for (ref htlc_output, source_option) in $htlcs {
if Some(input.previous_output.vout) == htlc_output.transaction_output_index {
if let Some(ref source) = source_option {
@@ -5629,12 +5671,12 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
// resolve the source HTLC with the original sender.
payment_data = Some(((*source).clone(), htlc_output.payment_hash, htlc_output.amount_msat));
} else if !$holder_tx {
- if let Some(current_counterparty_commitment_txid) = &self.funding.current_counterparty_commitment_txid {
- check_htlc_valid_counterparty!(htlc_output, self.funding.counterparty_claimable_outpoints.get(current_counterparty_commitment_txid).unwrap());
+ if let Some(current_counterparty_commitment_txid) = &$funding_spent.current_counterparty_commitment_txid {
+ check_htlc_valid_counterparty!(htlc_output, $funding_spent.counterparty_claimable_outpoints.get(current_counterparty_commitment_txid).unwrap());
}
if payment_data.is_none() {
- if let Some(prev_counterparty_commitment_txid) = &self.funding.prev_counterparty_commitment_txid {
- check_htlc_valid_counterparty!(htlc_output, self.funding.counterparty_claimable_outpoints.get(prev_counterparty_commitment_txid).unwrap());
+ if let Some(prev_counterparty_commitment_txid) = &$funding_spent.prev_counterparty_commitment_txid {
+ check_htlc_valid_counterparty!(htlc_output, $funding_spent.counterparty_claimable_outpoints.get(prev_counterparty_commitment_txid).unwrap());
}
}
}
@@ -5662,23 +5704,24 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}
}
- if input.previous_output.txid == self.funding.current_holder_commitment_tx.trust().txid() {
+ if input.previous_output.txid == funding_spent.current_holder_commitment_tx.trust().txid() {
scan_commitment!(
- holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES),
+ funding_spent, holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES),
"our latest holder commitment tx", true
);
}
- if let Some(prev_holder_commitment_tx) = self.funding.prev_holder_commitment_tx.as_ref() {
+ if let Some(prev_holder_commitment_tx) = funding_spent.prev_holder_commitment_tx.as_ref() {
if input.previous_output.txid == prev_holder_commitment_tx.trust().txid() {
scan_commitment!(
- holder_commitment_htlcs!(self, PREV_WITH_SOURCES).unwrap(),
+ funding_spent, holder_commitment_htlcs!(self, PREV_WITH_SOURCES).unwrap(),
"our previous holder commitment tx", true
);
}
}
- if let Some(ref htlc_outputs) = self.funding.counterparty_claimable_outpoints.get(&input.previous_output.txid) {
- scan_commitment!(htlc_outputs.iter().map(|&(ref a, ref b)| (a, b.as_ref().map(|boxed| &**boxed))),
- "counterparty commitment tx", false);
+ if let Some(ref htlc_outputs) = funding_spent.counterparty_claimable_outpoints.get(&input.previous_output.txid) {
+ let htlcs = htlc_outputs.iter()
+ .map(|&(ref a, ref b)| (a, b.as_ref().map(|boxed| &**boxed)));
+ scan_commitment!(funding_spent, htlcs, "counterparty commitment tx", false);
}
// Check that scan_commitment, above, decided there is some source worth relaying an
@@ -5758,7 +5801,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
}
#[rustfmt::skip]
- fn get_spendable_outputs(&self, tx: &Transaction) -> Vec<SpendableOutputDescriptor> {
+ fn get_spendable_outputs(&self, funding_spent: &FundingScope, tx: &Transaction) -> Vec<SpendableOutputDescriptor> {
let mut spendable_outputs = Vec::new();
for (i, outp) in tx.output.iter().enumerate() {
if outp.script_pubkey == self.destination_script {
@@ -5777,8 +5820,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
output: outp.clone(),
revocation_pubkey: broadcasted_holder_revokable_script.2,
channel_keys_id: self.channel_keys_id,
- channel_value_satoshis: self.funding.channel_parameters.channel_value_satoshis,
- channel_transaction_parameters: Some(self.funding.channel_parameters.clone()),
+ channel_value_satoshis: funding_spent.channel_parameters.channel_value_satoshis,
+ channel_transaction_parameters: Some(funding_spent.channel_parameters.clone()),
}));
}
}
@@ -5787,8 +5830,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
outpoint: OutPoint { txid: tx.compute_txid(), index: i as u16 },
output: outp.clone(),
channel_keys_id: self.channel_keys_id,
- channel_value_satoshis: self.funding.channel_parameters.channel_value_satoshis,
- channel_transaction_parameters: Some(self.funding.channel_parameters.clone()),
+ channel_value_satoshis: funding_spent.channel_parameters.channel_value_satoshis,
+ channel_transaction_parameters: Some(funding_spent.channel_parameters.clone()),
}));
}
if self.shutdown_script.as_ref() == Some(&outp.script_pubkey) {
@@ -5808,7 +5851,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
fn check_tx_and_push_spendable_outputs<L: Deref>(
&mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &WithChannelMonitor<L>,
) where L::Target: Logger {
- for spendable_output in self.get_spendable_outputs(tx) {
+ let funding_spent = get_confirmed_funding_scope!(self);
+ for spendable_output in self.get_spendable_outputs(funding_spent, tx) {
let entry = OnchainEventEntry {
txid: tx.compute_txid(),
transaction: Some(tx.clone()),
Why this scored 63/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.