Repository: lightningnetwork/lnd
Pull request #3732 (2019-11-15 19:46:24)

routing: fix race condition when resuming payments
Found while running the integration tests with `-race`. Reuse of `err` variable when resuming payments in goroutines.

```
==================
WARNING: DATA RACE
Write at 0x00c000a680f0 by goroutine 147:
  github.com/lightningnetwork/lnd/routing.(*ChannelRouter).Start.func1()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/routing/router.go:536 +0x251

Previous write at 0x00c000a680f0 by goroutine 143:
  github.com/lightningnetwork/lnd/routing.(*ChannelRouter).Start.func1()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/routing/router.go:536 +0x251

Goroutine 147 (running) created at:
  github.com/lightningnetwork/lnd/routing.(*ChannelRouter).Start()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/routing/router.go:521 +0x42c
  github.com/lightningnetwork/lnd.(*server).Start.func1()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/server.go:1283 +0xaa0
  sync.(*Once).doSlow()
      /usr/local/Cellar/go/1.13/libexec/src/sync/once.go:66 +0x100
  sync.(*Once).Do()
      /usr/local/Cellar/go/1.13/libexec/src/sync/once.go:57 +0x68
  github.com/lightningnetwork/lnd.(*server).Start()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/server.go:1207 +0x7d
  github.com/lightningnetwork/lnd.Main()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/lnd.go:624 +0x29d3
  main.main()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/cmd/lnd/main.go:14 +0x3f

Goroutine 143 (running) created at:
  github.com/lightningnetwork/lnd/routing.(*ChannelRouter).Start()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/routing/router.go:521 +0x42c
  github.com/lightningnetwork/lnd.(*server).Start.func1()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/server.go:1283 +0xaa0
  sync.(*Once).doSlow()
      /usr/local/Cellar/go/1.13/libexec/src/sync/once.go:66 +0x100
  sync.(*Once).Do()
      /usr/local/Cellar/go/1.13/libexec/src/sync/once.go:57 +0x68
  github.com/lightningnetwork/lnd.(*server).Start()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/server.go:1207 +0x7d
  github.com/lightningnetwork/lnd.Main()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/lnd.go:624 +0x29d3
  main.main()
      /Users/nsa/go/src/github.com/lightningnetwork/lnd/cmd/lnd/main.go:14 +0x3f
==================
```


Patch:
diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go
index d06c2de89..eb1f85072 100644
--- a/cmd/lncli/main.go
+++ b/cmd/lncli/main.go
@@ -39,8 +39,8 @@ var (
 	defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename)
 
 	// maxMsgRecvSize is the largest message our client will receive. We
