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.
This commit is contained in:
yyforyongyu 2024-05-14 16:55:45 +08:00
parent 2906b8b20c
commit 1470adbed2
No known key found for this signature in database
GPG Key ID: 9BCD95C4FF296868
2 changed files with 120 additions and 37 deletions

View File

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

View File

@ -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],
)
}