Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Anchor Output Sweeps configurable. #7939

Closed
Closed
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
5 changes: 5 additions & 0 deletions contractcourt/chain_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ type ChainArbitratorConfig struct {
// Sweeper allows resolvers to sweep their final outputs.
Sweeper UtxoSweeper

// NotRecoverAnchorOutputs stops the sweeping of anchor outputs when
// they were not used to fee bump (CPFP) their corresponding commitment
// tx.
NotRecoverAnchorOutputs bool

// Registry is the invoice database that is used by resolvers to lookup
// preimages and settle invoices.
Registry Registry
Expand Down
10 changes: 6 additions & 4 deletions contractcourt/channel_arbitrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,9 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet,
}

// The anchor resolver is stateless and can always be re-instantiated.
if contractResolutions.AnchorResolution != nil {
if contractResolutions.AnchorResolution != nil &&
!c.cfg.NotRecoverAnchorOutputs {

anchorResolver := newAnchorResolver(
contractResolutions.AnchorResolution.AnchorSignDescriptor,
contractResolutions.AnchorResolution.CommitAnchor,
Expand All @@ -771,8 +773,6 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet,
anchorResolver.SupplementState(chanState)

unresolvedContracts = append(unresolvedContracts, anchorResolver)

// TODO(roasbeef): this isn't re-launched?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not sure what you meant by this todo, according to the code the resolvers are relaunched ? The linter would complain about this comment so I removed it, happy to add it back if it still holds true.

Copy link
Member

Choose a reason for hiding this comment

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

IIUC, if all the contract for a resolution are resolved, but the anchor isn't, then it's never re-created.

}

c.launchResolvers(unresolvedContracts)
Expand Down Expand Up @@ -2192,7 +2192,9 @@ func (c *ChannelArbitrator) prepContractResolutions(

// We instantiate an anchor resolver if the commitment tx has an
// anchor.
if contractResolutions.AnchorResolution != nil {
if contractResolutions.AnchorResolution != nil &&
!c.cfg.NotRecoverAnchorOutputs {

anchorResolver := newAnchorResolver(
contractResolutions.AnchorResolution.AnchorSignDescriptor,
contractResolutions.AnchorResolution.CommitAnchor,
Expand Down
91 changes: 57 additions & 34 deletions contractcourt/channel_arbitrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,9 +761,14 @@ func TestChannelArbitratorBreachClose(t *testing.T) {
// StateWaitingFullResolution.
chanArbCtx.AssertStateTransitions(StateWaitingFullResolution)

// One of the resolvers should be an anchor resolver and the other
// should be a breach resolver.
require.Equal(t, 2, len(chanArb.activeResolvers))
// Depending on whether we recover anchor outputs one of the resolvers
// should be an anchor resolver and the other should be a breach
// resovler
numResovlers := 1
if !chanArb.cfg.NotRecoverAnchorOutputs {
numResovlers = 2
}
require.Equal(t, numResovlers, len(chanArb.activeResolvers))

var anchorExists, breachExists bool
for _, resolver := range chanArb.activeResolvers {
Expand All @@ -776,14 +781,25 @@ func TestChannelArbitratorBreachClose(t *testing.T) {
t.Fatalf("did not expect resolver %T", resolver)
}
}
require.True(t, anchorExists && breachExists)
require.True(t, breachExists)
if numResovlers == 2 {
require.True(t, anchorExists)

// The anchor resolver is expected to re-offer the anchor input to the
// sweeper.
<-chanArbCtx.sweeper.sweptInputs
// The anchor resolver is expected to re-offer the anchor input
// to the sweeper.
select {
case <-chanArbCtx.sweeper.sweptInputs:
case <-time.After(defaultTimeout):
t.Fatalf("anchor input not re-offered to sweeper")
}
}

// Wait for SubscribeBreachComplete to be called.
<-chanArbCtx.breachSubscribed
select {
case <-chanArbCtx.breachSubscribed:
case <-time.After(defaultTimeout):
t.Fatalf("SubscribeBreachComplete not called")
}

// We'll now close the breach channel so that the state transitions to
// StateFullyResolved.
Expand Down Expand Up @@ -2633,22 +2649,27 @@ func TestChannelArbitratorAnchors(t *testing.T) {
StateWaitingFullResolution,
)

// We expect to only have the anchor resolver active.
if len(chanArb.activeResolvers) != 1 {
t.Fatalf("expected single resolver, instead got: %v",
len(chanArb.activeResolvers))
}

resolver := chanArb.activeResolvers[0]
_, ok := resolver.(*anchorResolver)
if !ok {
t.Fatalf("expected anchor resolver, got %T", resolver)
}
if !chanArb.cfg.NotRecoverAnchorOutputs {
// We expect to only have the anchor resolver active.
if len(chanArb.activeResolvers) != 1 {
t.Fatalf("expected single resolver, instead got: %v",
len(chanArb.activeResolvers))
}

// The anchor resolver is expected to re-offer the anchor input to the
// sweeper.
<-chanArbCtx.sweeper.sweptInputs
resolver := chanArb.activeResolvers[0]
_, ok := resolver.(*anchorResolver)
if !ok {
t.Fatalf("expected anchor resolver, got %T", resolver)
}

// The anchor resolver is expected to re-offer the anchor input
// to the sweeper.
select {
case <-chanArbCtx.sweeper.sweptInputs:
case <-time.After(defaultTimeout):
t.Fatalf("anchor input not re-offered to sweeper")
}
}
// The mock sweeper immediately signals success for that input. This
// should transition the channel to the resolved state.
chanArbCtx.AssertStateTransitions(StateFullyResolved)
Expand All @@ -2658,19 +2679,21 @@ func TestChannelArbitratorAnchors(t *testing.T) {
t.Fatalf("contract was not resolved")
}

anchorAmt := btcutil.Amount(
anchorResolution.AnchorSignDescriptor.Output.Value,
)
spendTx := chanArbCtx.sweeper.sweepTx.TxHash()
expectedReport := &channeldb.ResolverReport{
OutPoint: anchorResolution.CommitAnchor,
Amount: anchorAmt,
ResolverType: channeldb.ResolverTypeAnchor,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &spendTx,
}
if !chanArb.cfg.NotRecoverAnchorOutputs {
anchorAmt := btcutil.Amount(
anchorResolution.AnchorSignDescriptor.Output.Value,
)
spendTx := chanArbCtx.sweeper.sweepTx.TxHash()
expectedReport := &channeldb.ResolverReport{
OutPoint: anchorResolution.CommitAnchor,
Amount: anchorAmt,
ResolverType: channeldb.ResolverTypeAnchor,
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
SpendTxID: &spendTx,
}

assertResolverReport(t, reports, expectedReport)
assertResolverReport(t, reports, expectedReport)
}

// 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
Expand Down
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.17.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ None
updated](https://github.com/lightningnetwork/lnd/pull/7877) in preparation for
work to be done on route blinding in LND.

* [Make the recovery of anchor outputs configurable. Default behaviour stays the
same but to prevent the blocking of wallet funds indefinitely when mempool
conditions are not in favour of confirming 1 sat/vbyte transactions node
runners can turn it off](https://github.com/lightningnetwork/lnd/pull/7939).

## RPC Updates
* [SendOutputs](https://github.com/lightningnetwork/lnd/pull/7631) now adheres
to the anchor channel reserve requirement.
Expand Down
3 changes: 2 additions & 1 deletion lncfg/sweeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (

//nolint:lll
type Sweeper struct {
BatchWindowDuration time.Duration `long:"batchwindowduration" description:"Duration of the sweep batch window. The sweep is held back during the batch window to allow more inputs to be added and thereby lower the fee per input."`
BatchWindowDuration time.Duration `long:"batchwindowduration" description:"Duration of the sweep batch window. The sweep is held back during the batch window to allow more inputs to be added and thereby lower the fee per input."`
NotRecoverAnchorOutputs bool `long:"notrecoveranchoroutputs" description:"Not recovering anchor outputs when they were not used to bump their commitment transaction."`
Copy link
Member

Choose a reason for hiding this comment

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

nit: maybe call it SkipAnchorRecover?

}

// Validate checks the values configured for the sweeper.
Expand Down
10 changes: 10 additions & 0 deletions sample-lnd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,16 @@
; window to allow more inputs to be added and thereby lower the fee per input.
; sweeper.batchwindowduration=30s

; If set to true lnd will not recover the anchor output which was not used
; bumping the fee of the commitment transaction (can be more than 1 anchor when
; more channels force close at the same time e.g. SCB). This is only a 330 sats
; output making them marginally positive yielding when sweeping with
; 1 sat/vbyte. Bad side effect is that it is blocking other utxos when sweeping
; this anchor output. Its recommended to set it to true if mempool conditions
; do not allow for 1 sat/vbyte transactions to be confirmed in a reasonable
; amount of time.
; sweeper.notrecoveranchoroutputs=false


[htlcswitch]

Expand Down
1 change: 1 addition & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
return s.chanStatusMgr.RequestDisable(chanPoint, false)
},
Sweeper: s.sweeper,
NotRecoverAnchorOutputs: cfg.Sweeper.NotRecoverAnchorOutputs,
Registry: s.invoices,
NotifyClosedChannel: s.channelNotifier.NotifyClosedChannelEvent,
NotifyFullyResolvedChannel: s.channelNotifier.NotifyFullyResolvedChannelEvent,
Expand Down
Loading