diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index 0cccd5a6cb1..5bf62daf20b 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -176,10 +176,6 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, t.Fatalf("expected %v, got %v", ogRes.outputIncubating, diskRes.outputIncubating) } - if ogRes.resolved != diskRes.resolved { - t.Fatalf("expected %v, got %v", ogRes.resolved, - diskRes.resolved) - } if ogRes.broadcastHeight != diskRes.broadcastHeight { t.Fatalf("expected %v, got %v", ogRes.broadcastHeight, diskRes.broadcastHeight) @@ -208,7 +204,7 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, case *htlcIncomingContestResolver: diskRes := diskResolver.(*htlcIncomingContestResolver) assertSuccessResEqual( - &ogRes.htlcSuccessResolver, &diskRes.htlcSuccessResolver, + ogRes.htlcSuccessResolver, diskRes.htlcSuccessResolver, ) if ogRes.htlcExpiry != diskRes.htlcExpiry { @@ -283,7 +279,6 @@ func TestContractInsertionRetrieval(t *testing.T) { htlc: channeldb.HTLC{ RHash: testPreimage, }, - sweepTx: nil, } resolvers := []ContractResolver{ &timeoutResolver, @@ -312,7 +307,7 @@ func TestContractInsertionRetrieval(t *testing.T) { contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint() resolvers = append(resolvers, &htlcIncomingContestResolver{ htlcExpiry: 100, - htlcSuccessResolver: contestSuccess, + htlcSuccessResolver: &contestSuccess, }) // For quick lookup during the test, we'll create this map which allow diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 98e52500a9a..8622e89dfc8 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -30,7 +30,7 @@ type htlcIncomingContestResolver struct { // htlcSuccessResolver is the inner resolver that may be utilized if we // learn of the preimage. - htlcSuccessResolver + *htlcSuccessResolver } // newIncomingContestResolver instantiates a new incoming htlc contest resolver. @@ -44,7 +44,7 @@ func newIncomingContestResolver( return &htlcIncomingContestResolver{ htlcExpiry: htlc.RefundTimeout, - htlcSuccessResolver: *success, + htlcSuccessResolver: success, } } @@ -186,7 +186,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { return nil, err } - return &h.htlcSuccessResolver, nil + return h.htlcSuccessResolver, nil } // Create a buffered hodl chan to prevent deadlock. @@ -232,7 +232,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { return nil, err } - return &h.htlcSuccessResolver, nil + return h.htlcSuccessResolver, nil } for { @@ -252,7 +252,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) { // We've learned of the preimage and this information // has been added to our inner resolver. We return it so // it can continue contract resolution. - return &h.htlcSuccessResolver, nil + return h.htlcSuccessResolver, nil case hodlItem := <-hodlChan: htlcResolution := hodlItem.(invoices.HtlcResolution) @@ -352,7 +352,7 @@ func newIncomingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) ( if err != nil { return nil, err } - h.htlcSuccessResolver = *successResolver + h.htlcSuccessResolver = successResolver return h, nil } diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go index 773b22c003d..81564e26b8d 100644 --- a/contractcourt/htlc_incoming_resolver_test.go +++ b/contractcourt/htlc_incoming_resolver_test.go @@ -261,7 +261,7 @@ func newIncomingResolverTestContext(t *testing.T) *incomingResolverTestContext { }, } resolver := &htlcIncomingContestResolver{ - htlcSuccessResolver: htlcSuccessResolver{ + htlcSuccessResolver: &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(cfg), htlcResolution: lnwallet.IncomingHtlcResolution{}, htlc: channeldb.HTLC{ diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index c13c52e87ef..3c11557970b 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -2,9 +2,12 @@ package contractcourt import ( "encoding/binary" + "fmt" "io" + "sync" "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" @@ -37,16 +40,16 @@ type htlcSuccessResolver struct { // historical queries to the chain for spends/confirmations. broadcastHeight uint32 - // sweepTx will be non-nil if we've already crafted a transaction to - // sweep a direct HTLC output. This is only a concern if we're sweeping - // from the commitment transaction of the remote party. - // - // TODO(roasbeef): send off to utxobundler - sweepTx *wire.MsgTx - // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC + // 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 } @@ -55,12 +58,16 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution, broadcastHeight uint32, htlc channeldb.HTLC, resCfg ResolverConfig) *htlcSuccessResolver { - return &htlcSuccessResolver{ + r := &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(resCfg), htlcResolution: res, broadcastHeight: broadcastHeight, htlc: htlc, } + + r.initReport() + + return r } // ResolverKey returns an identifier which should be globally unique for this @@ -94,164 +101,133 @@ func (h *htlcSuccessResolver) ResolverKey() []byte { // // NOTE: Part of the ContractResolver interface. func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { - // If we're already resolved, then we can exit early. - if h.resolved { - return nil, nil - } + var sweepInput input.Input // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. - if h.htlcResolution.SignedSuccessTx == nil { - // If we don't already have the sweep transaction constructed, - // we'll do so and broadcast it. - if h.sweepTx == nil { - log.Infof("%T(%x): crafting sweep tx for "+ - "incoming+remote htlc confirmed", h, - h.htlc.RHash[:]) - - // Before we can craft out sweeping transaction, we - // need to create an input which contains all the items - // required to add this input to a sweeping transaction, - // and generate a witness. - inp := input.MakeHtlcSucceedInput( - &h.htlcResolution.ClaimOutpoint, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.Preimage[:], - h.broadcastHeight, - ) - - // With the input created, we can now generate the full - // sweep transaction, that we'll use to move these - // coins back into the backing wallet. - // - // TODO: Set tx lock time to current block height - // instead of zero. Will be taken care of once sweeper - // implementation is complete. - // - // TODO: Use time-based sweeper and result chan. - var err error - h.sweepTx, err = h.Sweeper.CreateSweepTx( - []input.Input{&inp}, - sweep.FeePreference{ - ConfTarget: sweepConfTarget, - }, 0, - ) - if err != nil { - return nil, err - } - - log.Infof("%T(%x): crafted sweep tx=%v", h, - h.htlc.RHash[:], spew.Sdump(h.sweepTx)) - - // With the sweep transaction signed, we'll now - // Checkpoint our state. - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } - } - - // Regardless of whether an existing transaction was found or newly - // constructed, we'll broadcast the sweep transaction to the - // network. - err := h.PublishTx(h.sweepTx) + if h.htlcResolution.SignedSuccessTx != nil { + log.Infof("%T(%x): broadcasting second-layer transition tx: %v", + h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) + + // We'll now broadcast the second layer transaction so we can kick off + // the claiming process. + // + // TODO(roasbeef): after changing sighashes send to tx bundler + err := h.PublishTx(h.htlcResolution.SignedSuccessTx) if err != nil { - log.Infof("%T(%x): unable to publish tx: %v", - h, h.htlc.RHash[:], err) return nil, err } - // With the sweep transaction broadcast, we'll wait for its - // confirmation. - sweepTXID := h.sweepTx.TxHash() - sweepScript := h.sweepTx.TxOut[0].PkScript - confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( - &sweepTXID, sweepScript, 1, h.broadcastHeight, - ) + // Wait for success tx to confirm. + confHeight, err := h.waitForSecondLevelConf(1) if err != nil { return nil, err } - log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+ - "confirmed", h, h.htlc.RHash[:], sweepTXID) + // Update reported maturity height and advance to stage two + // (waiting for csv lock). + h.reportLock.Lock() + h.currentReport.MaturityHeight = + confHeight + h.htlcResolution.CsvDelay - 1 + h.currentReport.Stage = 2 + h.reportLock.Unlock() - select { - case _, ok := <-confNtfn.Confirmed: - if !ok { - return nil, errResolverShuttingDown - } - - case <-h.quit: - return nil, errResolverShuttingDown + // Wait for csv lock to expire. + _, err = h.waitForSecondLevelConf(h.htlcResolution.CsvDelay) + if err != nil { + return nil, err } - // 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) + // Create final input for the sweeper. + sweepInput = input.NewCsvInput( + &h.htlcResolution.ClaimOutpoint, + input.HtlcAcceptedSuccessSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.broadcastHeight, h.htlcResolution.CsvDelay, + ) + } else { + // Create input to sweep htlc from commitment tx. + inp := input.MakeHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.Preimage[:], + h.broadcastHeight, + ) + sweepInput = &inp } - log.Infof("%T(%x): broadcasting second-layer transition tx: %v", - h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) - - // We'll now broadcast the second layer transaction so we can kick off - // the claiming process. - // - // TODO(roasbeef): after changing sighashes send to tx bundler - err := h.PublishTx(h.htlcResolution.SignedSuccessTx) + log.Infof("%T(%x): sweeping output for "+ + "incoming+remote htlc confirmed", h, + h.htlc.RHash[:]) + + // Offer the created input to the sweeper. + resultChan, err := h.Sweeper.SweepInput( + sweepInput, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, + }, + ) if err != nil { return nil, err } - // Otherwise, this is an output on our commitment transaction. In this - // case, we'll send it to the incubator, but only if we haven't already - // done so. - if !h.outputIncubating { - log.Infof("%T(%x): incubating incoming htlc output", - h, h.htlc.RHash[:]) - - err := h.IncubateOutputs( - h.ChanPoint, nil, &h.htlcResolution, - h.broadcastHeight, - ) - if err != nil { - return nil, err + // Wait for the sweep result. + select { + case result, ok := <-resultChan: + if !ok { + return nil, errResolverShuttingDown + } + if result.Err != nil { + return nil, result.Err } - h.outputIncubating = true + log.Infof("%T(%x): sweep tx (txid=%v) confirmed", + h, h.htlc.RHash[:], result.Tx) - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } + case <-h.quit: + return nil, errResolverShuttingDown } - // To wrap this up, we'll wait until the second-level transaction has - // been spent, then fully resolve the contract. - spendNtfn, err := h.Notifier.RegisterSpendNtfn( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, + // Funds have been swept and balance is no longer in limbo. + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() + + // Once the transaction has received a sufficient number of + // confirmations, we'll mark ourselves as fully resolved and exit. + h.resolved = true + return nil, nil +} + +func (h *htlcSuccessResolver) waitForSecondLevelConf(confDepth uint32) ( + uint32, error) { + + txID := h.htlcResolution.SignedSuccessTx.TxHash() + pkScript := h.htlcResolution.SignedSuccessTx.TxOut[0].PkScript + + confChan, err := h.Notifier.RegisterConfirmationsNtfn( + &txID, pkScript, confDepth, h.broadcastHeight, ) if err != nil { - return nil, err + return 0, err } - - log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ - "after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay) + defer confChan.Cancel() select { - case _, ok := <-spendNtfn.Spend: + case conf, ok := <-confChan.Confirmed: if !ok { - return nil, errResolverShuttingDown + return 0, fmt.Errorf("cannot get confirmation "+ + "for commit tx %v", txID) } + return conf.BlockHeight, nil + case <-h.quit: - return nil, errResolverShuttingDown + return 0, errResolverShuttingDown } - - h.resolved = true - return nil, h.Checkpoint(h) } // Stop signals the resolver to cancel any current resolution processes, and @@ -285,9 +261,13 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error { if err := binary.Write(w, endian, h.outputIncubating); err != nil { return err } - if err := binary.Write(w, endian, h.resolved); err != nil { + + // This was previously the resolved state of the resolver. Write a dummy + // value, because the resolver is stateless. + if err := binary.Write(w, endian, false); err != nil { return err } + if err := binary.Write(w, endian, h.broadcastHeight); err != nil { return err } @@ -318,9 +298,13 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( if err := binary.Read(r, endian, &h.outputIncubating); err != nil { return nil, err } - if err := binary.Read(r, endian, &h.resolved); err != nil { + + // Read a dummy byte that previously stored the resolved state. + var dummy bool + if err := binary.Read(r, endian, &dummy); err != nil { return nil, err } + if err := binary.Read(r, endian, &h.broadcastHeight); err != nil { return nil, err } @@ -328,6 +312,8 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + h.initReport() + return h, nil } @@ -346,6 +332,34 @@ func (h *htlcSuccessResolver) HtlcPoint() wire.OutPoint { return h.htlcResolution.HtlcPoint() } +// report returns a report on the resolution state of the contract. +func (h *htlcSuccessResolver) report() *ContractReport { + h.reportLock.Lock() + defer h.reportLock.Unlock() + + copy := h.currentReport + return © +} + +// initReport initializes the pending channels report for this resolver. +func (h *htlcSuccessResolver) initReport() { + amt := btcutil.Amount( + h.htlcResolution.SweepSignDesc.Output.Value, + ) + + // Set the initial report. Because we resolve via the success path, we + // don't need to wait for the htlc to expire and can start in stage 2 + // right away. + h.currentReport = ContractReport{ + Outpoint: h.htlcResolution.ClaimOutpoint, + Type: ReportOutputIncomingHtlc, + Amount: amt, + LimboBalance: amt, + RecoveredBalance: 0, + Stage: 1, + } +} + // A compile time assertion to ensure htlcSuccessResolver meets the // ContractResolver interface. var _ htlcContractResolver = (*htlcSuccessResolver)(nil) diff --git a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go index 123359dc49a..118e1cd8f25 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go @@ -212,8 +212,10 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest) assertTxInBlock(t, block, txid) } - // Keep track of the second level tx maturity. - carolSecondLevelCSV := uint32(defaultCSV) + // Keep track of the second level tx maturity. The transaction is + // already confirmed, so we don't need to wait for the first block of + // the csv lock. + carolSecondLevelCSV := uint32(defaultCSV) - 1 // When Bob notices Carol's second level transaction in the block, he // will extract the preimage and broadcast a second level tx to claim @@ -287,9 +289,10 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest) } assertTxInBlock(t, block, bobSecondLvlTx) - // Keep track of Bob's second level maturity, and decrement our track - // of Carol's. - bobSecondLevelCSV := uint32(defaultCSV) + // Keep track of Bob's second level maturity, and decrement our track of + // Carol's. Bob's second level tx is already confirmed, so we don't need + // to wait for the first block of that csv lock. + bobSecondLevelCSV := uint32(defaultCSV) - 1 carolSecondLevelCSV-- // Now that the preimage from Bob has hit the chain, restart Alice to