-	// set this to 200MiB atm.
-	maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
+	// set this to ~50Mb atm.
+	maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 50)
 )
 
 func fatal(err error) {
diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go
index 6d7c674a1..3b25abc75 100644
--- a/contractcourt/chain_arbitrator.go
+++ b/contractcourt/chain_arbitrator.go
@@ -15,6 +15,7 @@ import (
 	"github.com/lightningnetwork/lnd/lnwallet"
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/sweep"
 )
 
 // ErrChainArbExiting signals that the chain arbitrator is shutting down.
@@ -112,7 +113,8 @@ type ChainArbitratorConfig struct {
 	// the process of incubation. This is used when a resolver wishes to
 	// pass off the output to the nursery as we're only waiting on an
 	// absolute/relative item block.
-	IncubateOutputs func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution,
+	IncubateOutputs func(wire.OutPoint, *lnwallet.CommitOutputResolution,
+		*lnwallet.OutgoingHtlcResolution,
 		*lnwallet.IncomingHtlcResolution, uint32) error
 
 	// PreimageDB is a global store of all known pre-images. We'll use this
@@ -140,7 +142,7 @@ type ChainArbitratorConfig struct {
 	DisableChannel func(wire.OutPoint) error
 
 	// Sweeper allows resolvers to sweep their final outputs.
-	Sweeper UtxoSweeper
+	Sweeper *sweep.UtxoSweeper
 
 	// Registry is the invoice database that is used by resolvers to lookup
 	// preimages and settle invoices.
diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go
index bb0821709..e63a20461 100644
--- a/contractcourt/channel_arbitrator.go
+++ b/contractcourt/channel_arbitrator.go
@@ -129,31 +129,13 @@ type ChannelArbitratorConfig struct {
 	ChainArbitratorConfig
 }
 
-// ReportOutputType describes the type of output that is being reported
-// on.
-type ReportOutputType uint8
-
-const (
-	// ReportOutputIncomingHtlc is an incoming hash time locked contract on
-	// the commitment tx.
-	ReportOutputIncomingHtlc ReportOutputType = iota
-
-	// ReportOutputOutgoingHtlc is an outgoing hash time locked contract on
-	// the commitment tx.
-	ReportOutputOutgoingHtlc
-
-	// ReportOutputUnencumbered is an uncontested output on the commitment
-	// transaction paying to us directly.
-	ReportOutputUnencumbered
-)
-
 // ContractReport provides a summary of a commitment tx output.
 type ContractReport struct {
 	// Outpoint is the final output that will be swept back to the wallet.
 	Outpoint wire.OutPoint
 
-	// Type indicates the type of the reported output.
-	Type ReportOutputType
+	// Incoming indicates whether the htlc was incoming to this channel.
+	Incoming bool
 
 	// Amount is the final value that will be swept in back to the wallet.
 	Amount btcutil.Amount
@@ -877,6 +859,27 @@ func (c *ChannelArbitrator) stateStep(
 			break
 		}
 
+		// If we've have broadcast the commitment transaction, we send
+		// our commitment output for incubation, but only if it wasn't
+		// trimmed.  We'll need to wait for a CSV timeout before we can
+		// reclaim the funds.
+		commitRes := contractResolutions.CommitResolution
+		if commitRes != nil && commitRes.MaturityDelay > 0 {
+			log.Infof("ChannelArbitrator(%v): sending commit "+
+				"output for incubation", c.cfg.ChanPoint)
+
+			err = c.cfg.IncubateOutputs(
+				c.cfg.ChanPoint, commitRes,
+				nil, nil, triggerHeight,
+			)
+			if err != nil {
+				// TODO(roasbeef): check for AlreadyExists errors
+				log.Errorf("unable to incubate commitment "+
+					"output: %v", err)
+				return StateError, closeTx, err
+			}
+		}
+
 		// Now that we know we'll need to act, we'll process the htlc
 		// actions, wen create the structures we need to resolve all
 		// outstanding contracts.
diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go
index 62a455f1d..3b7f3b855 100644
--- a/contractcourt/commit_sweep_resolver.go
+++ b/contractcourt/commit_sweep_resolver.go
@@ -2,12 +2,9 @@ package contractcourt
 
 import (
 	"encoding/binary"
-	"fmt"
 	"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"
@@ -39,13 +36,6 @@ type commitSweepResolver struct {
 	// 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
 }
 
@@ -54,17 +44,12 @@ func newCommitSweepResolver(res lnwallet.CommitOutputResolution,
 	broadcastHeight uint32,
 	chanPoint wire.OutPoint, resCfg ResolverConfig) *commitSweepResolver {
 
-	r := &commitSweepResolver{
+	return &commitSweepResolver{
 		contractResolverKit: *newContractResolverKit(resCfg),
 		commitResolution:    res,
 		broadcastHeight:     broadcastHeight,
 		chanPoint:           chanPoint,
 	}
-
-	r.initLogger(r)
-	r.initReport()
-
-	return r
 }
 
 // ResolverKey returns an identifier which should be globally unique for this
@@ -74,63 +59,6 @@ func (c *commitSweepResolver) ResolverKey() []byte {
 	return key[:]
 }
 
-// waitForHeight registers for block notifications and waits for the provided
-// block height to be reached.
-func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error {
-	// Register for block epochs. After registration, the current height
-	// will be sent on the channel immediately.
-	blockEpochs, err := c.Notifier.RegisterBlockEpochNtfn(nil)
-	if err != nil {
-		return err
-	}
-	defer blockEpochs.Cancel()
-
-	for {
-		select {
-		case newBlock, ok := <-blockEpochs.Epochs:
-			if !ok {
-				return errResolverShuttingDown
-			}
-			height := newBlock.Height
-			if height >= int32(waitHeight) {
-				return nil
-			}
-
-		case <-c.quit:
-			return errResolverShuttingDown
-		}
-	}
-}
-
-// getCommitTxConfHeight waits for confirmation of the commitment tx and returns
-// the confirmation height.
-func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) {
-	txID := c.commitResolution.SelfOutPoint.Hash
-	signDesc := c.commitResolution.SelfOutputSignDesc
-	pkScript := signDesc.Output.PkScript
-	const confDepth = 1
-	confChan, err := c.Notifier.RegisterConfirmationsNtfn(
-		&txID, pkScript, confDepth, c.broadcastHeight,
-	)
-	if err != nil {
-		return 0, err
-	}
-	defer confChan.Cancel()
-
-	select {
-	case txConfirmation, ok := <-confChan.Confirmed:
-		if !ok {
-			return 0, fmt.Errorf("cannot get confirmation "+
-				"for commit tx %v", txID)
-		}
-
-		return txConfirmation.BlockHeight, nil
-
-	case <-c.quit:
-		return 0, errResolverShuttingDown
-	}
-}
-
 // Resolve instructs the contract resolver to resolve the output on-chain. Once
 // the output has been *fully* resolved, the function should return immediately
 // with a nil ContractResolver value for the first return value.  In the case
@@ -144,100 +72,159 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
 		return nil, nil
 	}
 
-	confHeight, err := c.getCommitTxConfHeight()
+	// First, we'll register for a notification once the commitment output
+	// itself has been confirmed.
+	//
+	// TODO(roasbeef): instead sweep asap if remote commit? yeh
+	commitTXID := c.commitResolution.SelfOutPoint.Hash
+	sweepScript := c.commitResolution.SelfOutputSignDesc.Output.PkScript
+	confNtfn, err := c.Notifier.RegisterConfirmationsNtfn(
+		&commitTXID, sweepScript, 1, c.broadcastHeight,
+	)
 	if err != nil {
 		return nil, err
 	}
 
-	unlockHeight := confHeight + c.commitResolution.MaturityDelay
+	log.Debugf("%T(%v): waiting for commit tx to confirm", c, c.chanPoint)
+
+	select {
+	case _, ok := <-confNtfn.Confirmed:
+		if !ok {
+			return nil, errResolverShuttingDown
+		}
 
-	c.log.Debugf("commit conf_height=%v, unlock_height=%v",
-		confHeight, unlockHeight)
+	case <-c.quit:
+		return nil, errResolverShuttingDown
+	}
 
-	// Update report now that we learned the confirmation height.
-	c.reportLock.Lock()
-	c.currentReport.MaturityHeight = unlockHeight
-	c.reportLock.Unlock()
+	// We're dealing with our commitment transaction if the delay on the
+	// resolution isn't zero.
+	isLocalCommitTx := c.commitResolution.MaturityDelay != 0
 
-	// If there is a csv delay, we'll wait for that.
-	if c.commitResolution.MaturityDelay > 0 {
-		c.log.Debugf("waiting for csv lock to expire at height %v",
-			unlockHeight)
+	if !isLocalCommitTx {
+		// There're two types of commitments, those that have tweaks
+		// for the remote key (us in this case), and those that don't.
+		// We'll rely on the presence of the commitment tweak to to
+		// discern which type of commitment this is.
+		var witnessType input.WitnessType
+		if c.commitResolution.SelfOutputSignDesc.SingleTweak == nil {
+			witnessType = input.CommitSpendNoDelayTweakless
+		} else {
+			witnessType = input.CommitmentNoDelay
+		}
 
-		// We only need to wait for the block before the block that
-		// unlocks the spend path.
-		err := c.waitForHeight(unlockHeight - 1)
+		// We'll craft an input with all the information required for
+		// the sweeper to create a fully valid sweeping transaction to
+		// recover these coins.
+		inp := input.MakeBaseInput(
+			&c.commitResolution.SelfOutPoint,
+			witnessType,
+			&c.commitResolution.SelfOutputSignDesc,
+			c.broadcastHeight,
+		)
+
+		// With our input constructed, we'll now offer it to the
+		// sweeper.
+		log.Infof("%T(%v): sweeping commit output", c, c.chanPoint)
+
+		feePref := sweep.FeePreference{ConfTarget: commitOutputConfTarget}
+		resultChan, err := c.Sweeper.SweepInput(&inp, feePref)
 		if err != nil {
+			log.Errorf("%T(%v): unable to sweep input: %v",
+				c, c.chanPoint, err)
+
 			return nil, err
 		}
-	}
 
-	// We're dealing with our commitment transaction if the delay on the
-	// resolution isn't zero.
-	isLocalCommitTx := c.commitResolution.MaturityDelay != 0
+		// Sweeper is going to join this input with other inputs if
+		// possible and publish the sweep tx. When the sweep tx
+		// confirms, it signals us through the result channel with the
+		// outcome. Wait for this to happen.
+		select {
+		case sweepResult := <-resultChan:
+			if sweepResult.Err != nil {
+				log.Errorf("%T(%v): unable to sweep input: %v",
+					c, c.chanPoint, sweepResult.Err)
 
-	// There're two types of commitments, those that have tweaks
-	// for the remote key (us in this case), and those that don't.
-	// We'll rely on the presence of the commitment tweak to to
-	// discern which type of commitment this is.
-	var witnessType input.WitnessType
-	switch {
-	case isLocalCommitTx:
-		witnessType = input.CommitmentTimeLock
-	case c.commitResolution.SelfOutputSignDesc.SingleTweak == nil:
-		witnessType = input.CommitSpendNoDelayTweakless
-	default:
-		witnessType = input.CommitmentNoDelay
+				return nil, sweepResult.Err
+			}
+
+			log.Infof("ChannelPoint(%v) commit tx is fully resolved by "+
+				"sweep tx: %v", c.chanPoint, sweepResult.Tx.TxHash())
+		case <-c.quit:
+			return nil, errResolverShuttingDown
+		}
+
+		c.resolved = true
+		return nil, c.Checkpoint(c)
 	}
 
-	// We'll craft an input with all the information required for
-	// the sweeper to create a fully valid sweeping transaction to
-	// recover these coins.
-	inp := input.NewCsvInput(
+	// Otherwise we are dealing with a local commitment transaction and the
+	// output we need to sweep has been sent to the nursery for incubation.
+	// In this case, we'll wait until the commitment output has been spent.
+	spendNtfn, err := c.Notifier.RegisterSpendNtfn(
 		&c.commitResolution.SelfOutPoint,
-		witnessType,
-		&c.commitResolution.SelfOutputSignDesc,
+		c.commitResolution.SelfOutputSignDesc.Output.PkScript,
 		c.broadcastHeight,
-		c.commitResolution.MaturityDelay,
 	)
-
-	// With our input constructed, we'll now offer it to the
-	// sweeper.
-	c.log.Infof("sweeping commit output")
-
-	feePref := sweep.FeePreference{ConfTarget: commitOutputConfTarget}
-	resultChan, err := c.Sweeper.SweepInput(inp, feePref)
 	if err != nil {
-		c.log.Errorf("unable to sweep input: %v", err)
-
 		return nil, err
 	}
 
-	// Sweeper is going to join this input with other inputs if
-	// possible and publish the sweep tx. When the sweep tx
-	// confirms, it signals us through the result channel with the
-	// outcome. Wait for this to happen.
-	select {
-	case sweepResult := <-resultChan:
-		if sweepResult.Err != nil {
-			c.log.Errorf("unable to sweep input: %v",
-				sweepResult.Err)
+	log.Infof("%T(%v): waiting for commit output to be swept", c,
+		c.chanPoint)
 
-			return nil, sweepResult.Err
+	var sweepTx *wire.MsgTx
+	select {
+	case commitSpend, ok := <-spendNtfn.Spend:
+		if !ok {
+			return nil, errResolverShuttingDown
 		}
 
-		c.log.Infof("commit tx fully resolved by sweep tx: %v",
-			sweepResult.Tx.TxHash())
+		// Once we detect the commitment output has been spent,
+		// we'll extract the spending transaction itself, as we
+		// now consider this to be our sweep transaction.
+		sweepTx = commitSpend.SpendingTx
+
+		log.Infof("%T(%v): commit output swept by txid=%v",
+			c, c.chanPoint, sweepTx.TxHash())
+
+		if err := c.Checkpoint(c); err != nil {
+			log.Errorf("unable to Checkpoint: %v", err)
+			return nil, err
+		}
 	case <-c.quit:
 		return nil, errResolverShuttingDown
 	}
 
-	// Funds have been swept and balance is no longer in limbo.
-	c.reportLock.Lock()
-	c.currentReport.RecoveredBalance = c.currentReport.LimboBalance
-	c.currentReport.LimboBalance = 0
-	c.reportLock.Unlock()
+	log.Infof("%T(%v): waiting for commit sweep txid=%v conf", c, c.chanPoint,
+		sweepTx.TxHash())
 
+	// Now we'll wait until the sweeping transaction has been fully
+	// confirmed.  Once it's confirmed, we can mark this contract resolved.
+	sweepTXID := sweepTx.TxHash()
+	sweepingScript := sweepTx.TxOut[0].PkScript
+	confNtfn, err = c.Notifier.RegisterConfirmationsNtfn(
+		&sweepTXID, sweepingScript, 1, c.broadcastHeight,
+	)
+	if err != nil {
+		return nil, err
+	}
+	select {
+	case confInfo, ok := <-confNtfn.Confirmed:
+		if !ok {
+			return nil, errResolverShuttingDown
+		}
+
+		log.Infof("ChannelPoint(%v) commit tx is fully resolved, at height: %v",
+			c.chanPoint, confInfo.BlockHeight)
+
+	case <-c.quit:
+		return nil, errResolverShuttingDown
+	}
+
+	// Once the transaction has received a sufficient number of
+	// confirmations, we'll mark ourselves as fully resolved and exit.
 	c.resolved = true
 	return nil, c.Checkpoint(c)
 }
@@ -321,43 +308,9 @@ func newCommitSweepResolverFromReader(r io.Reader, resCfg ResolverConfig) (
 	// removed this, but keep in mind that this data may still be present in
 	// the database.
 
-	c.initLogger(c)
-	c.initReport()
-
 	return c, nil
 }
 
-// report returns a report on the resolution state of the contract.
-func (c *commitSweepResolver) report() *ContractReport {
-	c.reportLock.Lock()
-	defer c.reportLock.Unlock()
-
-	copy := c.currentReport
-	return &copy
-}
-
-// initReport initializes the pending channels report for this resolver.
-func (c *commitSweepResolver) initReport() {
-	amt := btcutil.Amount(
-		c.commitResolution.SelfOutputSignDesc.Output.Value,
-	)
-
-	// Set the initial report. All fields are filled in, except for the
-	// maturity height which remains 0 until Resolve() is executed.
-	//
-	// TODO(joostjager): Resolvers only activate after the commit tx
-	// confirms. With more refactoring in channel arbitrator, it would be
-	// possible to make the confirmation height part of ResolverConfig and
-	// populate MaturityHeight here.
-	c.currentReport = ContractReport{
-		Outpoint:         c.commitResolution.SelfOutPoint,
-		Type:             ReportOutputUnencumbered,
-		Amount:           amt,
-		LimboBalance:     amt,
-		RecoveredBalance: 0,
-	}
-}
-
 // A compile time assertion to ensure commitSweepResolver meets the
 // ContractResolver interface.
-var _ reportingContractResolver = (*commitSweepResolver)(nil)
+var _ ContractResolver = (*commitSweepResolver)(nil)
diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go
index a5fe119ad..2cda229a8 100644
--- a/contractcourt/contract_resolvers.go
+++ b/contractcourt/contract_resolvers.go
@@ -3,12 +3,9 @@ package contractcourt
 import (
 	"encoding/binary"
 	"errors"
-	"fmt"
 	"io"
 
 	"github.com/btcsuite/btcd/wire"
-	"github.com/btcsuite/btclog"
-	"github.com/lightningnetwork/lnd/build"
 	"github.com/lightningnetwork/lnd/channeldb"
 )
 
@@ -96,8 +93,6 @@ type ResolverConfig struct {
 type contractResolverKit struct {
 	ResolverConfig
 
-	log btclog.Logger
-
 	quit chan struct{}
 }
 
@@ -109,12 +104,6 @@ func newContractResolverKit(cfg ResolverConfig) *contractResolverKit {
 	}
 }
 
-// initLogger initializes the resolver-specific logger.
-func (r *contractResolverKit) initLogger(resolver ContractResolver) {
-	logPrefix := fmt.Sprintf("%T(%v):", resolver, r.ChanPoint)
-	r.log = build.NewPrefixLog(logPrefix, log)
-}
-
 var (
 	// errResolverShuttingDown is returned when the resolver stops
 	// progressing because it received the quit signal.
diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go
index c13c52e87..73d370632 100644
--- a/contractcourt/htlc_success_resolver.go
+++ b/contractcourt/htlc_success_resolver.go
@@ -211,7 +211,7 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
 			h, h.htlc.RHash[:])
 
 		err := h.IncubateOutputs(
-			h.ChanPoint, nil, &h.htlcResolution,
+			h.ChanPoint, nil, nil, &h.htlcResolution,
 			h.broadcastHeight,
 		)
 		if err != nil {
diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go
index 439babc4e..2469f5318 100644
--- a/contractcourt/htlc_timeout_resolver.go
+++ b/contractcourt/htlc_timeout_resolver.go
@@ -247,7 +247,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
 			h.htlcResolution.ClaimOutpoint)
 
 		err := h.IncubateOutputs(
-			h.ChanPoint, &h.htlcResolution, nil,
+			h.ChanPoint, nil, &h.htlcResolution, nil,
 			h.broadcastHeight,
 		)
 		if err != nil {
diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go
index 682eb2acb..a88477856 100644
--- a/contractcourt/interfaces.go
+++ b/contractcourt/interfaces.go
@@ -3,14 +3,11 @@ package contractcourt
 import (
 	"io"
 
-	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/htlcswitch/hop"
-	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/invoices"
 	"github.com/lightningnetwork/lnd/lntypes"
 	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/lightningnetwork/lnd/sweep"
 )
 
 // Registry is an interface which represents the invoice registry.
@@ -39,16 +36,3 @@ type OnionProcessor interface {
 	// the passed io.Reader instance.
 	ReconstructHopIterator(r io.Reader, rHash []byte) (hop.Iterator, error)
 }
-
-// UtxoSweeper defines the sweep functions that contract court requires.
-type UtxoSweeper interface {
-	// SweepInput sweeps inputs back into the wallet.
-	SweepInput(input input.Input,
-		feePreference sweep.FeePreference) (chan sweep.Result, error)
-
-	// CreateSweepTx accepts a list of inputs and signs and generates a txn
-	// that spends from them. This method also makes an accurate fee
-	// estimate before generating the required witnesses.
-	CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
-		currentBlockHeight uint32) (*wire.MsgTx, error)
-}
diff --git a/htlcswitch/failure.go b/htlcswitch/failure.go
index 0d817a07d..4526e3560 100644
--- a/htlcswitch/failure.go
+++ b/htlcswitch/failure.go
@@ -30,14 +30,10 @@ type ForwardingError struct {
 // returned.
 func (f *ForwardingError) Error() string {
 	if f.ExtraMsg == "" {
-		return fmt.Sprintf(
-			"%v@%v", f.FailureMessage, f.FailureSourceIdx,
-		)
+		return fmt.Sprintf("%v", f.FailureMessage)
 	}
 
-	return fmt.Sprintf(
-		"%v@%v: %v", f.FailureMessage, f.FailureSourceIdx, f.ExtraMsg,
-	)
+	return fmt.Sprintf("%v: %v", f.FailureMessage, f.ExtraMsg)
 }
 
 // ErrorDecrypter is an interface that is used to decrypt the onion encrypted
diff --git a/input/input.go b/input/input.go
index 7f5593ec5..a63048cbf 100644
--- a/input/input.go
+++ b/input/input.go
@@ -44,11 +44,10 @@ type Input interface {
 }
 
 type inputKit struct {
-	outpoint        wire.OutPoint
-	witnessType     WitnessType
-	signDesc        SignDescriptor
-	heightHint      uint32
-	blockToMaturity uint32
+	outpoint    wire.OutPoint
+	witnessType WitnessType
+	signDesc    SignDescriptor
+	heightHint  uint32
 }
 
 // OutPoint returns the breached output's identifier that is to be included as
@@ -75,13 +74,6 @@ func (i *inputKit) HeightHint() uint32 {
 	return i.heightHint
 }
 
-// BlocksToMaturity returns the relative timelock, as a number of blocks, that
-// must be built on top of the confirmation height before the output can be
-// spent. For non-CSV locked inputs this is always zero.
-func (i *inputKit) BlocksToMaturity() uint32 {
-	return i.blockToMaturity
-}
-
 // BaseInput contains all the information needed to sweep a basic output
 // (CSV/CLTV/no time lock)
 type BaseInput struct {
@@ -115,23 +107,6 @@ func NewBaseInput(outpoint *wire.OutPoint, witnessType WitnessType,
 	return &input
 }
 
-// NewCsvInput assembles a new csv-locked input that can be used to
-// construct a sweep transaction.
-func NewCsvInput(outpoint *wire.OutPoint, witnessType WitnessType,
-	signDescriptor *SignDescriptor, heightHint uint32,
-	blockToMaturity uint32) *BaseInput {
-
-	return &BaseInput{
-		inputKit{
-			outpoint:        *outpoint,
-			witnessType:     witnessType,
-			signDesc:        *signDescriptor,
-			heightHint:      heightHint,
-			blockToMaturity: blockToMaturity,
-		},
-	}
-}
-
 // CraftInputScript returns a valid set of input scripts allowing this output
 // to be spent. The returned input scripts should target the input at location
 // txIndex within the passed transaction. The input scripts generated by this
@@ -144,6 +119,13 @@ func (bi *BaseInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
 	return witnessFunc(txn, hashCache, txinIdx)
 }
 
+// BlocksToMaturity returns the relative timelock, as a number of blocks, that
+// must be built on top of the confirmation height before the output can be
+// spent. For non-CSV locked inputs this is always zero.
+func (bi *BaseInput) BlocksToMaturity() uint32 {
+	return 0
+}
+
 // HtlcSucceedInput constitutes a sweep input that needs a pre-image. The input
 // is expected to reside on the commitment tx of the remote party and should
 // not be a second level tx output.
@@ -193,6 +175,13 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
 	}, nil
 }
 
+// BlocksToMaturity returns the relative timelock, as a number of blocks, that
+// must be built on top of the confirmation height before the output can be
+// spent.
+func (h *HtlcSucceedInput) BlocksToMaturity() uint32 {
+	return 0
+}
+
 // Compile-time constraints to ensure each input struct implement the Input
 // interface.
 var _ Input = (*BaseInput)(nil)
diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go
index a8017eaa3..695a0d498 100644
--- a/routing/payment_lifecycle.go
+++ b/routing/payment_lifecycle.go
@@ -341,8 +341,8 @@ func (p *paymentLifecycle) sendPaymentAttempt(firstHop lnwire.ShortChannelID,
 		return err
 	}
 
-	log.Debugf("Payment %x (pid=%v) successfully sent to switch, route: %v",
-		p.payment.PaymentHash, p.attempt.PaymentID, &p.attempt.Route)
+	log.Debugf("Payment %x (pid=%v) successfully sent to switch",
+		p.payment.PaymentHash, p.attempt.PaymentID)
 
 	return nil
 }
diff --git a/routing/route/route.go b/routing/route/route.go
index 6858f3b8d..62511c3f1 100644
--- a/routing/route/route.go
+++ b/routing/route/route.go
@@ -340,19 +340,15 @@ func (r *Route) ToSphinxPath() (*sphinx.PaymentPath, error) {
 func (r *Route) String() string {
 	var b strings.Builder
 
-	amt := r.TotalAmount
 	for i, hop := range r.Hops {
 		if i > 0 {
-			b.WriteString(" -> ")
+			b.WriteString(",")
 		}
-		b.WriteString(fmt.Sprintf("%v (%v)",
-			strconv.FormatUint(hop.ChannelID, 10),
-			amt,
-		))
-		amt = hop.AmtToForward
+		b.WriteString(strconv.FormatUint(hop.ChannelID, 10))
 	}
 
-	return fmt.Sprintf("%v, cltv %v",
-		b.String(), r.TotalTimeLock,
+	return fmt.Sprintf("amt=%v, fees=%v, tl=%v, chans=%v",
+		r.TotalAmount-r.TotalFees(), r.TotalFees(), r.TotalTimeLock,
+		b.String(),
 	)
 }
diff --git a/routing/router.go b/routing/router.go
index 049011a90..75db5a9d2 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -533,7 +533,7 @@ func (r *ChannelRouter) Start() error {
 				PaymentHash: payment.Info.PaymentHash,
 			}
 
-			_, _, err = r.sendPayment(payment.Attempt, lPayment, paySession)
+			_, _, err := r.sendPayment(payment.Attempt, lPayment, paySession)
 			if err != nil {
 				log.Errorf("Resuming payment with hash %v "+
 					"failed: %v.", payment.Info.PaymentHash, err)
diff --git a/rpcserver.go b/rpcserver.go
index 13cb66b15..c3bc50a04 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -34,7 +34,6 @@ import (
 	"github.com/lightningnetwork/lnd/chanbackup"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channelnotifier"
-	"github.com/lightningnetwork/lnd/contractcourt"
 	"github.com/lightningnetwork/lnd/discovery"
 	"github.com/lightningnetwork/lnd/htlcswitch"
 	"github.com/lightningnetwork/lnd/input"
@@ -2452,51 +2451,23 @@ func (r *rpcServer) arbitratorPopulateForceCloseResp(chanPoint *wire.OutPoint,
 	reports := arbitrator.Report()
 
 	for _, report := range reports {
-		switch report.Type {
-
-		// For a direct output, populate/update the top level
-		// response properties.
-		case contractcourt.ReportOutputUnencumbered:
-			// Populate the maturity height fields for the direct
-			// commitment output to us.
-			forceClose.MaturityHeight = report.MaturityHeight
-
-			// If the transaction has been confirmed, then we can
-			// compute how many blocks it has left.
-			if forceClose.MaturityHeight != 0 {
-				forceClose.BlocksTilMaturity =
-					int32(forceClose.MaturityHeight) -
-						currentHeight
-			}
-
-		// Add htlcs to the PendingHtlcs response property.
-		case contractcourt.ReportOutputIncomingHtlc,
-			contractcourt.ReportOutputOutgoingHtlc:
-
-			incoming := report.Type == contractcourt.ReportOutputIncomingHtlc
-			htlc := &lnrpc.PendingHTLC{
-				Incoming:       incoming,
-				Amount:         int64(report.Amount),
-				Outpoint:       report.Outpoint.String(),
-				MaturityHeight: report.MaturityHeight,
-				Stage:          report.Stage,
-			}
-
-			if htlc.MaturityHeight != 0 {
-				htlc.BlocksTilMaturity =
-					int32(htlc.MaturityHeight) - currentHeight
-			}
-
-			forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc)
+		htlc := &lnrpc.PendingHTLC{
+			Incoming:       report.Incoming,
+			Amount:         int64(report.Amount),
+			Outpoint:       report.Outpoint.String(),
+			MaturityHeight: report.MaturityHeight,
+			Stage:          report.Stage,
+		}
 
-		default:
-			return fmt.Errorf("unknown report output type: %v",
-				report.Type)
+		if htlc.MaturityHeight != 0 {
+			htlc.BlocksTilMaturity =
+				int32(htlc.MaturityHeight) - currentHeight
 		}
 
 		forceClose.LimboBalance += int64(report.LimboBalance)
 		forceClose.RecoveredBalance += int64(report.RecoveredBalance)
 
+		forceClose.PendingHtlcs = append(forceClose.PendingHtlcs, htlc)
 	}
 
 	return nil
@@ -2527,6 +2498,15 @@ func (r *rpcServer) nurseryPopulateForceCloseResp(chanPoint *wire.OutPoint,
 	// wallet.
 	forceClose.LimboBalance = int64(nurseryInfo.limboBalance)
 	forceClose.RecoveredBalance = int64(nurseryInfo.recoveredBalance)
+	forceClose.MaturityHeight = nurseryInfo.maturityHeight
+
+	// If the transaction has been confirmed, then we can compute how many
+	// blocks it has left.
+	if forceClose.MaturityHeight != 0 {
+		forceClose.BlocksTilMaturity =
+			int32(forceClose.MaturityHeight) -
+				currentHeight
+	}
 
 	for _, htlcReport := range nurseryInfo.htlcs {
 		// TODO(conner) set incoming flag appropriately after handling
diff --git a/server.go b/server.go
index c4c997f56..ccdd13ae0 100644
--- a/server.go
+++ b/server.go
@@ -847,6 +847,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
 			return nil
 		},
 		IncubateOutputs: func(chanPoint wire.OutPoint,
+			commitRes *lnwallet.CommitOutputResolution,
 			outHtlcRes *lnwallet.OutgoingHtlcResolution,
 			inHtlcRes *lnwallet.IncomingHtlcResolution,
 			broadcastHeight uint32) error {
@@ -863,7 +864,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
 			}
 
 			return s.utxoNursery.IncubateOutputs(
-				chanPoint, outRes, inRes,
+				chanPoint, commitRes, outRes, inRes,
 				broadcastHeight,
 			)
 		},
diff --git a/utxonursery.go b/utxonursery.go
index 2e749130f..b08cf0e2a 100644
--- a/utxonursery.go
+++ b/utxonursery.go
@@ -330,6 +330,7 @@ func (u *utxoNursery) Stop() error {
 // they're CLTV absolute time locked, or if they're CSV relative time locked.
 // Once all outputs reach maturity, they'll be swept back into the wallet.
 func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
+	commitResolution *lnwallet.CommitOutputResolution,
 	outgoingHtlcs []lnwallet.OutgoingHtlcResolution,
 	incomingHtlcs []lnwallet.IncomingHtlcResolution,
 	broadcastHeight uint32) error {
@@ -351,6 +352,8 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
 
 	numHtlcs := len(incomingHtlcs) + len(outgoingHtlcs)
 	var (
+		hasCommit bool
+
 		// Kid outputs can be swept after an initial confirmation
 		// followed by a maturity period.Baby outputs are two stage and
 		// will need to wait for an absolute time out to reach a
@@ -361,6 +364,28 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
 
 	// 1. Build all the spendable outputs that we will try to incubate.
 
+	// It could be that our to-self output was below the dust limit. In
+	// that case the commit resolution would be nil and we would not have
+	// that output to incubate.
+	if commitResolution != nil {
+		hasCommit = true
+		selfOutput := makeKidOutput(
+			&commitResolution.SelfOutPoint,
+			&chanPoint,
+			commitResolution.MaturityDelay,
+			input.CommitmentTimeLock,
+			&commitResolution.SelfOutputSignDesc,
+			0,
+		)
+
+		// We'll skip any zero valued outputs as this indicates we
+		// don't have a settled balance within the commitment
+		// transaction.
+		if selfOutput.Amount() > 0 {
+			kidOutputs = append(kidOutputs, selfOutput)
+		}
+	}
+
 	// TODO(roasbeef): query and see if we already have, if so don't add?
 
 	// For each incoming HTLC, we'll register a kid output marked as a
@@ -411,8 +436,8 @@ func (u *utxoNursery) IncubateOutputs(chanPoint wire.OutPoint,
 	//  * need ability to cancel in the case that we learn of pre-image or
 	//    remote party pulls
 
-	utxnLog.Infof("Incubating Channel(%s) num-htlcs=%d",
-		chanPoint, numHtlcs)
+	utxnLog.Infof("Incubating Channel(%s) has-commit=%v, num-htlcs=%d",
+		chanPoint, hasCommit, numHtlcs)
 
 	u.mu.Lock()
 	defer u.mu.Unlock()
@@ -513,6 +538,8 @@ func (u *utxoNursery) NurseryReport(
 				// Preschool outputs are awaiting the
 				// confirmation of the commitment transaction.
 				switch kid.WitnessType() {
+				case input.CommitmentTimeLock:
+					report.AddLimboCommitment(&kid)
 
 				case input.HtlcAcceptedSuccessSecondLevel:
 					// An HTLC output on our commitment transaction
@@ -534,6 +561,11 @@ func (u *utxoNursery) NurseryReport(
 				// We can distinguish them via their witness
 				// types.
 				switch kid.WitnessType() {
+				case input.CommitmentTimeLock:
+					// The commitment transaction has been
+					// confirmed, and we are waiting the CSV
+					// delay to expire.
+					report.AddLimboCommitment(&kid)
 
 				case input.HtlcOfferedRemoteTimeout:
 					// This is an HTLC output on the
@@ -558,6 +590,11 @@ func (u *utxoNursery) NurseryReport(
 				// will contribute towards the recovered
 				// balance.
 				switch kid.WitnessType() {
+				case input.CommitmentTimeLock:
+					// The commitment output was
+					// successfully swept back into a
+					// regular p2wkh output.
+					report.AddRecoveredCommitment(&kid)
 
 				case input.HtlcAcceptedSuccessSecondLevel:
 					fallthrough
@@ -1034,6 +1071,11 @@ type contractMaturityReport struct {
 	// recoveredBalance is the total value that has been successfully swept
 	// back to the user's wallet.
 	recoveredBalance btcutil.Amount
+
+	// maturityHeight is the absolute block height that this output will
+	// mature at.
+	maturityHeight uint32
+
 	// htlcs records a maturity report for each htlc output in this channel.
 	htlcs []htlcMaturityReport
 }
@@ -1058,6 +1100,26 @@ type htlcMaturityReport struct {
 	stage uint32
 }
 
+// AddLimboCommitment adds an incubating commitment output to maturity
+// report's htlcs, and contributes its amount to the limbo balance.
+func (c *contractMaturityReport) AddLimboCommitment(kid *kidOutput) {
+	c.limboBalance += kid.Amount()
+
+	// If the confirmation height is set, then this means the contract has
+	// been confirmed, and we know the final maturity height.
+	if kid.ConfHeight() != 0 {
+		c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight()
+	}
+}
+
+// AddRecoveredCommitment adds a graduated commitment output to maturity
+// report's  htlcs, and contributes its amount to the recovered balance.
+func (c *contractMaturityReport) AddRecoveredCommitment(kid *kidOutput) {
+	c.recoveredBalance += kid.Amount()
+
+	c.maturityHeight = kid.BlocksToMaturity() + kid.ConfHeight()
+}
+
 // AddLimboStage1TimeoutHtlc adds an htlc crib output to the maturity report's
 // htlcs, and contributes its amount to the limbo balance.
 func (c *contractMaturityReport) AddLimboStage1TimeoutHtlc(baby *babyOutput) {


Test patch:
diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go
index 466662c13..f0e07628b 100644
--- a/contractcourt/chain_watcher_test.go
+++ b/contractcourt/chain_watcher_test.go
@@ -26,7 +26,6 @@ func (m *mockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, _ []byte,
 	heightHint uint32) (*chainntnfs.ConfirmationEvent, error) {
 	return &chainntnfs.ConfirmationEvent{
 		Confirmed: m.confChan,
-		Cancel:    func() {},
 	}, nil
 }
 
diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go
index af4681a30..599028736 100644
--- a/contractcourt/channel_arbitrator_test.go
+++ b/contractcourt/channel_arbitrator_test.go
@@ -301,7 +301,7 @@ func createTestChannelArbitrator(t *testing.T, log ArbitratorLog) (*chanArbTestC
 			spendChan: make(chan *chainntnfs.SpendDetail),
 			confChan:  make(chan *chainntnfs.TxConfirmation),
 		},
-		IncubateOutputs: func(wire.OutPoint,
+		IncubateOutputs: func(wire.OutPoint, *lnwallet.CommitOutputResolution,
 			*lnwallet.OutgoingHtlcResolution,
 			*lnwallet.IncomingHtlcResolution, uint32) error {
 
diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go
deleted file mode 100644
index 957e0802c..000000000
--- a/contractcourt/commit_sweep_resolver_test.go
+++ /dev/null
@@ -1,225 +0,0 @@
-package contractcourt
-
-import (
-	"reflect"
-	"testing"
-	"time"
-
-	"github.com/btcsuite/btcd/wire"
-	"github.com/btcsuite/btcutil"
-	"github.com/lightningnetwork/lnd/chainntnfs"
-	"github.com/lightningnetwork/lnd/input"
-	"github.com/lightningnetwork/lnd/lnwallet"
-	"github.com/lightningnetwork/lnd/sweep"
-)
-
-type commitSweepResolverTestContext struct {
-	resolver           *commitSweepResolver
-	notifier           *mockNotifier
-	sweeper            *mockSweeper
-	resolverResultChan chan resolveResult
-	t                  *testing.T
-}
-
-func newCommitSweepResolverTestContext(t *testing.T,
-	resolution *lnwallet.CommitOutputResolution) *commitSweepResolverTestContext {
-
-	notifier := &mockNotifier{
-		epochChan: make(chan *chainntnfs.BlockEpoch),
-		spendChan: make(chan *chainntnfs.SpendDetail),
-		confChan:  make(chan *chainntnfs.TxConfirmation),
-	}
-
-	sweeper := newMockSweeper()
-
-	checkPointChan := make(chan struct{}, 1)
-
-	chainCfg := ChannelArbitratorConfig{
-		ChainArbitratorConfig: ChainArbitratorConfig{
-			Notifier: notifier,
-			Sweeper:  sweeper,
-		},
-	}
-
-	cfg := ResolverConfig{
-		ChannelArbitratorConfig: chainCfg,
-		Checkpoint: func(_ ContractResolver) error {
-			checkPointChan <- struct{}{}
-			return nil
-		},
-	}
-
-	resolver := newCommitSweepResolver(
-		*resolution, 0, wire.OutPoint{}, cfg,
-	)
-
-	return &commitSweepResolverTestContext{
-		resolver: resolver,
-		notifier: notifier,
-		sweeper:  sweeper,
-		t:        t,
-	}
-}
-
-func (i *commitSweepResolverTestContext) resolve() {
-	// Start resolver.
-	i.resolverResultChan = make(chan resolveResult, 1)
-	go func() {
-		nextResolver, err := i.resolver.Resolve()
-		i.resolverResultChan <- resolveResult{
-			nextResolver: nextResolver,
-			err:          err,
-		}
-	}()
-}
-
-func (i *commitSweepResolverTestContext) notifyEpoch(height int32) {
-	i.notifier.epochChan <- &chainntnfs.BlockEpoch{
-		Height: height,
-	}
-}
-
-func (i *commitSweepResolverTestContext) waitForResult() {
-	i.t.Helper()
-
-	result := <-i.resolverResultChan
-	if result.err != nil {
-		i.t.Fatal(result.err)
-	}
-
-	if result.nextResolver != nil {
-		i.t.Fatal("expected no next resolver")
-	}
-}
-
-type mockSweeper struct {
-	sweptInputs chan input.Input
-}
-
-func newMockSweeper() *mockSweeper {
-	return &mockSweeper{
-		sweptInputs: make(chan input.Input),
-	}
-}
-
-func (s *mockSweeper) SweepInput(input input.Input,
-	feePreference sweep.FeePreference) (chan sweep.Result, error) {
-
-	s.sweptInputs <- input
-
-	result := make(chan sweep.Result, 1)
-	result <- sweep.Result{
-		Tx: &wire.MsgTx{},
-	}
-	return result, nil
-}
-
-func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
-	currentBlockHeight uint32) (*wire.MsgTx, error) {
-
-	return nil, nil
-}
-
-var _ UtxoSweeper = &mockSweeper{}
-
-// TestCommitSweepResolverNoDelay tests resolution of a direct commitment output
-// unencumbered by a time lock.
-func TestCommitSweepResolverNoDelay(t *testing.T) {
-	t.Parallel()
-	defer timeout(t)()
-
-	res := lnwallet.CommitOutputResolution{
-		SelfOutputSignDesc: input.SignDescriptor{
-			Output: &wire.TxOut{
-				Value: 100,
-			},
-		},
-	}
-
-	ctx := newCommitSweepResolverTestContext(t, &res)
-	ctx.resolve()
-
-	ctx.notifier.confChan <- &chainntnfs.TxConfirmation{}
-
-	// No csv delay, so the input should be swept immediately.
-	<-ctx.sweeper.sweptInputs
-
-	ctx.waitForResult()
-}
-
-// TestCommitSweepResolverDelay tests resolution of a direct commitment output
-// that is encumbered by a time lock.
-func TestCommitSweepResolverDelay(t *testing.T) {
-	t.Parallel()
-	defer timeout(t)()
-
-	amt := int64(100)
-	outpoint := wire.OutPoint{
-		Index: 5,
-	}
-	res := lnwallet.CommitOutputResolution{
-		SelfOutputSignDesc: input.SignDescriptor{
-			Output: &wire.TxOut{
-				Value: amt,
-			},
-		},
-		MaturityDelay: 3,
-		SelfOutPoint:  outpoint,
-	}
-
-	ctx := newCommitSweepResolverTestContext(t, &res)
-
-	report := ctx.resolver.report()
-	if !reflect.DeepEqual(report, &ContractReport{
-		Outpoint:     outpoint,
-		Type:         ReportOutputUnencumbered,
-		Amount:       btcutil.Amount(amt),
-		LimboBalance: btcutil.Amount(amt),
-	}) {
-		t.Fatal("unexpected resolver report")
-	}
-
-	ctx.resolve()
-
-	ctx.notifier.confChan <- &chainntnfs.TxConfirmation{
-		BlockHeight: testInitialBlockHeight - 1,
-	}
-
-	// Allow resolver to process confirmation.
-	time.Sleep(100 * time.Millisecond)
-
-	// Expect report to be updated.
-	report = ctx.resolver.report()
-	if report.MaturityHeight != testInitialBlockHeight+2 {
-		t.Fatal("report maturity height incorrect")
-	}
-
-	// Notify initial block height. The csv lock is still in effect, so we
-	// don't expect any sweep to happen yet.
-	ctx.notifyEpoch(testInitialBlockHeight)
-
-	select {
-	case <-ctx.sweeper.sweptInputs:
-		t.Fatal("no sweep expected")
-	case <-time.After(100 * time.Millisecond):
-	}
-
-	// A new block arrives. The commit tx confirmed at height -1 and the csv
-	// is 3, so a spend will be valid in the first block after height +1.
-	ctx.notifyEpoch(testInitialBlockHeight + 1)
-
-	<-ctx.sweeper.sweptInputs
-
-	ctx.waitForResult()
-
-	report = ctx.resolver.report()
-	if !reflect.DeepEqual(report, &ContractReport{
-		Outpoint:         outpoint,
-		Type:             ReportOutputUnencumbered,
-		Amount:           btcutil.Amount(amt),
-		RecoveredBalance: btcutil.Amount(amt),
-		MaturityHeight:   testInitialBlockHeight + 2,
-	}) {
-		t.Fatal("unexpected resolver report")
-	}
-}
diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go
index abedcc14e..2e089724f 100644
--- a/contractcourt/htlc_incoming_contest_resolver.go
+++ b/contractcourt/htlc_incoming_contest_resolver.go
@@ -293,7 +293,7 @@ func (h *htlcIncomingContestResolver) report() *ContractReport {
 
 	return &ContractReport{
 		Outpoint:       h.htlcResolution.ClaimOutpoint,
-		Type:           ReportOutputIncomingHtlc,
+		Incoming:       true,
 		Amount:         finalAmt,
 		MaturityHeight: h.htlcExpiry,
 		LimboBalance:   finalAmt,
diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go
index 850126c06..ab8ad6ec4 100644
--- a/contractcourt/htlc_incoming_resolver_test.go
+++ b/contractcourt/htlc_incoming_resolver_test.go
@@ -294,7 +294,7 @@ func (i *incomingResolverTestContext) waitForResult(expectSuccessRes bool) {
 	}
 
 	if !expectSuccessRes {
-		if i.nextResolver != nil {
+		if err != nil {
 			i.t.Fatal("expected no next resolver")
 		}
 		return
diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go
index 28d95247a..93a4adfa9 100644
--- a/contractcourt/htlc_outgoing_contest_resolver.go
+++ b/contractcourt/htlc_outgoing_contest_resolver.go
@@ -166,7 +166,7 @@ func (h *htlcOutgoingContestResolver) report() *ContractReport {
 
 	return &ContractReport{
 		Outpoint:       h.htlcResolution.ClaimOutpoint,
-		Type:           ReportOutputOutgoingHtlc,
+		Incoming:       false,
 		Amount:         finalAmt,
 		MaturityHeight: h.htlcResolution.Expiry,
 		LimboBalance:   finalAmt,
diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go
index 1e3daa59c..981cefc41 100644
--- a/contractcourt/htlc_timeout_resolver_test.go
+++ b/contractcourt/htlc_timeout_resolver_test.go
@@ -216,6 +216,7 @@ func TestHtlcTimeoutResolver(t *testing.T) {
 				Notifier:   notifier,
 				PreimageDB: witnessBeacon,
 				IncubateOutputs: func(wire.OutPoint,
+					*lnwallet.CommitOutputResolution,
 					*lnwallet.OutgoingHtlcResolution,
 					*lnwallet.IncomingHtlcResolution,
 					uint32) error {
diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go
index 06613aa1a..2ad917067 100644
--- a/htlcswitch/link_test.go
+++ b/htlcswitch/link_test.go
@@ -1248,14 +1248,8 @@ func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
 		totalTimelock).Wait(30 * time.Second)
 	if err == nil {
 		t.Fatal("error haven't been received")
-	}
-	fErr, ok := err.(*ForwardingError)
-	if !ok {
-		t.Fatalf("expected ForwardingError")
-	}
-	if _, ok = fErr.FailureMessage.(*lnwire.FailUnknownNextPeer); !ok {
-		t.Fatalf("wrong error has been received: %T",
-			fErr.FailureMessage)
+	} else if err.Error() != lnwire.CodeUnknownNextPeer.String() {
+		t.Fatalf("wrong error have been received: %v", err)
 	}
 
 	// Wait for Alice to receive the revocation.
diff --git a/utxonursery_test.go b/utxonursery_test.go
index 6b0d6de00..6671c0701 100644
--- a/utxonursery_test.go
+++ b/utxonursery_test.go
@@ -650,6 +650,7 @@ func incubateTestOutput(t *testing.T, nursery *utxoNursery,
 	// Hand off to nursery.
 	err := nursery.IncubateOutputs(
 		testChanPoint,
+		nil,
 		[]lnwallet.OutgoingHtlcResolution{*outgoingRes},
 		nil, 0,
 	)
@@ -838,6 +839,59 @@ func testNurseryOutgoingHtlcSuccessOnRemote(t *testing.T,
 	ctx.finish()
 }
 
+func TestNurseryCommitSuccessOnLocal(t *testing.T) {
+	testRestartLoop(t, testNurseryCommitSuccessOnLocal)
+}
+
+func testNurseryCommitSuccessOnLocal(t *testing.T,
+	checkStartStop func(func()) bool) {
+
+	ctx := createNurseryTestContext(t, checkStartStop)
+
+	commitRes := createCommitmentRes()
+
+	// Hand off to nursery.
+	err := ctx.nursery.IncubateOutputs(
+		testChanPoint,
+		commitRes, nil, nil, 0,
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Verify that commitment output is showing up in nursery report as
+	// limbo balance.
+	assertNurseryReport(t, ctx.nursery, 0, 0, 10000)
+
+	ctx.restart()
+
+	// Notify confirmation of the commitment tx.
+	err = ctx.notifier.ConfirmTx(&commitRes.SelfOutPoint.Hash, 124)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Wait for output to be promoted from PSCL to KNDR.
+	select {
+	case <-ctx.store.preschoolToKinderChan:
+	case <-time.After(defaultTestTimeout):
+		t.Fatalf("output not promoted to KNDR")
+	}
+
+	ctx.restart()
+
+	// Notify arrival of block where commit output CSV expires.
+	ctx.notifyEpoch(126)
+
+	// Check final sweep into wallet.
+	testSweep(t, ctx, func() {
+		// Check limbo balance after sweep publication
+		assertNurseryReport(t, ctx.nursery, 0, 0, 10000)
+	})
+
+	ctx.finish()
+}
+
 func testSweepHtlc(t *testing.T, ctx *nurseryTestContext) {
 	testSweep(t, ctx, func() {
 		// Verify stage in nursery report. HTLCs should now both still