Repository: lightningnetwork/lnd
Pull request #3964 (2020-01-27 14:41:02)

routing/test: integrated routing test
Adds an integrated routing test of probability extrapolation for untried channels. 

The difference between this test and the existing pathfinding tests, is that this test focuses on the feedback loop from result interpretation via mission control updates and probability estimation back to pathfinding. Improvements like probability extrapolation were previously only validated by reasoning, while this setup makes it possible to assert the improvement in a test and guard it for the future.

joostjager: >One offhand thought while looking at this was that it would be nice to not need so much logic in the mock, but I do not think it's avoidable.

Yes, I already tried to minimize it by adding the graph interface. I could have returned canned responses from the mock graph and not instantiate those mock nodes, but didn't chose to do so because:
* It is trickier to set up the responses so that they really mimic what would happen on the network in reality. Every attempt needs a response that takes into account the previous fails and successes.
* If we decide to extend the test later, writing down the canned responses may become even more difficult. I am thinking for example of adding timing to it.
joostjager: >Just nits from me, and not going to review the last commit if it'll be removed for the final PR.

Yes, it doesn't need to be reviewed. The intention was just to provide some background on where this is going.
joostjager: Async payments itest is consistently failing. Need to see if there is a problem with the routing tx.

Patch:
diff --git a/channeldb/db.go b/channeldb/db.go
index 696f3f008..c9410bfbb 100644
--- a/channeldb/db.go
+++ b/channeldb/db.go
@@ -556,28 +556,42 @@ func (d *DB) FetchChannel(chanPoint wire.OutPoint) (*OpenChannel, error) {
 // within the database, including pending open, fully open and channels waiting
 // for a closing transaction to confirm.
 func (d *DB) FetchAllChannels() ([]*OpenChannel, error) {
-	return fetchChannels(d)
+	var channels []*OpenChannel
+
+	// TODO(halseth): fetch all in one db tx.
+	openChannels, err := d.FetchAllOpenChannels()
+	if err != nil {
+		return nil, err
+	}
+	channels = append(channels, openChannels...)
+
+	pendingChannels, err := d.FetchPendingChannels()
+	if err != nil {
+		return nil, err
+	}
+	channels = append(channels, pendingChannels...)
+
+	waitingClose, err := d.FetchWaitingCloseChannels()
+	if err != nil {
+		return nil, err
+	}
+	channels = append(channels, waitingClose...)
+
+	return channels, nil
 }
 
 // FetchAllOpenChannels will return all channels that have the funding
 // transaction confirmed, and is not waiting for a closing transaction to be
 // confirmed.
 func (d *DB) FetchAllOpenChannels() ([]*OpenChannel, error) {
-	return fetchChannels(
-		d,
-		pendingChannelFilter(false),
-		waitingCloseFilter(false),
-	)
+	return fetchChannels(d, false, false)
 }
 
 // FetchPendingChannels will return channels that have completed the process of
 // generating and broadcasting funding transactions, but whose funding
 // transactions have yet to be confirmed on the blockchain.
 func (d *DB) FetchPendingChannels() ([]*OpenChannel, error) {
-	return fetchChannels(d,
-		pendingChannelFilter(true),
-		waitingCloseFilter(false),
-	)
+	return fetchChannels(d, true, false)
 }
 
 // FetchWaitingCloseChannels will return all channels that have been opened,
@@ -585,49 +599,25 @@ func (d *DB) FetchPendingChannels() ([]*OpenChannel, error) {
 //
 // NOTE: This includes channels that are also pending to be opened.
 func (d *DB) FetchWaitingCloseChannels() ([]*OpenChannel, error) {
-	return fetchChannels(
-		d, waitingCloseFilter(true),
-	)
-}
-
-// fetchChannelsFilter applies a filter to channels retrieved in fetchchannels.
-// A set of filters can be combined to filter across multiple dimensions.
-type fetchChannelsFilter func(channel *OpenChannel) bool
-
-// pendingChannelFilter returns a filter based on whether channels are pending
-// (ie, their funding transaction still needs to confirm). If pending is false,
-// channels with confirmed funding transactions are returned.
-func pendingChannelFilter(pending bool) fetchChannelsFilter {
-	return func(channel *OpenChannel) bool {
-		return channel.IsPending == pending
+	waitingClose, err := fetchChannels(d, false, true)
+	if err != nil {
+		return nil, err
 	}
-}
-
-// waitingCloseFilter returns a filter which filters channels based on whether
-// they are awaiting the confirmation of their closing transaction. If waiting
-// close is true, channels that have had their closing tx broadcast are
-// included. If it is false, channels that are not awaiting confirmation of
-// their close transaction are returned.
-func waitingCloseFilter(waitingClose bool) fetchChannelsFilter {
-	return func(channel *OpenChannel) bool {
-		// If the channel is in any other state than Default,
-		// then it means it is waiting to be closed.
-		channelWaitingClose :=
-			channel.ChanStatus() != ChanStatusDefault
-
-		// Include the channel if it matches the value for
-		// waiting close that we are filtering on.
-		return channelWaitingClose == waitingClose
+	pendingWaitingClose, err := fetchChannels(d, true, true)
+	if err != nil {
+		return nil, err
 	}
+
+	return append(waitingClose, pendingWaitingClose...), nil
 }
 
 // fetchChannels attempts to retrieve channels currently stored in the
-// database. It takes a set of filters which are applied to each channel to
-// obtain a set of channels with the desired set of properties. Only channels
-// which have a true value returned for *all* of the filters will be returned.
-// If no filters are provided, every channel in the open channels bucket will
-// be returned.
-func fetchChannels(d *DB, filters ...fetchChannelsFilter) ([]*OpenChannel, error) {
+// database. The pending parameter determines whether only pending channels
+// will be returned, or only open channels will be returned. The waitingClose
+// parameter determines whether only channels waiting for a closing transaction
+// to be confirmed should be returned. If no active channels exist within the
+// network, then ErrNoActiveChannels is returned.
+func fetchChannels(d *DB, pending, waitingClose bool) ([]*OpenChannel, error) {
 	var channels []*OpenChannel
 
 	err := d.View(func(tx *bbolt.Tx) error {
@@ -677,27 +667,24 @@ func fetchChannels(d *DB, filters ...fetchChannelsFilter) ([]*OpenChannel, error
 						"node_key=%x: %v", chainHash[:], k, err)
 				}
 				for _, channel := range nodeChans {
-					// includeChannel indicates whether the channel
-					// meets the criteria specified by our filters.
-					includeChannel := true
-
-					// Run through each filter and check whether the
-					// channel should be included.
-					for _, f := range filters {
-						// If the channel fails the filter, set
-						// includeChannel to false and don't bother
-						// checking the remaining filters.
-						if !f(channel) {
-							includeChannel = false
-							break
-						}
+					if channel.IsPending != pending {
+						continue
 					}
 
-					// If the channel passed every filter, include it in
-					// our set of channels.
-					if includeChannel {
-						channels = append(channels, channel)
+					// If the channel is in any other state
+					// than Default, then it means it is
+					// waiting to be closed.
+					channelWaitingClose :=
+						channel.ChanStatus() != ChanStatusDefault
+
+					// Only include it if we requested
+					// channels with the same waitingClose
+					// status.
+					if channelWaitingClose != waitingClose {
+						continue
 					}
+
+					channels = append(channels, channel)
 				}
 				return nil
 			})
diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go
index c08916d01..e9eb8e396 100644
--- a/contractcourt/chain_watcher.go
+++ b/contractcourt/chain_watcher.go
@@ -607,9 +607,8 @@ func (c *chainWatcher) closeObserver(spendNtfn *chainntnfs.SpendEvent) {
 					c.cfg.chanState.FundingOutpoint)
 
 			} else {
-				log.Infof("ChannelPoint(%v) is tweakless, "+
-					"moving to sweep directly on chain",
-					c.cfg.chanState.FundingOutpoint)
+				log.Infof("ChannelPoint(%v) is tweakless, " +
+					"moving to sweep directly on chain")
 			}
 
 			// Since we don't have the commitment stored for this
diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go
index 18dfbc0e8..086a2aee5 100644
--- a/contractcourt/interfaces.go
+++ b/contractcourt/interfaces.go
@@ -27,7 +27,7 @@ type Registry interface {
 	NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
 		expiry uint32, currentHeight int32,
 		circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-		payload invoices.Payload) (invoices.HtlcResolution, error)
+		payload invoices.Payload) (*invoices.HtlcResolution, error)
 
 	// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
 	HodlUnsubscribeAll(subscriber chan<- interface{})
diff --git a/htlcswitch/failure.go b/htlcswitch/failure.go
index 373263381..35d78590b 100644
--- a/htlcswitch/failure.go
+++ b/htlcswitch/failure.go
@@ -72,11 +72,11 @@ func (l *LinkError) WireMessage() lnwire.FailureMessage {
 func (l *LinkError) Error() string {
 	// If the link error has no failure detail, return the wire message's
 	// error.
-	if l.FailureDetail == nil {
+	if l.FailureDetail == FailureDetailNone {
 		return l.msg.Error()
 	}
 
-	return l.FailureDetail.FailureString()
+	return fmt.Sprintf("%v: %v", l.msg.Error(), l.FailureDetail)
 }
 
 // ForwardingError wraps an lnwire.FailureMessage in a struct that also
diff --git a/htlcswitch/failure_detail.go b/htlcswitch/failure_detail.go
index 341688d12..976015b8c 100644
--- a/htlcswitch/failure_detail.go
+++ b/htlcswitch/failure_detail.go
@@ -1,96 +1,65 @@
 package htlcswitch
 
-// FailureDetail is an interface implemented by failures that occur on
-// our incoming or outgoing link, or within the switch itself.
-type FailureDetail interface {
-	// FailureString returns the string representation of a failure
-	// detail.
-	FailureString() string
-}
-
-// OutgoingFailure is an enum which is used to enrich failures which occur in
-// the switch or on our outgoing link with additional metadata.
-type OutgoingFailure int
+// FailureDetail is an enum which is used to enrich failures with
+// additional information.
+type FailureDetail int
 
 const (
-	// OutgoingFailureNone is returned when the wire message contains
+	// FailureDetailNone is returned when the wire message contains
 	// sufficient information.
-	OutgoingFailureNone OutgoingFailure = iota
+	FailureDetailNone = iota
 
-	// OutgoingFailureDecodeError indicates that we could not decode the
-	// failure reason provided for a failed payment.
-	OutgoingFailureDecodeError
+	// FailureDetailOnionDecode indicates that we could not decode an
+	// onion error.
+	FailureDetailOnionDecode
 
-	// OutgoingFailureLinkNotEligible indicates that a routing attempt was
+	// FailureDetailLinkNotEligible indicates that a routing attempt was
 	// made over a link that is not eligible for routing.
-	OutgoingFailureLinkNotEligible
+	FailureDetailLinkNotEligible
 
-	// OutgoingFailureOnChainTimeout indicates that a payment had to be
-	// timed out on chain before it got past the first hop by us or the
-	// remote party.
-	OutgoingFailureOnChainTimeout
+	// FailureDetailOnChainTimeout indicates that a payment had to be timed
+	// out on chain before it got past the first hop by us or the remote
+	// party.
+	FailureDetailOnChainTimeout
 
-	// OutgoingFailureHTLCExceedsMax is returned when a htlc exceeds our
+	// FailureDetailHTLCExceedsMax is returned when a htlc exceeds our
 	// policy's maximum htlc amount.
-	OutgoingFailureHTLCExceedsMax
+	FailureDetailHTLCExceedsMax
 
-	// OutgoingFailureInsufficientBalance is returned when we cannot route a
+	// FailureDetailInsufficientBalance is returned when we cannot route a
 	// htlc due to insufficient outgoing capacity.
-	OutgoingFailureInsufficientBalance
+	FailureDetailInsufficientBalance
 
-	// OutgoingFailureCircularRoute is returned when an attempt is made
+	// FailureDetailCircularRoute is returned when an attempt is made
 	// to forward a htlc through our node which arrives and leaves on the
 	// same channel.
-	OutgoingFailureCircularRoute
-
-	// OutgoingFailureIncompleteForward is returned when we cancel an incomplete
-	// forward.
-	OutgoingFailureIncompleteForward
-
-	// OutgoingFailureDownstreamHtlcAdd is returned when we fail to add a
-	// downstream htlc to our outgoing link.
-	OutgoingFailureDownstreamHtlcAdd
-
-	// OutgoingFailureForwardsDisabled is returned when the switch is
-	// configured to disallow forwards.
-	OutgoingFailureForwardsDisabled
+	FailureDetailCircularRoute
 )
 
-// FailureString returns the string representation of a failure detail.
-//
-// Note: it is part of the FailureDetail interface.
-func (fd OutgoingFailure) FailureString() string {
+// String returns the string representation of a failure detail.
+func (fd FailureDetail) String() string {
 	switch fd {
-	case OutgoingFailureNone:
+	case FailureDetailNone:
 		return "no failure detail"
 
-	case OutgoingFailureDecodeError:
-		return "could not decode wire failure"
+	case FailureDetailOnionDecode:
+		return "could not decode onion"
 
-	case OutgoingFailureLinkNotEligible:
+	case FailureDetailLinkNotEligible:
 		return "link not eligible"
 
-	case OutgoingFailureOnChainTimeout:
+	case FailureDetailOnChainTimeout:
 		return "payment was resolved on-chain, then canceled back"
 
-	case OutgoingFailureHTLCExceedsMax:
+	case FailureDetailHTLCExceedsMax:
 		return "htlc exceeds maximum policy amount"
 
-	case OutgoingFailureInsufficientBalance:
+	case FailureDetailInsufficientBalance:
 		return "insufficient bandwidth to route htlc"
 
-	case OutgoingFailureCircularRoute:
+	case FailureDetailCircularRoute:
 		return "same incoming and outgoing channel"
 
-	case OutgoingFailureIncompleteForward:
-		return "failed after detecting incomplete forward"
-
-	case OutgoingFailureDownstreamHtlcAdd:
-		return "could not add downstream htlc"
-
-	case OutgoingFailureForwardsDisabled:
-		return "node configured to disallow forwards"
-
 	default:
 		return "unknown failure detail"
 	}
diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go
index f0eae99dd..1281e9515 100644
--- a/htlcswitch/interfaces.go
+++ b/htlcswitch/interfaces.go
@@ -27,7 +27,7 @@ type InvoiceDatabase interface {
 	NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
 		expiry uint32, currentHeight int32,
 		circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-		payload invoices.Payload) (invoices.HtlcResolution, error)
+		payload invoices.Payload) (*invoices.HtlcResolution, error)
 
 	// CancelInvoice attempts to cancel the invoice corresponding to the
 	// passed payment hash.
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index 80b90f483..92ecba65c 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -1158,7 +1158,7 @@ loop:
 	for {
 		// Lookup all hodl htlcs that can be failed or settled with this event.
 		// The hodl htlc must be present in the map.
-		circuitKey := htlcResolution.CircuitKey()
+		circuitKey := htlcResolution.CircuitKey
 		hodlHtlc, ok := l.hodlMap[circuitKey]
 		if !ok {
 			return fmt.Errorf("hodl htlc not found: %v", circuitKey)
@@ -1193,68 +1193,49 @@ loop:
 func (l *channelLink) processHtlcResolution(resolution invoices.HtlcResolution,
 	htlc hodlHtlc) error {
 
-	circuitKey := resolution.CircuitKey()
+	circuitKey := resolution.CircuitKey
 
-	// Determine required action for the resolution based on the type of
-	// resolution we have received.
-	switch res := resolution.(type) {
-	// Settle htlcs that returned a settle resolution using the preimage
-	// in the resolution.
-	case *invoices.HtlcSettleResolution:
-		l.log.Debugf("received settle resolution for %v"+
-			"with outcome: %v", circuitKey, res.Outcome)
+	// Determine required action for the resolution. If the event's preimage is
+	// non-nil, the htlc must be settled. Otherwise, it should be canceled.
+	if resolution.Preimage != nil {
+		l.log.Debugf("received settle resolution for %v", circuitKey)
 
 		return l.settleHTLC(
-			res.Preimage, htlc.pd.HtlcIndex,
+			*resolution.Preimage, htlc.pd.HtlcIndex,
 			htlc.pd.SourceRef,
 		)
+	}
 
-	// For htlc failures, we get the relevant failure message based
-	// on the failure resolution and then fail the htlc.
-	case *invoices.HtlcFailResolution:
-		l.log.Debugf("received cancel resolution for "+
-			"%v with outcome: %v", circuitKey, res.Outcome)
-
-		// Get the lnwire failure message based on the resolution
-		// result.
-		failure := getResolutionFailure(res, htlc.pd.Amount)
+	l.log.Debugf("received cancel resolution for %v with outcome: %v",
+		circuitKey, resolution.Outcome)
 
-		l.sendHTLCError(
-			htlc.pd.HtlcIndex, failure,
-			htlc.obfuscator, htlc.pd.SourceRef,
-		)
-		return nil
+	// Get the lnwire failure message based on the resolution result.
+	failure := getResolutionFailure(resolution, htlc.pd.Amount)
 
-	// Fail if we do not get a settle of fail resolution, since we
-	// are only expecting to handle settles and fails.
-	default:
-		return fmt.Errorf("unknown htlc resolution type: %T",
-			resolution)
-	}
+	l.sendHTLCError(
+		htlc.pd.HtlcIndex, failure, htlc.obfuscator,
+		htlc.pd.SourceRef,
+	)
+	return nil
 }
 
 // getResolutionFailure returns the wire message that a htlc resolution should
 // be failed with.
-func getResolutionFailure(resolution *invoices.HtlcFailResolution,
-	amount lnwire.MilliSatoshi) *LinkError {
+func getResolutionFailure(resolution invoices.HtlcResolution,
+	amount lnwire.MilliSatoshi) lnwire.FailureMessage {
 
-	// If the resolution has been resolved as part of a MPP timeout,
-	// we need to fail the htlc with lnwire.FailMppTimeout.
+	// If the resolution has been resolved as part of a MPP timeout, we need
+	// to fail the htlc with lnwire.FailMppTimeout.
 	if resolution.Outcome == invoices.ResultMppTimeout {
-		return NewDetailedLinkError(
-			&lnwire.FailMPPTimeout{}, resolution.Outcome,
-		)
+		return &lnwire.FailMPPTimeout{}
 	}
 
-	// If the htlc is not a MPP timeout, we fail it with
-	// FailIncorrectDetails. This error is sent for invoice payment
-	// failures such as underpayment/ expiry too soon and hodl invoices
-	// (which return FailIncorrectDetails to avoid leaking information).
-	incorrectDetails := lnwire.NewFailIncorrectDetails(
+	// If the htlc is not a MPP timeout, we fail it with FailIncorrectDetails
+	// This covers hodl cancels (which return it to avoid leaking information
+	// and other invoice failures such as underpayment or expiry too soon.
+	return lnwire.NewFailIncorrectDetails(
 		amount, uint32(resolution.AcceptHeight),
 	)
-
-	return NewDetailedLinkError(incorrectDetails, resolution.Outcome)
 }
 
 // randomFeeUpdateTimeout returns a random timeout between the bounds defined
@@ -1318,10 +1299,6 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
 					reason       lnwire.OpaqueReason
 				)
 
-				// Create a temporary channel failure which we
-				// will send back to our peer if this is a
-				// forward, or report to the user if the failed
-				// payment was locally initiated.
 				failure := l.createFailureWithUpdate(
 					func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
 						return lnwire.NewTemporaryChannelFailure(
@@ -1330,43 +1307,28 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
 					},
 				)
 
-				// If the payment was locally initiated (which
-				// is indicated by a nil obfuscator), we do
-				// not need to encrypt it back to the sender.
+				// Encrypt the error back to the source unless
+				// the payment was generated locally.
 				if pkt.obfuscator == nil {
 					var b bytes.Buffer
 					err := lnwire.EncodeFailure(&b, failure, 0)
 					if err != nil {
-						l.log.Errorf("unable to "+
-							"encode failure: %v", err)
+						l.log.Errorf("unable to encode failure: %v", err)
 						l.mailBox.AckPacket(pkt.inKey())
 						return
 					}
 					reason = lnwire.OpaqueReason(b.Bytes())
 					localFailure = true
 				} else {
-					// If the packet is part of a forward,
-					// (identified by a non-nil obfuscator)
-					// we need to encrypt the error back to
-					// the source.
 					var err error
 					reason, err = pkt.obfuscator.EncryptFirstHop(failure)
 					if err != nil {
-						l.log.Errorf("unable to "+
-							"obfuscate error: %v", err)
+						l.log.Errorf("unable to obfuscate error: %v", err)
 						l.mailBox.AckPacket(pkt.inKey())
 						return
 					}
 				}
 
-				// Create a link error containing the temporary
-				// channel failure and a detail which indicates
-				// the we failed to add the htlc.
-				linkError := NewDetailedLinkError(
-					failure,
-					OutgoingFailureDownstreamHtlcAdd,
-				)
-
 				failPkt := &htlcPacket{
 					incomingChanID: pkt.incomingChanID,
 					incomingHTLCID: pkt.incomingHTLCID,
@@ -1374,7 +1336,6 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
 					sourceRef:      pkt.sourceRef,
 					hasSource:      true,
 					localFailure:   localFailure,
-					linkFailure:    linkError,
 					htlc: &lnwire.UpdateFailHTLC{
 						Reason: reason,
 					},
@@ -2308,7 +2269,7 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
 				return lnwire.NewTemporaryChannelFailure(upd)
 			},
 		)
-		return NewDetailedLinkError(failure, OutgoingFailureHTLCExceedsMax)
+		return NewDetailedLinkError(failure, FailureDetailHTLCExceedsMax)
 	}
 
 	// We want to avoid offering an HTLC which will expire in the near
@@ -2343,7 +2304,7 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
 			},
 		)
 		return NewDetailedLinkError(
-			failure, OutgoingFailureInsufficientBalance,
+			failure, FailureDetailInsufficientBalance,
 		)
 	}
 
@@ -2485,9 +2446,7 @@ func (l *channelLink) processRemoteSettleFails(fwdPkg *channeldb.FwdPkg,
 			}
 
 			// Fetch the reason the HTLC was canceled so we can
-			// continue to propagate it. This failure originated
-			// from another node, so the linkFailure field is not
-			// set on the packet.
+			// continue to propagate it.
 			failPacket := &htlcPacket{
 				outgoingChanID: l.ShortChanID(),
 				outgoingHTLCID: pd.ParentIndex,
@@ -2654,10 +2613,9 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
 			// for TLV payloads that also supports injecting invalid
 			// payloads. Deferring this non-trival effort till a
 			// later date
-			failure := lnwire.NewInvalidOnionPayload(failedType, 0)
 			l.sendHTLCError(
 				pd.HtlcIndex,
-				NewLinkError(failure),
+				lnwire.NewInvalidOnionPayload(failedType, 0),
 				obfuscator, pd.SourceRef,
 			)
 
@@ -2771,10 +2729,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
 				)
 
 				l.sendHTLCError(
-					pd.HtlcIndex,
-					NewLinkError(failure),
-					obfuscator,
-					pd.SourceRef,
+					pd.HtlcIndex, failure, obfuscator, pd.SourceRef,
 				)
 				continue
 			}
@@ -2857,9 +2812,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
 			"value: expected %v, got %v", pd.RHash,
 			pd.Amount, fwdInfo.AmountToForward)
 
-		failure := NewLinkError(
-			lnwire.NewFinalIncorrectHtlcAmount(pd.Amount),
-		)
+		failure := lnwire.NewFinalIncorrectHtlcAmount(pd.Amount)
 		l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
 
 		return nil
@@ -2872,9 +2825,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
 			"time-lock: expected %v, got %v",
 			pd.RHash[:], pd.Timeout, fwdInfo.OutgoingCTLV)
 
-		failure := NewLinkError(
-			lnwire.NewFinalIncorrectCltvExpiry(pd.Timeout),
-		)
+		failure := lnwire.NewFinalIncorrectCltvExpiry(pd.Timeout)
 		l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
 
 		return nil
@@ -2912,7 +2863,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
 	}
 
 	// Process the received resolution.
-	return l.processHtlcResolution(event, htlc)
+	return l.processHtlcResolution(*event, htlc)
 }
 
 // settleHTLC settles the HTLC on the channel.
@@ -2991,10 +2942,10 @@ func (l *channelLink) handleBatchFwdErrs(errChan chan error) {
 
 // sendHTLCError functions cancels HTLC and send cancel message back to the
 // peer from which HTLC was received.
-func (l *channelLink) sendHTLCError(htlcIndex uint64, failure *LinkError,
+func (l *channelLink) sendHTLCError(htlcIndex uint64, failure lnwire.FailureMessage,
 	e hop.ErrorEncrypter, sourceRef *channeldb.AddRef) {
 
-	reason, err := e.EncryptFirstHop(failure.WireMessage())
+	reason, err := e.EncryptFirstHop(failure)
 	if err != nil {
 		l.log.Errorf("unable to obfuscate error: %v", err)
 		return
diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go
index 7cc9fb04a..02737a715 100644
--- a/htlcswitch/mock.go
+++ b/htlcswitch/mock.go
@@ -816,7 +816,7 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error
 func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
 	amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
 	circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-	payload invoices.Payload) (invoices.HtlcResolution, error) {
+	payload invoices.Payload) (*invoices.HtlcResolution, error) {
 
 	event, err := i.registry.NotifyExitHopHtlc(
 		rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
diff --git a/htlcswitch/packet.go b/htlcswitch/packet.go
index e0aa75275..3e0816e6e 100644
--- a/htlcswitch/packet.go
+++ b/htlcswitch/packet.go
@@ -54,11 +54,6 @@ type htlcPacket struct {
 	// encrypted with any shared secret.
 	localFailure bool
 
-	// linkFailure is non-nil for htlcs that fail at our node. This may
-	// occur for our own payments which fail on the outgoing link,
-	// or for forwards which fail in the switch or on the outgoing link.
-	linkFailure *LinkError
-
 	// convertedError is set to true if this is an HTLC fail that was
 	// created using an UpdateFailMalformedHTLC from the remote party. If
 	// this is true, then when forwarding this failure packet, we'll need
diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go
index f4370635d..760aadf00 100644
--- a/htlcswitch/switch.go
+++ b/htlcswitch/switch.go
@@ -45,6 +45,11 @@ var (
 	// through the switch and is locked into another commitment txn.
 	ErrDuplicateAdd = errors.New("duplicate add HTLC detected")
 
+	// ErrIncompleteForward is used when an htlc was already forwarded
+	// through the switch, but did not get locked into another commitment
+	// txn.
+	ErrIncompleteForward = errors.New("incomplete forward detected")
+
 	// ErrUnknownErrorDecryptor signals that we were unable to locate the
 	// error decryptor for this payment. This is likely due to restarting
 	// the daemon.
@@ -491,12 +496,9 @@ func (s *Switch) forward(packet *htlcPacket) error {
 			} else {
 				failure = lnwire.NewTemporaryChannelFailure(update)
 			}
+			addErr := ErrIncompleteForward
 
-			linkError := NewDetailedLinkError(
-				failure, OutgoingFailureIncompleteForward,
-			)
-
-			return s.failAddPacket(packet, linkError)
+			return s.failAddPacket(packet, failure, addErr)
 		}
 
 		packet.circuit = circuit
@@ -645,14 +647,14 @@ func (s *Switch) ForwardPackets(linkQuit chan struct{},
 		} else {
 			failure = lnwire.NewTemporaryChannelFailure(update)
 		}
-		linkError := NewDetailedLinkError(
-			failure, OutgoingFailureIncompleteForward,
-		)
 
 		for _, packet := range failedPackets {
+			addErr := errors.New("failing packet after " +
+				"detecting incomplete forward")
+
 			// We don't handle the error here since this method
 			// always returns an error.
-			_ = s.failAddPacket(packet, linkError)
+			s.failAddPacket(packet, failure, addErr)
 		}
 	}
 
@@ -769,7 +771,7 @@ func (s *Switch) handleLocalDispatch(pkt *htlcPacket) error {
 			// will be returned back to the router.
 			return NewDetailedLinkError(
 				lnwire.NewTemporaryChannelFailure(nil),
-				OutgoingFailureLinkNotEligible,
+				FailureDetailLinkNotEligible,
 			)
 		}
 
@@ -917,12 +919,12 @@ func (s *Switch) parseFailedPayment(deobfuscator ErrorDecrypter,
 				// need to apply an update here since it goes
 				// directly to the router.
 				lnwire.NewTemporaryChannelFailure(nil),
-				OutgoingFailureDecodeError,
+				FailureDetailOnionDecode,
 			)
 
 			log.Errorf("%v: (hash=%v, pid=%d): %v",
-				linkError.FailureDetail.FailureString(),
-				paymentHash, paymentID, err)
+				linkError.FailureDetail, paymentHash, paymentID,
+				err)
 
 			return linkError
 		}
@@ -937,11 +939,10 @@ func (s *Switch) parseFailedPayment(deobfuscator ErrorDecrypter,
 	case isResolution && htlc.Reason == nil:
 		linkError := NewDetailedLinkError(
 			&lnwire.FailPermanentChannelFailure{},
-			OutgoingFailureOnChainTimeout,
+			FailureDetailOnChainTimeout,
 		)
 
-		log.Info("%v: hash=%v, pid=%d",
-			linkError.FailureDetail.FailureString(),
+		log.Info("%v: hash=%v, pid=%d", linkError.FailureDetail,
 			paymentHash, paymentID)
 
 		return linkError
@@ -977,12 +978,10 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
 		// Check if the node is set to reject all onward HTLCs and also make
 		// sure that HTLC is not from the source node.
 		if s.cfg.RejectHTLC && packet.incomingChanID != hop.Source {
-			failure := NewDetailedLinkError(
-				&lnwire.FailChannelDisabled{},
-				OutgoingFailureForwardsDisabled,
-			)
+			failure := &lnwire.FailChannelDisabled{}
+			addErr := fmt.Errorf("unable to forward any htlcs")
 
-			return s.failAddPacket(packet, failure)
+			return s.failAddPacket(packet, failure, addErr)
 		}
 
 		if packet.incomingChanID == hop.Source {
@@ -1002,7 +1001,9 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
 			s.cfg.AllowCircularRoute, htlc.PaymentHash,
 		)
 		if linkErr != nil {
-			return s.failAddPacket(packet, linkErr)
+			return s.failAddPacket(
+				packet, linkErr.WireMessage(), linkErr,
+			)
 		}
 
 		s.indexMtx.RLock()
@@ -1010,17 +1011,14 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
 		if err != nil {
 			s.indexMtx.RUnlock()
 
-			log.Debugf("unable to find link with "+
-				"destination %v", packet.outgoingChanID)
-
 			// If packet was forwarded from another channel link
 			// than we should notify this link that some error
 			// occurred.
-			linkError := NewLinkError(
-				&lnwire.FailUnknownNextPeer{},
-			)
+			failure := &lnwire.FailUnknownNextPeer{}
+			addErr := fmt.Errorf("unable to find link with "+
+				"destination %v", packet.outgoingChanID)
 
-			return s.failAddPacket(packet, linkError)
+			return s.failAddPacket(packet, failure, addErr)
 		}
 		targetPeerKey := targetLink.Peer().PubKey()
 		interfaceLinks, _ := s.getLinks(targetPeerKey)
@@ -1043,7 +1041,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
 			if !link.EligibleToForward() {
 				failure = NewDetailedLinkError(
 					&lnwire.FailUnknownNextPeer{},
-					OutgoingFailureLinkNotEligible,
+					FailureDetailLinkNotEligible,
 				)
 			} else {
 				// We'll ensure that the HTLC satisfies the
@@ -1089,12 +1087,12 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
 					}))
 			}
 
-			log.Tracef("incoming HTLC(%x) violated "+
+			addErr := fmt.Errorf("incoming HTLC(%x) violated "+
 				"target outgoing link (id=%v) policy: %v",
 				htlc.PaymentHash[:], packet.outgoingChanID,
 				linkErr)
 
-			return s.failAddPacket(packet, linkErr)
+			return s.failAddPacket(packet, linkErr.WireMessage(), addErr)
 		}
 
 		// Send the packet to the destination channel link which
@@ -1219,18 +1217,20 @@ func checkCircularForward(incoming, outgoing lnwire.ShortChannelID,
 	// node, so we do not include a channel update.
 	return NewDetailedLinkError(
 		lnwire.NewTemporaryChannelFailure(nil),
-		OutgoingFailureCircularRoute,
+		FailureDetailCircularRoute,
 	)
 }
 
 // failAddPacket encrypts a fail packet back to an add packet's source.
 // The ciphertext will be derived from the failure message proivded by context.
 // This method returns the failErr if all other steps complete successfully.
-func (s *Switch) failAddPacket(packet *htlcPacket, failure *LinkError) error {
+func (s *Switch) failAddPacket(packet *htlcPacket,
+	failure lnwire.FailureMessage, failErr error) error {
+
 	// Encrypt the failure so that the sender will be able to read the error
 	// message. Since we failed this packet, we use EncryptFirstHop to
 	// obfuscate the failure for their eyes only.
-	reason, err := packet.obfuscator.EncryptFirstHop(failure.WireMessage())
+	reason, err := packet.obfuscator.EncryptFirstHop(failure)
 	if err != nil {
 		err := fmt.Errorf("unable to obfuscate "+
 			"error: %v", err)
@@ -1238,14 +1238,13 @@ func (s *Switch) failAddPacket(packet *htlcPacket, failure *LinkError) error {
 		return err
 	}
 
-	log.Error(failure.Error())
+	log.Error(failErr)
 
 	failPkt := &htlcPacket{
 		sourceRef:      packet.sourceRef,
 		incomingChanID: packet.incomingChanID,
 		incomingHTLCID: packet.incomingHTLCID,
 		circuit:        packet.circuit,
-		linkFailure:    failure,
 		htlc: &lnwire.UpdateFailHTLC{
 			Reason: reason,
 		},
@@ -1261,7 +1260,7 @@ func (s *Switch) failAddPacket(packet *htlcPacket, failure *LinkError) error {
 		return err
 	}
 
-	return failure
+	return failErr
 }
 
 // closeCircuit accepts a settle or fail htlc and the associated htlc packet and
@@ -1869,11 +1868,8 @@ func (s *Switch) reforwardSettleFails(fwdPkgs []*channeldb.FwdPkg) {
 			// commitment state, so we'll forward this to the switch so the
 			// backwards undo can continue.
 			case lnwallet.Fail:
-				// Fetch the reason the HTLC was canceled so
-				// we can continue to propagate it. This
-				// failure originated from another node, so
-				// the linkFailure field is not set on this
-				// packet.
+				// Fetch the reason the HTLC was canceled so we can
+				// continue to propagate it.
 				failPacket := &htlcPacket{
 					outgoingChanID: fwdPkg.Source,
 					outgoingHTLCID: pd.ParentIndex,
diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go
index 00885edb9..115b38af7 100644
--- a/invoices/invoiceregistry.go
+++ b/invoices/invoiceregistry.go
@@ -36,6 +36,48 @@ const (
 	DefaultHtlcHoldDuration = 120 * time.Second
 )
 
+// HtlcResolution describes how an htlc should be resolved. If the preimage
+// field is set, the event indicates a settle event. If Preimage is nil, it is
+// a cancel event.
+type HtlcResolution struct {
+	// Preimage is the htlc preimage. Its value is nil in case of a cancel.
+	Preimage *lntypes.Preimage
+
+	// CircuitKey is the key of the htlc for which we have a resolution
+	// decision.
+	CircuitKey channeldb.CircuitKey
+
+	// AcceptHeight is the original height at which the htlc was accepted.
+	AcceptHeight int32
+
+	// Outcome indicates the outcome of the invoice registry update.
+	Outcome ResolutionResult
+}
+
+// NewFailureResolution returns a htlc failure resolution.
+func NewFailureResolution(key channeldb.CircuitKey,
+	acceptHeight int32, outcome ResolutionResult) *HtlcResolution {
+
+	return &HtlcResolution{
+		CircuitKey:   key,
+		AcceptHeight: acceptHeight,
+		Outcome:      outcome,
+	}
+}
+
+// NewSettleResolution returns a htlc resolution which is associated with a
+// settle.
+func NewSettleResolution(preimage lntypes.Preimage, key channeldb.CircuitKey,
+	acceptHeight int32, outcome ResolutionResult) *HtlcResolution {
+
+	return &HtlcResolution{
+		Preimage:     &preimage,
+		CircuitKey:   key,
+		AcceptHeight: acceptHeight,
+		Outcome:      outcome,
+	}
+}
+
 // RegistryConfig contains the configuration parameters for invoice registry.
 type RegistryConfig struct {
 	// FinalCltvRejectDelta defines the number of blocks before the expiry
@@ -565,7 +607,7 @@ func (i *InvoiceRegistry) startHtlcTimer(hash lntypes.Hash,
 // a resolution result which will be used to notify subscribed links and
 // resolvers of the details of the htlc cancellation.
 func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
-	key channeldb.CircuitKey, result FailResolutionResult) error {
+	key channeldb.CircuitKey, result ResolutionResult) error {
 
 	i.Lock()
 	defer i.Unlock()
@@ -641,7 +683,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
 		return fmt.Errorf("htlc %v not found", key)
 	}
 	if htlc.State == channeldb.HtlcStateCanceled {
-		resolution := NewFailResolution(
+		resolution := *NewFailureResolution(
 			key, int32(htlc.AcceptHeight), result,
 		)
 
@@ -735,7 +777,7 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error {
 func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 	amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
 	circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-	payload Payload) (HtlcResolution, error) {
+	payload Payload) (*HtlcResolution, error) {
 
 	mpp := payload.MultiPath()
 
@@ -760,7 +802,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 		if err != nil {
 			updateCtx.log(fmt.Sprintf("keysend error: %v", err))
 
-			return NewFailResolution(
+			return NewFailureResolution(
 				circuitKey, currentHeight, ResultKeySendError,
 			), nil
 		}
@@ -775,10 +817,15 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 	}
 
 	switch r := resolution.(type) {
+
+	// A direct resolution was received for this htlc.
+	case *HtlcResolution:
+		return r, nil
+
 	// The htlc is held. Start a timer outside the lock if the htlc should
 	// be auto-released, because otherwise a deadlock may happen with the
 	// main event loop.
-	case *htlcAcceptResolution:
+	case *acceptResolution:
 		if r.autoRelease {
 			err := i.startHtlcTimer(rHash, circuitKey, r.acceptTime)
 			if err != nil {
@@ -786,31 +833,33 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 			}
 		}
 
-		// We return a nil resolution because htlc acceptances are
-		// represented as nil resolutions externally.
-		// TODO(carla) update calling code to handle accept resolutions.
 		return nil, nil
 
-	// A direct resolution was received for this htlc.
-	case HtlcResolution:
-		return r, nil
-
-	// Fail if an unknown resolution type was received.
 	default:
 		return nil, errors.New("invalid resolution type")
 	}
 }
 
+// acceptResolution is returned when the htlc should be held.
+type acceptResolution struct {
+	// autoRelease signals that the htlc should be automatically released
+	// after a timeout.
+	autoRelease bool
+
+	// acceptTime is the time at which this htlc was accepted.
+	acceptTime time.Time
+}
+
 // notifyExitHopHtlcLocked is the internal implementation of NotifyExitHopHtlc
 // that should be executed inside the registry lock.
 func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
 	ctx *invoiceUpdateCtx, hodlChan chan<- interface{}) (
-	HtlcResolution, error) {
+	interface{}, error) {
 
 	// We'll attempt to settle an invoice matching this rHash on disk (if
 	// one exists). The callback will update the invoice state and/or htlcs.
 	var (
-		resolution        HtlcResolution
+		result            ResolutionResult
 		updateSubscribers bool
 	)
 	invoice, err := i.cdb.UpdateInvoice(
@@ -827,8 +876,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
 			updateSubscribers = updateDesc != nil &&
 				updateDesc.State != nil
 
-			// Assign resolution to outer scope variable.
-			resolution = res
+			// Assign result to outer scope variable.
+			result = res
 
 			return updateDesc, nil
 		},
@@ -837,7 +886,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
 	case channeldb.ErrInvoiceNotFound:
 		// If the invoice was not found, return a failure resolution
 		// with an invoice not found result.
-		return NewFailResolution(
+		return NewFailureResolution(
 			ctx.circuitKey, ctx.currentHeight,
 			ResultInvoiceNotFound,
 		), nil
@@ -849,40 +898,38 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
 		return nil, err
 	}
 
+	ctx.log(result.String())
+
 	if updateSubscribers {
 		i.notifyClients(ctx.hash, invoice, invoice.State)
 	}
 
-	switch res := resolution.(type) {
-	case *HtlcFailResolution:
-		// Inspect latest htlc state on the invoice. If it is found,
-		// we will update the accept height as it was recorded in the
-		// invoice database (which occurs in the case where the htlc
-		// reached the database in a previous call). If the htlc was
-		// not found on the invoice, it was immediately failed so we
-		// send the failure resolution as is, which has the current
-		// height set as the accept height.
-		invoiceHtlc, ok := invoice.Htlcs[ctx.circuitKey]
-		if ok {
-			res.AcceptHeight = int32(invoiceHtlc.AcceptHeight)
-		}
+	// Inspect latest htlc state on the invoice.
+	invoiceHtlc, ok := invoice.Htlcs[ctx.circuitKey]
 
-		ctx.log(fmt.Sprintf("failure resolution result "+
-			"outcome: %v, at accept height: %v",
-			res.Outcome, res.AcceptHeight))
+	// If it isn't recorded, cancel htlc.
+	if !ok {
+		return NewFailureResolution(
+			ctx.circuitKey, ctx.currentHeight, result,
+		), nil
+	}
 
-		return res, nil
+	// Determine accepted height of this htlc. If the htlc reached the
+	// invoice database (possibly in a previous call to the invoice
+	// registry), we'll take the original accepted height as it was recorded
+	// in the database.
+	acceptHeight := int32(invoiceHtlc.AcceptHeight)
 
-	// If the htlc was settled, we will settle any previously accepted
-	// htlcs and notify our peer to settle them.
-	case *HtlcSettleResolution:
-		ctx.log(fmt.Sprintf("settle resolution result "+
-			"outcome: %v, at accept height: %v",
-			res.Outcome, res.AcceptHeight))
+	switch invoiceHtlc.State {
+	case channeldb.HtlcStateCanceled:
+		return NewFailureResolution(
+			ctx.circuitKey, acceptHeight, result,
+		), nil
 
-		// Also settle any previously accepted htlcs. If a htlc is
-		// marked as settled, we should follow now and settle the htlc
-		// with our peer.
+	case channeldb.HtlcStateSettled:
+		// Also settle any previously accepted htlcs. The invoice state
+		// is leading. If an htlc is marked as settled, we should follow
+		// now and settle the htlc with our peer.
 		for key, htlc := range invoice.Htlcs {
 			if htlc.State != channeldb.HtlcStateSettled {
 				continue
@@ -893,49 +940,34 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
 			// resolution is set based on the outcome of the single
 			// htlc that we just settled, so may not be accurate
 			// for all htlcs.
-			htlcSettleResolution := NewSettleResolution(
-				res.Preimage, key,
-				int32(htlc.AcceptHeight), res.Outcome,
+			resolution := *NewSettleResolution(
+				invoice.Terms.PaymentPreimage, key,
+				acceptHeight, result,
 			)
 
-			// Notify subscribers that the htlc should be settled
-			// with our peer.
-			i.notifyHodlSubscribers(htlcSettleResolution)
+			i.notifyHodlSubscribers(resolution)
 		}
 
+		resolution := NewSettleResolution(
+			invoice.Terms.PaymentPreimage, ctx.circuitKey,
+			acceptHeight, result,
+		)
 		return resolution, nil
 
-	// If we accepted the htlc, subscribe to the hodl invoice and return
-	// an accept resolution with the htlc's accept time on it.
-	case *htlcAcceptResolution:
-		invoiceHtlc, ok := invoice.Htlcs[ctx.circuitKey]
-		if !ok {
-			return nil, fmt.Errorf("accepted htlc: %v not"+
-				" present on invoice: %x", ctx.circuitKey,
-				ctx.hash[:])
-		}
-
-		// Determine accepted height of this htlc. If the htlc reached
-		// the invoice database (possibly in a previous call to the
-		// invoice registry), we'll take the original accepted height
-		// as it was recorded in the database.
-		acceptHeight := int32(invoiceHtlc.AcceptHeight)
-
-		ctx.log(fmt.Sprintf("accept resolution result "+
-			"outcome: %v, at accept height: %v",
-			res.outcome, acceptHeight))
+	case channeldb.HtlcStateAccepted:
+		var resolution acceptResolution
 
 		// Auto-release the htlc if the invoice is still open. It can
 		// only happen for mpp payments that there are htlcs in state
 		// Accepted while the invoice is Open.
 		if invoice.State == channeldb.ContractOpen {
-			res.acceptTime = invoiceHtlc.AcceptTime
-			res.autoRelease = true
+			resolution.acceptTime = invoiceHtlc.AcceptTime
+			resolution.autoRelease = true
 
 		}
 
 		i.hodlSubscribe(hodlChan, ctx.circuitKey)
-		return res, nil
+		return &resolution, nil
 
 	default:
 		panic("unknown action")
@@ -990,7 +1022,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
 			continue
 		}
 
-		resolution := NewSettleResolution(
+		resolution := *NewSettleResolution(
 			preimage, key, int32(htlc.AcceptHeight), ResultSettled,
 		)
 
@@ -1070,7 +1102,7 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
 		}
 
 		i.notifyHodlSubscribers(
-			NewFailResolution(
+			*NewFailureResolution(
 				key, int32(htlc.AcceptHeight), ResultCanceled,
 			),
 		)
@@ -1342,7 +1374,7 @@ func (i *InvoiceRegistry) SubscribeSingleInvoice(
 // notifyHodlSubscribers sends out the htlc resolution to all current
 // subscribers.
 func (i *InvoiceRegistry) notifyHodlSubscribers(htlcResolution HtlcResolution) {
-	subscribers, ok := i.hodlSubscriptions[htlcResolution.CircuitKey()]
+	subscribers, ok := i.hodlSubscriptions[htlcResolution.CircuitKey]
 	if !ok {
 		return
 	}
@@ -1359,11 +1391,11 @@ func (i *InvoiceRegistry) notifyHodlSubscribers(htlcResolution HtlcResolution) {
 
 		delete(
 			i.hodlReverseSubscriptions[subscriber],
-			htlcResolution.CircuitKey(),
+			htlcResolution.CircuitKey,
 		)
 	}
 
-	delete(i.hodlSubscriptions, htlcResolution.CircuitKey())
+	delete(i.hodlSubscriptions, htlcResolution.CircuitKey)
 }
 
 // hodlSubscribe adds a new invoice subscription.
diff --git a/invoices/resolution.go b/invoices/resolution.go
deleted file mode 100644
index e6f313303..000000000
--- a/invoices/resolution.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package invoices
-
-import (
-	"time"
-
-	"github.com/lightningnetwork/lnd/channeldb"
-	"github.com/lightningnetwork/lnd/lntypes"
-)
-
-// HtlcResolution describes how an htlc should be resolved.
-type HtlcResolution interface {
-	// CircuitKey returns the circuit key for the htlc that we have a
-	// resolution for.
-	CircuitKey() channeldb.CircuitKey
-}
-
-// HtlcFailResolution is an implementation of the HtlcResolution interface
-// which is returned when a htlc is failed.
-type HtlcFailResolution struct {
-	// circuitKey is the key of the htlc for which we have a resolution.
-	circuitKey channeldb.CircuitKey
-
-	// AcceptHeight is the original height at which the htlc was accepted.
-	AcceptHeight int32
-
-	// Outcome indicates the outcome of the invoice registry update.
-	Outcome FailResolutionResult
-}
-
-// NewFailResolution returns a htlc failure resolution.
-func NewFailResolution(key channeldb.CircuitKey,
-	acceptHeight int32, outcome FailResolutionResult) *HtlcFailResolution {
-
-	return &HtlcFailResolution{
-		circuitKey:   key,
-		AcceptHeight: acceptHeight,
-		Outcome:      outcome,
-	}
-}
-
-// CircuitKey returns the circuit key for the htlc that we have a
-// resolution for.
-//
-// Note: it is part of the HtlcResolution interface.
-func (f *HtlcFailResolution) CircuitKey() channeldb.CircuitKey {
-	return f.circuitKey
-}
-
-// HtlcSettleResolution is an implementation of the HtlcResolution interface
-// which is returned when a htlc is settled.
-type HtlcSettleResolution struct {
-	// Preimage is the htlc preimage. Its value is nil in case of a cancel.
-	Preimage lntypes.Preimage
-
-	// circuitKey is the key of the htlc for which we have a resolution.
-	circuitKey channeldb.CircuitKey
-
-	// acceptHeight is the original height at which the htlc was accepted.
-	AcceptHeight int32
-
-	// Outcome indicates the outcome of the invoice registry update.
-	Outcome SettleResolutionResult
-}
-
-// NewSettleResolution returns a htlc resolution which is associated with a
-// settle.
-func NewSettleResolution(preimage lntypes.Preimage,
-	key channeldb.CircuitKey, acceptHeight int32,
-	outcome SettleResolutionResult) *HtlcSettleResolution {
-
-	return &HtlcSettleResolution{
-		Preimage:     preimage,
-		circuitKey:   key,
-		AcceptHeight: acceptHeight,
-		Outcome:      outcome,
-	}
-}
-
-// CircuitKey returns the circuit key for the htlc that we have a
-// resolution for.
-//
-// Note: it is part of the HtlcResolution interface.
-func (s *HtlcSettleResolution) CircuitKey() channeldb.CircuitKey {
-	return s.circuitKey
-}
-
-// htlcAcceptResolution is an implementation of the HtlcResolution interface
-// which is returned when a htlc is accepted. This struct is not exported
-// because the codebase uses a nil resolution to indicate that a htlc was
-// accepted. This struct is used internally in the invoice registry to
-// surface accept resolution results. When an invoice update returns an
-// acceptResolution, a nil resolution should be surfaced.
-type htlcAcceptResolution struct {
-	// circuitKey is the key of the htlc for which we have a resolution.
-	circuitKey channeldb.CircuitKey
-
-	// autoRelease signals that the htlc should be automatically released
-	// after a timeout.
-	autoRelease bool
-
-	// acceptTime is the time at which this htlc was accepted.
-	acceptTime time.Time
-
-	// outcome indicates the outcome of the invoice registry update.
-	outcome acceptResolutionResult
-}
-
-// newAcceptResolution returns a htlc resolution which is associated with a
-// htlc accept.
-func newAcceptResolution(key channeldb.CircuitKey,
-	outcome acceptResolutionResult) *htlcAcceptResolution {
-
-	return &htlcAcceptResolution{
-		circuitKey: key,
-		outcome:    outcome,
-	}
-}
-
-// CircuitKey returns the circuit key for the htlc that we have a
-// resolution for.
-//
-// Note: it is part of the HtlcResolution interface.
-func (a *htlcAcceptResolution) CircuitKey() channeldb.CircuitKey {
-	return a.circuitKey
-}
diff --git a/invoices/resolution_result.go b/invoices/resolution_result.go
deleted file mode 100644
index 41da902a6..000000000
--- a/invoices/resolution_result.go
+++ /dev/null
@@ -1,202 +0,0 @@
-package invoices
-
-// acceptResolutionResult provides metadata which about a htlc that was
-// accepted by the registry.
-type acceptResolutionResult uint8
-
-const (
-	resultInvalidAccept acceptResolutionResult = iota
-
-	// resultReplayToAccepted is returned when we replay an accepted
-	// invoice.
-	resultReplayToAccepted
-
-	// resultDuplicateToAccepted is returned when we accept a duplicate
-	// htlc.
-	resultDuplicateToAccepted
-
-	// resultAccepted is returned when we accept a hodl invoice.
-	resultAccepted
-
-	// resultPartialAccepted is returned when we have partially received
-	// payment.
-	resultPartialAccepted
-)
-
-// String returns a string representation of the result.
-func (a acceptResolutionResult) String() string {
-	switch a {
-	case resultInvalidAccept:
-		return "invalid accept result"
-
-	case resultReplayToAccepted:
-		return "replayed htlc to accepted invoice"
-
-	case resultDuplicateToAccepted:
-		return "accepting duplicate payment to accepted invoice"
-
-	case resultAccepted:
-		return "accepted"
-
-	case resultPartialAccepted:
-		return "partial payment accepted"
-
-	default:
-		return "unknown accept resolution result"
-	}
-}
-
-// FailResolutionResult provides metadata about a htlc that was failed by
-// the registry. It can be used to take custom actions on resolution of the
-// htlc.
-type FailResolutionResult uint8
-
-const (
-	resultInvalidFailure FailResolutionResult = iota
-
-	// ResultReplayToCanceled is returned when we replay a canceled invoice.
-	ResultReplayToCanceled
-
-	// ResultInvoiceAlreadyCanceled is returned when trying to pay an
-	// invoice that is already canceled.
-	ResultInvoiceAlreadyCanceled
-
-	// ResultAmountTooLow is returned when an invoice is underpaid.
-	ResultAmountTooLow
-
-	// ResultExpiryTooSoon is returned when we do not accept an invoice
-	// payment because it expires too soon.
-	ResultExpiryTooSoon
-
-	// ResultCanceled is returned when we cancel an invoice and its
-	// associated htlcs.
-	ResultCanceled
-
-	// ResultInvoiceNotOpen is returned when a mpp invoice is not open.
-	ResultInvoiceNotOpen
-
-	// ResultMppTimeout is returned when an invoice paid with multiple
-	// partial payments times out before it is fully paid.
-	ResultMppTimeout
-
-	// ResultAddressMismatch is returned when the payment address for a mpp
-	// invoice does not match.
-	ResultAddressMismatch
-
-	// ResultHtlcSetTotalMismatch is returned when the amount paid by a
-	// htlc does not match its set total.
-	ResultHtlcSetTotalMismatch
-
-	// ResultHtlcSetTotalTooLow is returned when a mpp set total is too low
-	// for an invoice.
-	ResultHtlcSetTotalTooLow
-
-	// ResultHtlcSetOverpayment is returned when a mpp set is overpaid.
-	ResultHtlcSetOverpayment
-
-	// ResultInvoiceNotFound is returned when an attempt is made to pay an
-	// invoice that is unknown to us.
-	ResultInvoiceNotFound
-
-	// ResultKeySendError is returned when we receive invalid keysend
-	// parameters.
-	ResultKeySendError
-
-	// ResultMppInProgress is returned when we are busy receiving a mpp
-	// payment.
-	ResultMppInProgress
-)
-
-// FailureString returns a string representation of the result.
-//
-// Note: it is part of the FailureDetail interface.
-func (f FailResolutionResult) FailureString() string {
-	switch f {
-	case resultInvalidFailure:
-		return "invalid failure result"
-
-	case ResultReplayToCanceled:
-		return "replayed htlc to canceled invoice"
-
-	case ResultInvoiceAlreadyCanceled:
-		return "invoice already canceled"
-
-	case ResultAmountTooLow:
-		return "amount too low"
-
-	case ResultExpiryTooSoon:
-		return "expiry too soon"
-
-	case ResultCanceled:
-		return "canceled"
-
-	case ResultInvoiceNotOpen:
-		return "invoice no longer open"
-
-	case ResultMppTimeout:
-		return "mpp timeout"
-
-	case ResultAddressMismatch:
-		return "payment address mismatch"
-
-	case ResultHtlcSetTotalMismatch:
-		return "htlc total amt doesn't match set total"
-
-	case ResultHtlcSetTotalTooLow:
-		return "set total too low for invoice"
-
-	case ResultHtlcSetOverpayment:
-		return "mpp is overpaying set total"
-
-	case ResultInvoiceNotFound:
-		return "invoice not found"
-
-	case ResultKeySendError:
-		return "invalid keysend parameters"
-
-	case ResultMppInProgress:
-		return "mpp reception in progress"
-
-	default:
-		return "unknown failure resolution result"
-	}
-}
-
-// SettleResolutionResult provides metadata which about a htlc that was failed
-// by the registry. It can be used to take custom actions on resolution of the
-// htlc.
-type SettleResolutionResult uint8
-
-const (
-	resultInvalidSettle SettleResolutionResult = iota
-
-	// ResultSettled is returned when we settle an invoice.
-	ResultSettled
-
-	// ResultReplayToSettled is returned when we replay a settled invoice.
-	ResultReplayToSettled
-
-	// ResultDuplicateToSettled is returned when we settle an invoice which
-	// has already been settled at least once.
-	ResultDuplicateToSettled
-)
-
-// String returns a string representation of the result.
-func (s SettleResolutionResult) String() string {
-	switch s {
-	case resultInvalidSettle:
-		return "invalid settle result"
-
-	case ResultSettled:
-		return "settled"
-
-	case ResultReplayToSettled:
-		return "replayed htlc to settled invoice"
-
-	case ResultDuplicateToSettled:
-		return "accepting duplicate payment to settled invoice"
-
-	default:
-		return "unknown settle resolution result"
-	}
-}
diff --git a/invoices/update.go b/invoices/update.go
index 3226779ce..e6feaba06 100644
--- a/invoices/update.go
+++ b/invoices/update.go
@@ -9,6 +9,164 @@ import (
 	"github.com/lightningnetwork/lnd/record"
 )
 
+// ResolutionResult provides metadata which about an invoice update which can
+// be used to take custom actions on resolution of the htlc. Only results which
+// are actionable by the link are exported.
+type ResolutionResult uint8
+
+const (
+	resultInvalid ResolutionResult = iota
+
+	// ResultReplayToCanceled is returned when we replay a canceled invoice.
+	ResultReplayToCanceled
+
+	// ResultReplayToAccepted is returned when we replay an accepted invoice.
+	ResultReplayToAccepted
+
+	// ResultReplayToSettled is returned when we replay a settled invoice.
+	ResultReplayToSettled
+
+	// ResultInvoiceAlreadyCanceled is returned when trying to pay an invoice
+	// that is already canceled.
+	ResultInvoiceAlreadyCanceled
+
+	// ResultAmountTooLow is returned when an invoice is underpaid.
+	ResultAmountTooLow
+
+	// ResultExpiryTooSoon is returned when we do not accept an invoice payment
+	// because it expires too soon.
+	ResultExpiryTooSoon
+
+	// ResultDuplicateToAccepted is returned when we accept a duplicate htlc.
+	ResultDuplicateToAccepted
+
+	// ResultDuplicateToSettled is returned when we settle an invoice which has
+	// already been settled at least once.
+	ResultDuplicateToSettled
+
+	// ResultAccepted is returned when we accept a hodl invoice.
+	ResultAccepted
+
+	// ResultSettled is returned when we settle an invoice.
+	ResultSettled
+
+	// ResultCanceled is returned when we cancel an invoice and its associated
+	// htlcs.
+	ResultCanceled
+
+	// ResultInvoiceNotOpen is returned when a mpp invoice is not open.
+	ResultInvoiceNotOpen
+
+	// ResultPartialAccepted is returned when we have partially received
+	// payment.
+	ResultPartialAccepted
+
+	// ResultMppInProgress is returned when we are busy receiving a mpp payment.
+	ResultMppInProgress
+
+	// ResultMppTimeout is returned when an invoice paid with multiple partial
+	// payments times out before it is fully paid.
+	ResultMppTimeout
+
+	// ResultAddressMismatch is returned when the payment address for a mpp
+	// invoice does not match.
+	ResultAddressMismatch
+
+	// ResultHtlcSetTotalMismatch is returned when the amount paid by a htlc
+	// does not match its set total.
+	ResultHtlcSetTotalMismatch
+
+	// ResultHtlcSetTotalTooLow is returned when a mpp set total is too low for
+	// an invoice.
+	ResultHtlcSetTotalTooLow
+
+	// ResultHtlcSetOverpayment is returned when a mpp set is overpaid.
+	ResultHtlcSetOverpayment
+
+	// ResultInvoiceNotFound is returned when an attempt is made to pay an
+	// invoice that is unknown to us.
+	ResultInvoiceNotFound
+
+	// ResultKeySendError is returned when we receive invalid keysend
+	// parameters.
+	ResultKeySendError
+)
+
+// String returns a human-readable representation of the invoice update result.
+func (u ResolutionResult) String() string {
+	switch u {
+
+	case resultInvalid:
+		return "invalid"
+
+	case ResultReplayToCanceled:
+		return "replayed htlc to canceled invoice"
+
+	case ResultReplayToAccepted:
+		return "replayed htlc to accepted invoice"
+
+	case ResultReplayToSettled:
+		return "replayed htlc to settled invoice"
+
+	case ResultInvoiceAlreadyCanceled:
+		return "invoice already canceled"
+
+	case ResultAmountTooLow:
+		return "amount too low"
+
+	case ResultExpiryTooSoon:
+		return "expiry too soon"
+
+	case ResultDuplicateToAccepted:
+		return "accepting duplicate payment to accepted invoice"
+
+	case ResultDuplicateToSettled:
+		return "accepting duplicate payment to settled invoice"
+
+	case ResultAccepted:
+		return "accepted"
+
+	case ResultSettled:
+		return "settled"
+
+	case ResultCanceled:
+		return "canceled"
+
+	case ResultInvoiceNotOpen:
+		return "invoice no longer open"
+
+	case ResultPartialAccepted:
+		return "partial payment accepted"
+
+	case ResultMppInProgress:
+		return "mpp reception in progress"
+
+	case ResultMppTimeout:
+		return "mpp timeout"
+
+	case ResultAddressMismatch:
+		return "payment address mismatch"
+
+	case ResultHtlcSetTotalMismatch:
+		return "htlc total amt doesn't match set total"
+
+	case ResultHtlcSetTotalTooLow:
+		return "set total too low for invoice"
+
+	case ResultHtlcSetOverpayment:
+		return "mpp is overpaying set total"
+
+	case ResultKeySendError:
+		return "invalid keysend parameters"
+
+	case ResultInvoiceNotFound:
+		return "invoice not found"
+
+	default:
+		return "unknown"
+	}
+}
+
 // invoiceUpdateCtx is an object that describes the context for the invoice
 // update to be carried out.
 type invoiceUpdateCtx struct {
@@ -28,55 +186,26 @@ func (i *invoiceUpdateCtx) log(s string) {
 		i.hash[:], s, i.amtPaid, i.expiry, i.circuitKey, i.mpp)
 }
 
-// failRes is a helper function which creates a failure resolution with
-// the information contained in the invoiceUpdateCtx and the fail resolution
-// result provided.
-func (i invoiceUpdateCtx) failRes(outcome FailResolutionResult) *HtlcFailResolution {
-	return NewFailResolution(i.circuitKey, i.currentHeight, outcome)
-}
-
-// settleRes is a helper function which creates a settle resolution with
-// the information contained in the invoiceUpdateCtx and the preimage and
-// the settle resolution result provided.
-func (i invoiceUpdateCtx) settleRes(preimage lntypes.Preimage,
-	outcome SettleResolutionResult) *HtlcSettleResolution {
-
-	return NewSettleResolution(
-		preimage, i.circuitKey, i.currentHeight, outcome,
-	)
-}
-
-// acceptRes is a helper function which creates an accept resolution with
-// the information contained in the invoiceUpdateCtx and the accept resolution
-// result provided.
-func (i invoiceUpdateCtx) acceptRes(outcome acceptResolutionResult) *htlcAcceptResolution {
-	return newAcceptResolution(i.circuitKey, outcome)
-}
-
 // updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
-// settlement logic. It returns a hltc resolution that indicates what the
-// outcome of the update was.
+// settlement logic.
 func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
-	*channeldb.InvoiceUpdateDesc, HtlcResolution, error) {
+	*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
 
 	// Don't update the invoice when this is a replayed htlc.
 	htlc, ok := inv.Htlcs[ctx.circuitKey]
 	if ok {
 		switch htlc.State {
 		case channeldb.HtlcStateCanceled:
-			return nil, ctx.failRes(ResultReplayToCanceled), nil
+			return nil, ResultReplayToCanceled, nil
 
 		case channeldb.HtlcStateAccepted:
-			return nil, ctx.acceptRes(resultReplayToAccepted), nil
+			return nil, ResultReplayToAccepted, nil
 
 		case channeldb.HtlcStateSettled:
-			return nil, ctx.settleRes(
-				inv.Terms.PaymentPreimage,
-				ResultReplayToSettled,
-			), nil
+			return nil, ResultReplayToSettled, nil
 
 		default:
-			return nil, nil, errors.New("unknown htlc state")
+			return nil, 0, errors.New("unknown htlc state")
 		}
 	}
 
@@ -89,9 +218,8 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 
 // updateMpp is a callback for DB.UpdateInvoice that contains the invoice
 // settlement logic for mpp payments.
-func updateMpp(ctx *invoiceUpdateCtx,
-	inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
-	HtlcResolution, error) {
+func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
+	*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
 
 	// Start building the accept descriptor.
 	acceptDesc := &channeldb.HtlcAcceptDesc{
@@ -107,23 +235,23 @@ func updateMpp(ctx *invoiceUpdateCtx,
 	// Because non-mpp payments don't have a payment address, this is needed
 	// to thwart probing.
 	if inv.State != channeldb.ContractOpen {
-		return nil, ctx.failRes(ResultInvoiceNotOpen), nil
+		return nil, ResultInvoiceNotOpen, nil
 	}
 
 	// Check the payment address that authorizes the payment.
 	if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
-		return nil, ctx.failRes(ResultAddressMismatch), nil
+		return nil, ResultAddressMismatch, nil
 	}
 
 	// Don't accept zero-valued sets.
 	if ctx.mpp.TotalMsat() == 0 {
-		return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
+		return nil, ResultHtlcSetTotalTooLow, nil
 	}
 
 	// Check that the total amt of the htlc set is high enough. In case this
 	// is a zero-valued invoice, it will always be enough.
 	if ctx.mpp.TotalMsat() < inv.Terms.Value {
-		return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
+		return nil, ResultHtlcSetTotalTooLow, nil
 	}
 
 	// Check whether total amt matches other htlcs in the set.
@@ -137,7 +265,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
 		}
 
 		if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
-			return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil
+			return nil, ResultHtlcSetTotalMismatch, nil
 		}
 
 		newSetTotal += htlc.Amt
@@ -148,16 +276,16 @@ func updateMpp(ctx *invoiceUpdateCtx,
 
 	// Make sure the communicated set total isn't overpaid.
 	if newSetTotal > ctx.mpp.TotalMsat() {
-		return nil, ctx.failRes(ResultHtlcSetOverpayment), nil
+		return nil, ResultHtlcSetOverpayment, nil
 	}
 
 	// The invoice is still open. Check the expiry.
 	if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
-		return nil, ctx.failRes(ResultExpiryTooSoon), nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
-		return nil, ctx.failRes(ResultExpiryTooSoon), nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	// Record HTLC in the invoice database.
@@ -172,7 +300,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
 	// If the invoice cannot be settled yet, only record the htlc.
 	setComplete := newSetTotal == ctx.mpp.TotalMsat()
 	if !setComplete {
-		return &update, ctx.acceptRes(resultPartialAccepted), nil
+		return &update, ResultPartialAccepted, nil
 	}
 
 	// Check to see if we can settle or this is an hold invoice and
@@ -182,7 +310,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
 		update.State = &channeldb.InvoiceStateUpdateDesc{
 			NewState: channeldb.ContractAccepted,
 		}
-		return &update, ctx.acceptRes(resultAccepted), nil
+		return &update, ResultAccepted, nil
 	}
 
 	update.State = &channeldb.InvoiceStateUpdateDesc{
@@ -190,20 +318,18 @@ func updateMpp(ctx *invoiceUpdateCtx,
 		Preimage: inv.Terms.PaymentPreimage,
 	}
 
-	return &update, ctx.settleRes(
-		inv.Terms.PaymentPreimage, ResultSettled,
-	), nil
+	return &update, ResultSettled, nil
 }
 
 // updateLegacy is a callback for DB.UpdateInvoice that contains the invoice
 // settlement logic for legacy payments.
-func updateLegacy(ctx *invoiceUpdateCtx,
-	inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc, HtlcResolution, error) {
+func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
+	*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
 
 	// If the invoice is already canceled, there is no further
 	// checking to do.
 	if inv.State == channeldb.ContractCanceled {
-		return nil, ctx.failRes(ResultInvoiceAlreadyCanceled), nil
+		return nil, ResultInvoiceAlreadyCanceled, nil
 	}
 
 	// If an invoice amount is specified, check that enough is paid. Also
@@ -211,7 +337,7 @@ func updateLegacy(ctx *invoiceUpdateCtx,
 	// or accepted. In case this is a zero-valued invoice, it will always be
 	// enough.
 	if ctx.amtPaid < inv.Terms.Value {
-		return nil, ctx.failRes(ResultAmountTooLow), nil
+		return nil, ResultAmountTooLow, nil
 	}
 
 	// TODO(joostjager): Check invoice mpp required feature
@@ -224,17 +350,17 @@ func updateLegacy(ctx *invoiceUpdateCtx,
 		if htlc.State == channeldb.HtlcStateAccepted &&
 			htlc.MppTotalAmt > 0 {
 
-			return nil, ctx.failRes(ResultMppInProgress), nil
+			return nil, ResultMppInProgress, nil
 		}
 	}
 
 	// The invoice is still open. Check the expiry.
 	if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
-		return nil, ctx.failRes(ResultExpiryTooSoon), nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
-		return nil, ctx.failRes(ResultExpiryTooSoon), nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	// Record HTLC in the invoice database.
@@ -255,12 +381,10 @@ func updateLegacy(ctx *invoiceUpdateCtx,
 	// We do accept or settle the HTLC.
 	switch inv.State {
 	case channeldb.ContractAccepted:
-		return &update, ctx.acceptRes(resultDuplicateToAccepted), nil
+		return &update, ResultDuplicateToAccepted, nil
 
 	case channeldb.ContractSettled:
-		return &update, ctx.settleRes(
-			inv.Terms.PaymentPreimage, ResultDuplicateToSettled,
-		), nil
+		return &update, ResultDuplicateToSettled, nil
 	}
 
 	// Check to see if we can settle or this is an hold invoice and we need
@@ -270,8 +394,7 @@ func updateLegacy(ctx *invoiceUpdateCtx,
 		update.State = &channeldb.InvoiceStateUpdateDesc{
 			NewState: channeldb.ContractAccepted,
 		}
-
-		return &update, ctx.acceptRes(resultAccepted), nil
+		return &update, ResultAccepted, nil
 	}
 
 	update.State = &channeldb.InvoiceStateUpdateDesc{
@@ -279,7 +402,5 @@ func updateLegacy(ctx *invoiceUpdateCtx,
 		Preimage: inv.Terms.PaymentPreimage,
 	}
 
-	return &update, ctx.settleRes(
-		inv.Terms.PaymentPreimage, ResultSettled,
-	), nil
+	return &update, ResultSettled, nil
 }
diff --git a/lnrpc/signrpc/signer_server.go b/lnrpc/signrpc/signer_server.go
index e51fe5d00..b3da2f938 100644
--- a/lnrpc/signrpc/signer_server.go
+++ b/lnrpc/signrpc/signer_server.go
@@ -373,9 +373,8 @@ func (s *Server) ComputeInputScript(ctx context.Context,
 				Value:    signDesc.Output.Value,
 				PkScript: signDesc.Output.PkScript,
 			},
-			HashType:   txscript.SigHashType(signDesc.Sighash),
-			SigHashes:  sigHashCache,
-			InputIndex: int(signDesc.InputIndex),
+			HashType:  txscript.SigHashType(signDesc.Sighash),
+			SigHashes: sigHashCache,
 		})
 	}
 
diff --git a/routing/graph.go b/routing/graph.go
index f3dfa121d..14eca1786 100644
--- a/routing/graph.go
+++ b/routing/graph.go
@@ -1,4 +1,104 @@
 package routing
 
-// TODO(roasbeef): abstract out graph to interface
-//  * add in-memory version of graph for tests
+import (
+	"github.com/coreos/bbolt"
+	"github.com/lightningnetwork/lnd/channeldb"
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/routing/route"
+)
+
+// routingGraph is an abstract interface that provides information about nodes
+// and edges to pathfinding.
+type routingGraph interface {
+	// forEachNodeChannel calls the callback for every channel of the given node.
+	forEachNodeChannel(nodePub route.Vertex,
+		cb func(*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
+			*channeldb.ChannelEdgePolicy) error) error
+
+	// sourceNode returns the source node of the graph.
+	sourceNode() route.Vertex
+
+	// fetchNodeFeatures returns the features of the given node.
+	fetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error)
+}
+
+// dbRoutingTx is a routingGraph implementation that retrieves from the
+// database.
+type dbRoutingTx struct {
+	graph  *channeldb.ChannelGraph
+	tx     *bbolt.Tx
+	source route.Vertex
+}
+
+// newDbRoutingTx instantiates a new db-connected routing graph. It implictly
+// instantiates a new read transaction.
+func newDbRoutingTx(graph *channeldb.ChannelGraph) (*dbRoutingTx, error) {
+	sourceNode, err := graph.SourceNode()
+	if err != nil {
+		return nil, err
+	}
+
+	tx, err := graph.Database().Begin(false)
+	if err != nil {
+		return nil, err
+	}
+
+	return &dbRoutingTx{
+		graph:  graph,
+		tx:     tx,
+		source: sourceNode.PubKeyBytes,
+	}, nil
+}
+
+// close closes the underlying db transaction.
+func (g *dbRoutingTx) close() error {
+	return g.tx.Rollback()
+}
+
+// forEachNodeChannel calls the callback for every channel of the given node.
+//
+// NOTE: Part of the routingGraph interface.
+func (g *dbRoutingTx) forEachNodeChannel(nodePub route.Vertex,
+	cb func(*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
+		*channeldb.ChannelEdgePolicy) error) error {
+
+	txCb := func(_ *bbolt.Tx, info *channeldb.ChannelEdgeInfo,
+		p1, p2 *channeldb.ChannelEdgePolicy) error {
+
+		return cb(info, p1, p2)
+	}
+
+	return g.graph.ForEachNodeChannel(g.tx, nodePub[:], txCb)
+}
+
+// sourceNode returns the source node of the graph.
+//
+// NOTE: Part of the routingGraph interface.
+func (g *dbRoutingTx) sourceNode() route.Vertex {
+	return g.source
+}
+
+// fetchNodeFeatures returns the features of the given node. If the node is
+// unknown, assume no additional features are supported.
+//
+// NOTE: Part of the routingGraph interface.
+func (g *dbRoutingTx) fetchNodeFeatures(nodePub route.Vertex) (
+	*lnwire.FeatureVector, error) {
+
+	targetNode, err := g.graph.FetchLightningNode(g.tx, nodePub)
+	switch err {
+
+	// If the node exists and has features, return them directly.
+	case nil:
+		return targetNode.Features, nil
+
+	// If we couldn't find a node announcement, populate a blank feature
+	// vector.
+	case channeldb.ErrGraphNodeNotFound:
+		return lnwire.EmptyFeatureVector(), nil
+
+	// Otherwise bubble the error up.
+	default:
+		return nil, err
+	}
+}
diff --git a/routing/pathfind.go b/routing/pathfind.go
index 23bc1f9e9..f2ea782d9 100644
--- a/routing/pathfind.go
+++ b/routing/pathfind.go
@@ -7,7 +7,6 @@ import (
 	"math"
 	"time"
 
-	"github.com/coreos/bbolt"
 	sphinx "github.com/lightningnetwork/lightning-onion"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/feature"
@@ -271,10 +270,6 @@ func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi,
 
 // graphParams wraps the set of graph parameters passed to findPath.
 type graphParams struct {
-	// tx can be set to an existing db transaction. If not set, a new
-	// transaction will be started.
-	tx *bbolt.Tx
-
 	// graph is the ChannelGraph to be used during path finding.
 	graph *channeldb.ChannelGraph
 
@@ -350,10 +345,11 @@ type PathFindingConfig struct {
 // getMaxOutgoingAmt returns the maximum available balance in any of the
 // channels of the given node.
 func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
-	g *graphParams, tx *bbolt.Tx) (lnwire.MilliSatoshi, error) {
+	bandwidthHints map[uint64]lnwire.MilliSatoshi,
+	g routingGraph) (lnwire.MilliSatoshi, error) {
 
 	var max lnwire.MilliSatoshi
-	cb := func(_ *bbolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, outEdge,
+	cb := func(edgeInfo *channeldb.ChannelEdgeInfo, outEdge,
 		_ *channeldb.ChannelEdgePolicy) error {
 
 		if outEdge == nil {
@@ -367,7 +363,7 @@ func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
 			return nil
 		}
 
-		bandwidth, ok := g.bandwidthHints[chanID]
+		bandwidth, ok := bandwidthHints[chanID]
 
 		// If the bandwidth is not available for whatever reason, don't
 		// fail the pathfinding early.
@@ -384,28 +380,54 @@ func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
 	}
 
 	// Iterate over all channels of the to node.
-	err := g.graph.ForEachNodeChannel(tx, node[:], cb)
+	err := g.forEachNodeChannel(node, cb)
 	if err != nil {
 		return 0, err
 	}
 	return max, err
 }
 
-// findPath attempts to find a path from the source node within the
-// ChannelGraph to the target node that's capable of supporting a payment of
-// `amt` value. The current approach implemented is modified version of
-// Dijkstra's algorithm to find a single shortest path between the source node
-// and the destination. The distance metric used for edges is related to the
-// time-lock+fee costs along a particular edge. If a path is found, this
-// function returns a slice of ChannelHop structs which encoded the chosen path
-// from the target to the source. The search is performed backwards from
-// destination node back to source. This is to properly accumulate fees
-// that need to be paid along the path and accurately check the amount
-// to forward at every node against the available bandwidth.
+// findPath attempts to find a path from the source node within the ChannelGraph
+// to the target node that's capable of supporting a payment of `amt` value. The
+// current approach implemented is modified version of Dijkstra's algorithm to
+// find a single shortest path between the source node and the destination. The
+// distance metric used for edges is related to the time-lock+fee costs along a
+// particular edge. If a path is found, this function returns a slice of
+// ChannelHop structs which encoded the chosen path from the target to the
+// source. The search is performed backwards from destination node back to
+// source. This is to properly accumulate fees that need to be paid along the
+// path and accurately check the amount to forward at every node against the
+// available bandwidth.
 func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 	source, target route.Vertex, amt lnwire.MilliSatoshi, finalHtlcExpiry int32) (
 	[]*channeldb.ChannelEdgePolicy, error) {
 
+	routingTx, err := newDbRoutingTx(g.graph)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		err := routingTx.close()
+		if err != nil {
+			log.Errorf("Error closing db tx: %v", err)
+		}
+	}()
+
+	return findPathInternal(
+		g.additionalEdges, g.bandwidthHints, routingTx, r, cfg, source,
+		target, amt, finalHtlcExpiry,
+	)
+}
+
+// findPathInternal is the internal implementation of findPath.
+func findPathInternal(
+	additionalEdges map[route.Vertex][]*channeldb.ChannelEdgePolicy,
+	bandwidthHints map[uint64]lnwire.MilliSatoshi,
+	graph routingGraph,
+	r *RestrictParams, cfg *PathFindingConfig,
+	source, target route.Vertex, amt lnwire.MilliSatoshi, finalHtlcExpiry int32) (
+	[]*channeldb.ChannelEdgePolicy, error) {
+
 	// Pathfinding can be a significant portion of the total payment
 	// latency, especially on low-powered devices. Log several metrics to
 	// aid in the analysis performance problems in this area.
@@ -418,47 +440,20 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 			"time=%v", nodesVisited, edgesExpanded, timeElapsed)
 	}()
 
-	// Get source node outside of the pathfinding tx, to prevent a deadlock.
-	selfNode, err := g.graph.SourceNode()
-	if err != nil {
-		return nil, err
-	}
-	self := selfNode.PubKeyBytes
-
-	tx := g.tx
-	if tx == nil {
-		tx, err = g.graph.Database().Begin(false)
-		if err != nil {
-			return nil, err
-		}
-		defer tx.Rollback()
-	}
-
 	// If no destination features are provided, we will load what features
 	// we have for the target node from our graph.
 	features := r.DestFeatures
 	if features == nil {
-		targetNode, err := g.graph.FetchLightningNode(tx, target)
-		switch {
-
-		// If the node exists and has features, use them directly.
-		case err == nil:
-			features = targetNode.Features
-
-		// If an error other than the node not existing is hit, abort.
-		case err != channeldb.ErrGraphNodeNotFound:
+		var err error
+		features, err = graph.fetchNodeFeatures(target)
+		if err != nil {
 			return nil, err
-
-		// Otherwise, we couldn't find a node announcement, populate a
-		// blank feature vector.
-		default:
-			features = lnwire.EmptyFeatureVector()
 		}
 	}
 
 	// Ensure that the destination's features don't include unknown
 	// required features.
-	err = feature.ValidateRequired(features)
+	err := feature.ValidateRequired(features)
 	if err != nil {
 		return nil, err
 	}
@@ -491,8 +486,12 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 
 	// If we are routing from ourselves, check that we have enough local
 	// balance available.
+	self := graph.sourceNode()
+
 	if source == self {
-		max, err := getMaxOutgoingAmt(self, r.OutgoingChannelID, g, tx)
+		max, err := getMaxOutgoingAmt(
+			self, r.OutgoingChannelID, bandwidthHints, graph,
+		)
 		if err != nil {
 			return nil, err
 		}
@@ -510,7 +509,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 	distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount)
 
 	additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
-	for vertex, outgoingEdgePolicies := range g.additionalEdges {
+	for vertex, outgoingEdgePolicies := range additionalEdges {
 		// Build reverse lookup to find incoming edges. Needed because
 		// search is taken place from target to source.
 		for _, outgoingEdgePolicy := range outgoingEdgePolicies {
@@ -752,45 +751,35 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 
 		// Check cache for features of the fromNode.
 		fromFeatures, ok := featureCache[node]
-		if !ok {
-			targetNode, err := g.graph.FetchLightningNode(tx, node)
-			switch {
-
-			// If the node exists and has valid features, use them.
-			case err == nil:
-				nodeFeatures := targetNode.Features
-
-				// Don't route through nodes that contain
-				// unknown required features.
-				err = feature.ValidateRequired(nodeFeatures)
-				if err != nil {
-					break
-				}
-
-				// Don't route through nodes that don't properly
-				// set all transitive feature dependencies.
-				err = feature.ValidateDeps(nodeFeatures)
-				if err != nil {
-					break
-				}
-
-				fromFeatures = nodeFeatures
-
-			// If an error other than the node not existing is hit,
-			// abort.
-			case err != channeldb.ErrGraphNodeNotFound:
-				return nil, err
+		if ok {
+			return fromFeatures, nil
+		}
 
-			// Otherwise, we couldn't find a node announcement,
-			// populate a blank feature vector.
-			default:
-				fromFeatures = lnwire.EmptyFeatureVector()
-			}
+		// Fetch node features fresh from the graph.
+		fromFeatures, err := graph.fetchNodeFeatures(node)
+		if err != nil {
+			return nil, err
+		}
 
-			// Update cache.
-			featureCache[node] = fromFeatures
+		// Don't route through nodes that contain unknown required
+		// features and mark as nil in the cache.
+		err = feature.ValidateRequired(fromFeatures)
+		if err != nil {
+			featureCache[node] = nil
+			return nil, nil
 		}
 
+		// Don't route through nodes that don't properly set all
+		// transitive feature dependencies and mark as nil in the cache.
+		err = feature.ValidateDeps(fromFeatures)
+		if err != nil {
+			featureCache[node] = nil
+			return nil, nil
+		}
+
+		// Update cache.
+		featureCache[node] = fromFeatures
+
 		return fromFeatures, nil
 	}
 
@@ -803,7 +792,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 		// Create unified policies for all incoming connections.
 		u := newUnifiedPolicies(self, pivot, r.OutgoingChannelID)
 
-		err := u.addGraphPolicies(g.graph, tx)
+		err := u.addGraphPolicies(graph)
 		if err != nil {
 			return nil, err
 		}
@@ -834,7 +823,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 			}
 
 			policy := unifiedPolicy.getPolicy(
-				amtToSend, g.bandwidthHints,
+				amtToSend, bandwidthHints,
 			)
 
 			if policy == nil {
diff --git a/routing/router.go b/routing/router.go
index a7c24ba4e..e42ce7a0e 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -2311,6 +2311,13 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
 		return nil, err
 	}
 
+	// Fetch the current block height outside the routing transaction, to
+	// prevent the rpc call blocking the database.
+	_, height, err := r.cfg.Chain.GetBestBlock()
+	if err != nil {
+		return nil, err
+	}
+
 	// Allocate a list that will contain the unified policies for this
 	// route.
 	edges := make([]*unifiedPolicy, len(hops))
@@ -2328,6 +2335,18 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
 		runningAmt = *amt
 	}
 
+	// Open a transaction to execute the graph queries in.
+	routingTx, err := newDbRoutingTx(r.cfg.Graph)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		err := routingTx.close()
+		if err != nil {
+			log.Errorf("Error closing db tx: %v", err)
+		}
+	}()
+
 	// Traverse hops backwards to accumulate fees in the running amounts.
 	source := r.selfNode.PubKeyBytes
 	for i := len(hops) - 1; i >= 0; i-- {
@@ -2346,7 +2365,7 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
 		// known in the graph.
 		u := newUnifiedPolicies(source, toNode, outgoingChan)
 
-		err := u.addGraphPolicies(r.cfg.Graph, nil)
+		err := u.addGraphPolicies(routingTx)
 		if err != nil {
 			return nil, err
 		}
@@ -2414,11 +2433,6 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
 	}
 
 	// Build and return the final route.
-	_, height, err := r.cfg.Chain.GetBestBlock()
-	if err != nil {
-		return nil, err
-	}
-
 	return newRoute(
 		source, pathEdges, uint32(height),
 		finalHopParams{
diff --git a/routing/unified_policies.go b/routing/unified_policies.go
index 81e646c29..3759175a6 100644
--- a/routing/unified_policies.go
+++ b/routing/unified_policies.go
@@ -2,7 +2,6 @@ package routing
 
 import (
 	"github.com/btcsuite/btcutil"
-	"github.com/coreos/bbolt"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
@@ -69,10 +68,8 @@ func (u *unifiedPolicies) addPolicy(fromNode route.Vertex,
 
 // addGraphPolicies adds all policies that are known for the toNode in the
 // graph.
-func (u *unifiedPolicies) addGraphPolicies(g *channeldb.ChannelGraph,
-	tx *bbolt.Tx) error {
-
-	cb := func(_ *bbolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, _,
+func (u *unifiedPolicies) addGraphPolicies(g routingGraph) error {
+	cb := func(edgeInfo *channeldb.ChannelEdgeInfo, _,
 		inEdge *channeldb.ChannelEdgePolicy) error {
 
 		// If there is no edge policy for this candidate node, skip.
@@ -95,7 +92,7 @@ func (u *unifiedPolicies) addGraphPolicies(g *channeldb.ChannelGraph,
 	}
 
 	// Iterate over all channels of the to node.
-	return g.ForEachNodeChannel(tx, u.toNode[:], cb)
+	return g.forEachNodeChannel(u.toNode, cb)
 }
 
 // unifiedPolicyEdge is the individual channel data that is kept inside an
diff --git a/rpcserver.go b/rpcserver.go
index 305ddc95c..f4e13cd65 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -200,6 +200,7 @@ var (
 	validEntities = []string{
 		"onchain", "offchain", "address", "message",
 		"peers", "info", "invoices", "signer", "macaroon",
+		"address",
 	}
 )
 


Test patch:
diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go
index cb29b5213..891f0d824 100644
--- a/channeldb/channel_test.go
+++ b/channeldb/channel_test.go
@@ -71,17 +71,6 @@ var (
 	wireSig, _ = lnwire.NewSigFromSignature(testSig)
 
 	testClock = clock.NewTestClock(testNow)
-
-	// defaultPendingHeight is the default height at which we set
-	// channels to pending.
-	defaultPendingHeight = 100
-
-	// defaultAddr is the default address that we mark test channels pending
-	// with.
-	defaultAddr = &net.TCPAddr{
-		IP:   net.ParseIP("127.0.0.1"),
-		Port: 18555,
-	}
 )
 
 // makeTestDB creates a new instance of the ChannelDB for testing purposes. A
@@ -109,148 +98,21 @@ func makeTestDB() (*DB, func(), error) {
 	return cdb, cleanUp, nil
 }
 
-// testChannelParams is a struct which details the specifics of how a channel
-// should be created.
-type testChannelParams struct {
-	// channel is the channel that will be written to disk.
-	channel *OpenChannel
-
-	// addr is the address that the channel will be synced pending with.
-	addr *net.TCPAddr
-
-	// pendingHeight is the height that the channel should be recorded as
-	// pending.
-	pendingHeight uint32
-
-	// openChannel is set to true if the channel should be fully marked as
-	// open if this is false, the channel will be left in pending state.
-	openChannel bool
-}
-
-// testChannelOption is a functional option which can be used to alter the
-// default channel that is creates for testing.
-type testChannelOption func(params *testChannelParams)
-
-// pendingHeightOption is an option which can be used to set the height the
-// channel is marked as pending at.
-func pendingHeightOption(height uint32) testChannelOption {
-	return func(params *testChannelParams) {
-		params.pendingHeight = height
-	}
-}
-
-// openChannelOption is an option which can be used to create a test channel
-// that is open.
-func openChannelOption() testChannelOption {
-	return func(params *testChannelParams) {
-		params.openChannel = true
-	}
-}
-
-// localHtlcsOption is an option which allows setting of htlcs on the local
-// commitment.
-func localHtlcsOption(htlcs []HTLC) testChannelOption {
-	return func(params *testChannelParams) {
-		params.channel.LocalCommitment.Htlcs = htlcs
-	}
-}
-
-// remoteHtlcsOption is an option which allows setting of htlcs on the remote
-// commitment.
-func remoteHtlcsOption(htlcs []HTLC) testChannelOption {
-	return func(params *testChannelParams) {
-		params.channel.RemoteCommitment.Htlcs = htlcs
-	}
-}
-
-// localShutdownOption is an option which sets the local upfront shutdown
-// script for the channel.
-func localShutdownOption(addr lnwire.DeliveryAddress) testChannelOption {
-	return func(params *testChannelParams) {
-		params.channel.LocalShutdownScript = addr
-	}
-}
-
-// remoteShutdownOption is an option which sets the remote upfront shutdown
-// script for the channel.
-func remoteShutdownOption(addr lnwire.DeliveryAddress) testChannelOption {
-	return func(params *testChannelParams) {
-		params.channel.RemoteShutdownScript = addr
-	}
-}
-
-// fundingPointOption is an option which sets the funding outpoint of the
-// channel.
-func fundingPointOption(chanPoint wire.OutPoint) testChannelOption {
-	return func(params *testChannelParams) {
-		params.channel.FundingOutpoint = chanPoint
-	}
-}
-
-// channelIDOption is an option which sets the short channel ID of the channel.
-var channelIDOption = func(chanID lnwire.ShortChannelID) testChannelOption {
-	return func(params *testChannelParams) {
-		params.channel.ShortChannelID = chanID
-	}
-}
-
-// createTestChannel writes a test channel to the database. It takes a set of
-// functional options which can be used to overwrite the default of creating
-// a pending channel that was broadcast at height 100.
-func createTestChannel(t *testing.T, cdb *DB,
-	opts ...testChannelOption) *OpenChannel {
-
-	// Create a default set of parameters.
-	params := &testChannelParams{
-		channel:       createTestChannelState(t, cdb),
-		addr:          defaultAddr,
-		openChannel:   false,
-		pendingHeight: uint32(defaultPendingHeight),
-	}
-
-	// Apply all functional options to the test channel params.
-	for _, o := range opts {
-		o(params)
-	}
-
-	// Mark the channel as pending.
-	err := params.channel.SyncPending(params.addr, params.pendingHeight)
-	if err != nil {
-		t.Fatalf("unable to save and serialize channel "+
-			"state: %v", err)
-	}
-
-	// If the parameters do not specify that we should open the channel
-	// fully, we return the pending channel.
-	if !params.openChannel {
-		return params.channel
-	}
-
-	// Mark the channel as open with the short channel id provided.
-	err = params.channel.MarkAsOpen(params.channel.ShortChannelID)
-	if err != nil {
-		t.Fatalf("unable to mark channel open: %v", err)
-	}
-
-	return params.channel
-}
-
-func createTestChannelState(t *testing.T, cdb *DB) *OpenChannel {
+func createTestChannelState(cdb *DB) (*OpenChannel, error) {
 	// Simulate 1000 channel updates.
 	producer, err := shachain.NewRevocationProducerFromBytes(key[:])
 	if err != nil {
-		t.Fatalf("could not get producer: %v", err)
+		return nil, err
 	}
 	store := shachain.NewRevocationStore()
 	for i := 0; i < 1; i++ {
 		preImage, err := producer.AtIndex(uint64(i))
 		if err != nil {
-			t.Fatalf("could not get "+
-				"preimage: %v", err)
+			return nil, err
 		}
 
 		if err := store.AddNextEntry(preImage); err != nil {
-			t.Fatalf("could not add entry: %v", err)
+			return nil, err
 		}
 	}
 
@@ -366,7 +228,7 @@ func createTestChannelState(t *testing.T, cdb *DB) *OpenChannel {
 		Db:                      cdb,
 		Packager:                NewChannelPackager(chanID),
 		FundingTxn:              testTx,
-	}
+	}, nil
 }
 
 func TestOpenChannelPutGetDelete(t *testing.T) {
@@ -378,10 +240,15 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
 	}
 	defer cleanUp()
 
-	// Create the test channel state, with additional htlcs on the local
-	// and remote commitment.
-	localHtlcs := []HTLC{
-		{Signature: testSig.Serialize(),
+	// Create the test channel state, then add an additional fake HTLC
+	// before syncing to disk.
+	state, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+	state.LocalCommitment.Htlcs = []HTLC{
+		{
+			Signature:     testSig.Serialize(),
 			Incoming:      true,
 			Amt:           10,
 			RHash:         key,
@@ -389,8 +256,7 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
 			OnionBlob:     []byte("onionblob"),
 		},
 	}
-
-	remoteHtlcs := []HTLC{
+	state.RemoteCommitment.Htlcs = []HTLC{
 		{
 			Signature:     testSig.Serialize(),
 			Incoming:      false,
@@ -401,11 +267,13 @@ func TestOpenChannelPutGetDelete(t *testing.T) {
 		},
 	}
 
-	state := createTestChannel(
-		t, cdb,
-		remoteHtlcsOption(remoteHtlcs),
-		localHtlcsOption(localHtlcs),
-	)
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18556,
+	}
+	if err := state.SyncPending(addr, 101); err != nil {
+		t.Fatalf("unable to save and serialize channel state: %v", err)
+	}
 
 	openChannels, err := cdb.FetchOpenChannels(state.IdentityPub)
 	if err != nil {
@@ -492,28 +360,36 @@ func TestOptionalShutdown(t *testing.T) {
 
 	tests := []struct {
 		name           string
-		localShutdown  lnwire.DeliveryAddress
-		remoteShutdown lnwire.DeliveryAddress
+		modifyChannel  func(channel *OpenChannel)
+		expectedLocal  lnwire.DeliveryAddress
+		expectedRemote lnwire.DeliveryAddress
 	}{
 		{
-			name:           "no shutdown scripts",
-			localShutdown:  nil,
-			remoteShutdown: nil,
+			name:          "no shutdown scripts",
+			modifyChannel: func(channel *OpenChannel) {},
 		},
 		{
-			name:           "local shutdown script",
-			localShutdown:  local,
-			remoteShutdown: nil,
+			name: "local shutdown script",
+			modifyChannel: func(channel *OpenChannel) {
+				channel.LocalShutdownScript = local
+			},
+			expectedLocal: local,
 		},
 		{
-			name:           "remote shutdown script",
-			localShutdown:  nil,
-			remoteShutdown: remote,
+			name: "remote shutdown script",
+			modifyChannel: func(channel *OpenChannel) {
+				channel.RemoteShutdownScript = remote
+			},
+			expectedRemote: remote,
 		},
 		{
-			name:           "both scripts set",
-			localShutdown:  local,
-			remoteShutdown: remote,
+			name: "both scripts set",
+			modifyChannel: func(channel *OpenChannel) {
+				channel.LocalShutdownScript = local
+				channel.RemoteShutdownScript = remote
+			},
+			expectedLocal:  local,
+			expectedRemote: remote,
 		},
 	}
 
@@ -527,40 +403,40 @@ func TestOptionalShutdown(t *testing.T) {
 			}
 			defer cleanUp()
 
-			// Create a channel with upfront scripts set as
-			// specified in the test.
-			state := createTestChannel(
-				t, cdb,
-				localShutdownOption(test.localShutdown),
-				remoteShutdownOption(test.remoteShutdown),
-			)
-
-			openChannels, err := cdb.FetchOpenChannels(
-				state.IdentityPub,
-			)
+			// Create the test channel state, then add an additional fake HTLC
+			// before syncing to disk.
+			state, err := createTestChannelState(cdb)
 			if err != nil {
-				t.Fatalf("unable to fetch open"+
-					" channel: %v", err)
+				t.Fatalf("unable to create channel state: %v", err)
 			}
 
-			if len(openChannels) != 1 {
-				t.Fatalf("Expected one channel open,"+
-					" got: %v", len(openChannels))
+			test.modifyChannel(state)
+
+			// Write channels to Db.
+			addr := &net.TCPAddr{
+				IP:   net.ParseIP("127.0.0.1"),
+				Port: 18556,
+			}
+			if err := state.SyncPending(addr, 101); err != nil {
+				t.Fatalf("unable to save and serialize channel state: %v", err)
 			}
 
-			if !bytes.Equal(openChannels[0].LocalShutdownScript,
-				test.localShutdown) {
+			openChannels, err := cdb.FetchOpenChannels(state.IdentityPub)
+			if err != nil {
+				t.Fatalf("unable to fetch open channel: %v", err)
+			}
 
-				t.Fatalf("Expected local: %x, got: %x",
-					test.localShutdown,
-					openChannels[0].LocalShutdownScript)
+			if len(openChannels) != 1 {
+				t.Fatalf("Expected one channel open, got: %v", len(openChannels))
 			}
 
-			if !bytes.Equal(openChannels[0].RemoteShutdownScript,
-				test.remoteShutdown) {
+			if !bytes.Equal(openChannels[0].LocalShutdownScript, test.expectedLocal) {
+				t.Fatalf("Expected local: %x, got: %x", test.expectedLocal,
+					openChannels[0].LocalShutdownScript)
+			}
 
-				t.Fatalf("Expected remote: %x, got: %x",
-					test.remoteShutdown,
+			if !bytes.Equal(openChannels[0].RemoteShutdownScript, test.expectedRemote) {
+				t.Fatalf("Expected remote: %x, got: %x", test.expectedRemote,
 					openChannels[0].RemoteShutdownScript)
 			}
 		})
@@ -586,7 +462,18 @@ func TestChannelStateTransition(t *testing.T) {
 
 	// First create a minimal channel, then perform a full sync in order to
 	// persist the data.
-	channel := createTestChannel(t, cdb)
+	channel, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18556,
+	}
+	if err := channel.SyncPending(addr, 101); err != nil {
+		t.Fatalf("unable to save and serialize channel state: %v", err)
+	}
 
 	// Add some HTLCs which were added during this new state transition.
 	// Half of the HTLCs are incoming, while the other half are outgoing.
@@ -889,9 +776,21 @@ func TestFetchPendingChannels(t *testing.T) {
 	}
 	defer cleanUp()
 
-	// Create a pending channel that was broadcast at height 99.
+	// Create first test channel state
+	state, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18555,
+	}
+
 	const broadcastHeight = 99
-	createTestChannel(t, cdb, pendingHeightOption(broadcastHeight))
+	if err := state.SyncPending(addr, broadcastHeight); err != nil {
+		t.Fatalf("unable to save and serialize channel state: %v", err)
+	}
 
 	pendingChannels, err := cdb.FetchPendingChannels()
 	if err != nil {
@@ -968,8 +867,35 @@ func TestFetchClosedChannels(t *testing.T) {
 	}
 	defer cleanUp()
 
-	// Create an open channel in the database.
-	state := createTestChannel(t, cdb, openChannelOption())
+	// First create a test channel, that we'll be closing within this pull
+	// request.
+	state, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+
+	// Next sync the channel to disk, marking it as being in a pending open
+	// state.
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18555,
+	}
+	const broadcastHeight = 99
+	if err := state.SyncPending(addr, broadcastHeight); err != nil {
+		t.Fatalf("unable to save and serialize channel state: %v", err)
+	}
+
+	// Next, simulate the confirmation of the channel by marking it as
+	// pending within the database.
+	chanOpenLoc := lnwire.ShortChannelID{
+		BlockHeight: 5,
+		TxIndex:     10,
+		TxPosition:  15,
+	}
+	err = state.MarkAsOpen(chanOpenLoc)
+	if err != nil {
+		t.Fatalf("unable to mark channel as open: %v", err)
+	}
 
 	// Next, close the channel by including a close channel summary in the
 	// database.
@@ -1049,6 +975,7 @@ func TestFetchWaitingCloseChannels(t *testing.T) {
 
 	const numChannels = 2
 	const broadcastHeight = 99
+	addr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 18555}
 
 	// We'll start by creating two channels within our test database. One of
 	// them will have their funding transaction confirmed on-chain, while
@@ -1061,11 +988,15 @@ func TestFetchWaitingCloseChannels(t *testing.T) {
 
 	channels := make([]*OpenChannel, numChannels)
 	for i := 0; i < numChannels; i++ {
-		// Create a pending channel in the database at the broadcast
-		// height.
-		channels[i] = createTestChannel(
-			t, db, pendingHeightOption(broadcastHeight),
-		)
+		channel, err := createTestChannelState(db)
+		if err != nil {
+			t.Fatalf("unable to create channel: %v", err)
+		}
+		err = channel.SyncPending(addr, broadcastHeight)
+		if err != nil {
+			t.Fatalf("unable to sync channel: %v", err)
+		}
+		channels[i] = channel
 	}
 
 	// We'll only confirm the first one.
@@ -1175,7 +1106,21 @@ func TestRefreshShortChanID(t *testing.T) {
 	defer cleanUp()
 
 	// First create a test channel.
-	state := createTestChannel(t, cdb)
+	state, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18555,
+	}
+
+	// Mark the channel as pending within the channeldb.
+	const broadcastHeight = 99
+	if err := state.SyncPending(addr, broadcastHeight); err != nil {
+		t.Fatalf("unable to save and serialize channel state: %v", err)
+	}
 
 	// Next, locate the pending channel with the database.
 	pendingChannels, err := cdb.FetchPendingChannels()
diff --git a/channeldb/db_test.go b/channeldb/db_test.go
index 935a287ce..b9b1189be 100644
--- a/channeldb/db_test.go
+++ b/channeldb/db_test.go
@@ -100,7 +100,10 @@ func TestFetchClosedChannelForID(t *testing.T) {
 
 	// Create the test channel state, that we will mutate the index of the
 	// funding point.
-	state := createTestChannelState(t, cdb)
+	state, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
 
 	// Now run through the number of channels, and modify the outpoint index
 	// to create new channel IDs.
@@ -108,12 +111,14 @@ func TestFetchClosedChannelForID(t *testing.T) {
 		// Save the open channel to disk.
 		state.FundingOutpoint.Index = i
 
-		// Write the channel to disk in a pending state.
-		createTestChannel(
-			t, cdb,
-			fundingPointOption(state.FundingOutpoint),
-			openChannelOption(),
-		)
+		addr := &net.TCPAddr{
+			IP:   net.ParseIP("127.0.0.1"),
+			Port: 18556,
+		}
+		if err := state.SyncPending(addr, 101); err != nil {
+			t.Fatalf("unable to save and serialize channel "+
+				"state: %v", err)
+		}
 
 		// Close the channel. To make sure we retrieve the correct
 		// summary later, we make them differ in the SettledBalance.
@@ -230,8 +235,26 @@ func TestFetchChannel(t *testing.T) {
 	}
 	defer cleanUp()
 
-	// Create an open channel.
-	channelState := createTestChannel(t, cdb, openChannelOption())
+	// Create the test channel state that we'll sync to the database
+	// shortly.
+	channelState, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+
+	// Mark the channel as pending, then immediately mark it as open to it
+	// can be fully visible.
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18555,
+	}
+	if err := channelState.SyncPending(addr, 9); err != nil {
+		t.Fatalf("unable to save and serialize channel state: %v", err)
+	}
+	err = channelState.MarkAsOpen(lnwire.NewShortChanIDFromInt(99))
+	if err != nil {
+		t.Fatalf("unable to mark channel open: %v", err)
+	}
 
 	// Next, attempt to fetch the channel by its chan point.
 	dbChannel, err := cdb.FetchChannel(channelState.FundingOutpoint)
@@ -248,7 +271,7 @@ func TestFetchChannel(t *testing.T) {
 
 	// If we attempt to query for a non-exist ante channel, then we should
 	// get an error.
-	channelState2 := createTestChannelState(t, cdb)
+	channelState2, err := createTestChannelState(cdb)
 	if err != nil {
 		t.Fatalf("unable to create channel state: %v", err)
 	}
@@ -468,9 +491,19 @@ func TestAbandonChannel(t *testing.T) {
 		t.Fatalf("removing non-existent channel should have failed")
 	}
 
-	// We'll now create a new channel in a pending state to abandon
-	// shortly.
-	chanState := createTestChannel(t, cdb)
+	// We'll now create a new channel to abandon shortly.
+	chanState, err := createTestChannelState(cdb)
+	if err != nil {
+		t.Fatalf("unable to create channel state: %v", err)
+	}
+	addr := &net.TCPAddr{
+		IP:   net.ParseIP("127.0.0.1"),
+		Port: 18555,
+	}
+	err = chanState.SyncPending(addr, 10)
+	if err != nil {
+		t.Fatalf("unable to sync pending channel: %v", err)
+	}
 
 	// We should now be able to abandon the channel without any errors.
 	closeHeight := uint32(11)
@@ -500,196 +533,3 @@ func TestAbandonChannel(t *testing.T) {
 		t.Fatalf("unable to abandon channel: %v", err)
 	}
 }
-
-// TestFetchChannels tests the filtering of open channels in fetchChannels.
-// It tests the case where no filters are provided (which is equivalent to
-// FetchAllOpenChannels) and every combination of pending and waiting close.
-func TestFetchChannels(t *testing.T) {
-	// Create static channel IDs for each kind of channel retrieved by
-	// fetchChannels so that the expected channel IDs can be set in tests.
-	var (
-		// Pending is a channel that is pending open, and has not had
-		// a close initiated.
-		pendingChan = lnwire.NewShortChanIDFromInt(1)
-
-		// pendingWaitingClose is a channel that is pending open and
-		// has has its closing transaction broadcast.
-		pendingWaitingChan = lnwire.NewShortChanIDFromInt(2)
-
-		// openChan is a channel that has confirmed on chain.
-		openChan = lnwire.NewShortChanIDFromInt(3)
-
-		// openWaitingChan is a channel that has confirmed on chain,
-		// and it waiting for its close transaction to confirm.
-		openWaitingChan = lnwire.NewShortChanIDFromInt(4)
-	)
-
-	tests := []struct {
-		name             string
-		filters          []fetchChannelsFilter
-		expectedChannels map[lnwire.ShortChannelID]bool
-	}{
-		{
-			name:    "get all channels",
-			filters: []fetchChannelsFilter{},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				pendingChan:        true,
-				pendingWaitingChan: true,
-				openChan:           true,
-				openWaitingChan:    true,
-			},
-		},
-		{
-			name: "pending channels",
-			filters: []fetchChannelsFilter{
-				pendingChannelFilter(true),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				pendingChan:        true,
-				pendingWaitingChan: true,
-			},
-		},
-		{
-			name: "open channels",
-			filters: []fetchChannelsFilter{
-				pendingChannelFilter(false),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				openChan:        true,
-				openWaitingChan: true,
-			},
-		},
-		{
-			name: "waiting close channels",
-			filters: []fetchChannelsFilter{
-				waitingCloseFilter(true),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				pendingWaitingChan: true,
-				openWaitingChan:    true,
-			},
-		},
-		{
-			name: "not waiting close channels",
-			filters: []fetchChannelsFilter{
-				waitingCloseFilter(false),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				pendingChan: true,
-				openChan:    true,
-			},
-		},
-		{
-			name: "pending waiting",
-			filters: []fetchChannelsFilter{
-				pendingChannelFilter(true),
-				waitingCloseFilter(true),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				pendingWaitingChan: true,
-			},
-		},
-		{
-			name: "pending, not waiting",
-			filters: []fetchChannelsFilter{
-				pendingChannelFilter(true),
-				waitingCloseFilter(false),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				pendingChan: true,
-			},
-		},
-		{
-			name: "open waiting",
-			filters: []fetchChannelsFilter{
-				pendingChannelFilter(false),
-				waitingCloseFilter(true),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				openWaitingChan: true,
-			},
-		},
-		{
-			name: "open, not waiting",
-			filters: []fetchChannelsFilter{
-				pendingChannelFilter(false),
-				waitingCloseFilter(false),
-			},
-			expectedChannels: map[lnwire.ShortChannelID]bool{
-				openChan: true,
-			},
-		},
-	}
-
-	for _, test := range tests {
-		test := test
-
-		t.Run(test.name, func(t *testing.T) {
-			t.Parallel()
-
-			cdb, cleanUp, err := makeTestDB()
-			if err != nil {
-				t.Fatalf("unable to make test "+
-					"database: %v", err)
-			}
-			defer cleanUp()
-
-			// Create a pending channel that is not awaiting close.
-			createTestChannel(
-				t, cdb, channelIDOption(pendingChan),
-			)
-
-			// Create a pending channel which has has been marked as
-			// broadcast, indicating that its closing transaction is
-			// waiting to confirm.
-			pendingClosing := createTestChannel(
-				t, cdb,
-				channelIDOption(pendingWaitingChan),
-			)
-
-			err = pendingClosing.MarkCoopBroadcasted(nil)
-			if err != nil {
-				t.Fatalf("unexpected error: %v", err)
-			}
-
-			// Create a open channel that is not awaiting close.
-			createTestChannel(
-				t, cdb,
-				channelIDOption(openChan),
-				openChannelOption(),
-			)
-
-			// Create a open channel which has has been marked as
-			// broadcast, indicating that its closing transaction is
-			// waiting to confirm.
-			openClosing := createTestChannel(
-				t, cdb,
-				channelIDOption(openWaitingChan),
-				openChannelOption(),
-			)
-			err = openClosing.MarkCoopBroadcasted(nil)
-			if err != nil {
-				t.Fatalf("unexpected error: %v", err)
-			}
-
-			channels, err := fetchChannels(cdb, test.filters...)
-			if err != nil {
-				t.Fatalf("unexpected error: %v", err)
-			}
-
-			if len(channels) != len(test.expectedChannels) {
-				t.Fatalf("expected: %v channels, "+
-					"got: %v", len(test.expectedChannels),
-					len(channels))
-			}
-
-			for _, ch := range channels {
-				_, ok := test.expectedChannels[ch.ShortChannelID]
-				if !ok {
-					t.Fatalf("fetch channels unexpected "+
-						"channel: %v", ch.ShortChannelID)
-				}
-			}
-		})
-	}
-}
diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go
index cc4c0f2b2..98e52500a 100644
--- a/contractcourt/htlc_incoming_contest_resolver.go
+++ b/contractcourt/htlc_incoming_contest_resolver.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"encoding/binary"
 	"errors"
-	"fmt"
 	"io"
 
 	"github.com/btcsuite/btcutil"
@@ -173,23 +172,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 	processHtlcResolution := func(e invoices.HtlcResolution) (
 		ContractResolver, error) {
 
-		// Take action based on the type of resolution we have
-		// received.
-		switch resolution := e.(type) {
-
-		// If the htlc resolution was a settle, apply the
-		// preimage and return a success resolver.
-		case *invoices.HtlcSettleResolution:
-			err := applyPreimage(resolution.Preimage)
-			if err != nil {
-				return nil, err
-			}
-
-			return &h.htlcSuccessResolver, nil
-
-		// If the htlc was failed, mark the htlc as
-		// resolved.
-		case *invoices.HtlcFailResolution:
+		if e.Preimage == nil {
 			log.Infof("%T(%v): Exit hop HTLC canceled "+
 				"(expiry=%v, height=%v), abandoning", h,
 				h.htlcResolution.ClaimOutpoint,
@@ -197,13 +180,13 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 
 			h.resolved = true
 			return nil, h.Checkpoint(h)
+		}
 
-		// Error if the resolution type is unknown, we are only
-		// expecting settles and fails.
-		default:
-			return nil, fmt.Errorf("unknown resolution"+
-				" type: %v", e)
+		if err := applyPreimage(*e.Preimage); err != nil {
+			return nil, err
 		}
+
+		return &h.htlcSuccessResolver, nil
 	}
 
 	// Create a buffered hodl chan to prevent deadlock.
@@ -228,29 +211,14 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 
 	defer h.Registry.HodlUnsubscribeAll(hodlChan)
 
-	// Take action based on the resolution we received. If the htlc was
-	// settled, or a htlc for a known invoice failed we can resolve it
-	// directly. If the resolution is nil, the htlc was neither accepted
-	// nor failed, so we cannot take action yet.
-	switch res := resolution.(type) {
-	case *invoices.HtlcFailResolution:
-		// In the case where the htlc failed, but the invoice was known
-		// to the registry, we can directly resolve the htlc.
-		if res.Outcome != invoices.ResultInvoiceNotFound {
-			return processHtlcResolution(resolution)
-		}
+	// If the resolution is non-nil (indicating that a settle or cancel has
+	// occurred), and the invoice is known to the registry (indicating that
+	// the htlc is paying one of our invoices and is not a forward), try to
+	// resolve it directly.
+	if resolution != nil &&
+		resolution.Outcome != invoices.ResultInvoiceNotFound {
 
-	// If we settled the htlc, we can resolve it.
-	case *invoices.HtlcSettleResolution:
-		return processHtlcResolution(resolution)
-
-	// If the resolution is nil, the htlc was neither settled nor failed so
-	// we cannot take action at present.
-	case nil:
-
-	default:
-		return nil, fmt.Errorf("unknown htlc resolution type: %T",
-			resolution)
+		return processHtlcResolution(*resolution)
 	}
 
 	// With the epochs and preimage subscriptions initialized, we'll query
@@ -288,6 +256,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 
 		case hodlItem := <-hodlChan:
 			htlcResolution := hodlItem.(invoices.HtlcResolution)
+
 			return processHtlcResolution(htlcResolution)
 
 		case newBlock, ok := <-blockEpochs.Epochs:
diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go
index 400662d73..773b22c00 100644
--- a/contractcourt/htlc_incoming_resolver_test.go
+++ b/contractcourt/htlc_incoming_resolver_test.go
@@ -35,7 +35,7 @@ func TestHtlcIncomingResolverFwdPreimageKnown(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyResolution = invoices.NewFailResolution(
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
 		testResCircuitKey, testHtlcExpiry,
 		invoices.ResultInvoiceNotFound,
 	)
@@ -52,7 +52,7 @@ func TestHtlcIncomingResolverFwdContestedSuccess(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyResolution = invoices.NewFailResolution(
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
 		testResCircuitKey, testHtlcExpiry,
 		invoices.ResultInvoiceNotFound,
 	)
@@ -72,7 +72,7 @@ func TestHtlcIncomingResolverFwdContestedTimeout(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyResolution = invoices.NewFailResolution(
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
 		testResCircuitKey, testHtlcExpiry,
 		invoices.ResultInvoiceNotFound,
 	)
@@ -91,7 +91,7 @@ func TestHtlcIncomingResolverFwdTimeout(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyResolution = invoices.NewFailResolution(
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
 		testResCircuitKey, testHtlcExpiry,
 		invoices.ResultInvoiceNotFound,
 	)
@@ -139,7 +139,7 @@ func TestHtlcIncomingResolverExitCancel(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyResolution = invoices.NewFailResolution(
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
 		testResCircuitKey, testAcceptHeight,
 		invoices.ResultInvoiceAlreadyCanceled,
 	)
@@ -158,7 +158,7 @@ func TestHtlcIncomingResolverExitSettleHodl(t *testing.T) {
 	ctx.resolve()
 
 	notifyData := <-ctx.registry.notifyChan
-	notifyData.hodlChan <- invoices.NewSettleResolution(
+	notifyData.hodlChan <- *invoices.NewSettleResolution(
 		testResPreimage, testResCircuitKey, testAcceptHeight,
 		invoices.ResultSettled,
 	)
@@ -187,7 +187,7 @@ func TestHtlcIncomingResolverExitCancelHodl(t *testing.T) {
 	ctx := newIncomingResolverTestContext(t)
 	ctx.resolve()
 	notifyData := <-ctx.registry.notifyChan
-	notifyData.hodlChan <- invoices.NewFailResolution(
+	notifyData.hodlChan <- *invoices.NewFailureResolution(
 		testResCircuitKey, testAcceptHeight, invoices.ResultCanceled,
 	)
 
diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go
index a7f430f23..dca27d629 100644
--- a/contractcourt/mock_registry_test.go
+++ b/contractcourt/mock_registry_test.go
@@ -18,13 +18,13 @@ type notifyExitHopData struct {
 type mockRegistry struct {
 	notifyChan       chan notifyExitHopData
 	notifyErr        error
-	notifyResolution invoices.HtlcResolution
+	notifyResolution *invoices.HtlcResolution
 }
 
 func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
 	paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
 	circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-	payload invoices.Payload) (invoices.HtlcResolution, error) {
+	payload invoices.Payload) (*invoices.HtlcResolution, error) {
 
 	r.notifyChan <- notifyExitHopData{
 		hodlChan:      hodlChan,
diff --git a/htlcswitch/switch_test.go b/htlcswitch/switch_test.go
index adc196499..828ef232c 100644
--- a/htlcswitch/switch_test.go
+++ b/htlcswitch/switch_test.go
@@ -211,13 +211,10 @@ func TestSwitchSendPending(t *testing.T) {
 	// Send the ADD packet, this should not be forwarded out to the link
 	// since there are no eligible links.
 	err = s.forward(packet)
-	linkErr, ok := err.(*LinkError)
-	if !ok {
-		t.Fatalf("expected link error, got: %T", err)
-	}
-	if linkErr.WireMessage().Code() != lnwire.CodeUnknownNextPeer {
-		t.Fatalf("expected fail unknown next peer, got: %T",
-			linkErr.WireMessage().Code())
+	expErr := fmt.Sprintf("unable to find link with destination %v",
+		aliceChanID)
+	if err != nil && err.Error() != expErr {
+		t.Fatalf("expected forward failure: %v", err)
 	}
 
 	// No message should be sent, since the packet was failed.
@@ -1073,13 +1070,9 @@ func TestSwitchForwardFailAfterHalfAdd(t *testing.T) {
 	// Resend the failed htlc, it should be returned to alice since the
 	// switch will detect that it has been half added previously.
 	err = s2.forward(ogPacket)
-	linkErr, ok := err.(*LinkError)
-	if !ok {
-		t.Fatalf("expected link error, got: %T", err)
-	}
-	if linkErr.FailureDetail != OutgoingFailureIncompleteForward {
-		t.Fatalf("expected incomplete forward, got: %v",
-			linkErr.FailureDetail)
+	if err != ErrIncompleteForward {
+		t.Fatal("unexpected error when reforwarding a "+
+			"failed packet", err)
 	}
 
 	// After detecting an incomplete forward, the fail packet should have
@@ -1355,7 +1348,7 @@ func TestCircularForwards(t *testing.T) {
 			allowCircularPayment: false,
 			expectedErr: NewDetailedLinkError(
 				lnwire.NewTemporaryChannelFailure(nil),
-				OutgoingFailureCircularRoute,
+				FailureDetailCircularRoute,
 			),
 		},
 	}
@@ -1472,7 +1465,7 @@ func TestCheckCircularForward(t *testing.T) {
 			outgoingLink:  lnwire.NewShortChanIDFromInt(123),
 			expectedErr: NewDetailedLinkError(
 				lnwire.NewTemporaryChannelFailure(nil),
-				OutgoingFailureCircularRoute,
+				FailureDetailCircularRoute,
 			),
 		},
 	}
@@ -1534,7 +1527,7 @@ func TestSkipIneligibleLinksMultiHopForward(t *testing.T) {
 			eligible1: true,
 			failure1: NewDetailedLinkError(
 				lnwire.NewTemporaryChannelFailure(nil),
-				OutgoingFailureInsufficientBalance,
+				FailureDetailInsufficientBalance,
 			),
 			eligible2: true,
 			failure2: NewLinkError(
diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go
index 319c30cf0..9f360e267 100644
--- a/invoices/invoiceregistry_test.go
+++ b/invoices/invoiceregistry_test.go
@@ -74,38 +74,29 @@ func TestSettleInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	failResolution, ok := resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
 	}
-	if failResolution.AcceptHeight != testCurrentHeight {
+	if resolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, failResolution.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
 	}
-	if failResolution.Outcome != ResultExpiryTooSoon {
+	if resolution.Outcome != ResultExpiryTooSoon {
 		t.Fatalf("expected expiry too soon, got: %v",
-			failResolution.Outcome)
+			resolution.Outcome)
 	}
 
 	// Settle invoice with a slightly higher amount.
 	amtPaid := lnwire.MilliSatoshi(100500)
 	resolution, err = ctx.registry.NotifyExitHopHtlc(
-		testInvoicePaymentHash, amtPaid, testHtlcExpiry,
-		testCurrentHeight, getCircuitKey(0), hodlChan,
-		testPayload,
+		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
+		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatal(err)
 	}
-	settleResolution, ok := resolution.(*HtlcSettleResolution)
-	if !ok {
-		t.Fatalf("expected settle resolution, got: %T",
-			resolution)
-	}
-	if settleResolution.Outcome != ResultSettled {
-		t.Fatalf("expected settled, got: %v",
-			settleResolution.Outcome)
+	if resolution.Outcome != ResultSettled {
+		t.Fatalf("expected settled, got: %v", resolution.Outcome)
 	}
 
 	// We expect the settled state to be sent to the single invoice
@@ -143,14 +134,12 @@ func TestSettleInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
 	}
-	settleResolution, ok = resolution.(*HtlcSettleResolution)
-	if !ok {
-		t.Fatalf("expected settle resolution, got: %T",
-			resolution)
+	if resolution.Preimage == nil {
+		t.Fatal("expected settle resolution")
 	}
-	if settleResolution.Outcome != ResultReplayToSettled {
+	if resolution.Outcome != ResultReplayToSettled {
 		t.Fatalf("expected replay settled, got: %v",
-			settleResolution.Outcome)
+			resolution.Outcome)
 	}
 
 	// Try to settle again with a new higher-valued htlc. This payment
@@ -163,14 +152,12 @@ func TestSettleInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
 	}
-	settleResolution, ok = resolution.(*HtlcSettleResolution)
-	if !ok {
-		t.Fatalf("expected settle resolution, got: %T",
-			resolution)
+	if resolution.Preimage == nil {
+		t.Fatal("expected settle resolution")
 	}
-	if settleResolution.Outcome != ResultDuplicateToSettled {
+	if resolution.Outcome != ResultDuplicateToSettled {
 		t.Fatalf("expected duplicate settled, got: %v",
-			settleResolution.Outcome)
+			resolution.Outcome)
 	}
 
 	// Try to settle again with a lower amount. This should fail just as it
@@ -182,14 +169,12 @@ func TestSettleInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
 	}
-	failResolution, ok = resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
 	}
-	if failResolution.Outcome != ResultAmountTooLow {
+	if resolution.Outcome != ResultAmountTooLow {
 		t.Fatalf("expected amount too low, got: %v",
-			failResolution.Outcome)
+			resolution.Outcome)
 	}
 
 	// Check that settled amount is equal to the sum of values of the htlcs
@@ -313,18 +298,17 @@ func TestCancelInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatal("expected settlement of a canceled invoice to succeed")
 	}
-	failResolution, ok := resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
+
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel htlc resolution")
 	}
-	if failResolution.AcceptHeight != testCurrentHeight {
+	if resolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, failResolution.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
 	}
-	if failResolution.Outcome != ResultInvoiceAlreadyCanceled {
-		t.Fatalf("expected expiry too soon, got: %v",
-			failResolution.Outcome)
+	if resolution.Outcome != ResultInvoiceAlreadyCanceled {
+		t.Fatalf("expected invoice already canceled, got: %v",
+			resolution.Outcome)
 	}
 }
 
@@ -438,14 +422,12 @@ func TestSettleHoldInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	failResolution, ok := resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
+	if resolution == nil || resolution.Preimage != nil {
+		t.Fatalf("expected htlc to be canceled")
 	}
-	if failResolution.Outcome != ResultExpiryTooSoon {
+	if resolution.Outcome != ResultExpiryTooSoon {
 		t.Fatalf("expected expiry too soon, got: %v",
-			failResolution.Outcome)
+			resolution.Outcome)
 	}
 
 	// We expect the accepted state to be sent to the single invoice
@@ -467,21 +449,16 @@ func TestSettleHoldInvoice(t *testing.T) {
 	}
 
 	htlcResolution := (<-hodlChan).(HtlcResolution)
-	settleResolution, ok := htlcResolution.(*HtlcSettleResolution)
-	if !ok {
-		t.Fatalf("expected settle resolution, got: %T",
-			htlcResolution)
-	}
-	if settleResolution.Preimage != testInvoicePreimage {
+	if *htlcResolution.Preimage != testInvoicePreimage {
 		t.Fatal("unexpected preimage in hodl resolution")
 	}
-	if settleResolution.AcceptHeight != testCurrentHeight {
+	if htlcResolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, settleResolution.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
 	}
-	if settleResolution.Outcome != ResultSettled {
+	if htlcResolution.Outcome != ResultSettled {
 		t.Fatalf("expected result settled, got: %v",
-			settleResolution.Outcome)
+			htlcResolution.Outcome)
 	}
 
 	// We expect a settled notification to be sent out for both all and
@@ -568,10 +545,8 @@ func TestCancelHoldInvoice(t *testing.T) {
 	}
 
 	htlcResolution := (<-hodlChan).(HtlcResolution)
-	_, ok := htlcResolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			htlcResolution)
+	if htlcResolution.Preimage != nil {
+		t.Fatal("expected cancel htlc resolution")
 	}
 
 	// Offering the same htlc again at a higher height should still result
@@ -584,18 +559,16 @@ func TestCancelHoldInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	failResolution, ok := resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
+	if resolution.Preimage != nil {
+		t.Fatalf("expected htlc to be canceled")
 	}
-	if failResolution.AcceptHeight != testCurrentHeight {
+	if resolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, failResolution.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
 	}
-	if failResolution.Outcome != ResultReplayToCanceled {
+	if resolution.Outcome != ResultReplayToCanceled {
 		t.Fatalf("expected replay to canceled, got %v",
-			failResolution.Outcome)
+			resolution.Outcome)
 	}
 }
 
@@ -612,21 +585,16 @@ func TestUnknownInvoice(t *testing.T) {
 	// succeed.
 	hodlChan := make(chan interface{})
 	amt := lnwire.MilliSatoshi(100000)
-	resolution, err := ctx.registry.NotifyExitHopHtlc(
+	result, err := ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatal("unexpected error")
 	}
-	failResolution, ok := resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
-	}
-	if failResolution.Outcome != ResultInvoiceNotFound {
+	if result.Outcome != ResultInvoiceNotFound {
 		t.Fatalf("expected ResultInvoiceNotFound, got: %v",
-			failResolution.Outcome)
+			result.Outcome)
 	}
 }
 
@@ -678,17 +646,16 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	failResolution, ok := resolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
-	}
 
+	// Expect a cancel resolution with the correct outcome.
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
+	}
 	switch {
-	case !keySendEnabled && failResolution.Outcome != ResultInvoiceNotFound:
+	case !keySendEnabled && resolution.Outcome != ResultInvoiceNotFound:
 		t.Fatal("expected invoice not found outcome")
 
-	case keySendEnabled && failResolution.Outcome != ResultKeySendError:
+	case keySendEnabled && resolution.Outcome != ResultKeySendError:
 		t.Fatal("expected keysend error")
 	}
 
@@ -709,25 +676,15 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
 
 	// Expect a cancel resolution if keysend is disabled.
 	if !keySendEnabled {
-		failResolution, ok = resolution.(*HtlcFailResolution)
-		if !ok {
-			t.Fatalf("expected fail resolution, got: %T",
-				resolution)
-		}
-		if failResolution.Outcome != ResultInvoiceNotFound {
+		if resolution.Outcome != ResultInvoiceNotFound {
 			t.Fatal("expected keysend payment not to be accepted")
 		}
 		return
 	}
 
 	// Otherwise we expect no error and a settle resolution for the htlc.
-	settleResolution, ok := resolution.(*HtlcSettleResolution)
-	if !ok {
-		t.Fatalf("expected settle resolution, got: %T",
-			resolution)
-	}
-	if settleResolution.Preimage != preimage {
-		t.Fatalf("expected settle with matching preimage")
+	if resolution.Preimage == nil || *resolution.Preimage != preimage {
+		t.Fatal("expected valid settle event")
 	}
 
 	// We expect a new invoice notification to be sent out.
@@ -782,14 +739,12 @@ func TestMppPayment(t *testing.T) {
 	ctx.clock.SetTime(testTime.Add(30 * time.Second))
 
 	htlcResolution := (<-hodlChan1).(HtlcResolution)
-	failResolution, ok := htlcResolution.(*HtlcFailResolution)
-	if !ok {
-		t.Fatalf("expected fail resolution, got: %T",
-			resolution)
+	if htlcResolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
 	}
-	if failResolution.Outcome != ResultMppTimeout {
+	if htlcResolution.Outcome != ResultMppTimeout {
 		t.Fatalf("expected mpp timeout, got: %v",
-			failResolution.Outcome)
+			htlcResolution.Outcome)
 	}
 
 	// Send htlc 2.
@@ -816,14 +771,12 @@ func TestMppPayment(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	settleResolution, ok := resolution.(*HtlcSettleResolution)
-	if !ok {
-		t.Fatalf("expected settle resolution, got: %T",
-			htlcResolution)
+	if resolution == nil {
+		t.Fatal("expected a settle resolution")
 	}
-	if settleResolution.Outcome != ResultSettled {
+	if resolution.Outcome != ResultSettled {
 		t.Fatalf("expected result settled, got: %v",
-			settleResolution.Outcome)
+			resolution.Outcome)
 	}
 
 	// Check that settled amount is equal to the sum of values of the htlcs
diff --git a/routing/integrated_routing_context_test.go b/routing/integrated_routing_context_test.go
new file mode 100644
index 000000000..eb59473e6
--- /dev/null
+++ b/routing/integrated_routing_context_test.go
@@ -0,0 +1,194 @@
+package routing
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/coreos/bbolt"
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/routing/route"
+)
+
+// integratedRoutingContext defines the context in which integrated routing
+// tests run.
+type integratedRoutingContext struct {
+	graph *mockGraph
+	t     *testing.T
+
+	source *mockNode
+	target *mockNode
+
+	amt         lnwire.MilliSatoshi
+	finalExpiry int32
+
+	mcCfg          MissionControlConfig
+	pathFindingCfg PathFindingConfig
+}
+
+// newIntegratedRoutingContext instantiates a new integrated routing test
+// context with a source and a target node.
+func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext {
+	// Instantiate a mock graph.
+	source := newMockNode()
+	target := newMockNode()
+
+	graph := newMockGraph(t)
+	graph.addNode(source)
+	graph.addNode(target)
+	graph.source = source
+
+	// Initiate the test context with a set of default configuration values.
+	// We don't use the lnd defaults here, because otherwise changing the
+	// defaults would break the unit tests. The actual values picked aren't
+	// critical to excite certain behavior, but do need to be aligned with
+	// the test case assertions.
+	ctx := integratedRoutingContext{
+		t:           t,
+		graph:       graph,
+		amt:         100000,
+		finalExpiry: 40,
+
+		mcCfg: MissionControlConfig{
+			PenaltyHalfLife:       30 * time.Minute,
+			AprioriHopProbability: 0.6,
+			AprioriWeight:         0.5,
+			SelfNode:              source.pubkey,
+		},
+
+		pathFindingCfg: PathFindingConfig{
+			PaymentAttemptPenalty: 1000,
+		},
+
+		source: source,
+		target: target,
+	}
+
+	return &ctx
+}
+
+// testPayment launches a test payment and asserts that it is completed after
+// the expected number of attempts.
+func (c *integratedRoutingContext) testPayment(expectedNofAttempts int) {
+	var nextPid uint64
+
+	// Create temporary database for mission control.
+	file, err := ioutil.TempFile("", "*.db")
+	if err != nil {
+		c.t.Fatal(err)
+	}
+
+	dbPath := file.Name()
+	defer os.Remove(dbPath)
+
+	db, err := bbolt.Open(dbPath, 0600, nil)
+	if err != nil {
+		c.t.Fatal(err)
+	}
+	defer db.Close()
+
+	// Instantiate a new mission control with the current configuration
+	// values.
+	mc, err := NewMissionControl(db, &c.mcCfg)
+	if err != nil {
+		c.t.Fatal(err)
+	}
+
+	// Instruct pathfinding to use mission control as a probabiltiy source.
+	restrictParams := RestrictParams{
+		ProbabilitySource: mc.GetProbability,
+		FeeLimit:          lnwire.MaxMilliSatoshi,
+	}
+
+	// Now the payment control loop starts. It will keep trying routes until
+	// the payment succeeds.
+	for {
+		// Create bandwidth hints based on local channel balances.
+		bandwidthHints := map[uint64]lnwire.MilliSatoshi{}
+		for _, ch := range c.graph.nodes[c.source.pubkey].channels {
+			bandwidthHints[ch.id] = ch.balance
+		}
+
+		// Find a route.
+		path, err := findPathInternal(
+			nil, bandwidthHints, c.graph,
+			&restrictParams,
+			&c.pathFindingCfg,
+			c.source.pubkey, c.target.pubkey,
+			c.amt, c.finalExpiry,
+		)
+		if err != nil {
+			c.t.Fatal(err)
+		}
+
+		finalHop := finalHopParams{
+			amt:       c.amt,
+			cltvDelta: uint16(c.finalExpiry),
+		}
+
+		route, err := newRoute(c.source.pubkey, path, 0, finalHop)
+		if err != nil {
+			c.t.Fatal(err)
+		}
+
+		// Send out the htlc on the mock graph.
+		pid := nextPid
+		nextPid++
+		htlcResult, err := c.graph.sendHtlc(route)
+		if err != nil {
+			c.t.Fatal(err)
+		}
+
+		// Process the result.
+		if htlcResult.failure == nil {
+			err := mc.ReportPaymentSuccess(pid, route)
+			if err != nil {
+				c.t.Fatal(err)
+			}
+
+			// If the payment is successful, the control loop can be
+			// broken out of.
+			break
+		}
+
+		// Failure, update mission control and retry.
+		c.t.Logf("fail: %v @ %v\n", htlcResult.failure, htlcResult.failureSource)
+
+		finalResult, err := mc.ReportPaymentFail(
+			pid, route,
+			getNodeIndex(route, htlcResult.failureSource),
+			htlcResult.failure,
+		)
+		if err != nil {
+			c.t.Fatal(err)
+		}
+
+		if finalResult != nil {
+			c.t.Logf("final result: %v\n", finalResult)
+			break
+		}
+	}
+
+	c.t.Logf("Payment attempts: %v\n", nextPid)
+	if expectedNofAttempts != int(nextPid) {
+		c.t.Fatalf("expected %v attempts, but needed %v",
+			expectedNofAttempts, nextPid)
+	}
+}
+
+// getNodeIndex returns the zero-based index of the given node in the route.
+func getNodeIndex(route *route.Route, failureSource route.Vertex) *int {
+	if failureSource == route.SourcePubKey {
+		idx := 0
+		return &idx
+	}
+
+	for i, h := range route.Hops {
+		if h.PubKeyBytes == failureSource {
+			idx := i + 1
+			return &idx
+		}
+	}
+	return nil
+}
diff --git a/routing/integrated_routing_test.go b/routing/integrated_routing_test.go
new file mode 100644
index 000000000..19df17a35
--- /dev/null
+++ b/routing/integrated_routing_test.go
@@ -0,0 +1,57 @@
+package routing
+
+import (
+	"testing"
+)
+
+// TestProbabilityExtrapolation tests that probabilities for tried channels are
+// extrapolated to untried channels. This is a way to improve pathfinding
+// success by steering away from bad nodes.
+func TestProbabilityExtrapolation(t *testing.T) {
+	ctx := newIntegratedRoutingContext(t)
+
+	// Create the following network of nodes:
+	// source -> expensiveNode (charges routing fee) -> target
+	// source -> intermediate1 (free routing) -> intermediate(1-10) (free routing) -> target
+	g := ctx.graph
+
+	expensiveNode := newMockNode()
+	expensiveNode.baseFee = 10000
+	g.addNode(expensiveNode)
+
+	g.addChannel(ctx.source, expensiveNode, 100000)
+	g.addChannel(ctx.target, expensiveNode, 100000)
+
+	intermediate1 := newMockNode()
+	g.addNode(intermediate1)
+	g.addChannel(ctx.source, intermediate1, 100000)
+
+	for i := 0; i < 10; i++ {
+		imNode := newMockNode()
+		g.addNode(imNode)
+		g.addChannel(imNode, ctx.target, 100000)
+		g.addChannel(imNode, intermediate1, 100000)
+
+		// The channels from intermediate1 all have insufficient balance.
+		g.nodes[intermediate1.pubkey].channels[imNode.pubkey].balance = 0
+	}
+
+	// It is expected that pathfinding will try to explore the routes via
+	// intermediate1 first, because those are free. But as failures happen,
+	// the node probability of intermediate1 will go down in favor of the
+	// paid route via expensiveNode.
+	//
+	// The exact number of attempts required is dependent on mission control
+	// config. For this test, it would have been enough to only assert that
+	// we are not trying all routes via intermediate1. However, we do assert
+	// a specific number of attempts to safe-guard against accidental
+	// modifications anywhere in the chain of components that is involved in
+	// this test.
+	ctx.testPayment(5)
+
+	// If we use a static value for the node probability (no extrapolation
+	// of data from other channels), all ten bad channels will be tried
+	// first before switching to the paid channel.
+	ctx.mcCfg.AprioriWeight = 1
+	ctx.testPayment(11)
+}
diff --git a/routing/mock_graph_test.go b/routing/mock_graph_test.go
new file mode 100644
index 000000000..075a416a1
--- /dev/null
+++ b/routing/mock_graph_test.go
@@ -0,0 +1,265 @@
+package routing
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/btcsuite/btcutil"
+	"github.com/lightningnetwork/lnd/channeldb"
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/routing/route"
+)
+
+// nextTestPubkey is global variable that is used to deterministically generate
+// test keys.
+var nextTestPubkey byte
+
+// createPubkey return a new test pubkey.
+func createPubkey() route.Vertex {
+	pubkey := route.Vertex{nextTestPubkey}
+	nextTestPubkey++
+	return pubkey
+}
+
+// mockChannel holds the channel state of a channel in the mock graph.
+type mockChannel struct {
+	id       uint64
+	capacity btcutil.Amount
+	balance  lnwire.MilliSatoshi
+}
+
+// mockNode holds a set of mock channels and routing policies for a node in the
+// mock graph.
+type mockNode struct {
+	channels map[route.Vertex]*mockChannel
+	baseFee  lnwire.MilliSatoshi
+	pubkey   route.Vertex
+}
+
+// newMockNode instantiates a new mock node with a newly generated pubkey.
+func newMockNode() *mockNode {
+	pubkey := createPubkey()
+	return &mockNode{
+		channels: make(map[route.Vertex]*mockChannel),
+		pubkey:   pubkey,
+	}
+}
+
+// fwd simulates an htlc forward through this node. If the from parameter is
+// nil, this node is considered to be the sender of the payment. The route
+// parameter describes the remaining route from this node onwards. If route.next
+// is nil, this node is the final hop.
+func (m *mockNode) fwd(from *mockNode, route *hop) (htlcResult, error) {
+	next := route.next
+
+	// Get the incoming channel, if any.
+	var inChan *mockChannel
+	if from != nil {
+		inChan = m.channels[from.pubkey]
+	}
+
+	// If there is no next node, this is the final node and we can settle the htlc.
+	if next == nil {
+		// Update the incoming balance.
+		inChan.balance += route.amtToFwd
+
+		return htlcResult{}, nil
+	}
+
+	// Check if the outgoing channel has enough balance.
+	outChan, ok := m.channels[next.node.pubkey]
+	if !ok {
+		return htlcResult{},
+			fmt.Errorf("%v: unknown next %v",
+				m.pubkey, next.node.pubkey)
+	}
+	if outChan.balance < route.amtToFwd {
+		return htlcResult{
+			failureSource: m.pubkey,
+			failure:       lnwire.NewTemporaryChannelFailure(nil),
+		}, nil
+	}
+
+	// Htlc can be forwarded, update channel balances.
+	outChan.balance -= route.amtToFwd
+	if inChan != nil {
+		inChan.balance += route.amtToFwd
+	}
+
+	// Recursively forward down the given route.
+	result, err := next.node.fwd(m, route.next)
+	if err != nil {
+		return htlcResult{}, err
+	}
+
+	// Revert balances when a failure occurs.
+	if result.failure != nil {
+		outChan.balance += route.amtToFwd
+		if inChan != nil {
+			inChan.balance -= route.amtToFwd
+		}
+	}
+
+	return result, nil
+}
+
+// mockGraph contains a set of nodes that together for a mocked graph.
+type mockGraph struct {
+	t          *testing.T
+	nodes      map[route.Vertex]*mockNode
+	nextChanID uint64
+	source     *mockNode
+}
+
+// newMockGraph instantiates a new mock graph.
+func newMockGraph(t *testing.T) *mockGraph {
+	return &mockGraph{
+		nodes: make(map[route.Vertex]*mockNode),
+		t:     t,
+	}
+}
+
+// addNode adds the given mock node to the network.
+func (m *mockGraph) addNode(node *mockNode) {
+	m.nodes[node.pubkey] = node
+}
+
+// addChannel adds a new channel between two existing nodes on the network. It
+// sets the channel balance to 50/50%.
+//
+// Ignore linter error because addChannel isn't yet called with different
+// capacities.
+// nolint:unparam
+func (m *mockGraph) addChannel(node1, node2 *mockNode, capacity btcutil.Amount) {
+	id := m.nextChanID
+	m.nextChanID++
+
+	m.nodes[node1.pubkey].channels[node2.pubkey] = &mockChannel{
+		capacity: capacity,
+		id:       id,
+		balance:  lnwire.NewMSatFromSatoshis(capacity / 2),
+	}
+	m.nodes[node2.pubkey].channels[node1.pubkey] = &mockChannel{
+		capacity: capacity,
+		id:       id,
+		balance:  lnwire.NewMSatFromSatoshis(capacity / 2),
+	}
+}
+
+// forEachNodeChannel calls the callback for every channel of the given node.
+//
+// NOTE: Part of the routingGraph interface.
+func (m *mockGraph) forEachNodeChannel(nodePub route.Vertex,
+	cb func(*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
+		*channeldb.ChannelEdgePolicy) error) error {
+
+	// Look up the mock node.
+	node, ok := m.nodes[nodePub]
+	if !ok {
+		return channeldb.ErrGraphNodeNotFound
+	}
+
+	// Iterate over all of its channels.
+	for peer, channel := range node.channels {
+		// Lexicographically sort the pubkeys.
+		var node1, node2 route.Vertex
+		if bytes.Compare(nodePub[:], peer[:]) == -1 {
+			node1, node2 = peer, nodePub
+		} else {
+			node1, node2 = nodePub, peer
+		}
+
+		peerNode := m.nodes[peer]
+
+		// Call the per channel callback.
+		err := cb(
+			&channeldb.ChannelEdgeInfo{
+				NodeKey1Bytes: node1,
+				NodeKey2Bytes: node2,
+			},
+			&channeldb.ChannelEdgePolicy{
+				ChannelID: channel.id,
+				Node: &channeldb.LightningNode{
+					PubKeyBytes: peer,
+					Features:    lnwire.EmptyFeatureVector(),
+				},
+				FeeBaseMSat: node.baseFee,
+			},
+			&channeldb.ChannelEdgePolicy{
+				ChannelID: channel.id,
+				Node: &channeldb.LightningNode{
+					PubKeyBytes: nodePub,
+					Features:    lnwire.EmptyFeatureVector(),
+				},
+				FeeBaseMSat: peerNode.baseFee,
+			},
+		)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// sourceNode returns the source node of the graph.
+//
+// NOTE: Part of the routingGraph interface.
+func (m *mockGraph) sourceNode() route.Vertex {
+	return m.source.pubkey
+}
+
+// fetchNodeFeatures returns the features of the given node.
+//
+// NOTE: Part of the routingGraph interface.
+func (m *mockGraph) fetchNodeFeatures(nodePub route.Vertex) (
+	*lnwire.FeatureVector, error) {
+
+	return lnwire.EmptyFeatureVector(), nil
+}
+
+// htlcResult describes the resolution of an htlc. If failure is nil, the htlc
+// was settled.
+type htlcResult struct {
+	failureSource route.Vertex
+	failure       lnwire.FailureMessage
+}
+
+// hop describes one hop of a route.
+type hop struct {
+	node     *mockNode
+	amtToFwd lnwire.MilliSatoshi
+	next     *hop
+}
+
+// sendHtlc sends out an htlc on the mock network and synchronously returns the
+// final resolution of the htlc.
+func (m *mockGraph) sendHtlc(route *route.Route) (htlcResult, error) {
+	var next *hop
+
+	// Convert the route into a structure that is suitable for recursive
+	// processing.
+	for i := len(route.Hops) - 1; i >= 0; i-- {
+		routeHop := route.Hops[i]
+		node := m.nodes[routeHop.PubKeyBytes]
+		next = &hop{
+			node:     node,
+			next:     next,
+			amtToFwd: routeHop.AmtToForward,
+		}
+	}
+
+	// Create the starting hop instance.
+	source := m.nodes[route.SourcePubKey]
+	next = &hop{
+		node:     source,
+		next:     next,
+		amtToFwd: route.TotalAmount,
+	}
+
+	// Recursively walk the path and obtain the htlc resolution.
+	return source.fwd(nil, next)
+}
+
+// Compile-time check for the routingGraph interface.
+var _ routingGraph = &mockGraph{}