Skip to content

Commit 83e1ae9

Browse files
committed
Avoid force-closing 0-conf channels when funding is reorg'd
When we see a funding transaction for one of our chanels reorg'd out, we worry that its possible we've been double-spent and immediately force-close the channel to avoid accepting any more HTLCs on it. This isn't ideal, but is mostly fine as most nodes require 6 confirmations and 6 block reorgs are exceedingly rare. However, this isn't so okay for 0-conf channels - in that case we elected to trust the funder anyway, so reorgs shouldn't worry us. Still, to handle this correctly we needed to track the old SCID and ensure our logic is safe across an SCID change. Luckily, we did that work for splices, and can now take advantage of it here. Fixes #3836.
1 parent 7ca08a9 commit 83e1ae9

File tree

3 files changed

+256
-60
lines changed

3 files changed

+256
-60
lines changed

lightning/src/ln/channel.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11344,9 +11344,13 @@ where
1134411344
}
1134511345

1134611346
// Check if the funding transaction was unconfirmed
11347+
let original_scid = self.funding.short_channel_id;
11348+
let was_confirmed = self.funding.funding_tx_confirmed_in.is_some();
1134711349
let funding_tx_confirmations = self.funding.get_funding_tx_confirmations(height);
1134811350
if funding_tx_confirmations == 0 {
1134911351
self.funding.funding_tx_confirmation_height = 0;
11352+
self.funding.short_channel_id = None;
11353+
self.funding.funding_tx_confirmed_in = None;
1135011354
}
1135111355

