diff --git a/channeldb/reports.go b/channeldb/reports.go index 85f6b2331..5781e5ba8 100644 --- a/channeldb/reports.go +++ b/channeldb/reports.go @@ -46,11 +46,26 @@ var ( // ResolverType indicates the type of resolver that was resolved on chain. type ResolverType uint8 +const ( + // ResolverTypeAnchor represents a resolver for an anchor output. + ResolverTypeAnchor ResolverType = 0 +) + // ResolverOutcome indicates the outcome for the resolver that that the contract // court reached. This state is not necessarily final, since htlcs on our own // commitment are resolved across two resolvers. type ResolverOutcome uint8 +const ( + // ResolverOutcomeClaimed indicates that funds were claimed on chain. + ResolverOutcomeClaimed ResolverOutcome = 0 + + // ResolverOutcomeUnclaimed indicates that we did not claim our funds on + // chain. This may be the case for anchors that we did not sweep, or + // outputs that were not economical to sweep. + ResolverOutcomeUnclaimed ResolverOutcome = 1 +) + // ResolverReport provides an account of the outcome of a resolver. This differs // from a ContractReport because it does not necessarily fully resolve the // contract; each step of two stage htlc resolution is included. diff --git a/contractcourt/anchor_resolver.go b/contractcourt/anchor_resolver.go index 1cf7a820a..c67265c77 100644 --- a/contractcourt/anchor_resolver.go +++ b/contractcourt/anchor_resolver.go @@ -5,8 +5,10 @@ import ( "io" "sync" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/sweep" @@ -121,22 +123,27 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { } } - var anchorRecovered bool + var ( + outcome channeldb.ResolverOutcome + spendTx *chainhash.Hash + ) + select { case sweepRes := <-resultChan: switch sweepRes.Err { // Anchor was swept successfully. case nil: - c.log.Debugf("anchor swept by tx %v", - sweepRes.Tx.TxHash()) + sweepTxID := sweepRes.Tx.TxHash() - anchorRecovered = true + spendTx = &sweepTxID + outcome = channeldb.ResolverOutcomeClaimed // 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") + outcome = channeldb.ResolverOutcomeUnclaimed // The sweeper gave up on sweeping the anchor. This happens // after the maximum number of sweep attempts has been reached. @@ -147,6 +154,7 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { // We consider the anchor as being lost. case sweep.ErrTooManyAttempts: c.log.Warnf("anchor sweep abandoned") + outcome = channeldb.ResolverOutcomeUnclaimed // An unexpected error occurred. default: @@ -161,14 +169,17 @@ func (c *anchorResolver) Resolve() (ContractResolver, error) { // Update report to reflect that funds are no longer in limbo. c.reportLock.Lock() - if anchorRecovered { + if outcome == channeldb.ResolverOutcomeClaimed { c.currentReport.RecoveredBalance = c.currentReport.LimboBalance } c.currentReport.LimboBalance = 0 + report := c.currentReport.resolverReport( + spendTx, channeldb.ResolverTypeAnchor, outcome, + ) c.reportLock.Unlock() c.resolved = true - return nil, nil + return nil, c.PutResolverReport(nil, report) } // Stop signals the resolver to cancel any current resolution processes, and diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go index fe2d1bb47..ce0963ece 100644 --- a/contractcourt/channel_arbitrator_test.go +++ b/contractcourt/channel_arbitrator_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "sync" "testing" "time" @@ -2109,6 +2110,15 @@ func TestChannelArbitratorAnchors(t *testing.T) { if err != nil { t.Fatalf("unable to create ChannelArbitrator: %v", err) } + + // Replace our mocked put report function with one which will push + // reports into a channel for us to consume. We update this function + // because our resolver will be created from the existing chanArb cfg. + reports := make(chan *channeldb.ResolverReport) + chanArbCtx.chanArb.cfg.PutResolverReport = putResolverReportInChannel( + reports, + ) + chanArb := chanArbCtx.chanArb chanArb.cfg.PreimageDB = newMockWitnessBeacon() chanArb.cfg.Registry = &mockRegistry{} @@ -2187,18 +2197,20 @@ func TestChannelArbitratorAnchors(t *testing.T) { }, } + anchorResolution := &lnwallet.AnchorResolution{ + AnchorSignDescriptor: input.SignDescriptor{ + Output: &wire.TxOut{ + Value: 1, + }, + }, + } + 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, - }, - }, - }, + CloseTx: closeTx, + HtlcResolutions: &lnwallet.HtlcResolutions{}, + AnchorResolution: anchorResolution, }, ChannelCloseSummary: &channeldb.ChannelCloseSummary{}, CommitSet: CommitSet{ @@ -2236,6 +2248,47 @@ func TestChannelArbitratorAnchors(t *testing.T) { case <-time.After(5 * time.Second): 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, + } + + assertResolverReport(t, reports, expectedReport) +} + +// putResolverReportInChannel returns a put report function which will pipe +// reports into the channel provided. +func putResolverReportInChannel(reports chan *channeldb.ResolverReport) func( + _ kvdb.RwTx, report *channeldb.ResolverReport) error { + + return func(_ kvdb.RwTx, report *channeldb.ResolverReport) error { + reports <- report + return nil + } +} + +// assertResolverReport checks that a set of reports only contains a single +// report, and that it is equal to the expected report passed in. +func assertResolverReport(t *testing.T, reports chan *channeldb.ResolverReport, + expected *channeldb.ResolverReport) { + + select { + case report := <-reports: + if !reflect.DeepEqual(report, expected) { + t.Fatalf("expected: %v, got: %v", expected, report) + } + + case <-time.After(defaultTimeout): + t.Fatalf("no reports present") + } } type mockChannel struct { diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index d995b2dec..89c173f75 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -104,6 +104,7 @@ func (i *commitSweepResolverTestContext) waitForResult() { type mockSweeper struct { sweptInputs chan input.Input updatedInputs chan wire.OutPoint + sweepTx *wire.MsgTx sweepErr error } @@ -111,6 +112,7 @@ func newMockSweeper() *mockSweeper { return &mockSweeper{ sweptInputs: make(chan input.Input), updatedInputs: make(chan wire.OutPoint), + sweepTx: &wire.MsgTx{}, } } @@ -121,7 +123,7 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) ( result := make(chan sweep.Result, 1) result <- sweep.Result{ - Tx: &wire.MsgTx{}, + Tx: s.sweepTx, Err: s.sweepErr, } return result, nil @@ -144,7 +146,7 @@ func (s *mockSweeper) UpdateParams(input wire.OutPoint, result := make(chan sweep.Result, 1) result <- sweep.Result{ - Tx: &wire.MsgTx{}, + Tx: s.sweepTx, } return result, nil }