Skip to content

Commit fbf0c61

Browse files
authored
Merge pull request #4231 from TheBlueMatt/2025-11-0conf-no-reorg-close
Avoid force-closing 0-conf channels when funding is reorg'd
2 parents 5bf0d1e + 173481f commit fbf0c61

File tree

4 files changed

+261
-63
lines changed

4 files changed

+261
-63
lines changed

lightning/src/ln/channel.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11433,9 +11433,13 @@ where
1143311433
}
1143411434

1143511435
// Check if the funding transaction was unconfirmed
11436+
let original_scid = self.funding.short_channel_id;
11437+
let was_confirmed = self.funding.funding_tx_confirmed_in.is_some();
1143611438
let funding_tx_confirmations = self.funding.get_funding_tx_confirmations(height);
1143711439
if funding_tx_confirmations == 0 {
1143811440
self.funding.funding_tx_confirmation_height = 0;
11441+
self.funding.short_channel_id = None;
11442+
self.funding.funding_tx_confirmed_in = None;
1143911443
}
1144011444

1144111445
if let Some(channel_ready) = self.check_get_channel_ready(height, logger) {
@@ -11450,18 +11454,33 @@ where
1145011454
self.context.channel_state.is_our_channel_ready() {
1145111455

1145211456
// If we've sent channel_ready (or have both sent and received channel_ready), and
11453-
// the funding transaction has become unconfirmed,
11454-
// close the channel and hope we can get the latest state on chain (because presumably
11455-
// the funding transaction is at least still in the mempool of most nodes).
11457+
// the funding transaction has become unconfirmed, we'll probably get a new SCID when
11458+
// it re-confirms.
1145611459
//
11457-
// Note that ideally we wouldn't force-close if we see *any* reorg on a 1-conf or
11458-
// 0-conf channel, but not doing so may lead to the
11459-
// `ChannelManager::short_to_chan_info` map being inconsistent, so we currently have
11460-
// to.
11461-
if funding_tx_confirmations == 0 && self.funding.funding_tx_confirmed_in.is_some() {
11462-
let err_reason = format!("Funding transaction was un-confirmed. Locked at {} confs, now have {} confs.",
11463-
self.context.minimum_depth.unwrap(), funding_tx_confirmations);
11464-
return Err(ClosureReason::ProcessingError { err: err_reason });
11460+
// Worse, if the funding has un-confirmed we could have accepted some HTLC(s) over it
11461+
// and are now at risk of double-spend. While its possible, even likely, that this is
11462+
// just a trivial reorg and we should wait to see the new block connected in the next
11463+
// call, its also possible we've been double-spent. To avoid further loss of funds, we
11464+
// need some kind of method to freeze the channel and avoid accepting further HTLCs,
11465+
// but absent such a method, we just force-close.
11466+
//
11467+
// The one exception we make is for 0-conf channels, which we decided to trust anyway,
11468+
// in which case we simply track the previous SCID as a `historical_scids` the same as
11469+
// after a channel is spliced.
11470+
if funding_tx_confirmations == 0 && was_confirmed {
11471+
if let Some(scid) = original_scid {
11472+
self.context.historical_scids.push(scid);
11473+
} else {
11474+
debug_assert!(false);
11475+
}
11476+
if self.context.minimum_depth(&self.funding).expect("set for a ready channel") > 0 {
11477+
// Reset the original short_channel_id so that we'll generate a closure
11478+
// `channel_update` broadcast event.
11479+
self.funding.short_channel_id = original_scid;
11480+
let err_reason = format!("Funding transaction was un-confirmed, originally locked at {} confs.",
11481+
self.context.minimum_depth.unwrap());
11482+
return Err(ClosureReason::ProcessingError { err: err_reason });
11483+
}
1146511484
}
1146611485
} else if !self.funding.is_outbound() && self.funding.funding_tx_confirmed_in.is_none() &&
1146711486
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)