From 1470adbed241f5c24cb367da79a841f1c9eb79a3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 14 May 2024 16:55:45 +0800 Subject: [PATCH] contractcourt: share deadlines between CPFP anchors and HTLCs This commit changes how the deadline is calculated for CPFP anchor sweeping. In order to sweep the second-level HTLCs, we need to first get the FC tx confirmed. If we use a larger conf target for CPFP, we'd end up having few blocks to sweep the HTLCs, as these two sweeping txns share the deadline of the HTLC, as shown below, ``` More aggressive on the CPFP part. |-CPFP-|-----HTLC-----| Share the deadlines evenly. |---CPFP---|---HTLC---| More aggressive on the HTLC part. |-----CPFP-----|-HTLC-| ``` In this commit, we decide to share the deadlines evenly as a starting point so neither side will have a short of deadlines. --- contractcourt/channel_arbitrator.go | 39 ++++++-- contractcourt/channel_arbitrator_test.go | 118 +++++++++++++++++------ 2 files changed, 120 insertions(+), 37 deletions(-) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 64d062e99..f9df71c19 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -1436,9 +1436,13 @@ func (c *ChannelArbitrator) sweepAnchors(anchors *lnwallet.AnchorResolutions, // findCommitmentDeadlineAndValue finds the deadline (relative block height) // for a commitment transaction by extracting the minimum CLTV from its HTLCs. -// From our PoV, the deadline is defined to be the smaller of, -// - the least CLTV from outgoing HTLCs, or, -// - the least CLTV from incoming HTLCs if the preimage is available. +// From our PoV, the deadline delta is defined to be the smaller of, +// - half of the least CLTV from outgoing HTLCs' corresponding incoming +// HTLCs, or, +// - half of the least CLTV from incoming HTLCs if the preimage is available. +// +// We use half of the CTLV value to ensure that we have enough time to sweep +// the second-level HTLCs. // // It also finds the total value that are time-sensitive, which is the sum of // all the outgoing HTLCs plus incoming HTLCs whose preimages are known. It @@ -1468,10 +1472,24 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, } value := htlc.Amt.ToSatoshis() - totalValue += value - if htlc.RefundTimeout < deadlineMinHeight { - deadlineMinHeight = htlc.RefundTimeout + // Find the expiry height for this outgoing HTLC's incoming + // HTLC. + deadlineOpt := c.cfg.FindOutgoingHTLCDeadline(htlc) + + // The deadline is default to the current deadlineMinHeight, + // and it's overwritten when it's not none. + deadline := deadlineMinHeight + deadlineOpt.WhenSome(func(d int32) { + deadline = uint32(d) + + // We only consider the value is under protection when + // it's time-sensitive. + totalValue += value + }) + + if deadline < deadlineMinHeight { + deadlineMinHeight = deadline log.Tracef("ChannelArbitrator(%v): outgoing HTLC has "+ "deadline=%v, value=%v", c.cfg.ChanPoint, @@ -1521,7 +1539,7 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, // * none of the HTLCs are preimageAvailable. // - when our deadlineMinHeight is no greater than the heightHint, // which means we are behind our schedule. - deadline := deadlineMinHeight - heightHint + var deadline uint32 switch { // When we couldn't find a deadline height from our HTLCs, we will fall // back to the default value as there's no time pressure here. @@ -1535,6 +1553,11 @@ func (c *ChannelArbitrator) findCommitmentDeadlineAndValue(heightHint uint32, "deadlineMinHeight=%d, heightHint=%d", c.cfg.ChanPoint, deadlineMinHeight, heightHint) deadline = 1 + + // Use half of the deadline delta, and leave the other half to be used + // to sweep the HTLCs. + default: + deadline = (deadlineMinHeight - heightHint) / 2 } // Calculate the value left after subtracting the budget used for @@ -2800,7 +2823,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) { // state, so we'll get the most up to date signals to we can // properly do our job. case signalUpdate := <-c.signalUpdates: - log.Tracef("ChannelArbitrator(%v) got new signal "+ + log.Tracef("ChannelArbitrator(%v): got new signal "+ "update!", c.cfg.ChanPoint) // We'll update the ShortChannelID. diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 77c9597c0..43238494e 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -2305,20 +2305,22 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) { } testCases := []struct { - name string - htlcs htlcSet - err error - deadline fn.Option[int32] - expectedBudget btcutil.Amount + name string + htlcs htlcSet + err error + deadline fn.Option[int32] + mockFindOutgoingHTLCDeadline func() + expectedBudget btcutil.Amount }{ { // When we have no HTLCs, the default value should be // used. - name: "use default conf target", - htlcs: htlcSet{}, - err: nil, - deadline: fn.None[int32](), - expectedBudget: 0, + name: "use default conf target", + htlcs: htlcSet{}, + err: nil, + deadline: fn.None[int32](), + mockFindOutgoingHTLCDeadline: func() {}, + expectedBudget: 0, }, { // When we have a preimage available in the local HTLC @@ -2329,8 +2331,17 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) { htlcs: makeHTLCSet(htlcPreimage, htlcLargeExpiry), err: nil, deadline: fn.Some(int32( - htlcPreimage.RefundTimeout - heightHint, + (htlcPreimage.RefundTimeout - heightHint) / 2, )), + mockFindOutgoingHTLCDeadline: func() { + chanArb.cfg.FindOutgoingHTLCDeadline = func( + htlc channeldb.HTLC) fn.Option[int32] { + + return fn.Some(int32( + htlcLargeExpiry.RefundTimeout, + )) + } + }, expectedBudget: htlcAmt.ToSatoshis(), }, { @@ -2342,8 +2353,18 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) { htlcs: makeHTLCSet(htlcSmallExipry, htlcLargeExpiry), err: nil, deadline: fn.Some(int32( - htlcLargeExpiry.RefundTimeout - heightHint, + (htlcLargeExpiry.RefundTimeout - + heightHint) / 2, )), + mockFindOutgoingHTLCDeadline: func() { + chanArb.cfg.FindOutgoingHTLCDeadline = func( + htlc channeldb.HTLC) fn.Option[int32] { + + return fn.Some(int32( + htlcLargeExpiry.RefundTimeout, + )) + } + }, expectedBudget: htlcAmt.ToSatoshis() / 2, }, { @@ -2354,18 +2375,36 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) { htlcs: makeHTLCSet(htlcPreimage, htlcDust), err: nil, deadline: fn.Some(int32( - htlcPreimage.RefundTimeout - heightHint, + (htlcPreimage.RefundTimeout - heightHint) / 2, )), + mockFindOutgoingHTLCDeadline: func() { + chanArb.cfg.FindOutgoingHTLCDeadline = func( + htlc channeldb.HTLC) fn.Option[int32] { + + return fn.Some(int32( + htlcDust.RefundTimeout, + )) + } + }, expectedBudget: htlcAmt.ToSatoshis() / 2, }, { // When we've reached our deadline, use conf target of // 1 as our deadline. And the value left should be // htlcAmt. - name: "use conf target 1", - htlcs: makeHTLCSet(htlcPreimage, htlcExpired), - err: nil, - deadline: fn.Some(int32(1)), + name: "use conf target 1", + htlcs: makeHTLCSet(htlcPreimage, htlcExpired), + err: nil, + deadline: fn.Some(int32(1)), + mockFindOutgoingHTLCDeadline: func() { + chanArb.cfg.FindOutgoingHTLCDeadline = func( + htlc channeldb.HTLC) fn.Option[int32] { + + return fn.Some(int32( + htlcExpired.RefundTimeout, + )) + } + }, expectedBudget: htlcAmt.ToSatoshis(), }, } @@ -2373,7 +2412,9 @@ func TestFindCommitmentDeadlineAndValue(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() + // Mock the method `FindOutgoingHTLCDeadline`. + tc.mockFindOutgoingHTLCDeadline() + deadline, budget, err := chanArb. findCommitmentDeadlineAndValue( heightHint, tc.htlcs, @@ -2412,31 +2453,35 @@ func TestSweepAnchors(t *testing.T) { chanArbCtx.chanArb.blocks <- int32(heightHint) htlcIndexBase := uint64(99) - htlcExpiryBase := heightHint + uint32(10) + deadlineDelta := uint32(10) htlcAmt := lnwire.MilliSatoshi(1_000_000) // Create three testing HTLCs. htlcDust := channeldb.HTLC{ HtlcIndex: htlcIndexBase + 1, - RefundTimeout: htlcExpiryBase + 1, + RefundTimeout: heightHint + 1, OutputIndex: -1, } + + deadlinePreimageDelta := deadlineDelta + 2 htlcWithPreimage := channeldb.HTLC{ HtlcIndex: htlcIndexBase + 2, - RefundTimeout: htlcExpiryBase + 2, + RefundTimeout: heightHint + deadlinePreimageDelta, RHash: rHash, Amt: htlcAmt, } + + deadlineSmallDelta := deadlineDelta + 4 htlcSmallExipry := channeldb.HTLC{ HtlcIndex: htlcIndexBase + 3, - RefundTimeout: htlcExpiryBase + 3, + RefundTimeout: heightHint + deadlineSmallDelta, Amt: htlcAmt, } // Setup our local HTLC set such that we will use the HTLC's CLTV from // the incoming HTLC set. - expectedLocalDeadline := htlcWithPreimage.RefundTimeout + expectedLocalDeadline := heightHint + deadlinePreimageDelta/2 chanArb.activeHTLCs[LocalHtlcSet] = htlcSet{ incomingHTLCs: map[uint64]channeldb.HTLC{ htlcWithPreimage.HtlcIndex: htlcWithPreimage, @@ -2475,7 +2520,7 @@ func TestSweepAnchors(t *testing.T) { // Setup out pending remote HTLC set such that we will use the HTLC's // CLTV from the outgoing HTLC set. - expectedPendingDeadline := htlcSmallExipry.RefundTimeout + expectedPendingDeadline := heightHint + deadlineSmallDelta/2 chanArb.activeHTLCs[RemotePendingHtlcSet] = htlcSet{ incomingHTLCs: map[uint64]channeldb.HTLC{ htlcDust.HtlcIndex: htlcDust, @@ -2493,6 +2538,18 @@ func TestSweepAnchors(t *testing.T) { }, } + // Mock FindOutgoingHTLCDeadline so the pending remote's outgoing HTLC + // returns the small expiry value. + chanArb.cfg.FindOutgoingHTLCDeadline = func( + htlc channeldb.HTLC) fn.Option[int32] { + + if htlc.RHash != htlcSmallExipry.RHash { + return fn.None[int32]() + } + + return fn.Some(int32(htlcSmallExipry.RefundTimeout)) + } + // Create AnchorResolutions. anchors := &lnwallet.AnchorResolutions{ Local: &lnwallet.AnchorResolution{ @@ -2599,17 +2656,20 @@ func TestChannelArbitratorAnchors(t *testing.T) { htlcAmt := lnwire.MilliSatoshi(1_000_000) // Create testing HTLCs. - htlcExpiryBase := heightHint + uint32(10) + deadlineDelta := uint32(10) + deadlinePreimageDelta := deadlineDelta + 2 htlcWithPreimage := channeldb.HTLC{ HtlcIndex: 99, - RefundTimeout: htlcExpiryBase + 2, + RefundTimeout: heightHint + deadlinePreimageDelta, RHash: rHash, Incoming: true, Amt: htlcAmt, } + + deadlineHTLCdelta := deadlineDelta + 3 htlc := channeldb.HTLC{ HtlcIndex: 100, - RefundTimeout: htlcExpiryBase + 3, + RefundTimeout: heightHint + deadlineHTLCdelta, Amt: htlcAmt, } @@ -2755,11 +2815,11 @@ func TestChannelArbitratorAnchors(t *testing.T) { // to htlcWithPreimage's CLTV. require.Equal(t, 2, len(chanArbCtx.sweeper.deadlines)) require.EqualValues(t, - htlcWithPreimage.RefundTimeout, + heightHint+deadlinePreimageDelta/2, chanArbCtx.sweeper.deadlines[0], ) require.EqualValues(t, - htlcWithPreimage.RefundTimeout, + heightHint+deadlinePreimageDelta/2, chanArbCtx.sweeper.deadlines[1], ) }