diff --git a/channeldb/reports.go b/channeldb/reports.go index 00a7468f7..ad1e1a839 100644 --- a/channeldb/reports.go +++ b/channeldb/reports.go @@ -77,6 +77,11 @@ const ( // ResolverOutcomeTimeout indicates that a contract was timed out on // chain. ResolverOutcomeTimeout ResolverOutcome = 3 + + // ResolverOutcomeFirstStage indicates that a htlc had to be claimed + // over two stages, with this outcome representing the confirmation + // of our success/timeout tx. + ResolverOutcomeFirstStage ResolverOutcome = 4 ) // ResolverReport provides an account of the outcome of a resolver. This differs diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 7c32e154d..7b2620915 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -4,7 +4,9 @@ import ( "encoding/binary" "io" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" @@ -189,7 +191,12 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // Once the transaction has received a sufficient number of // confirmations, we'll mark ourselves as fully resolved and exit. h.resolved = true - return nil, h.Checkpoint(h) + + // Checkpoint the resolver, and write the outcome to disk. + return nil, h.checkpointClaim( + &sweepTXID, + channeldb.ResolverOutcomeClaimed, + ) } log.Infof("%T(%x): broadcasting second-layer transition tx: %v", @@ -241,18 +248,63 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ "after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay) + var spendTxid *chainhash.Hash select { - case _, ok := <-spendNtfn.Spend: + case spend, ok := <-spendNtfn.Spend: if !ok { return nil, errResolverShuttingDown } + spendTxid = spend.SpenderTxHash case <-h.quit: return nil, errResolverShuttingDown } h.resolved = true - return nil, h.Checkpoint(h) + return nil, h.checkpointClaim( + spendTxid, channeldb.ResolverOutcomeClaimed, + ) +} + +// checkpointClaim checkpoints the success resolver with the reports it needs. +// If this htlc was claimed two stages, it will write reports for both stages, +// otherwise it will just write for the single htlc claim. +func (h *htlcSuccessResolver) checkpointClaim(spendTx *chainhash.Hash, + outcome channeldb.ResolverOutcome) error { + + // Create a resolver report for claiming of the htlc itself. + amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) + reports := []*channeldb.ResolverReport{ + { + OutPoint: h.htlcResolution.ClaimOutpoint, + Amount: amt, + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: outcome, + SpendTxID: spendTx, + }, + } + + // If we have a success tx, we append a report to represent our first + // stage claim. + if h.htlcResolution.SignedSuccessTx != nil { + // If the SignedSuccessTx is not nil, we are claiming the htlc + // in two stages, so we need to create a report for the first + // stage transaction as well. + spendTx := h.htlcResolution.SignedSuccessTx + spendTxID := spendTx.TxHash() + + report := &channeldb.ResolverReport{ + OutPoint: spendTx.TxIn[0].PreviousOutPoint, + Amount: h.htlc.Amt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &spendTxID, + } + reports = append(reports, report) + } + + // Finally, we checkpoint the resolver with our report(s). + return h.Checkpoint(h, reports...) } // Stop signals the resolver to cancel any current resolution processes, and diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index 75a7265b1..0c700969f 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -4,12 +4,16 @@ import ( "testing" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/kvdb" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" ) +var testHtlcAmt = lnwire.MilliSatoshi(200000) + type htlcSuccessResolverTestContext struct { resolver *htlcSuccessResolver notifier *mockNotifier @@ -61,6 +65,7 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon htlc: channeldb.HTLC{ RHash: testResHash, OnionBlob: testOnionBlob, + Amt: testHtlcAmt, }, } @@ -117,14 +122,23 @@ func TestSingleStageSuccess(t *testing.T) { } } + sweepTxid := sweepTx.TxHash() + claim := &channeldb.ResolverReport{ + OutPoint: htlcOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, + SpendTxID: &sweepTxid, + } testHtlcSuccess( - t, singleStageResolution, resolve, sweepTx, + t, singleStageResolution, resolve, sweepTx, claim, ) } // TestSecondStageResolution tests successful sweep of a second stage htlc // claim. func TestSecondStageResolution(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} htlcOutpoint := wire.OutPoint{Index: 3} sweepTx := &wire.MsgTx{ @@ -138,7 +152,11 @@ func TestSecondStageResolution(t *testing.T) { twoStageResolution := lnwallet.IncomingHtlcResolution{ Preimage: [32]byte{}, SignedSuccessTx: &wire.MsgTx{ - TxIn: []*wire.TxIn{}, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, TxOut: []*wire.TxOut{}, }, ClaimOutpoint: htlcOutpoint, @@ -153,17 +171,53 @@ func TestSecondStageResolution(t *testing.T) { } } - testHtlcSuccess(t, twoStageResolution, resolve, sweepTx) + successTx := twoStageResolution.SignedSuccessTx.TxHash() + firstStage := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: testHtlcAmt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &successTx, + } + + secondStage := &channeldb.ResolverReport{ + OutPoint: htlcOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, + SpendTxID: &sweepHash, + } + + testHtlcSuccess( + t, twoStageResolution, resolve, sweepTx, secondStage, firstStage, + ) } // testHtlcSuccess tests resolution of a success resolver. It takes a resolve // function which triggers resolution and the sweeptxid that will resolve it. func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, - resolve func(*htlcSuccessResolverTestContext), sweepTx *wire.MsgTx) { + resolve func(*htlcSuccessResolverTestContext), + sweepTx *wire.MsgTx, reports ...*channeldb.ResolverReport) { defer timeout(t)() ctx := newHtlcSuccessResolverTextContext(t) + + // Replace our checkpoint with one which will push reports into a + // channel for us to consume. We replace this function on the resolver + // itself because it is created by the test context. + reportChan := make(chan *channeldb.ResolverReport) + ctx.resolver.Checkpoint = func(_ ContractResolver, + reports ...*channeldb.ResolverReport) error { + + // Send all of our reports into the channel. + for _, report := range reports { + reportChan <- report + } + + return nil + } + ctx.resolver.htlcResolution = resolution // We set the sweepTx to be non-nil and mark the output as already @@ -178,6 +232,10 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, // Trigger and event that will resolve our test context. resolve(ctx) + for _, report := range reports { + assertResolverReport(t, reportChan, report) + } + // Wait for the resolver to fully complete. ctx.waitForResult() }