-
Notifications
You must be signed in to change notification settings - Fork 2.3k
cnct: anchor output sweeping and fee bumping #3758
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
Changes from all commits
30fc03d
dc6c463
1c93e9e
29e1489
c0b1b3b
7f3d4e7
dd77d92
c376cdb
3b51739
e8e99c6
d2b6472
07a39b1
d84b596
ea397c9
ab451f6
d60303b
fa115b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| package contractcourt | ||
|
|
||
| import ( | ||
| "errors" | ||
| "io" | ||
| "sync" | ||
|
|
||
| "github.com/btcsuite/btcd/wire" | ||
| "github.com/btcsuite/btcutil" | ||
| "github.com/lightningnetwork/lnd/input" | ||
| "github.com/lightningnetwork/lnd/lnwallet" | ||
| "github.com/lightningnetwork/lnd/sweep" | ||
| ) | ||
|
|
||
| // anchorResolver is a resolver that will attempt to sweep our anchor output. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this may actually need to live independent of any given channel arb. Otherwise, they'll never transition from pending close to fully closed, since there'll be a lingering contract that's unresolved.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added select clause to mark as resolved when the sweeper gives up. By that time, the 16 block csv delay has long expired, so if fees ever come down far enough, the anchor will probably be swept by one of the cleaning services that we envision.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When will the sweeper "give up"? Will check out the diff for more details...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a comment about this in the |
||
| type anchorResolver struct { | ||
| // anchorSignDescriptor contains the information that is required to | ||
| // sweep the anchor. | ||
| anchorSignDescriptor input.SignDescriptor | ||
|
|
||
| // anchor is the outpoint on the commitment transaction. | ||
| anchor wire.OutPoint | ||
|
|
||
| // resolved reflects if the contract has been fully resolved or not. | ||
| resolved bool | ||
|
|
||
| // broadcastHeight is the height that the original contract was | ||
| // broadcast to the main-chain at. We'll use this value to bound any | ||
| // historical queries to the chain for spends/confirmations. | ||
| broadcastHeight uint32 | ||
|
|
||
| // chanPoint is the channel point of the original contract. | ||
| chanPoint wire.OutPoint | ||
|
|
||
| // currentReport stores the current state of the resolver for reporting | ||
| // over the rpc interface. | ||
| currentReport ContractReport | ||
|
|
||
| // reportLock prevents concurrent access to the resolver report. | ||
| reportLock sync.Mutex | ||
|
|
||
| contractResolverKit | ||
| } | ||
|
|
||
| // newAnchorResolver instantiates a new anchor resolver. | ||
| func newAnchorResolver(anchorSignDescriptor input.SignDescriptor, | ||
| anchor wire.OutPoint, broadcastHeight uint32, | ||
| chanPoint wire.OutPoint, resCfg ResolverConfig) *anchorResolver { | ||
|
|
||
| amt := btcutil.Amount(anchorSignDescriptor.Output.Value) | ||
|
|
||
| report := ContractReport{ | ||
| Outpoint: anchor, | ||
| Type: ReportOutputAnchor, | ||
| Amount: amt, | ||
| LimboBalance: amt, | ||
| RecoveredBalance: 0, | ||
| } | ||
|
|
||
| r := &anchorResolver{ | ||
| contractResolverKit: *newContractResolverKit(resCfg), | ||
| anchorSignDescriptor: anchorSignDescriptor, | ||
| anchor: anchor, | ||
| broadcastHeight: broadcastHeight, | ||
| chanPoint: chanPoint, | ||
| currentReport: report, | ||
| } | ||
|
|
||
| r.initLogger(r) | ||
|
|
||
| return r | ||
| } | ||
|
|
||
| // ResolverKey returns an identifier which should be globally unique for this | ||
| // particular resolver within the chain the original contract resides within. | ||
| func (c *anchorResolver) ResolverKey() []byte { | ||
| // The anchor resolver is stateless and doesn't need a database key. | ||
| return nil | ||
| } | ||
|
|
||
| // Resolve offers the anchor output to the sweeper and waits for it to be swept. | ||
| func (c *anchorResolver) Resolve() (ContractResolver, error) { | ||
| // Attempt to update the sweep parameters to the post-confirmation | ||
|
joostjager marked this conversation as resolved.
Outdated
|
||
| // situation. We don't want to force sweep anymore, because the anchor | ||
| // lost its special purpose to get the commitment confirmed. It is just | ||
| // an output that we want to sweep only if it is economical to do so. | ||
| relayFeeRate := c.Sweeper.RelayFeePerKW() | ||
|
|
||
| resultChan, err := c.Sweeper.UpdateParams( | ||
| c.anchor, | ||
| sweep.ParamsUpdate{ | ||
| Fee: sweep.FeePreference{ | ||
| FeeRate: relayFeeRate, | ||
|
joostjager marked this conversation as resolved.
Outdated
|
||
| }, | ||
| Force: false, | ||
| }, | ||
| ) | ||
|
|
||
| // After a restart or when the remote force closes, the sweeper is not | ||
| // yet aware of the anchor. In that case, offer it as a new input to the | ||
| // sweeper. An exclusive group is not necessary anymore, because we know | ||
| // that this is the only anchor that can be swept. | ||
| if err == lnwallet.ErrNotMine { | ||
| anchorInput := input.MakeBaseInput( | ||
|
halseth marked this conversation as resolved.
Outdated
|
||
| &c.anchor, | ||
| input.CommitmentAnchor, | ||
| &c.anchorSignDescriptor, | ||
| c.broadcastHeight, | ||
| ) | ||
|
|
||
| resultChan, err = c.Sweeper.SweepInput( | ||
| &anchorInput, | ||
| sweep.Params{ | ||
| Fee: sweep.FeePreference{ | ||
| FeeRate: relayFeeRate, | ||
| }, | ||
| }, | ||
| ) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| var anchorRecovered bool | ||
| select { | ||
| case sweepRes := <-resultChan: | ||
|
joostjager marked this conversation as resolved.
Outdated
|
||
| switch sweepRes.Err { | ||
|
|
||
| // Anchor was swept successfully. | ||
| case nil: | ||
| c.log.Debugf("anchor swept by tx %v", | ||
| sweepRes.Tx.TxHash()) | ||
|
|
||
| anchorRecovered = true | ||
|
|
||
| // Anchor was swept by someone else. This is possible after the | ||
| // 16 block csv lock. | ||
| case sweep.ErrRemoteSpend: | ||
| c.log.Warnf("our anchor spent by someone else") | ||
|
|
||
| // The sweeper gave up on sweeping the anchor. This happens | ||
| // after the maximum number of sweep attempts has been reached. | ||
| // See sweep.DefaultMaxSweepAttempts. Sweep attempts are | ||
| // interspaced with random delays picked from a range that | ||
| // increases exponentially. | ||
| // | ||
| // We consider the anchor as being lost. | ||
| case sweep.ErrTooManyAttempts: | ||
|
joostjager marked this conversation as resolved.
Outdated
|
||
| c.log.Warnf("anchor sweep abandoned") | ||
|
|
||
| // An unexpected error occurred. | ||
| default: | ||
| c.log.Errorf("unable to sweep anchor: %v", sweepRes.Err) | ||
|
|
||
| return nil, sweepRes.Err | ||
| } | ||
|
|
||
| case <-c.quit: | ||
| return nil, errResolverShuttingDown | ||
| } | ||
|
|
||
| // Update report to reflect that funds are no longer in limbo. | ||
| c.reportLock.Lock() | ||
| if anchorRecovered { | ||
| c.currentReport.RecoveredBalance = c.currentReport.LimboBalance | ||
| } | ||
| c.currentReport.LimboBalance = 0 | ||
| c.reportLock.Unlock() | ||
|
|
||
| c.resolved = true | ||
|
joostjager marked this conversation as resolved.
Outdated
|
||
| return nil, nil | ||
| } | ||
|
|
||
| // Stop signals the resolver to cancel any current resolution processes, and | ||
| // suspend. | ||
| // | ||
| // NOTE: Part of the ContractResolver interface. | ||
| func (c *anchorResolver) Stop() { | ||
| close(c.quit) | ||
| } | ||
|
|
||
| // IsResolved returns true if the stored state in the resolve is fully | ||
| // resolved. In this case the target output can be forgotten. | ||
| // | ||
| // NOTE: Part of the ContractResolver interface. | ||
| func (c *anchorResolver) IsResolved() bool { | ||
| return c.resolved | ||
| } | ||
|
|
||
| // report returns a report on the resolution state of the contract. | ||
| func (c *anchorResolver) report() *ContractReport { | ||
| c.reportLock.Lock() | ||
| defer c.reportLock.Unlock() | ||
|
|
||
| reportCopy := c.currentReport | ||
| return &reportCopy | ||
| } | ||
|
|
||
| func (c *anchorResolver) Encode(w io.Writer) error { | ||
| return errors.New("serialization not supported") | ||
| } | ||
|
|
||
| // A compile time assertion to ensure anchorResolver meets the | ||
| // ContractResolver interface. | ||
| var _ ContractResolver = (*anchorResolver)(nil) | ||
Uh oh!
There was an error while loading. Please reload this page.