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 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