Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion contractcourt/anchor_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ func (c *anchorResolver) Launch() error {
// an output that we want to sweep only if it is economical to do so.
//
// An exclusive group is not necessary anymore, because we know that
// this is the only anchor that can be swept.
// this is the only anchor that can be swept. However, to avoid this
// anchor input being group with other inputs, we still keep the
// exclusive group here such that the anchor will be swept
// independently.
//
// We also clear the parent tx information for cpfp, because the
// commitment tx is confirmed.
Expand All @@ -222,6 +225,8 @@ func (c *anchorResolver) Launch() error {
c.broadcastHeight, nil,
)

exclusiveGroup := c.ShortChanID.ToUint64()

resultChan, err := c.Sweeper.SweepInput(
&anchorInput,
sweep.Params{
Expand All @@ -233,6 +238,10 @@ func (c *anchorResolver) Launch() error {
// There's no rush to sweep the anchor, so we use a nil
// deadline here.
DeadlineHeight: fn.None[int32](),

// Use the chan id as the exclusive group. This prevents
// any of the anchors from being batched together.
ExclusiveGroup: &exclusiveGroup,
},
)

Expand Down
11 changes: 10 additions & 1 deletion docs/release-notes/release-notes-0.19.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@

## Functional Enhancements

- Previously, when sweeping non-time sensitive anchor outputs, they might be
grouped with other non-time sensitive outputs such as `to_local` outputs,
which potentially allow the sweeping tx to be pinned. This is now
[fixed](https://github.com/lightningnetwork/lnd/pull/10117) by moving sweeping
anchors into its own tx, which means the anchor outputs won't be swept in a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be cool to actually group at least anchors together, seems like sub 1 sat/vbyte transactions can now be propagated quite reliably

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotta say I'm very confused, as on one hand you were saying you really wanna the anchor sweeping to be removed but now you think it's cool to group them?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not know about the sub 1 sat/vbyte rule going to be removed by the core-devs, moreover you explained to me that anchors don't block a wallet input so given this new knowledge I would say there is nothing wrong with grouping them and therefore being able to recover valuable sathosis ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was gonna create a PR to remove the anchor sweeping - based on your current view, it seems unnecessary to create that PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious what your opinion is, but I would say either we remove the anchors or we make sure we can group different anchors from different channel force closes and make also sure we can broadcast them sub 1 sat/vbyte ? Current version as mentioned before never sweeps the anchors anyways so I think we need an improvement anyways here going forward ?

high fee environment.

## RPC Additions

## lncli Additions
Expand Down Expand Up @@ -62,4 +69,6 @@
## Tooling and Documentation

# Contributors (Alphabetical Order)
* Olaoluwa Osuntokun

* Olaoluwa Osuntokun
* Yong Yu
20 changes: 7 additions & 13 deletions itest/lnd_channel_force_close_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,15 +978,6 @@ func runChannelForceClosureTestRestart(ht *lntest.HarnessTest,
Outpoint: commitSweep.Outpoint,
AmountSat: uint64(aliceBalance),
}
op = fmt.Sprintf("%v:%v", anchorSweep.Outpoint.TxidStr,
anchorSweep.Outpoint.OutputIndex)
aliceReports[op] = &lnrpc.Resolution{
ResolutionType: lnrpc.ResolutionType_ANCHOR,
Outcome: lnrpc.ResolutionOutcome_CLAIMED,
SweepTxid: sweepTxid.String(),
Outpoint: anchorSweep.Outpoint,
AmountSat: uint64(anchorSweep.AmountSat),
}

// Check that we can find the commitment sweep in our set of known
// sweeps, using the simple transaction id ListSweeps output.
Expand Down Expand Up @@ -1101,8 +1092,9 @@ func runChannelForceClosureTestRestart(ht *lntest.HarnessTest,

// Since Alice had numInvoices (6) htlcs extended to Carol before force
// closing, we expect Alice to broadcast an htlc timeout txn for each
// one.
ht.AssertNumPendingSweeps(alice, numInvoices)
// one. In addition, the anchor input is still pending due to it's
// uneconomical to sweep.
ht.AssertNumPendingSweeps(alice, numInvoices+1)

// Wait for them all to show up in the mempool
htlcTxid := ht.AssertNumTxsInMempool(1)[0]
Expand Down Expand Up @@ -1198,7 +1190,9 @@ func runChannelForceClosureTestRestart(ht *lntest.HarnessTest,
numBlocks := int(htlcCsvMaturityHeight - uint32(curHeight) - 1)
ht.MineEmptyBlocks(numBlocks)

ht.AssertNumPendingSweeps(alice, numInvoices)
// We should see numInvoices HTLC sweeps plus the uneconomical anchor
// sweep.
ht.AssertNumPendingSweeps(alice, numInvoices+1)

// Fetch the htlc sweep transaction from the mempool.
htlcSweepTx := ht.GetNumTxsFromMempool(1)[0]
Expand All @@ -1220,7 +1214,7 @@ func runChannelForceClosureTestRestart(ht *lntest.HarnessTest,
}, defaultTimeout)
require.NoError(ht, err, "timeout while checking force closed channel")

ht.AssertNumPendingSweeps(alice, numInvoices)
ht.AssertNumPendingSweeps(alice, numInvoices+1)

// Ensure the htlc sweep transaction only has one input for each htlc
// Alice extended before force closing.
Expand Down
43 changes: 29 additions & 14 deletions itest/lnd_sweep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1224,8 +1224,8 @@ func testSweepHTLCs(ht *lntest.HarnessTest) {
// 4. Alice force closes the channel.
//
// Test:
// 1. Alice's anchor sweeping is not attempted, instead, it should be swept
// together with her to_local output using the no deadline path.
// 1. Alice's CPFP-anchor sweeping is not attempted, instead, it should be
// swept using the no deadline path and failed due it's not economical.
Comment on lines +1227 to +1228
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should update the test to assert the behavior that the anchor is not spent. I see failures because the fee rate delta is zero, which is probably equivalent of it not being economical to spend the anchor, or is this very specific to the test? Would it be possible to also have the case where the anchor is swept as a non-CPFP?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated a bit.

I see failures because the fee rate delta is zero, which is probably equivalent of it not being economical to spend the anchor, or is this very specific to the test?

Correct, and there's another log, which also stops the anchor sweeping,

2025-08-04 16:39:41.442 [INF] SWPR fee_bumper.go:1768: Change amt 0.00000206 BTC below dustlimit 0.00000330 BTC, not adding change output

We have two places to stop the sweeping,

  • when the input has a budget that cannot create a non-zero delta fee func, we would stop, this is related to the starting fee rate, or in other words, the deadline delta.
  • when the fee func is created, but it can only have a dust output, we will also stop.

Would it be possible to also have the case where the anchor is swept as a non-CPFP?

This is not possible given we hardcoded the dust limit here,

func DustLimitForSize(scriptSize int) btcutil.Amount {

So it's always 330 sats, which is why the anchors won't be swept, until we make GetDustThreshold to also account the live min relay fee from the mempool (which we should btw).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok TIL that the 330 sat are computed with a static min relay fee of 3 sat/vb. So basically we'll never sweep if that is not made dynamic, because we'd always leave less than 330 sat?

// 2. Bob would also sweep his anchor and to_local outputs separately due to
// they have different deadline heights, which means only the to_local
// sweeping tx will succeed as the anchor sweeping is not economical.
Expand All @@ -1241,10 +1241,15 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// config.
deadline := uint32(1000)

// deadlineA is the deadline used for Alice, since her commit output is
// offered to the sweeper at CSV-1. With a deadline of 1000, her actual
// width of her fee func is CSV+1000-1. Given we are using a CSV of 2
// here, her fee func deadline then becomes 1001.
// deadlineA is the deadline used for Alice, given that,
// - the force close tx is broadcast at height 445, her inputs are
// registered at the same height, so her to_local and anchor outputs
// have a deadline height of 1445.
// - the force close tx is mined at 446, which means her anchor output
// now has a deadline delta of (1445-446) = 999 blocks.
// - for her to_local output, with a deadline of 1000, the width of the
// fee func is CSV+1000-1. Given we are using a CSV of 2 here, her fee
// func deadline then becomes 1001.
deadlineA := deadline + 1

// deadlineB is the deadline used for Bob, the actual deadline used by
Expand All @@ -1267,6 +1272,11 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// conf target is the deadline.
ht.SetFeeEstimateWithConf(startFeeRate, deadlineB)

// Set up the starting fee for Alice's anchor sweeping. With this low
// fee rate, her anchor sweeping should be attempted and failed due to
// dust output generated in the sweeping tx.
ht.SetFeeEstimateWithConf(startFeeRate, deadline-1)

// toLocalCSV is the CSV delay for Alice's to_local output. We use a
// small value to save us from mining blocks.
//
Expand Down Expand Up @@ -1427,10 +1437,10 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// With Alice's starting fee rate being validated, we now calculate her
// ending fee rate and fee rate delta.
//
// Alice sweeps two inputs - anchor and commit, so the starting budget
// should come from the sum of these two. However, due to the value
// being too large, the actual ending fee rate used should be the
// sweeper's max fee rate configured.
// Alice sweeps the to_local input, so the starting budget should come
// from the to_local balance. However, due to the value being too large,
// the actual ending fee rate used should be the sweeper's max fee rate
// configured.
aliceTxWeight := uint64(ht.CalculateTxWeight(aliceSweepTx))
aliceEndingFeeRate := sweep.DefaultMaxFeeRate.FeePerKWeight()
aliceFeeRateDelta := (aliceEndingFeeRate - aliceStartingFeeRate) /
Expand Down Expand Up @@ -1507,10 +1517,10 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
}

// We should see two txns in the mempool:
// - Alice's sweeping tx, which sweeps both her anchor and
// commit outputs, using the increased fee rate.
// - Bob's previous sweeping tx, which sweeps both his anchor
// and commit outputs, at the possible increased fee rate.
// - Alice's sweeping tx, which sweeps her commit output, using
// the increased fee rate.
// - Bob's previous sweeping tx, which sweeps his commit output,
// at the possible increased fee rate.
txns := ht.GetNumTxsFromMempool(2)

// Assume the first tx is Alice's sweeping tx, if the second tx
Expand Down Expand Up @@ -1577,6 +1587,11 @@ func testSweepCommitOutputAndAnchor(ht *lntest.HarnessTest) {
// Mine a block to confirm both sweeping txns, this is needed to clean
// up the mempool.
ht.MineBlocksAndAssertNumTxes(1, 2)

// Finally, assert that both Alice and Bob still have the anchor
// outputs, which cannot be swept due to it being uneconomical.
ht.AssertNumPendingSweeps(alice, 1)
ht.AssertNumPendingSweeps(bob, 1)
}

// testBumpForceCloseFee tests that when a force close transaction, in
Expand Down
12 changes: 6 additions & 6 deletions sweep/fee_bumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ const (
// error, which means they cannot be retried with increased budget.
TxFatal

// sentinalEvent is used to check if an event is unknown.
sentinalEvent
// sentinelEvent is used to check if an event is unknown.
sentinelEvent
)

// String returns a human-readable string for the event.
Expand All @@ -137,13 +137,13 @@ func (e BumpEvent) String() string {

// Unknown returns true if the event is unknown.
func (e BumpEvent) Unknown() bool {
return e >= sentinalEvent
return e >= sentinelEvent
}

// BumpRequest is used by the caller to give the Bumper the necessary info to
// create and manage potential fee bumps for a set of inputs.
type BumpRequest struct {
// Budget givens the total amount that can be used as fees by these
// Budget gives the total amount that can be used as fees by these
// inputs.
Budget btcutil.Amount

Expand Down Expand Up @@ -589,7 +589,7 @@ func (t *TxPublisher) createRBFCompliantTx(
// used up the budget, we will return an error
// indicating this tx cannot be made. The
// sweeper should handle this error and try to
// cluster these inputs differetly.
// cluster these inputs differently.
increased, err = f.Increment()
if err != nil {
return nil, err
Expand Down Expand Up @@ -1332,7 +1332,7 @@ func (t *TxPublisher) createAndPublishTx(
// the fee bumper retry it at next block.
//
// NOTE: we may get this error if we've bypassed the mempool check,
// which means we are suing neutrino backend.
// which means we are using neutrino backend.
if errors.Is(result.Err, chain.ErrInsufficientFee) ||
errors.Is(result.Err, lnwallet.ErrMempoolFee) {

Expand Down
28 changes: 14 additions & 14 deletions sweep/fee_bumper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestBumpResultValidate(t *testing.T) {
// Unknown event type will give an error.
b = BumpResult{
Tx: &wire.MsgTx{},
Event: sentinalEvent,
Event: sentinelEvent,
}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)

Expand Down Expand Up @@ -457,7 +457,7 @@ func TestCreateAndCheckTx(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down Expand Up @@ -550,7 +550,7 @@ func TestCreateRBFCompliantTx(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down Expand Up @@ -1120,9 +1120,9 @@ func TestBroadcastImmediate(t *testing.T) {
require.Empty(t, tp.subscriberChans.Len())
}

// TestCreateAnPublishFail checks all the error cases are handled properly in
// the method createAndPublish.
func TestCreateAnPublishFail(t *testing.T) {
// TestCreateAndPublishFail checks all the error cases are handled properly in
// the method createAndPublishTx.
func TestCreateAndPublishFail(t *testing.T) {
t.Parallel()

// Create a publisher using the mocks.
Expand Down Expand Up @@ -1152,7 +1152,7 @@ func TestCreateAnPublishFail(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down Expand Up @@ -1190,9 +1190,9 @@ func TestCreateAnPublishFail(t *testing.T) {
require.True(t, resultOpt.IsNone())
}

// TestCreateAnPublishSuccess checks the expected result is returned from the
// method createAndPublish.
func TestCreateAnPublishSuccess(t *testing.T) {
// TestCreateAndPublishSuccess checks the expected result is returned from the
// method createAndPublishTx.
func TestCreateAndPublishSuccess(t *testing.T) {
t.Parallel()

// Create a publisher using the mocks.
Expand All @@ -1218,7 +1218,7 @@ func TestCreateAnPublishSuccess(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down Expand Up @@ -1445,7 +1445,7 @@ func TestHandleFeeBumpTx(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down Expand Up @@ -1830,7 +1830,7 @@ func TestHandleInitialBroadcastSuccess(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down Expand Up @@ -1916,7 +1916,7 @@ func TestHandleInitialBroadcastFail(t *testing.T) {
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
// Signer check and always assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
Expand Down
Loading
Loading