From 6f0c2b5bab68c156262c1e8e2286f9a6b36bbbd7 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 19 Mar 2024 11:31:28 +0800 Subject: [PATCH] contractcourt: specify deadline and budget for anchor output --- contractcourt/anchor_resolver.go | 9 +++ contractcourt/channel_arbitrator.go | 77 ++++++++++++--------- contractcourt/channel_arbitrator_test.go | 51 ++++++++------ contractcourt/commit_sweep_resolver_test.go | 5 ++ 4 files changed, 91 insertions(+), 51 deletions(-) diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index 80e665dd7..0c95b9416 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/sweep" ) @@ -118,6 +119,14 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { Fee: sweep.FeeEstimateInfo{ FeeRate: relayFeeRate, }, + // For normal anchor sweeping, the budget is 330 sats. + Budget: btcutil.Amount( + anchorInput.SignDesc().Output.Value, + ), + + // There's no rush to sweep the anchor, so we use a nil + // deadline here. + DeadlineHeight: fn.None[int32](), }, ) if err != nil { diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 2a966629a..8b3e193ff 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -37,12 +37,6 @@ var ( ) const ( - // anchorSweepConfTarget is the conf target used when sweeping - // commitment anchors. This value is only used when the commitment - // transaction has no valid HTLCs for determining a confirmation - // deadline. - anchorSweepConfTarget = 144 - // arbitratorBlockBufferSize is the size of the buffer we give to each // channel arbitrator. arbitratorBlockBufferSize = 20 @@ -1321,36 +1315,23 @@ func (c *ChannelArbitrator) sweepAnchors(anchors *lnwallet.AnchorResolutions, htlcs htlcSet, anchorPath string) error { // Find the deadline for this specific anchor. - deadlineOpt, _, err := c.findCommitmentDeadlineAndValue( + deadline, value, err := c.findCommitmentDeadlineAndValue( heightHint, htlcs, ) if err != nil { return err } - deadline := uint32(1) - deadlineOpt.WhenSome(func(d int32) { - deadline = uint32(d) - }) + // If we cannot find a deadline, it means there's no HTLCs at + // stake, which means we can relax our anchor sweeping as we + // don't have any time sensitive outputs to sweep. + if deadline.IsNone() { + log.Infof("ChannelArbitrator(%v): no HTLCs at stake, "+ + "skipped anchor CPFP", c.cfg.ChanPoint) - // Create a force flag that's used to indicate whether we - // should force sweeping this anchor. - var force bool - - // Check the deadline against the default value. If it's less - // than the default value of 144, it means there is a deadline - // and we will perform a CPFP for this commitment tx. - if deadline < anchorSweepConfTarget { - // Signal that this is a force sweep, so that the - // anchor will be swept even if it isn't economical - // purely based on the anchor value. - force = true + return nil } - log.Debugf("ChannelArbitrator(%v): pre-confirmation sweep of "+ - "anchor of %s commit tx %v, force=%v", c.cfg.ChanPoint, - anchorPath, anchor.CommitAnchor, force) - witnessType := input.CommitmentAnchor // For taproot channels, we need to use the proper witness @@ -1374,6 +1355,28 @@ func (c *ChannelArbitrator) sweepAnchors(anchors *lnwallet.AnchorResolutions, }, ) + // If we have a deadline, we'll use it to calculate the + // deadline height, otherwise default to none. + deadlineDesc := "None" + deadlineHeight := fn.MapOption(func(d int32) int32 { + deadlineDesc = fmt.Sprintf("%d", d) + + return d + int32(heightHint) + })(deadline) + + // Calculate the budget based on the value under protection, + // which is the sum of all HTLCs on this commitment subtracted + // by their budgets. + budget := calculateBudget( + value, c.cfg.Budget.AnchorCPFPRatio, + c.cfg.Budget.AnchorCPFP, + ) + + log.Infof("ChannelArbitrator(%v): offering anchor from %s "+ + "commitment %v to sweeper with deadline=%v, budget=%v", + c.cfg.ChanPoint, anchorPath, anchor.CommitAnchor, + deadlineDesc, budget) + // Sweep anchor output with a confirmation target fee // preference. Because this is a cpfp-operation, the anchor // will only be attempted to sweep when the current fee @@ -1382,11 +1385,9 @@ func (c *ChannelArbitrator) sweepAnchors(anchors *lnwallet.AnchorResolutions, _, err = c.cfg.Sweeper.SweepInput( &anchorInput, sweep.Params{ - Fee: sweep.FeeEstimateInfo{ - ConfTarget: deadline, - }, - Force: force, ExclusiveGroup: &exclusiveGroup, + Budget: budget, + DeadlineHeight: deadlineHeight, }, ) if err != nil { @@ -2138,6 +2139,20 @@ func (c *ChannelArbitrator) checkRemoteDiffActions(height uint32, continue } + preimageAvailable, err := c.isPreimageAvailable(htlc.RHash) + if err != nil { + log.Errorf("ChannelArbitrator(%v): failed to query "+ + "preimage for dangling htlc=%x from remote "+ + "commitments diff", c.cfg.ChanPoint, + htlc.RHash[:]) + + continue + } + + if preimageAvailable { + continue + } + actionMap[HtlcFailNowAction] = append( actionMap[HtlcFailNowAction], htlc, ) diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index 96593a9d6..7bdc75652 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -391,7 +391,9 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog, return nil }, - Budget: *DefaultBudgetConfig(), + Budget: *DefaultBudgetConfig(), + PreimageDB: newMockWitnessBeacon(), + Registry: &mockRegistry{}, } // We'll use the resolvedChan to synchronize on call to @@ -2405,6 +2407,8 @@ func TestSweepAnchors(t *testing.T) { htlcIndexBase := uint64(99) htlcExpiryBase := heightHint + uint32(10) + htlcAmt := lnwire.MilliSatoshi(1_000_000) + // Create three testing HTLCs. htlcDust := channeldb.HTLC{ HtlcIndex: htlcIndexBase + 1, @@ -2415,15 +2419,17 @@ func TestSweepAnchors(t *testing.T) { HtlcIndex: htlcIndexBase + 2, RefundTimeout: htlcExpiryBase + 2, RHash: rHash, + Amt: htlcAmt, } htlcSmallExipry := channeldb.HTLC{ HtlcIndex: htlcIndexBase + 3, RefundTimeout: htlcExpiryBase + 3, + Amt: htlcAmt, } // Setup our local HTLC set such that we will use the HTLC's CLTV from // the incoming HTLC set. - expectedLocalDeadline := htlcWithPreimage.RefundTimeout - heightHint + expectedLocalDeadline := htlcWithPreimage.RefundTimeout chanArb.activeHTLCs[LocalHtlcSet] = htlcSet{ incomingHTLCs: map[uint64]channeldb.HTLC{ htlcWithPreimage.HtlcIndex: htlcWithPreimage, @@ -2442,8 +2448,7 @@ func TestSweepAnchors(t *testing.T) { } // Setup our remote HTLC set such that no valid HTLCs can be used, thus - // we default to anchorSweepConfTarget. - expectedRemoteDeadline := anchorSweepConfTarget + // the anchor sweeping is skipped. chanArb.activeHTLCs[RemoteHtlcSet] = htlcSet{ incomingHTLCs: map[uint64]channeldb.HTLC{ htlcSmallExipry.HtlcIndex: htlcSmallExipry, @@ -2463,7 +2468,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 - heightHint + expectedPendingDeadline := htlcSmallExipry.RefundTimeout chanArb.activeHTLCs[RemotePendingHtlcSet] = htlcSet{ incomingHTLCs: map[uint64]channeldb.HTLC{ htlcDust.HtlcIndex: htlcDust, @@ -2506,20 +2511,22 @@ func TestSweepAnchors(t *testing.T) { // Verify deadlines are used as expected. deadlines := chanArbCtx.sweeper.deadlines + + // We should see two `SweepInput` calls. + require.Len(t, deadlines, 2) + // Since there's no guarantee of the deadline orders, we sort it here // so they can be compared. - sort.Ints(deadlines) // [12, 13, 144] + sort.Ints(deadlines) // [12, 13] require.EqualValues( t, expectedLocalDeadline, deadlines[0], - "local deadline not matched", + "local deadline not matched, want %v, got %v", + expectedLocalDeadline, deadlines[0], ) require.EqualValues( t, expectedPendingDeadline, deadlines[1], - "pending remote deadline not matched", - ) - require.EqualValues( - t, expectedRemoteDeadline, deadlines[2], - "remote deadline not matched", + "pending remote deadline not matched, want %v, got %v", + expectedPendingDeadline, deadlines[1], ) } @@ -2582,6 +2589,8 @@ func TestChannelArbitratorAnchors(t *testing.T) { heightHint := uint32(1000) chanArbCtx.chanArb.blocks <- int32(heightHint) + htlcAmt := lnwire.MilliSatoshi(1_000_000) + // Create testing HTLCs. htlcExpiryBase := heightHint + uint32(10) htlcWithPreimage := channeldb.HTLC{ @@ -2589,10 +2598,12 @@ func TestChannelArbitratorAnchors(t *testing.T) { RefundTimeout: htlcExpiryBase + 2, RHash: rHash, Incoming: true, + Amt: htlcAmt, } htlc := channeldb.HTLC{ HtlcIndex: 100, RefundTimeout: htlcExpiryBase + 3, + Amt: htlcAmt, } // We now send two HTLC updates, one for local HTLC set and the other @@ -2600,9 +2611,9 @@ func TestChannelArbitratorAnchors(t *testing.T) { newUpdate := &ContractUpdate{ HtlcKey: LocalHtlcSet, // This will make the deadline of the local anchor resolution - // to be htlcWithPreimage's CLTV minus heightHint since the - // incoming HTLC (toLocalHTLCs) has a lower CLTV value and is - // preimage available. + // to be htlcWithPreimage's CLTV since the incoming HTLC + // (toLocalHTLCs) has a lower CLTV value and is preimage + // available. Htlcs: []channeldb.HTLC{htlc, htlcWithPreimage}, } chanArb.notifyContractUpdate(newUpdate) @@ -2610,8 +2621,8 @@ func TestChannelArbitratorAnchors(t *testing.T) { newUpdate = &ContractUpdate{ HtlcKey: RemoteHtlcSet, // This will make the deadline of the remote anchor resolution - // to be htlcWithPreimage's CLTV minus heightHint because the - // incoming HTLC (toRemoteHTLCs) has a lower CLTV. + // to be htlcWithPreimage's CLTV because the incoming HTLC + // (toRemoteHTLCs) has a lower CLTV. Htlcs: []channeldb.HTLC{htlc, htlcWithPreimage}, } chanArb.notifyContractUpdate(newUpdate) @@ -2734,14 +2745,14 @@ func TestChannelArbitratorAnchors(t *testing.T) { // We expect two anchor inputs, the local and the remote to be swept. // Thus we should expect there are two deadlines used, both are equal - // to htlcWithPreimage's CLTV minus current block height. + // to htlcWithPreimage's CLTV. require.Equal(t, 2, len(chanArbCtx.sweeper.deadlines)) require.EqualValues(t, - htlcWithPreimage.RefundTimeout-heightHint, + htlcWithPreimage.RefundTimeout, chanArbCtx.sweeper.deadlines[0], ) require.EqualValues(t, - htlcWithPreimage.RefundTimeout-heightHint, + htlcWithPreimage.RefundTimeout, chanArbCtx.sweeper.deadlines[1], ) } diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index 6730cddc8..9c6876269 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -143,6 +143,11 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) ( } } + // Update the deadlines used if it's set. + params.DeadlineHeight.WhenSome(func(d int32) { + s.deadlines = append(s.deadlines, int(d)) + }) + result := make(chan sweep.Result, 1) result <- sweep.Result{ Tx: s.sweepTx,