diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go new file mode 100644 index 00000000..02f44cfc --- /dev/null +++ b/contractcourt/anchor_resolver.go @@ -0,0 +1,165 @@ +package contractcourt + +import ( + "errors" + "io" + + "github.com/btcsuite/btcd/wire" + "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. +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 + + contractResolverKit +} + +// newAnchorResolver instantiates a new anchor resolver. +func newAnchorResolver(anchorSignDescriptor input.SignDescriptor, + anchor wire.OutPoint, broadcastHeight uint32, + chanPoint wire.OutPoint, resCfg ResolverConfig) *anchorResolver { + + r := &anchorResolver{ + contractResolverKit: *newContractResolverKit(resCfg), + anchorSignDescriptor: anchorSignDescriptor, + anchor: anchor, + broadcastHeight: broadcastHeight, + chanPoint: chanPoint, + } + + 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 + // 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, + }, + 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( + &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 + } + } + + select { + case sweepRes := <-resultChan: + switch sweepRes.Err { + + // Anchor was swept successfully. + case nil: + c.log.Debugf("anchor swept by tx %v", + sweepRes.Tx.TxHash()) + + // 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: + 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 + } + + c.resolved = true + 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 +} + +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) diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go index 34ed98d3..fe2289ab 100644 --- a/contractcourt/channel_arbitrator.go +++ b/contractcourt/channel_arbitrator.go @@ -467,7 +467,7 @@ func (c *ChannelArbitrator) Start() error { // receive a chain event from the chain watcher than the // commitment has been confirmed on chain, and before we // advance our state step, we call InsertConfirmedCommitSet. - if err := c.relaunchResolvers(commitSet); err != nil { + if err := c.relaunchResolvers(commitSet, triggerHeight); err != nil { c.cfg.BlockEpochs.Cancel() return err } @@ -483,7 +483,9 @@ func (c *ChannelArbitrator) Start() error { // starting the ChannelArbitrator. This information should ideally be stored in // the database, so this only serves as a intermediate work-around to prevent a // migration. -func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet) error { +func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet, + heightHint uint32) error { + // We'll now query our log to see if there are any active unresolved // contracts. If this is the case, then we'll relaunch all contract // resolvers. @@ -558,6 +560,19 @@ func (c *ChannelArbitrator) relaunchResolvers(commitSet *CommitSet) error { htlcResolver.Supplement(*htlc) } + // The anchor resolver is stateless and can always be re-instantiated. + if contractResolutions.AnchorResolution != nil { + anchorResolver := newAnchorResolver( + contractResolutions.AnchorResolution.AnchorSignDescriptor, + contractResolutions.AnchorResolution.CommitAnchor, + heightHint, c.cfg.ChanPoint, + ResolverConfig{ + ChannelArbitratorConfig: c.cfg, + }, + ) + unresolvedContracts = append(unresolvedContracts, anchorResolver) + } + c.launchResolvers(unresolvedContracts) return nil @@ -1856,8 +1871,8 @@ func (c *ChannelArbitrator) prepContractResolutions( } } - // Finally, if this is was a unilateral closure, then we'll also create - // a resolver to sweep our commitment output (but only if it wasn't + // If this is was an unilateral closure, then we'll also create a + // resolver to sweep our commitment output (but only if it wasn't // trimmed). if contractResolutions.CommitResolution != nil { resolver := newCommitSweepResolver( @@ -1867,6 +1882,17 @@ func (c *ChannelArbitrator) prepContractResolutions( htlcResolvers = append(htlcResolvers, resolver) } + // We instantiate an anchor resolver if the commitmentment tx has an + // anchor. + if contractResolutions.AnchorResolution != nil { + anchorResolver := newAnchorResolver( + contractResolutions.AnchorResolution.AnchorSignDescriptor, + contractResolutions.AnchorResolution.CommitAnchor, + height, c.cfg.ChanPoint, resolverCfg, + ) + htlcResolvers = append(htlcResolvers, anchorResolver) + } + return htlcResolvers, msgsToSend, nil } diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index dcf969e9..bbe1c51f 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -2168,6 +2168,69 @@ func TestChannelArbitratorAnchors(t *testing.T) { case <-time.After(5 * time.Second): t.Fatalf("no response received") } + + // Now notify about the local force close getting confirmed. + closeTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{}, + Witness: [][]byte{ + {0x1}, + {0x2}, + }, + }, + }, + } + + chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{ + SpendDetail: &chainntnfs.SpendDetail{}, + LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{ + CloseTx: closeTx, + HtlcResolutions: &lnwallet.HtlcResolutions{}, + AnchorResolution: &lnwallet.AnchorResolution{ + AnchorSignDescriptor: input.SignDescriptor{ + Output: &wire.TxOut{ + Value: 1, + }, + }, + }, + }, + ChannelCloseSummary: &channeldb.ChannelCloseSummary{}, + CommitSet: CommitSet{ + ConfCommitKey: &LocalHtlcSet, + HtlcSets: map[HtlcSetKey][]channeldb.HTLC{}, + }, + } + + chanArbCtx.AssertStateTransitions( + StateContractClosed, + 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) + } + + // The anchor resolver is expected to offer the anchor input to the + // sweeper. + <-chanArbCtx.sweeper.updatedInputs + + // The mock sweeper immediately signals success for that input. This + // should transition the channel to the resolved state. + chanArbCtx.AssertStateTransitions(StateFullyResolved) + select { + case <-chanArbCtx.resolvedChan: + case <-time.After(5 * time.Second): + t.Fatalf("contract was not resolved") + } } type mockChannel struct { diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index e5c7df05..279cc6e8 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -94,12 +94,14 @@ func (i *commitSweepResolverTestContext) waitForResult() { } type mockSweeper struct { - sweptInputs chan input.Input + sweptInputs chan input.Input + updatedInputs chan wire.OutPoint } func newMockSweeper() *mockSweeper { return &mockSweeper{ - sweptInputs: make(chan input.Input), + sweptInputs: make(chan input.Input), + updatedInputs: make(chan wire.OutPoint), } } @@ -125,6 +127,18 @@ func (s *mockSweeper) RelayFeePerKW() chainfee.SatPerKWeight { return 253 } +func (s *mockSweeper) UpdateParams(input wire.OutPoint, + params sweep.ParamsUpdate) (chan sweep.Result, error) { + + s.updatedInputs <- input + + result := make(chan sweep.Result, 1) + result <- sweep.Result{ + Tx: &wire.MsgTx{}, + } + return result, nil +} + var _ UtxoSweeper = &mockSweeper{} // TestCommitSweepResolverNoDelay tests resolution of a direct commitment output diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go index 5497dad8..ded07f32 100644 --- a/contractcourt/interfaces.go +++ b/contractcourt/interfaces.go @@ -56,4 +56,12 @@ type UtxoSweeper interface { // RelayFeePerKW returns the minimum fee rate required for transactions // to be relayed. RelayFeePerKW() chainfee.SatPerKWeight + + // UpdateParams allows updating the sweep parameters of a pending input + // in the UtxoSweeper. This function can be used to provide an updated + // fee preference that will be used for a new sweep transaction of the + // input that will act as a replacement transaction (RBF) of the + // original sweeping transaction, if any. + UpdateParams(input wire.OutPoint, params sweep.ParamsUpdate) ( + chan sweep.Result, error) }