From 5ea52afa687d9fa5f8a0b81848f0768e27ff84d3 Mon Sep 17 00:00:00 2001 From: ziggie Date: Wed, 5 Aug 2026 23:20:23 -0300 Subject: [PATCH 1/2] contractcourt: retain deadline across contest resolution Forward the supplied incoming HTLC expiry from the outgoing contest resolver to its embedded timeout resolver. This keeps the deadline available when resolution transitions after the outgoing HTLC expires. (cherry picked from commit f77606851f34cd1bbf0c8f1e8cbc2e95601732da) --- .../htlc_outgoing_contest_resolver.go | 8 +++-- .../htlc_outgoing_contest_resolver_test.go | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 9e94587cc..973051ae6 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -229,10 +229,14 @@ func (h *htlcOutgoingContestResolver) Encode(w io.Writer) error { return h.htlcTimeoutResolver.Encode(w) } -// SupplementDeadline does nothing for an incoming htlc resolver. +// SupplementDeadline forwards the incoming HTLC's expiry height to the inner +// timeout resolver. This resolver morphs into that timeout resolver once the +// outgoing HTLC expires on-chain, so the deadline is retained across the +// transition. // // NOTE: Part of the htlcContractResolver interface. -func (h *htlcOutgoingContestResolver) SupplementDeadline(_ fn.Option[int32]) { +func (h *htlcOutgoingContestResolver) SupplementDeadline(d fn.Option[int32]) { + h.htlcTimeoutResolver.SupplementDeadline(d) } // newOutgoingContestResolverFromReader attempts to decode an encoded ContractResolver diff --git a/contractcourt/htlc_outgoing_contest_resolver_test.go b/contractcourt/htlc_outgoing_contest_resolver_test.go index 625df60bf..fedc84801 100644 --- a/contractcourt/htlc_outgoing_contest_resolver_test.go +++ b/contractcourt/htlc_outgoing_contest_resolver_test.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/kvdb" @@ -20,6 +21,10 @@ import ( const ( outgoingContestHtlcExpiry = 110 + + // outgoingContestIncomingHtlcExpiry is kept distinct from the outgoing + // HTLC expiry to verify that the supplied value is retained. + outgoingContestIncomingHtlcExpiry = 144 ) // TestHtlcOutgoingResolverTimeout tests resolution of an offered htlc that @@ -116,6 +121,36 @@ type resolveResult struct { nextResolver ContractResolver } +// TestHtlcOutgoingResolverSupplementDeadline checks that the outgoing contest +// resolver forwards the incoming HTLC deadline to the timeout resolver it +// transitions into once the outgoing HTLC expires on-chain. +func TestHtlcOutgoingResolverSupplementDeadline(t *testing.T) { + t.Parallel() + defer timeout()() + + ctx := newOutgoingResolverTestContext(t) + + // Initially the embedded timeout resolver carries no deadline. + require.True(t, ctx.resolver.incomingHTLCExpiryHeight.IsNone()) + + // Supply the deadline through the contest resolver, as the channel + // arbitrator does when constructing the resolver. + deadline := fn.Some(int32(outgoingContestIncomingHtlcExpiry)) + ctx.resolver.SupplementDeadline(deadline) + + // Drive the contest resolver to the point where it returns the embedded + // timeout resolver. + ctx.resolve() + ctx.notifyEpoch(outgoingContestHtlcExpiry) + + result := <-ctx.resolverResultChan + require.NoError(t, result.err) + + timeoutRes, ok := result.nextResolver.(*htlcTimeoutResolver) + require.True(t, ok, "expected htlcTimeoutResolver") + require.Equal(t, deadline, timeoutRes.incomingHTLCExpiryHeight) +} + type outgoingResolverTestContext struct { resolver *htlcOutgoingContestResolver notifier *mock.ChainNotifier From f93ca9548195b6f6902d990649ebafb7b9b19328 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 6 Aug 2026 08:48:41 -0300 Subject: [PATCH 2/2] docs: update release notes --- docs/release-notes/release-notes-0.20.2.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/release-notes-0.20.2.md b/docs/release-notes/release-notes-0.20.2.md index 364fd1f88..2c39d95ca 100644 --- a/docs/release-notes/release-notes-0.20.2.md +++ b/docs/release-notes/release-notes-0.20.2.md @@ -51,6 +51,11 @@ and legacy payment paths, including keysend records and preimage-dependent settlement outcomes. +* Outgoing contest resolvers now [retain the corresponding incoming HTLC + expiry](https://github.com/lightningnetwork/lnd/pull/11032) when transitioning + to timeout resolution, allowing the sweeper to continue using an + expiry-aware confirmation target. + # New Features ## Functional Enhancements