1135211356
if let Some(channel_ready) = self.check_get_channel_ready(height, logger) {
@@ -11361,18 +11365,30 @@ where
1136111365
self.context.channel_state.is_our_channel_ready() {
1136211366

1136311367
// If we've sent channel_ready (or have both sent and received channel_ready), and
11364-
// the funding transaction has become unconfirmed,
11365-
// close the channel and hope we can get the latest state on chain (because presumably
11366-
// the funding transaction is at least still in the mempool of most nodes).
11368+
// the funding transaction has become unconfirmed, we'll probably get a new SCID when
11369+
// it re-confirms.
1136711370
//
11368-
// Note that ideally we wouldn't force-close if we see *any* reorg on a 1-conf or
11369-
// 0-conf channel, but not doing so may lead to the
11370-
// `ChannelManager::short_to_chan_info` map being inconsistent, so we currently have
11371-
// to.
11372-
if funding_tx_confirmations == 0 && self.funding.funding_tx_confirmed_in.is_some() {
11373-
let err_reason = format!("Funding transaction was un-confirmed. Locked at {} confs, now have {} confs.",
11374-
self.context.minimum_depth.unwrap(), funding_tx_confirmations);
11375-
return Err(ClosureReason::ProcessingError { err: err_reason });
11371+
// Worse, if the funding has un-confirmed we could have accepted some HTLC(s) over it
11372+
// and are now at risk of double-spend. While its possible, even likely, that this is
11373+
// just a trivial reorg and we should wait to see the new block connected in the next
11374+
// call, its also possible we've been double-spent. To avoid further loss of funds, we
11375+
// need some kind of method to freeze the channel and avoid accepting further HTLCs,
11376+
// but absent such a method, we just force-close.
11377+
//
11378+
// The one exception we make is for 0-conf channels, which we decided to trust anyway,
11379+
// in which case we simply track the previous SCID as a `historical_scids` the same as
11380+
// after a channel is spliced.
11381+
if funding_tx_confirmations == 0 && was_confirmed {
11382+
if let Some(scid) = original_scid {
11383+
self.context.historical_scids.push(scid);
11384+
} else {
11385+
debug_assert!(false);
11386+
}
11387+
if self.context.minimum_depth(&self.funding).expect("set for a ready channel") > 1 {
11388+
let err_reason = format!("Funding transaction was un-confirmed. Locked at {} confs, now have {} confs.",
11389+
self.context.minimum_depth.unwrap(), funding_tx_confirmations);
11390+
return Err(ClosureReason::ProcessingError { err: err_reason });
11391+
}
1137611392
}
1137711393
} else if !self.funding.is_outbound() && self.funding.funding_tx_confirmed_in.is_none() &&
1137811394
height >= self.context.channel_creation_height + FUNDING_CONF_DEADLINE_BLOCKS {

lightning/src/ln/functional_test_utils.rs

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3224,12 +3224,13 @@ pub fn expect_probe_successful_events(
32243224
}
32253225

32263226
pub struct PaymentFailedConditions<'a> {
3227-
pub(crate) expected_htlc_error_data: Option<(LocalHTLCFailureReason, &'a [u8])>,
3228-
pub(crate) expected_blamed_scid: Option<u64>,
3229-
pub(crate) expected_blamed_chan_closed: Option<bool>,
3230-
pub(crate) expected_mpp_parts_remain: bool,
3231-
pub(crate) retry_expected: bool,
3232-
pub(crate) from_mon_update: bool,
3227+
pub expected_htlc_error_data: Option<(LocalHTLCFailureReason, &'a [u8])>,
3228+
pub expected_blamed_scid: Option<u64>,
3229+
pub expected_blamed_chan_closed: Option<bool>,
3230+
pub expected_mpp_parts_remain: bool,
3231+
pub retry_expected: bool,
3232+
pub from_mon_update: bool,
3233+
pub reason: Option<PaymentFailureReason>,
32333234
}
32343235

32353236
impl<'a> PaymentFailedConditions<'a> {
@@ -3241,6 +3242,7 @@ impl<'a> PaymentFailedConditions<'a> {
32413242
expected_mpp_parts_remain: false,
32423243
retry_expected: false,
32433244
from_mon_update: false,
3245+
reason: None,
32443246
}
32453247
}
32463248
pub fn mpp_parts_remain(mut self) -> Self {
@@ -3321,14 +3323,21 @@ pub fn expect_payment_failed_conditions_event<'a, 'b, 'c, 'd, 'e>(
33213323
*payment_failed_permanently, expected_payment_failed_permanently,
33223324
"unexpected payment_failed_permanently value"
33233325
);
3324-
{
3325-
assert!(error_code.is_some(), "expected error_code.is_some() = true");
3326-
assert!(error_data.is_some(), "expected error_data.is_some() = true");
3327-
let reason: LocalHTLCFailureReason = error_code.unwrap().into();
3328-
if let Some((code, data)) = conditions.expected_htlc_error_data {
3329-
assert_eq!(reason, code, "unexpected error code");
3330-
assert_eq!(&error_data.as_ref().unwrap()[..], data, "unexpected error data");
3331-
}
3326+
match failure {
3327+
PathFailure::OnPath { .. } => {
3328+
assert!(error_code.is_some(), "expected error_code.is_some() = true");
3329+
assert!(error_data.is_some(), "expected error_data.is_some() = true");
3330+
let reason: LocalHTLCFailureReason = error_code.unwrap().into();
3331+
if let Some((code, data)) = conditions.expected_htlc_error_data {
3332+
assert_eq!(reason, code, "unexpected error code");
3333+
assert_eq!(&error_data.as_ref().unwrap()[..], data);
3334+
}
3335+
},
3336+
PathFailure::InitialSend { .. } => {
3337+
assert!(error_code.is_none());
3338+
assert!(error_data.is_none());
3339+
assert!(conditions.expected_htlc_error_data.is_none());
3340+
},
33323341
}
33333342

33343343
if let Some(chan_closed) = conditions.expected_blamed_chan_closed {
@@ -3362,7 +3371,9 @@ pub fn expect_payment_failed_conditions_event<'a, 'b, 'c, 'd, 'e>(
33623371
assert_eq!(*payment_id, expected_payment_id);
33633372
assert_eq!(
33643373
reason.unwrap(),
3365-
if expected_payment_failed_permanently {
3374+
if let Some(expected_reason) = conditions.reason {
3375+
expected_reason
3376+
} else if expected_payment_failed_permanently {
33663377
PaymentFailureReason::RecipientRejected
33673378
} else {
33683379
PaymentFailureReason::RetriesExhausted
@@ -3414,7 +3425,7 @@ pub fn send_along_route_with_secret<'a, 'b, 'c>(
34143425
payment_id
34153426
}
34163427

3417-
fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) {
3428+
pub fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) {
34183429
let origin_node_id = expected_path[0].node.get_our_node_id();
34193430

34203431
// iterate from the receiving node to the origin node and handle update fail htlc.

0 commit comments

Comments
 (0)