Skip to content

Fail HTLC backwards before upstream claims on-chain #2457

New issue

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

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

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2100,6 +2100,45 @@ impl<SP: Deref> Channel<SP> where
Ok(())
}

/// The total fee paid to close the channel and claim an HTLC-success transaction, accounting
/// for an added input (spending a P2WPKH) and P2WPKH output for fee bumping.
fn cost_to_claim_onchain<L: Deref>(&self, feerate_per_kw: u32, logger: &L) -> u64
where L::Target: Logger
{
let commitment_weight = if self.context.is_outbound() {
let keys = self.context.build_holder_transaction_keys(self.context.cur_holder_commitment_transaction_number);
let commitment_stats = self.context.build_commitment_transaction(self.context.cur_holder_commitment_transaction_number, &keys, true, true, logger);
commitment_stats.total_fee_sat
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be returning the weight here, not the fee.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should be able to compute the weight without re-building the transaction, you may need a few more consts than we have defined, and it'll need to depend on the channel type.

} else { 0 };

let htlc_weight = htlc_success_tx_weight(self.context.get_channel_type());
let p2wpkh_weight = 31 * 4;
let input_weight = 272; // Including witness, assuming it spends from a p2wpkh
Copy link
Contributor Author

@alecchendev alecchendev Aug 28, 2023

Choose a reason for hiding this comment

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

By the way I got this number from this post. Edit: oh it's in the spec too

Copy link
Contributor

Choose a reason for hiding this comment

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

FYI some of these are already defined in bump_transaction.rs.

Comment on lines +2115 to +2116
Copy link
Contributor

Choose a reason for hiding this comment

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

These only apply with anchors, otherwise the HTLC success transaction has the same feerate as the commitment transaction.


let total_weight = commitment_weight + htlc_weight + p2wpkh_weight + input_weight;
let total_fee = feerate_per_kw as u64 * total_weight / 1000;
total_fee
}
Comment on lines +2118 to +2121
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh also, for now I’ve only calculated the total fee if we were to claim just the one HTLC tx, but we may also want to consider all the fees we'd pay if we were to claim other pending HTLCs (though it's not clear which ones we'll end up having to claim ourselves vs our counterparty)

Copy link
Contributor

Choose a reason for hiding this comment

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

though it's not clear which ones we'll end up having to claim ourselves vs our counterparty

Ah because there could be HTLCs that expire much later than the HTLC we're attempting to fail back and the counterparty still has a chance to claim those onchain?


/// Check whether we should risk letting this channel force-close while waiting
/// for a downstream HTLC resolution on-chain. See [`ChannelConfig::early_fail_multiplier`]
/// for more info.
pub(super) fn check_worth_upstream_closing<F: Deref, L: Deref>(
&self, htlc_id: u64, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L
) -> Option<bool>
where F::Target: FeeEstimator, L::Target: Logger
{
let feerate_per_kw = fee_estimator.bounded_sat_per_1000_weight(ConfirmationTarget::HighPriority);
let htlc = self.context.pending_inbound_htlcs.iter().find(|htlc| htlc.htlc_id == htlc_id)?;
let total_fee = self.cost_to_claim_onchain(feerate_per_kw, logger);
let threshold = total_fee * self.context.config().early_fail_multiplier / 1_000_000;
if htlc.amount_msat / 1000 >= threshold {
Some(true)
} else {
Some(false)
}
}

#[inline]
fn get_closing_scriptpubkey(&self) -> Script {
// The shutdown scriptpubkey is set on channel opening when option_upfront_shutdown_script
Expand Down
41 changes: 37 additions & 4 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4791,6 +4791,37 @@ where
}
}

/// Check whether we should risk letting an upstream channel force-close while waiting
/// for a downstream HTLC resolution on-chain. See [`ChannelConfig::early_fail_multiplier`]
/// for more info.
fn check_worth_upstream_closing(&self, source: &HTLCSource) -> bool {
let (short_channel_id, htlc_id) = match source {
HTLCSource::OutboundRoute { .. } => {
debug_assert!(false, "This should not be called on outbound HTLCs");
return false;
},
HTLCSource::PreviousHopData(HTLCPreviousHopData { ref short_channel_id, ref htlc_id, .. }) => {
(short_channel_id, htlc_id)
},
};
let (counterparty_node_id, channel_id) = match self.short_to_chan_info.read().unwrap().get(short_channel_id) {
Some((cp_id, chan_id)) => (cp_id.clone(), chan_id.clone()),
None => return false,
};
let per_peer_state = self.per_peer_state.read().unwrap();
let mut peer_state_lock = match per_peer_state.get(&counterparty_node_id) {
Some(peer_state_mutex) => peer_state_mutex.lock().unwrap(),
None => return false,
};
let peer_state = &mut *peer_state_lock;
let chan = match peer_state.channel_by_id.get(&channel_id) {
Some(chan) => chan,
None => return false,
};
chan.check_worth_upstream_closing(
*htlc_id, &self.fee_estimator, &self.logger).unwrap_or(false)
}

/// Fails an HTLC backwards to the sender of it to us.
/// Note that we do not assume that channels corresponding to failed HTLCs are still available.
fn fail_htlc_backwards_internal(&self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason, destination: HTLCDestination) {
Expand Down Expand Up @@ -6338,10 +6369,12 @@ where
log_trace!(self.logger, "Claiming HTLC with preimage {} from our monitor", &preimage);
self.claim_funds_internal(htlc_update.source, preimage, htlc_update.htlc_value_satoshis.map(|v| v * 1000), true, funding_outpoint);
} else {
log_trace!(self.logger, "Failing HTLC with hash {} from our monitor", &htlc_update.payment_hash);
let receiver = HTLCDestination::NextHopChannel { node_id: counterparty_node_id, channel_id: funding_outpoint.to_channel_id() };
let reason = HTLCFailReason::from_failure_code(0x4000 | 8);
self.fail_htlc_backwards_internal(&htlc_update.source, &htlc_update.payment_hash, &reason, receiver);
if !htlc_update.awaiting_downstream_confirmation || !self.check_worth_upstream_closing(&htlc_update.source) {
log_trace!(self.logger, "Failing HTLC with hash {} from our monitor", &htlc_update.payment_hash);
let receiver = HTLCDestination::NextHopChannel { node_id: counterparty_node_id, channel_id: funding_outpoint.to_channel_id() };
let reason = HTLCFailReason::from_failure_code(0x4000 | 8);
self.fail_htlc_backwards_internal(&htlc_update.source, &htlc_update.payment_hash, &reason, receiver);
}
}
},
MonitorEvent::CommitmentTxConfirmed(funding_outpoint) |
Expand Down