Repository: lightningnetwork/lnd
Pull request #3831 (2019-12-12 20:01:12)

Add MPPTimeout ResolutionResult and expose to caller
This change adds a `ResolutionResult` to the `hodlEvent` struct to provide calling functions with additional information about the outcome of an invoice update in the invoice registry. 

Purpose:
1. Choose wire message based on update result (FailInvalidDetails vs MPPTimeout)
2. More details for htlc notifier

joostjager: Note: look at result interpretation of mpp timeout failure
carlaKC: Ready for another look. I haven't added nop `Encode`/`Decode` because `FailMppTimeout` passes the unit test without them (will happily add, just not sure if it's needed when there is no metadata).

Ended up doing a fair amount of renaming in this one, so I have split the test renames into separate commits so that it's hopefully more digestible to review.
joostjager: Looks like the commit order is off again.
carlaKC: > Looks like the commit order is off again.

Back in order 🕐 

> Only serious comment is to check whether we handle the new failure message as a receiver. We don't expect to receive it, but it can happen.

Updated the mission control test to check that we're ok on that side of things. Also ran a hideous hack locally returning `FailMppTimeout' for any failed invoice and that seemed to be ok, sender side logs: `RPCS: Unable to send payment: MPP Timeout@1`.
joostjager: Commits look out of order. You need that script :)

Patch:
diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go
index 45e9b0bd4..086a2aee5 100644
--- a/contractcourt/interfaces.go
+++ b/contractcourt/interfaces.go
@@ -27,9 +27,9 @@ type Registry interface {
 	NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
 		expiry uint32, currentHeight int32,
 		circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-		payload invoices.Payload) (*invoices.HodlEvent, error)
+		payload invoices.Payload) (*invoices.HtlcResolution, error)
 
-	// HodlUnsubscribeAll unsubscribes from all hodl events.
+	// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
 	HodlUnsubscribeAll(subscriber chan<- interface{})
 }
 
diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go
index 3b9ac6dff..bb34b283f 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.HodlEvent, error)
+		payload invoices.Payload) (*invoices.HtlcResolution, error)
 
 	// CancelInvoice attempts to cancel the invoice corresponding to the
 	// passed payment hash.
@@ -36,7 +36,7 @@ type InvoiceDatabase interface {
 	// SettleHodlInvoice settles a hold invoice.
 	SettleHodlInvoice(preimage lntypes.Preimage) error
 
-	// HodlUnsubscribeAll unsubscribes from all hodl events.
+	// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
 	HodlUnsubscribeAll(subscriber chan<- interface{})
 }
 
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index ce4d0073f..0bba3f656 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -489,8 +489,8 @@ func (l *channelLink) Stop() {
 
 	l.log.Info("stopping")
 
-	// As the link is stopping, we are no longer interested in hodl events
-	// coming from the invoice registry.
+	// As the link is stopping, we are no longer interested in htlc
+	// resolutions coming from the invoice registry.
 	l.cfg.Registry.HodlUnsubscribeAll(l.hodlQueue.ChanIn())
 
 	if l.cfg.ChainEvents.Cancel != nil {
@@ -1126,11 +1126,11 @@ out:
 		case msg := <-l.upstream:
 			l.handleUpstreamMsg(msg)
 
-		// A hodl event is received. This means that we now have a
+		// A htlc resolution is received. This means that we now have a
 		// resolution for a previously accepted htlc.
 		case hodlItem := <-l.hodlQueue.ChanOut():
-			hodlEvent := hodlItem.(invoices.HodlEvent)
-			err := l.processHodlQueue(hodlEvent)
+			htlcResolution := hodlItem.(invoices.HtlcResolution)
+			err := l.processHodlQueue(htlcResolution)
 			if err != nil {
 				l.fail(LinkFailureError{code: ErrInternalError},
 					fmt.Sprintf("process hodl queue: %v",
@@ -1145,24 +1145,26 @@ out:
 	}
 }
 
-// processHodlQueue processes a received hodl event and continues reading from
-// the hodl queue until no more events remain. When this function returns
-// without an error, the commit tx should be updated.
-func (l *channelLink) processHodlQueue(firstHodlEvent invoices.HodlEvent) error {
+// processHodlQueue processes a received htlc resolution and continues reading
+// from the hodl queue until no more resolutions remain. When this function
+// returns without an error, the commit tx should be updated.
+func (l *channelLink) processHodlQueue(
+	firstResolution invoices.HtlcResolution) error {
+
 	// Try to read all waiting resolution messages, so that they can all be
 	// processed in a single commitment tx update.
-	hodlEvent := firstHodlEvent
+	htlcResolution := firstResolution
 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 := hodlEvent.CircuitKey
+		circuitKey := htlcResolution.CircuitKey
 		hodlHtlc, ok := l.hodlMap[circuitKey]
 		if !ok {
 			return fmt.Errorf("hodl htlc not found: %v", circuitKey)
 		}
 
-		if err := l.processHodlEvent(hodlEvent, hodlHtlc); err != nil {
+		if err := l.processHtlcResolution(htlcResolution, hodlHtlc); err != nil {
 			return err
 		}
 
@@ -1171,7 +1173,7 @@ loop:
 
 		select {
 		case item := <-l.hodlQueue.ChanOut():
-			hodlEvent = item.(invoices.HodlEvent)
+			htlcResolution = item.(invoices.HtlcResolution)
 		default:
 			break loop
 		}
@@ -1185,30 +1187,30 @@ loop:
 	return nil
 }
 
-// processHodlEvent applies a received hodl event to the provided htlc. When
-// this function returns without an error, the commit tx should be updated.
-func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent,
+// processHtlcResolution applies a received htlc resolution to the provided
+// htlc. When this function returns without an error, the commit tx should be
+// updated.
+func (l *channelLink) processHtlcResolution(resolution invoices.HtlcResolution,
 	htlc hodlHtlc) error {
 
-	circuitKey := hodlEvent.CircuitKey
+	circuitKey := resolution.CircuitKey
 
-	// Determine required action for the resolution.
-	if hodlEvent.Preimage != nil {
-		l.log.Debugf("received hodl settle event for %v", circuitKey)
+	// 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(
-			*hodlEvent.Preimage, htlc.pd.HtlcIndex,
+			*resolution.Preimage, htlc.pd.HtlcIndex,
 			htlc.pd.SourceRef,
 		)
 	}
 
-	l.log.Debugf("received hodl cancel event for %v", circuitKey)
+	l.log.Debugf("received cancel resolution for %v with outcome: %v",
+		circuitKey, resolution.Outcome)
 
-	// In case of a cancel, always return
-	// incorrect_or_unknown_payment_details in order to avoid leaking info.
-	failure := lnwire.NewFailIncorrectDetails(
-		htlc.pd.Amount, uint32(hodlEvent.AcceptHeight),
-	)
+	// Get the lnwire failure message based on the resolution result.
+	failure := getResolutionFailure(resolution, htlc.pd.Amount)
 
 	l.sendHTLCError(
 		htlc.pd.HtlcIndex, failure, htlc.obfuscator,
@@ -1217,6 +1219,25 @@ func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent,
 	return nil
 }
 
+// getResolutionFailure returns the wire message that a htlc resolution should
+// be failed with.
+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 resolution.Outcome == invoices.ResultMppTimeout {
+		return &lnwire.FailMPPTimeout{}
+	}
+
+	// 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),
+	)
+}
+
 // randomFeeUpdateTimeout returns a random timeout between the bounds defined
 // within the link's configuration that will be used to determine when the link
 // should propose an update to its commitment fee rate.
@@ -2817,21 +2838,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
 		invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
 		circuitKey, l.hodlQueue.ChanIn(), payload,
 	)
-
-	switch err {
-
-	// Cancel htlc if we don't have an invoice for it.
-	case channeldb.ErrInvoiceNotFound:
-		failure := lnwire.NewFailIncorrectDetails(pd.Amount, heightNow)
-		l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
-
-		return nil
-
-	// No error.
-	case nil:
-
-	// Pass error to caller.
-	default:
+	if err != nil {
 		return err
 	}
 
@@ -2841,15 +2848,15 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
 		obfuscator: obfuscator,
 	}
 
+	// If the event is nil, the invoice is being held, so we save payment
+	// descriptor for future reference.
 	if event == nil {
-		// Save payment descriptor for future reference.
 		l.hodlMap[circuitKey] = htlc
-
 		return nil
 	}
 
 	// Process the received resolution.
-	return l.processHodlEvent(*event, htlc)
+	return l.processHtlcResolution(*event, htlc)
 }
 
 // settleHTLC settles the HTLC on the channel.
diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go
index 1d0731e8a..5c905822d 100644
--- a/htlcswitch/mock.go
+++ b/htlcswitch/mock.go
@@ -819,7 +819,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.HodlEvent, error) {
+	payload invoices.Payload) (*invoices.HtlcResolution, error) {
 
 	event, err := i.registry.NotifyExitHopHtlc(
 		rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go
index d4e77ad58..0b3163242 100644
--- a/invoices/invoiceregistry.go
+++ b/invoices/invoiceregistry.go
@@ -35,10 +35,10 @@ const (
 	DefaultHtlcHoldDuration = 120 * time.Second
 )
 
-// HodlEvent describes how an htlc should be resolved. If HodlEvent.Preimage is
-// set, the event indicates a settle event. If Preimage is nil, it is a cancel
-// event.
-type HodlEvent struct {
+// 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
 
@@ -48,6 +48,33 @@ type HodlEvent struct {
 
 	// 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.
@@ -325,7 +352,7 @@ func (i *InvoiceRegistry) invoiceEventLoop() {
 		case <-nextReleaseTick:
 			event := autoReleaseHeap.Pop().(*htlcReleaseEvent)
 			err := i.cancelSingleHtlc(
-				event.hash, event.key,
+				event.hash, event.key, ResultMppTimeout,
 			)
 			if err != nil {
 				log.Errorf("HTLC timer: %v", err)
@@ -574,9 +601,11 @@ func (i *InvoiceRegistry) startHtlcTimer(hash lntypes.Hash,
 	}
 }
 
-// cancelSingleHtlc cancels a single accepted htlc on an invoice.
+// cancelSingleHtlc cancels a single accepted htlc on an invoice. It takes
+// 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) error {
+	key channeldb.CircuitKey, result ResolutionResult) error {
 
 	i.Lock()
 	defer i.Unlock()
@@ -652,11 +681,11 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
 		return fmt.Errorf("htlc %v not found", key)
 	}
 	if htlc.State == channeldb.HtlcStateCanceled {
-		i.notifyHodlSubscribers(HodlEvent{
-			CircuitKey:   key,
-			AcceptHeight: int32(htlc.AcceptHeight),
-			Preimage:     nil,
-		})
+		resolution := *NewFailureResolution(
+			key, int32(htlc.AcceptHeight), result,
+		)
+
+		i.notifyHodlSubscribers(resolution)
 	}
 	return nil
 }
@@ -679,7 +708,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
 func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 	amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
 	circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
-	payload Payload) (*HodlEvent, error) {
+	payload Payload) (*HtlcResolution, error) {
 
 	i.Lock()
 	defer i.Unlock()
@@ -706,7 +735,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 	// 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 (
-		result            updateResult
+		result            ResolutionResult
 		updateSubscribers bool
 	)
 	invoice, err := i.cdb.UpdateInvoice(
@@ -729,11 +758,21 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 			return updateDesc, nil
 		},
 	)
-	if err != nil {
-		debugLog(err.Error())
+	switch err {
+	case channeldb.ErrInvoiceNotFound:
+		// If the invoice was not found, return a failure resolution
+		// with an invoice not found result.
+		return NewFailureResolution(
+			circuitKey, currentHeight, ResultInvoiceNotFound,
+		), nil
+
+	case nil:
 
+	default:
+		debugLog(err.Error())
 		return nil, err
 	}
+
 	debugLog(result.String())
 
 	if updateSubscribers {
@@ -745,10 +784,9 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 
 	// If it isn't recorded, cancel htlc.
 	if !ok {
-		return &HodlEvent{
-			CircuitKey:   circuitKey,
-			AcceptHeight: currentHeight,
-		}, nil
+		return NewFailureResolution(
+			circuitKey, currentHeight, result,
+		), nil
 	}
 
 	// Determine accepted height of this htlc. If the htlc reached the
@@ -759,10 +797,9 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 
 	switch invoiceHtlc.State {
 	case channeldb.HtlcStateCanceled:
-		return &HodlEvent{
-			CircuitKey:   circuitKey,
-			AcceptHeight: acceptHeight,
-		}, nil
+		return NewFailureResolution(
+			circuitKey, acceptHeight, result,
+		), nil
 
 	case channeldb.HtlcStateSettled:
 		// Also settle any previously accepted htlcs. The invoice state
@@ -773,18 +810,24 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
 				continue
 			}
 
-			i.notifyHodlSubscribers(HodlEvent{
-				CircuitKey:   key,
-				Preimage:     &invoice.Terms.PaymentPreimage,
-				AcceptHeight: int32(htlc.AcceptHeight),
-			})
+			// Notify subscribers that the htlcs should be settled
+			// with our peer. Note that the outcome of the
+			// resolution is set based on the outcome of the single
+			// htlc that we just settled, so may not be accurate
+			// for all htlcs.
+			resolution := *NewSettleResolution(
+				invoice.Terms.PaymentPreimage, key,
+				acceptHeight, result,
+			)
+
+			i.notifyHodlSubscribers(resolution)
 		}
 
-		return &HodlEvent{
-			CircuitKey:   circuitKey,
-			Preimage:     &invoice.Terms.PaymentPreimage,
-			AcceptHeight: acceptHeight,
-		}, nil
+		resolution := NewSettleResolution(
+			invoice.Terms.PaymentPreimage, circuitKey,
+			acceptHeight, result,
+		)
+		return resolution, nil
 
 	case channeldb.HtlcStateAccepted:
 		// (Re)start the htlc timer if the invoice is still open. It can
@@ -836,7 +879,9 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
 	hash := preimage.Hash()
 	invoice, err := i.cdb.UpdateInvoice(hash, updateInvoice)
 	if err != nil {
-		log.Errorf("SettleHodlInvoice with preimage %v: %v", preimage, err)
+		log.Errorf("SettleHodlInvoice with preimage %v: %v",
+			preimage, err)
+
 		return err
 	}
 
@@ -854,11 +899,11 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
 			continue
 		}
 
-		i.notifyHodlSubscribers(HodlEvent{
-			CircuitKey:   key,
-			Preimage:     &preimage,
-			AcceptHeight: int32(htlc.AcceptHeight),
-		})
+		resolution := *NewSettleResolution(
+			preimage, key, int32(htlc.AcceptHeight), ResultSettled,
+		)
+
+		i.notifyHodlSubscribers(resolution)
 	}
 	i.notifyClients(hash, invoice, invoice.State)
 
@@ -873,7 +918,8 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
 
 // cancelInvoice attempts to cancel the invoice corresponding to the passed
 // payment hash. Accepted invoices will only be canceled if explicitly
-// requested to do so.
+// requested to do so. It notifies subscribing links and resolvers that
+// the associated htlcs were canceled if they change state.
 func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
 	cancelAccepted bool) error {
 
@@ -932,10 +978,11 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
 			continue
 		}
 
-		i.notifyHodlSubscribers(HodlEvent{
-			CircuitKey:   key,
-			AcceptHeight: int32(htlc.AcceptHeight),
-		})
+		i.notifyHodlSubscribers(
+			*NewFailureResolution(
+				key, int32(htlc.AcceptHeight), ResultCanceled,
+			),
+		)
 	}
 	i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
 
@@ -1201,9 +1248,10 @@ func (i *InvoiceRegistry) SubscribeSingleInvoice(
 	return client, nil
 }
 
-// notifyHodlSubscribers sends out the hodl event to all current subscribers.
-func (i *InvoiceRegistry) notifyHodlSubscribers(hodlEvent HodlEvent) {
-	subscribers, ok := i.hodlSubscriptions[hodlEvent.CircuitKey]
+// notifyHodlSubscribers sends out the htlc resolution to all current
+// subscribers.
+func (i *InvoiceRegistry) notifyHodlSubscribers(htlcResolution HtlcResolution) {
+	subscribers, ok := i.hodlSubscriptions[htlcResolution.CircuitKey]
 	if !ok {
 		return
 	}
@@ -1213,18 +1261,18 @@ func (i *InvoiceRegistry) notifyHodlSubscribers(hodlEvent HodlEvent) {
 	// single resolution for each hash.
 	for subscriber := range subscribers {
 		select {
-		case subscriber <- hodlEvent:
+		case subscriber <- htlcResolution:
 		case <-i.quit:
 			return
 		}
 
 		delete(
 			i.hodlReverseSubscriptions[subscriber],
-			hodlEvent.CircuitKey,
+			htlcResolution.CircuitKey,
 		)
 	}
 
-	delete(i.hodlSubscriptions, hodlEvent.CircuitKey)
+	delete(i.hodlSubscriptions, htlcResolution.CircuitKey)
 }
 
 // hodlSubscribe adds a new invoice subscription.
diff --git a/invoices/update.go b/invoices/update.go
index 913caeb0d..2d512dbe3 100644
--- a/invoices/update.go
+++ b/invoices/update.go
@@ -8,86 +8,141 @@ import (
 	"github.com/lightningnetwork/lnd/record"
 )
 
-// updateResult is the result of the invoice update call.
-type updateResult uint8
+// 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 updateResult = iota
-	resultReplayToCanceled
-	resultReplayToAccepted
-	resultReplayToSettled
-	resultInvoiceAlreadyCanceled
-	resultAmountTooLow
-	resultExpiryTooSoon
-	resultDuplicateToAccepted
-	resultDuplicateToSettled
-	resultAccepted
-	resultSettled
-	resultInvoiceNotOpen
-	resultPartialAccepted
-	resultMppInProgress
-	resultAddressMismatch
-	resultHtlcSetTotalMismatch
-	resultHtlcSetTotalTooLow
-	resultHtlcSetOverpayment
+	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
 )
 
 // String returns a human-readable representation of the invoice update result.
-func (u updateResult) String() string {
+func (u ResolutionResult) String() string {
 	switch u {
 
 	case resultInvalid:
 		return "invalid"
 
-	case resultReplayToCanceled:
+	case ResultReplayToCanceled:
 		return "replayed htlc to canceled invoice"
 
-	case resultReplayToAccepted:
+	case ResultReplayToAccepted:
 		return "replayed htlc to accepted invoice"
 
-	case resultReplayToSettled:
+	case ResultReplayToSettled:
 		return "replayed htlc to settled invoice"
 
-	case resultInvoiceAlreadyCanceled:
+	case ResultInvoiceAlreadyCanceled:
 		return "invoice already canceled"
 
-	case resultAmountTooLow:
+	case ResultAmountTooLow:
 		return "amount too low"
 
-	case resultExpiryTooSoon:
+	case ResultExpiryTooSoon:
 		return "expiry too soon"
 
-	case resultDuplicateToAccepted:
+	case ResultDuplicateToAccepted:
 		return "accepting duplicate payment to accepted invoice"
 
-	case resultDuplicateToSettled:
+	case ResultDuplicateToSettled:
 		return "accepting duplicate payment to settled invoice"
 
-	case resultAccepted:
+	case ResultAccepted:
 		return "accepted"
 
-	case resultSettled:
+	case ResultSettled:
 		return "settled"
 
-	case resultInvoiceNotOpen:
+	case ResultInvoiceNotOpen:
 		return "invoice no longer open"
 
-	case resultPartialAccepted:
+	case ResultPartialAccepted:
 		return "partial payment accepted"
 
-	case resultMppInProgress:
+	case ResultMppInProgress:
 		return "mpp reception in progress"
 
-	case resultAddressMismatch:
+	case ResultAddressMismatch:
 		return "payment address mismatch"
 
-	case resultHtlcSetTotalMismatch:
+	case ResultHtlcSetTotalMismatch:
 		return "htlc total amt doesn't match set total"
 
-	case resultHtlcSetTotalTooLow:
+	case ResultHtlcSetTotalTooLow:
 		return "set total too low for invoice"
 
-	case resultHtlcSetOverpayment:
+	case ResultHtlcSetOverpayment:
 		return "mpp is overpaying set total"
 
 	default:
@@ -110,20 +165,20 @@ type invoiceUpdateCtx struct {
 // updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
 // settlement logic.
 func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
-	*channeldb.InvoiceUpdateDesc, updateResult, 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, resultReplayToCanceled, nil
+			return nil, ResultReplayToCanceled, nil
 
 		case channeldb.HtlcStateAccepted:
-			return nil, resultReplayToAccepted, nil
+			return nil, ResultReplayToAccepted, nil
 
 		case channeldb.HtlcStateSettled:
-			return nil, resultReplayToSettled, nil
+			return nil, ResultReplayToSettled, nil
 
 		default:
 			return nil, 0, errors.New("unknown htlc state")
@@ -140,7 +195,7 @@ 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, updateResult, error) {
+	*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
 
 	// Start building the accept descriptor.
 	acceptDesc := &channeldb.HtlcAcceptDesc{
@@ -156,23 +211,23 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 	// Because non-mpp payments don't have a payment address, this is needed
 	// to thwart probing.
 	if inv.State != channeldb.ContractOpen {
-		return nil, resultInvoiceNotOpen, nil
+		return nil, ResultInvoiceNotOpen, nil
 	}
 
 	// Check the payment address that authorizes the payment.
 	if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
-		return nil, resultAddressMismatch, nil
+		return nil, ResultAddressMismatch, nil
 	}
 
 	// Don't accept zero-valued sets.
 	if ctx.mpp.TotalMsat() == 0 {
-		return nil, 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, resultHtlcSetTotalTooLow, nil
+		return nil, ResultHtlcSetTotalTooLow, nil
 	}
 
 	// Check whether total amt matches other htlcs in the set.
@@ -186,7 +241,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 		}
 
 		if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
-			return nil, resultHtlcSetTotalMismatch, nil
+			return nil, ResultHtlcSetTotalMismatch, nil
 		}
 
 		newSetTotal += htlc.Amt
@@ -197,16 +252,16 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 
 	// Make sure the communicated set total isn't overpaid.
 	if newSetTotal > ctx.mpp.TotalMsat() {
-		return nil, resultHtlcSetOverpayment, nil
+		return nil, ResultHtlcSetOverpayment, nil
 	}
 
 	// The invoice is still open. Check the expiry.
 	if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
-		return nil, resultExpiryTooSoon, nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
-		return nil, resultExpiryTooSoon, nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	// Record HTLC in the invoice database.
@@ -221,7 +276,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 	// If the invoice cannot be settled yet, only record the htlc.
 	setComplete := newSetTotal == ctx.mpp.TotalMsat()
 	if !setComplete {
-		return &update, resultPartialAccepted, nil
+		return &update, ResultPartialAccepted, nil
 	}
 
 	// Check to see if we can settle or this is an hold invoice and
@@ -231,7 +286,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 		update.State = &channeldb.InvoiceStateUpdateDesc{
 			NewState: channeldb.ContractAccepted,
 		}
-		return &update, resultAccepted, nil
+		return &update, ResultAccepted, nil
 	}
 
 	update.State = &channeldb.InvoiceStateUpdateDesc{
@@ -239,18 +294,18 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 		Preimage: inv.Terms.PaymentPreimage,
 	}
 
-	return &update, 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, updateResult, error) {
+	*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
 
 	// If the invoice is already canceled, there is no further
 	// checking to do.
 	if inv.State == channeldb.ContractCanceled {
-		return nil, resultInvoiceAlreadyCanceled, nil
+		return nil, ResultInvoiceAlreadyCanceled, nil
 	}
 
 	// If an invoice amount is specified, check that enough is paid. Also
@@ -258,7 +313,7 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 	// or accepted. In case this is a zero-valued invoice, it will always be
 	// enough.
 	if ctx.amtPaid < inv.Terms.Value {
-		return nil, resultAmountTooLow, nil
+		return nil, ResultAmountTooLow, nil
 	}
 
 	// TODO(joostjager): Check invoice mpp required feature
@@ -271,17 +326,17 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 		if htlc.State == channeldb.HtlcStateAccepted &&
 			htlc.MppTotalAmt > 0 {
 
-			return nil, resultMppInProgress, nil
+			return nil, ResultMppInProgress, nil
 		}
 	}
 
 	// The invoice is still open. Check the expiry.
 	if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
-		return nil, resultExpiryTooSoon, nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
-		return nil, resultExpiryTooSoon, nil
+		return nil, ResultExpiryTooSoon, nil
 	}
 
 	// Record HTLC in the invoice database.
@@ -302,10 +357,10 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 	// We do accept or settle the HTLC.
 	switch inv.State {
 	case channeldb.ContractAccepted:
-		return &update, resultDuplicateToAccepted, nil
+		return &update, ResultDuplicateToAccepted, nil
 
 	case channeldb.ContractSettled:
-		return &update, resultDuplicateToSettled, nil
+		return &update, ResultDuplicateToSettled, nil
 	}
 
 	// Check to see if we can settle or this is an hold invoice and we need
@@ -315,7 +370,7 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 		update.State = &channeldb.InvoiceStateUpdateDesc{
 			NewState: channeldb.ContractAccepted,
 		}
-		return &update, resultAccepted, nil
+		return &update, ResultAccepted, nil
 	}
 
 	update.State = &channeldb.InvoiceStateUpdateDesc{
@@ -323,5 +378,5 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
 		Preimage: inv.Terms.PaymentPreimage,
 	}
 
-	return &update, resultSettled, nil
+	return &update, ResultSettled, nil
 }
diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go
index b80c3f2c4..1be1423ed 100644
--- a/lnrpc/routerrpc/router.pb.go
+++ b/lnrpc/routerrpc/router.pb.go
@@ -109,6 +109,7 @@ const (
 	Failure_PERMANENT_NODE_FAILURE               Failure_FailureCode = 20
 	Failure_PERMANENT_CHANNEL_FAILURE            Failure_FailureCode = 21
 	Failure_EXPIRY_TOO_FAR                       Failure_FailureCode = 22
+	Failure_MPP_TIMEOUT                          Failure_FailureCode = 23
 	//*
 	//The error source is known, but the failure itself couldn't be decoded.
 	Failure_UNKNOWN_FAILURE Failure_FailureCode = 998
@@ -142,6 +143,7 @@ var Failure_FailureCode_name = map[int32]string{
 	20:  "PERMANENT_NODE_FAILURE",
 	21:  "PERMANENT_CHANNEL_FAILURE",
 	22:  "EXPIRY_TOO_FAR",
+	23:  "MPP_TIMEOUT",
 	998: "UNKNOWN_FAILURE",
 	999: "UNREADABLE_FAILURE",
 }
@@ -170,6 +172,7 @@ var Failure_FailureCode_value = map[string]int32{
 	"PERMANENT_NODE_FAILURE":               20,
 	"PERMANENT_CHANNEL_FAILURE":            21,
 	"EXPIRY_TOO_FAR":                       22,
+	"MPP_TIMEOUT":                          23,
 	"UNKNOWN_FAILURE":                      998,
 	"UNREADABLE_FAILURE":                   999,
 }
@@ -1526,138 +1529,139 @@ func init() {
 func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) }
 
 var fileDescriptor_7a0613f69d37b0a5 = []byte{
-	// 2093 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x58, 0x4f, 0x73, 0xdb, 0xb8,
-	0x15, 0x5f, 0x5a, 0xff, 0x9f, 0xfe, 0xd1, 0xb0, 0xe3, 0x30, 0x72, 0xbc, 0xf1, 0x32, 0x69, 0x56,
-	0x93, 0xc9, 0xda, 0xa9, 0xdb, 0xcd, 0x64, 0x7a, 0x68, 0x47, 0x96, 0xa8, 0x35, 0x1d, 0x89, 0x72,
-	0x20, 0x29, 0xbb, 0xe9, 0x1e, 0x30, 0xb4, 0x04, 0x5b, 0x1c, 0x53, 0xa4, 0x96, 0x84, 0xb2, 0xf1,
-	0x77, 0xe8, 0xf7, 0x68, 0x0f, 0x6d, 0x2f, 0xfd, 0x4e, 0xed, 0xbd, 0x33, 0x3d, 0xf4, 0xd6, 0x01,
-	0x40, 0x4a, 0x94, 0x2c, 0x67, 0x7b, 0xb2, 0xf0, 0x7b, 0x3f, 0xbc, 0x07, 0xe2, 0xe1, 0xfd, 0xf0,
-	0x60, 0xd8, 0x0b, 0xfc, 0x39, 0xa3, 0x41, 0x30, 0x1b, 0x1d, 0xcb, 0x5f, 0x47, 0xb3, 0xc0, 0x67,
-	0x3e, 0x2a, 0x2c, 0xf0, 0x5a, 0x21, 0x98, 0x8d, 0x24, 0xaa, 0xff, 0x37, 0x03, 0xa8, 0x4f, 0xbd,
-	0xf1, 0x85, 0x7d, 0x3b, 0xa5, 0x1e, 0xc3, 0xf4, 0xa7, 0x39, 0x0d, 0x19, 0x42, 0x90, 0x1e, 0xd3,
-	0x90, 0x69, 0xca, 0xa1, 0x52, 0x2f, 0x61, 0xf1, 0x1b, 0xa9, 0x90, 0xb2, 0xa7, 0x4c, 0xdb, 0x3a,
-	0x54, 0xea, 0x29, 0xcc, 0x7f, 0xa2, 0x47, 0x90, 0xb7, 0xa7, 0x8c, 0x4c, 0x43, 0x9b, 0x69, 0x25,
-	0x01, 0xe7, 0xec, 0x29, 0xeb, 0x86, 0x36, 0x43, 0x5f, 0x41, 0x69, 0x26, 0x5d, 0x92, 0x89, 0x1d,
-	0x4e, 0xb4, 0x94, 0x70, 0x54, 0x8c, 0xb0, 0x33, 0x3b, 0x9c, 0xa0, 0x3a, 0xa8, 0x57, 0x8e, 0x67,
-	0xbb, 0x64, 0xe4, 0xb2, 0x8f, 0x64, 0x4c, 0x5d, 0x66, 0x6b, 0xe9, 0x43, 0xa5, 0x9e, 0xc1, 0x15,
-	0x81, 0x37, 0x5d, 0xf6, 0xb1, 0xc5, 0x51, 0xf4, 0x35, 0x54, 0x63, 0x67, 0x81, 0x5c, 0xa0, 0x96,
-	0x39, 0x54, 0xea, 0x05, 0x5c, 0x99, 0xad, 0x2e, 0xfb, 0x6b, 0xa8, 0x32, 0x67, 0x4a, 0xfd, 0x39,
-	0x23, 0x21, 0x1d, 0xf9, 0xde, 0x38, 0xd4, 0xb2, 0xd2, 0x63, 0x04, 0xf7, 0x25, 0x8a, 0x74, 0x28,
-	0x5f, 0x51, 0x4a, 0x5c, 0x67, 0xea, 0x30, 0xc2, 0x97, 0x9f, 0x13, 0xcb, 0x2f, 0x5e, 0x51, 0xda,
-	0xe1, 0x58, 0xdf, 0x66, 0xe8, 0x19, 0x54, 0x96, 0x1c, 0xf1, 0x8d, 0x65, 0x41, 0x2a, 0xc5, 0x24,
-	0xf1, 0xa1, 0x2f, 0x41, 0xf5, 0xe7, 0xec, 0xda, 0x77, 0xbc, 0x6b, 0x32, 0x9a, 0xd8, 0x1e, 0x71,
-	0xc6, 0x5a, 0xfe, 0x50, 0xa9, 0xa7, 0x4f, 0xb7, 0x5e, 0x29, 0xb8, 0x12, 0xdb, 0x9a, 0x13, 0xdb,
-	0x33, 0xc7, 0xe8, 0x39, 0x54, 0x5d, 0x3b, 0x64, 0x64, 0xe2, 0xcf, 0xc8, 0x6c, 0x7e, 0x79, 0x43,
-	0x6f, 0xb5, 0x8a, 0xd8, 0x99, 0x32, 0x87, 0xcf, 0xfc, 0xd9, 0x85, 0x00, 0xd1, 0x01, 0x80, 0xd8,
-	0x15, 0x11, 0x5c, 0x2b, 0x88, 0x6f, 0x28, 0x70, 0x44, 0x04, 0x46, 0x27, 0x50, 0x14, 0xd9, 0x24,
-	0x13, 0xc7, 0x63, 0xa1, 0x06, 0x87, 0xa9, 0x7a, 0xf1, 0x44, 0x3d, 0x72, 0x3d, 0x9e, 0x58, 0xcc,
-	0x2d, 0x67, 0x8e, 0xc7, 0x70, 0x92, 0x84, 0xc6, 0xb0, 0xc3, 0xd3, 0x48, 0x46, 0xf3, 0x90, 0xf9,
-	0x53, 0x12, 0xd0, 0x91, 0x1f, 0x8c, 0x43, 0xad, 0x28, 0xe6, 0xfe, 0xf6, 0x68, 0x71, 0x3a, 0x8e,
-	0xee, 0x1e, 0x87, 0xa3, 0x16, 0x0d, 0x59, 0x53, 0xcc, 0xc3, 0x72, 0x9a, 0xe1, 0xb1, 0xe0, 0x16,
-	0x6f, 0x8f, 0xd7, 0x71, 0xf4, 0x12, 0x90, 0xed, 0xba, 0xfe, 0xcf, 0x24, 0xa4, 0xee, 0x15, 0x89,
-	0xd2, 0xa3, 0x55, 0x0f, 0x95, 0x7a, 0x1e, 0xab, 0xc2, 0xd2, 0xa7, 0xee, 0x55, 0xe4, 0x1e, 0xbd,
-	0x86, 0xb2, 0x58, 0xd3, 0x15, 0xb5, 0xd9, 0x3c, 0xa0, 0xa1, 0xa6, 0x1e, 0xa6, 0xea, 0x95, 0x93,
-	0xed, 0xe8, 0x4b, 0xda, 0x12, 0x3e, 0x75, 0x18, 0x2e, 0x71, 0x5e, 0x34, 0x0e, 0x6b, 0x2d, 0xd8,
-	0xdb, 0xbc, 0x24, 0x7e, 0x48, 0xf9, 0xa6, 0xf2, 0x73, 0x9b, 0xc6, 0xfc, 0x27, 0xda, 0x85, 0xcc,
-	0x47, 0xdb, 0x9d, 0x53, 0x71, 0x70, 0x4b, 0x58, 0x0e, 0x7e, 0xb7, 0xf5, 0x46, 0xd1, 0xdf, 0xc0,
-	0xce, 0x20, 0xb0, 0x47, 0x37, 0x6b, 0x67, 0x7f, 0xfd, 0xe8, 0x2a, 0x77, 0x8e, 0xae, 0xfe, 0x17,
-	0x05, 0xca, 0xd1, 0xac, 0x3e, 0xb3, 0xd9, 0x3c, 0x44, 0xdf, 0x40, 0x26, 0x64, 0x36, 0xa3, 0x82,
-	0x5d, 0x39, 0x79, 0x98, 0xd8, 0xcf, 0x04, 0x91, 0x62, 0xc9, 0x42, 0x35, 0xc8, 0xcf, 0x02, 0xea,
-	0x4c, 0xed, 0xeb, 0x78, 0x5d, 0x8b, 0x31, 0xd2, 0x21, 0x23, 0x26, 0x8b, 0x9a, 0x29, 0x9e, 0x94,
-	0x92, 0x69, 0xc5, 0xd2, 0x84, 0xea, 0x90, 0x99, 0x30, 0x77, 0x14, 0x6a, 0x69, 0x91, 0x3e, 0x14,
-	0x71, 0xce, 0x06, 0x9d, 0x66, 0x83, 0x31, 0x3a, 0x9d, 0x31, 0x2c, 0x09, 0xfa, 0xef, 0xa1, 0x2a,
-	0x66, 0xb6, 0x29, 0xfd, 0x5c, 0x71, 0x3f, 0x04, 0x5e, 0xba, 0xa2, 0x14, 0x64, 0x81, 0x67, 0xed,
-	0x29, 0xaf, 0x02, 0x7d, 0x0c, 0xea, 0x72, 0x7e, 0x38, 0xf3, 0xbd, 0x90, 0x47, 0x57, 0xf9, 0x32,
-	0xf8, 0x91, 0xe7, 0x15, 0x22, 0x6a, 0x43, 0x11, 0xb3, 0x2a, 0x11, 0xde, 0xa6, 0x54, 0x54, 0xc7,
-	0x73, 0x59, 0x90, 0xc4, 0xf5, 0x47, 0x37, 0xbc, 0xc4, 0xed, 0xdb, 0xc8, 0x7d, 0x99, 0xc3, 0x1d,
-	0x7f, 0x74, 0xd3, 0xe2, 0xa0, 0xfe, 0xa3, 0x54, 0xa1, 0x81, 0x2f, 0xbf, 0xf2, 0xff, 0xce, 0xc4,
-	0x72, 0xb3, 0xb6, 0xee, 0xdd, 0x2c, 0x9d, 0xc0, 0xce, 0x8a, 0xf3, 0xe8, 0x2b, 0x92, 0x39, 0x50,
-	0xd6, 0x72, 0xf0, 0x12, 0x72, 0x57, 0xb6, 0xe3, 0xce, 0x83, 0xd8, 0x31, 0x4a, 0x24, 0xb4, 0x2d,
-	0x2d, 0x38, 0xa6, 0xe8, 0xff, 0xc9, 0x41, 0x2e, 0x02, 0xd1, 0x09, 0xa4, 0x47, 0xfe, 0x38, 0x3e,
-	0x07, 0x5f, 0xde, 0x9d, 0x16, 0xff, 0x6d, 0xfa, 0x63, 0x8a, 0x05, 0x17, 0xfd, 0x01, 0x2a, 0x5c,
-	0x3a, 0x3c, 0xea, 0x92, 0xf9, 0x6c, 0x6c, 0x2f, 0x52, 0xaf, 0x25, 0x66, 0x37, 0x25, 0x61, 0x28,
-	0xec, 0xb8, 0x3c, 0x4a, 0x0e, 0xd1, 0x3e, 0x14, 0x78, 0xb6, 0x65, 0x26, 0xd2, 0xe2, 0xec, 0xe7,
-	0x39, 0x20, 0x72, 0xa0, 0x43, 0xd9, 0xf7, 0x1c, 0xdf, 0x23, 0xe1, 0xc4, 0x26, 0x27, 0xdf, 0xbe,
-	0x16, 0xda, 0x59, 0xc2, 0x45, 0x01, 0xf6, 0x27, 0xf6, 0xc9, 0xb7, 0xaf, 0xd1, 0x13, 0x28, 0x0a,
-	0xbd, 0xa1, 0x9f, 0x66, 0x4e, 0x70, 0x2b, 0x44, 0xb3, 0x8c, 0x85, 0x04, 0x19, 0x02, 0xe1, 0x55,
-	0x74, 0xe5, 0xda, 0xd7, 0xa1, 0x10, 0xca, 0x32, 0x96, 0x03, 0xf4, 0x0a, 0x76, 0xa3, 0x3d, 0x20,
-	0xa1, 0x3f, 0x0f, 0x46, 0x94, 0x38, 0xde, 0x98, 0x7e, 0x12, 0x02, 0x58, 0xc6, 0x28, 0xb2, 0xf5,
-	0x85, 0xc9, 0xe4, 0x16, 0xb4, 0x07, 0xd9, 0x09, 0x75, 0xae, 0x27, 0x52, 0xd4, 0xca, 0x38, 0x1a,
-	0xe9, 0x7f, 0xcd, 0x40, 0x31, 0xb1, 0x31, 0xa8, 0x04, 0x79, 0x6c, 0xf4, 0x0d, 0xfc, 0xde, 0x68,
-	0xa9, 0x5f, 0xa0, 0x3a, 0x3c, 0x33, 0xad, 0x66, 0x0f, 0x63, 0xa3, 0x39, 0x20, 0x3d, 0x4c, 0x86,
-	0xd6, 0x5b, 0xab, 0xf7, 0xbd, 0x45, 0x2e, 0x1a, 0x1f, 0xba, 0x86, 0x35, 0x20, 0x2d, 0x63, 0xd0,
-	0x30, 0x3b, 0x7d, 0x55, 0x41, 0x8f, 0x41, 0x5b, 0x32, 0x63, 0x73, 0xa3, 0xdb, 0x1b, 0x5a, 0x03,
-	0x75, 0x0b, 0x3d, 0x81, 0xfd, 0xb6, 0x69, 0x35, 0x3a, 0x64, 0xc9, 0x69, 0x76, 0x06, 0xef, 0x89,
-	0xf1, 0xc3, 0x85, 0x89, 0x3f, 0xa8, 0xa9, 0x4d, 0x04, 0x5e, 0x53, 0xb1, 0x87, 0x34, 0x7a, 0x04,
-	0x0f, 0x24, 0x41, 0x4e, 0x21, 0x83, 0x5e, 0x8f, 0xf4, 0x7b, 0x3d, 0x4b, 0xcd, 0xa0, 0x6d, 0x28,
-	0x9b, 0xd6, 0xfb, 0x46, 0xc7, 0x6c, 0x11, 0x6c, 0x34, 0x3a, 0x5d, 0x35, 0x8b, 0x76, 0xa0, 0xba,
-	0xce, 0xcb, 0x71, 0x17, 0x31, 0xaf, 0x67, 0x99, 0x3d, 0x8b, 0xbc, 0x37, 0x70, 0xdf, 0xec, 0x59,
-	0x6a, 0x1e, 0xed, 0x01, 0x5a, 0x35, 0x9d, 0x75, 0x1b, 0x4d, 0xb5, 0x80, 0x1e, 0xc0, 0xf6, 0x2a,
-	0xfe, 0xd6, 0xf8, 0xa0, 0x02, 0xd2, 0x60, 0x57, 0x2e, 0x8c, 0x9c, 0x1a, 0x9d, 0xde, 0xf7, 0xa4,
-	0x6b, 0x5a, 0x66, 0x77, 0xd8, 0x55, 0x8b, 0x68, 0x17, 0xd4, 0xb6, 0x61, 0x10, 0xd3, 0xea, 0x0f,
-	0xdb, 0x6d, 0xb3, 0x69, 0x1a, 0xd6, 0x40, 0x2d, 0xc9, 0xc8, 0x9b, 0x3e, 0xbc, 0xcc, 0x27, 0x34,
-	0xcf, 0x1a, 0x96, 0x65, 0x74, 0x48, 0xcb, 0xec, 0x37, 0x4e, 0x3b, 0x46, 0x4b, 0xad, 0xa0, 0x03,
-	0x78, 0x34, 0x30, 0xba, 0x17, 0x3d, 0xdc, 0xc0, 0x1f, 0x48, 0x6c, 0x6f, 0x37, 0xcc, 0xce, 0x10,
-	0x1b, 0x6a, 0x15, 0x7d, 0x05, 0x07, 0xd8, 0x78, 0x37, 0x34, 0xb1, 0xd1, 0x22, 0x56, 0xaf, 0x65,
-	0x90, 0xb6, 0xd1, 0x18, 0x0c, 0xb1, 0x41, 0xba, 0x66, 0xbf, 0x6f, 0x5a, 0xdf, 0xa9, 0x2a, 0x7a,
-	0x06, 0x87, 0x0b, 0xca, 0xc2, 0xc1, 0x1a, 0x6b, 0x9b, 0x7f, 0x5f, 0x9c, 0x52, 0xcb, 0xf8, 0x61,
-	0x40, 0x2e, 0x0c, 0x03, 0xab, 0x08, 0xd5, 0x60, 0x6f, 0x19, 0x5e, 0x06, 0x88, 0x62, 0xef, 0x70,
-	0xdb, 0x85, 0x81, 0xbb, 0x0d, 0x8b, 0x27, 0x78, 0xc5, 0xb6, 0xcb, 0x97, 0xbd, 0xb4, 0xad, 0x2f,
-	0xfb, 0x01, 0x42, 0x50, 0x49, 0x64, 0xa5, 0xdd, 0xc0, 0xea, 0x1e, 0xda, 0x85, 0x6a, 0xbc, 0x82,
-	0x98, 0xf8, 0xcf, 0x1c, 0x7a, 0x08, 0x68, 0x68, 0x61, 0xa3, 0xd1, 0xe2, 0x1b, 0xb2, 0x30, 0xfc,
-	0x2b, 0x77, 0x9e, 0xce, 0x6f, 0xa9, 0x29, 0xfd, 0x1f, 0x29, 0x28, 0xaf, 0xd4, 0x25, 0x7a, 0x0c,
-	0x85, 0xd0, 0xb9, 0xf6, 0xc4, 0x35, 0x15, 0x89, 0xca, 0x12, 0x10, 0xb7, 0xfa, 0xc4, 0x76, 0x3c,
-	0xa9, 0x66, 0x52, 0xf7, 0x0b, 0x02, 0x11, 0x5a, 0xb6, 0x0f, 0xb9, 0xb8, 0x83, 0x48, 0x2d, 0x3a,
-	0x88, 0xec, 0x48, 0x76, 0x0e, 0x8f, 0xa1, 0xc0, 0x25, 0x33, 0x64, 0xf6, 0x74, 0x26, 0x4a, 0xbc,
-	0x8c, 0x97, 0x00, 0x7a, 0x0a, 0xe5, 0x29, 0x0d, 0x43, 0xfb, 0x9a, 0x12, 0x59, 0xa6, 0x20, 0x18,
-	0xa5, 0x08, 0x6c, 0x8b, 0x6a, 0x7d, 0x0a, 0xb1, 0x6c, 0x44, 0xa4, 0x8c, 0x24, 0x45, 0xa0, 0x24,
-	0xad, 0x2b, 0x36, 0xb3, 0x23, 0x35, 0x48, 0x2a, 0x36, 0xb3, 0xd1, 0x0b, 0xd8, 0x96, 0x92, 0xe3,
-	0x78, 0xce, 0x74, 0x3e, 0x95, 0xd2, 0x93, 0x13, 0xd2, 0x53, 0x15, 0xd2, 0x23, 0x71, 0xa1, 0x40,
-	0x8f, 0x20, 0x7f, 0x69, 0x87, 0x94, 0x5f, 0x16, 0x91, 0x34, 0xe4, 0xf8, 0xb8, 0x4d, 0x29, 0x37,
-	0xf1, 0x2b, 0x24, 0xe0, 0xa2, 0x27, 0x15, 0x21, 0x77, 0x45, 0x29, 0xe6, 0x7b, 0xb9, 0x88, 0x60,
-	0x7f, 0x5a, 0x46, 0x28, 0x26, 0x22, 0x48, 0x5c, 0x44, 0x78, 0x01, 0xdb, 0xf4, 0x13, 0x0b, 0x6c,
-	0xe2, 0xcf, 0xec, 0x9f, 0xe6, 0x94, 0x8c, 0x6d, 0x66, 0x8b, 0x96, 0xb4, 0x84, 0xab, 0xc2, 0xd0,
-	0x13, 0x78, 0xcb, 0x66, 0xb6, 0xfe, 0x18, 0x6a, 0x98, 0x86, 0x94, 0x75, 0x9d, 0x30, 0x74, 0x7c,
-	0xaf, 0xe9, 0x7b, 0x2c, 0xf0, 0xdd, 0xe8, 0xce, 0xd1, 0x0f, 0x60, 0x7f, 0xa3, 0x55, 0x5e, 0x1a,
-	0x7c, 0xf2, 0xbb, 0x39, 0x0d, 0x6e, 0x37, 0x4f, 0x7e, 0x07, 0xfb, 0x1b, 0xad, 0xd1, 0x8d, 0xf3,
-	0x12, 0x32, 0x33, 0xdb, 0x09, 0x42, 0x6d, 0x4b, 0xdc, 0xda, 0x7b, 0x2b, 0x4d, 0x82, 0x13, 0x9c,
-	0x39, 0x21, 0xf3, 0x83, 0x5b, 0x2c, 0x49, 0xe7, 0xe9, 0xbc, 0xa2, 0x6e, 0xe9, 0x7f, 0x52, 0xa0,
-	0x98, 0x30, 0xf2, 0x73, 0xe0, 0xf9, 0x63, 0x4a, 0xae, 0x02, 0x7f, 0x1a, 0x9f, 0xb0, 0x05, 0x80,
-	0x34, 0xc8, 0x89, 0x01, 0xf3, 0xa3, 0xe3, 0x15, 0x0f, 0xd1, 0x37, 0x90, 0x9b, 0x48, 0x17, 0x22,
-	0x4b, 0xc5, 0x93, 0x9d, 0xb5, 0xe8, 0x7c, 0x6f, 0x70, 0xcc, 0x39, 0x4f, 0xe7, 0x53, 0x6a, 0xfa,
-	0x3c, 0x9d, 0x4f, 0xab, 0x99, 0xf3, 0x74, 0x3e, 0xa3, 0x66, 0xcf, 0xd3, 0xf9, 0xac, 0x9a, 0xd3,
-	0xff, 0xad, 0x40, 0x3e, 0x66, 0xf3, 0xb5, 0x70, 0x89, 0x27, 0xfc, 0x64, 0x44, 0x0d, 0xc0, 0x12,
-	0x40, 0x3a, 0x94, 0xc4, 0x60, 0xb5, 0xaf, 0x58, 0xc1, 0xd0, 0x33, 0x28, 0x2f, 0xc6, 0x8b, 0xcb,
-	0x2b, 0x85, 0x57, 0x41, 0xee, 0x29, 0x9c, 0x8f, 0x46, 0x34, 0x0c, 0x65, 0xa8, 0x8c, 0xf4, 0x94,
-	0xc4, 0x50, 0x1d, 0xaa, 0xf1, 0x38, 0x0e, 0x98, 0x15, 0xb4, 0x75, 0x18, 0xbd, 0x00, 0x35, 0x09,
-	0x4d, 0x97, 0xed, 0xff, 0x1d, 0x5c, 0x6e, 0x83, 0x3e, 0x85, 0x87, 0x22, 0xad, 0x17, 0x81, 0x7f,
-	0x69, 0x5f, 0x3a, 0xae, 0xc3, 0x6e, 0xe3, 0x16, 0x85, 0x6f, 0x41, 0xe0, 0x4f, 0x89, 0x17, 0xdf,
-	0xf9, 0x25, 0xbc, 0x04, 0x78, 0x3a, 0x98, 0x2f, 0x6d, 0x51, 0x3a, 0xa2, 0x21, 0x6f, 0x3e, 0x16,
-	0xc1, 0x53, 0x22, 0xf8, 0x62, 0xac, 0xdf, 0x80, 0x76, 0x37, 0x5c, 0x74, 0x84, 0x0e, 0xa1, 0x38,
-	0x5b, 0xc2, 0x22, 0xa2, 0x82, 0x93, 0x50, 0x32, 0xd1, 0x5b, 0xbf, 0x9c, 0x68, 0xfd, 0xcf, 0x0a,
-	0x6c, 0x9f, 0xce, 0x1d, 0x77, 0xbc, 0xd2, 0x79, 0x25, 0x5f, 0x76, 0xca, 0xea, 0xcb, 0x6e, 0xd3,
-	0xb3, 0x6d, 0x6b, 0xe3, 0xb3, 0x6d, 0xd3, 0xd3, 0x28, 0x75, 0xef, 0xd3, 0xe8, 0x09, 0x14, 0x97,
-	0xaf, 0x22, 0xd9, 0xd8, 0x96, 0x30, 0x4c, 0xe2, 0x27, 0x51, 0xa8, 0xbf, 0x01, 0x94, 0x5c, 0x68,
-	0xb4, 0x21, 0x8b, 0x06, 0x50, 0xb9, 0xb7, 0x01, 0x7c, 0xf1, 0x77, 0x05, 0x4a, 0xc9, 0x2e, 0x1c,
-	0x95, 0xa1, 0x60, 0x5a, 0xa4, 0xdd, 0x31, 0xbf, 0x3b, 0x1b, 0xa8, 0x5f, 0xf0, 0x61, 0x7f, 0xd8,
-	0x6c, 0x1a, 0x46, 0xcb, 0x68, 0xa9, 0x0a, 0xbf, 0x1f, 0xb8, 0xd4, 0x1b, 0x2d, 0x32, 0x30, 0xbb,
-	0x46, 0x6f, 0xc8, 0x3b, 0x87, 0x1d, 0xa8, 0x46, 0x98, 0xd5, 0x23, 0xb8, 0x37, 0x1c, 0x18, 0x6a,
-	0x0a, 0xa9, 0x50, 0x8a, 0x40, 0x03, 0xe3, 0x1e, 0x56, 0xd3, 0xfc, 0xba, 0x8b, 0x90, 0xbb, 0x5d,
-	0x48, 0xdc, 0xa4, 0x64, 0x44, 0x97, 0x11, 0xb3, 0x96, 0x17, 0x34, 0x39, 0x6d, 0x74, 0x1a, 0x56,
-	0xd3, 0x50, 0xb3, 0x27, 0x7f, 0xcb, 0x40, 0x56, 0x7c, 0x41, 0x80, 0xce, 0xa0, 0x98, 0x78, 0x90,
-	0xa1, 0x83, 0xcf, 0x3e, 0xd4, 0x6a, 0xda, 0xe6, 0x77, 0xc7, 0x3c, 0x7c, 0xa5, 0xa0, 0x73, 0x28,
-	0x25, 0x9f, 0x3b, 0x28, 0xd9, 0x9b, 0x6e, 0x78, 0x07, 0x7d, 0xd6, 0xd7, 0x5b, 0x50, 0x8d, 0x90,
-	0x39, 0x53, 0xde, 0x8b, 0x46, 0xaf, 0x03, 0x54, 0x4b, 0xf0, 0xd7, 0x9e, 0x1c, 0xb5, 0xfd, 0x8d,
-	0xb6, 0x28, 0x85, 0x1d, 0xf9, 0x89, 0x51, 0x7f, 0x7e, 0xe7, 0x13, 0x57, 0x1f, 0x05, 0xb5, 0x2f,
-	0xef, 0x33, 0x47, 0xde, 0xc6, 0xb0, 0xb3, 0x41, 0xc0, 0xd1, 0xaf, 0x92, 0x2b, 0xb8, 0x57, 0xfe,
-	0x6b, 0xcf, 0x7f, 0x89, 0xb6, 0x8c, 0xb2, 0x41, 0xe9, 0x57, 0xa2, 0xdc, 0x7f, 0x4f, 0xac, 0x44,
-	0xf9, 0xdc, 0x85, 0xf1, 0x23, 0xa8, 0xeb, 0x4a, 0x80, 0xf4, 0xf5, 0xb9, 0x77, 0x55, 0xa9, 0xf6,
-	0xf4, 0xb3, 0x9c, 0xc8, 0xb9, 0x09, 0xb0, 0xac, 0x27, 0xf4, 0x38, 0x31, 0xe5, 0x8e, 0x1e, 0xd4,
-	0x0e, 0xee, 0xb1, 0x4a, 0x57, 0xa7, 0xbf, 0xfe, 0xe3, 0xf1, 0xb5, 0xc3, 0x26, 0xf3, 0xcb, 0xa3,
-	0x91, 0x3f, 0x3d, 0x76, 0x79, 0x47, 0xef, 0x39, 0xde, 0xb5, 0x47, 0xd9, 0xcf, 0x7e, 0x70, 0x73,
-	0xec, 0x7a, 0xe3, 0x63, 0x51, 0x96, 0xc7, 0x0b, 0x2f, 0x97, 0x59, 0xf1, 0xff, 0xa7, 0xdf, 0xfc,
-	0x2f, 0x00, 0x00, 0xff, 0xff, 0x42, 0x87, 0x8c, 0xeb, 0xaf, 0x12, 0x00, 0x00,
+	// 2102 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x58, 0xcb, 0x73, 0xdb, 0xc6,
+	0x19, 0x0f, 0xc4, 0xf7, 0xc7, 0x17, 0xb4, 0x92, 0x65, 0x98, 0xb2, 0x62, 0x05, 0x76, 0x1d, 0x8e,
+	0xc7, 0x91, 0x5c, 0xb5, 0xf1, 0x78, 0x7a, 0x68, 0x87, 0x22, 0xc1, 0x08, 0x32, 0x09, 0xca, 0x4b,
+	0xd2, 0x89, 0x9b, 0xc3, 0x0e, 0x44, 0xae, 0x44, 0x8c, 0x40, 0x80, 0x01, 0x96, 0x8e, 0xf5, 0x0f,
+	0xf4, 0xd4, 0xff, 0xa3, 0xbd, 0xb4, 0x97, 0x9e, 0xfb, 0xef, 0xb4, 0xf7, 0xde, 0x7a, 0xeb, 0xec,
+	0x2e, 0x40, 0x82, 0x14, 0xe5, 0xf4, 0x44, 0xec, 0xef, 0x7b, 0xed, 0xee, 0xb7, 0xdf, 0x8b, 0xb0,
+	0x17, 0xf8, 0x73, 0x46, 0x83, 0x60, 0x36, 0x3a, 0x96, 0x5f, 0x47, 0xb3, 0xc0, 0x67, 0x3e, 0x2a,
+	0x2c, 0xf0, 0x5a, 0x21, 0x98, 0x8d, 0x24, 0xaa, 0xff, 0x37, 0x03, 0xa8, 0x4f, 0xbd, 0xf1, 0x85,
+	0x7d, 0x3b, 0xa5, 0x1e, 0xc3, 0xf4, 0xa7, 0x39, 0x0d, 0x19, 0x42, 0x90, 0x1e, 0xd3, 0x90, 0x69,
+	0xca, 0xa1, 0x52, 0x2f, 0x61, 0xf1, 0x8d, 0x54, 0x48, 0xd9, 0x53, 0xa6, 0x6d, 0x1d, 0x2a, 0xf5,
+	0x14, 0xe6, 0x9f, 0xe8, 0x11, 0xe4, 0xed, 0x29, 0x23, 0xd3, 0xd0, 0x66, 0x5a, 0x49, 0xc0, 0x39,
+	0x7b, 0xca, 0xba, 0xa1, 0xcd, 0xd0, 0x57, 0x50, 0x9a, 0x49, 0x95, 0x64, 0x62, 0x87, 0x13, 0x2d,
+	0x25, 0x14, 0x15, 0x23, 0xec, 0xcc, 0x0e, 0x27, 0xa8, 0x0e, 0xea, 0x95, 0xe3, 0xd9, 0x2e, 0x19,
+	0xb9, 0xec, 0x23, 0x19, 0x53, 0x97, 0xd9, 0x5a, 0xfa, 0x50, 0xa9, 0x67, 0x70, 0x45, 0xe0, 0x4d,
+	0x97, 0x7d, 0x6c, 0x71, 0x14, 0x7d, 0x0d, 0xd5, 0x58, 0x59, 0x20, 0x37, 0xa8, 0x65, 0x0e, 0x95,
+	0x7a, 0x01, 0x57, 0x66, 0xab, 0xdb, 0xfe, 0x1a, 0xaa, 0xcc, 0x99, 0x52, 0x7f, 0xce, 0x48, 0x48,
+	0x47, 0xbe, 0x37, 0x0e, 0xb5, 0xac, 0xd4, 0x18, 0xc1, 0x7d, 0x89, 0x22, 0x1d, 0xca, 0x57, 0x94,
+	0x12, 0xd7, 0x99, 0x3a, 0x8c, 0xf0, 0xed, 0xe7, 0xc4, 0xf6, 0x8b, 0x57, 0x94, 0x76, 0x38, 0xd6,
+	0xb7, 0x19, 0x7a, 0x06, 0x95, 0x25, 0x8f, 0x38, 0x63, 0x59, 0x30, 0x95, 0x62, 0x26, 0x71, 0xd0,
+	0x97, 0xa0, 0xfa, 0x73, 0x76, 0xed, 0x3b, 0xde, 0x35, 0x19, 0x4d, 0x6c, 0x8f, 0x38, 0x63, 0x2d,
+	0x7f, 0xa8, 0xd4, 0xd3, 0xa7, 0x5b, 0xaf, 0x14, 0x5c, 0x89, 0x69, 0xcd, 0x89, 0xed, 0x99, 0x63,
+	0xf4, 0x1c, 0xaa, 0xae, 0x1d, 0x32, 0x32, 0xf1, 0x67, 0x64, 0x36, 0xbf, 0xbc, 0xa1, 0xb7, 0x5a,
+	0x45, 0xdc, 0x4c, 0x99, 0xc3, 0x67, 0xfe, 0xec, 0x42, 0x80, 0xe8, 0x00, 0x40, 0xdc, 0x8a, 0x30,
+	0xae, 0x15, 0xc4, 0x19, 0x0a, 0x1c, 0x11, 0x86, 0xd1, 0x09, 0x14, 0x85, 0x37, 0xc9, 0xc4, 0xf1,
+	0x58, 0xa8, 0xc1, 0x61, 0xaa, 0x5e, 0x3c, 0x51, 0x8f, 0x5c, 0x8f, 0x3b, 0x16, 0x73, 0xca, 0x99,
+	0xe3, 0x31, 0x9c, 0x64, 0x42, 0x63, 0xd8, 0xe1, 0x6e, 0x24, 0xa3, 0x79, 0xc8, 0xfc, 0x29, 0x09,
+	0xe8, 0xc8, 0x0f, 0xc6, 0xa1, 0x56, 0x14, 0xb2, 0xbf, 0x3d, 0x5a, 0xbc, 0x8e, 0xa3, 0xbb, 0xcf,
+	0xe1, 0xa8, 0x45, 0x43, 0xd6, 0x14, 0x72, 0x58, 0x8a, 0x19, 0x1e, 0x0b, 0x6e, 0xf1, 0xf6, 0x78,
+	0x1d, 0x47, 0x2f, 0x01, 0xd9, 0xae, 0xeb, 0xff, 0x4c, 0x42, 0xea, 0x5e, 0x91, 0xc8, 0x3d, 0x5a,
+	0xf5, 0x50, 0xa9, 0xe7, 0xb1, 0x2a, 0x28, 0x7d, 0xea, 0x5e, 0x45, 0xea, 0xd1, 0x6b, 0x28, 0x8b,
+	0x3d, 0x5d, 0x51, 0x9b, 0xcd, 0x03, 0x1a, 0x6a, 0xea, 0x61, 0xaa, 0x5e, 0x39, 0xd9, 0x8e, 0x4e,
+	0xd2, 0x96, 0xf0, 0xa9, 0xc3, 0x70, 0x89, 0xf3, 0x45, 0xeb, 0xb0, 0xd6, 0x82, 0xbd, 0xcd, 0x5b,
+	0xe2, 0x8f, 0x94, 0x5f, 0x2a, 0x7f, 0xb7, 0x69, 0xcc, 0x3f, 0xd1, 0x2e, 0x64, 0x3e, 0xda, 0xee,
+	0x9c, 0x8a, 0x87, 0x5b, 0xc2, 0x72, 0xf1, 0xbb, 0xad, 0x37, 0x8a, 0xfe, 0x06, 0x76, 0x06, 0x81,
+	0x3d, 0xba, 0x59, 0x7b, 0xfb, 0xeb, 0x4f, 0x57, 0xb9, 0xf3, 0x74, 0xf5, 0xbf, 0x2a, 0x50, 0x8e,
+	0xa4, 0xfa, 0xcc, 0x66, 0xf3, 0x10, 0x7d, 0x03, 0x99, 0x90, 0xd9, 0x8c, 0x0a, 0xee, 0xca, 0xc9,
+	0xc3, 0xc4, 0x7d, 0x26, 0x18, 0x29, 0x96, 0x5c, 0xa8, 0x06, 0xf9, 0x59, 0x40, 0x9d, 0xa9, 0x7d,
+	0x1d, 0xef, 0x6b, 0xb1, 0x46, 0x3a, 0x64, 0x84, 0xb0, 0x88, 0x99, 0xe2, 0x49, 0x29, 0xe9, 0x56,
+	0x2c, 0x49, 0xa8, 0x0e, 0x99, 0x09, 0x73, 0x47, 0xa1, 0x96, 0x16, 0xee, 0x43, 0x11, 0xcf, 0xd9,
+	0xa0, 0xd3, 0x6c, 0x30, 0x46, 0xa7, 0x33, 0x86, 0x25, 0x83, 0xfe, 0x7b, 0xa8, 0x0a, 0xc9, 0x36,
+	0xa5, 0x9f, 0x0b, 0xee, 0x87, 0xc0, 0x43, 0x57, 0x84, 0x82, 0x0c, 0xf0, 0xac, 0x3d, 0xe5, 0x51,
+	0xa0, 0x8f, 0x41, 0x5d, 0xca, 0x87, 0x33, 0xdf, 0x0b, 0xb9, 0x75, 0x95, 0x6f, 0x83, 0x3f, 0x79,
+	0x1e, 0x21, 0x22, 0x36, 0x14, 0x21, 0x55, 0x89, 0xf0, 0x36, 0xa5, 0x22, 0x3a, 0x9e, 0xcb, 0x80,
+	0x24, 0xae, 0x3f, 0xba, 0xe1, 0x21, 0x6e, 0xdf, 0x46, 0xea, 0xcb, 0x1c, 0xee, 0xf8, 0xa3, 0x9b,
+	0x16, 0x07, 0xf5, 0x1f, 0x65, 0x16, 0x1a, 0xf8, 0xf2, 0x94, 0xff, 0xb7, 0x27, 0x96, 0x97, 0xb5,
+	0x75, 0xef, 0x65, 0xe9, 0x04, 0x76, 0x56, 0x94, 0x47, 0xa7, 0x48, 0xfa, 0x40, 0x59, 0xf3, 0xc1,
+	0x4b, 0xc8, 0x5d, 0xd9, 0x8e, 0x3b, 0x0f, 0x62, 0xc5, 0x28, 0xe1, 0xd0, 0xb6, 0xa4, 0xe0, 0x98,
+	0x45, 0xff, 0x53, 0x1e, 0x72, 0x11, 0x88, 0x4e, 0x20, 0x3d, 0xf2, 0xc7, 0xf1, 0x3b, 0xf8, 0xf2,
+	0xae, 0x58, 0xfc, 0xdb, 0xf4, 0xc7, 0x14, 0x0b, 0x5e, 0xf4, 0x07, 0xa8, 0xf0, 0xd4, 0xe1, 0x51,
+	0x97, 0xcc, 0x67, 0x63, 0x7b, 0xe1, 0x7a, 0x2d, 0x21, 0xdd, 0x94, 0x0c, 0x43, 0x41, 0xc7, 0xe5,
+	0x51, 0x72, 0x89, 0xf6, 0xa1, 0xc0, 0xbd, 0x2d, 0x3d, 0x91, 0x16, 0x6f, 0x3f, 0xcf, 0x01, 0xe1,
+	0x03, 0x1d, 0xca, 0xbe, 0xe7, 0xf8, 0x1e, 0x09, 0x27, 0x36, 0x39, 0xf9, 0xf6, 0xb5, 0xc8, 0x9d,
+	0x25, 0x5c, 0x14, 0x60, 0x7f, 0x62, 0x9f, 0x7c, 0xfb, 0x1a, 0x3d, 0x81, 0xa2, 0xc8, 0x37, 0xf4,
+	0xd3, 0xcc, 0x09, 0x6e, 0x45, 0xd2, 0x2c, 0x63, 0x91, 0x82, 0x0c, 0x81, 0xf0, 0x28, 0xba, 0x72,
+	0xed, 0xeb, 0x50, 0x24, 0xca, 0x32, 0x96, 0x0b, 0xf4, 0x0a, 0x76, 0xa3, 0x3b, 0x20, 0xa1, 0x3f,
+	0x0f, 0x46, 0x94, 0x38, 0xde, 0x98, 0x7e, 0x12, 0x09, 0xb0, 0x8c, 0x51, 0x44, 0xeb, 0x0b, 0x92,
+	0xc9, 0x29, 0x68, 0x0f, 0xb2, 0x13, 0xea, 0x5c, 0x4f, 0x64, 0x52, 0x2b, 0xe3, 0x68, 0xa5, 0xff,
+	0x33, 0x03, 0xc5, 0xc4, 0xc5, 0xa0, 0x12, 0xe4, 0xb1, 0xd1, 0x37, 0xf0, 0x7b, 0xa3, 0xa5, 0x7e,
+	0x81, 0xea, 0xf0, 0xcc, 0xb4, 0x9a, 0x3d, 0x8c, 0x8d, 0xe6, 0x80, 0xf4, 0x30, 0x19, 0x5a, 0x6f,
+	0xad, 0xde, 0xf7, 0x16, 0xb9, 0x68, 0x7c, 0xe8, 0x1a, 0xd6, 0x80, 0xb4, 0x8c, 0x41, 0xc3, 0xec,
+	0xf4, 0x55, 0x05, 0x3d, 0x06, 0x6d, 0xc9, 0x19, 0x93, 0x1b, 0xdd, 0xde, 0xd0, 0x1a, 0xa8, 0x5b,
+	0xe8, 0x09, 0xec, 0xb7, 0x4d, 0xab, 0xd1, 0x21, 0x4b, 0x9e, 0x66, 0x67, 0xf0, 0x9e, 0x18, 0x3f,
+	0x5c, 0x98, 0xf8, 0x83, 0x9a, 0xda, 0xc4, 0xc0, 0x63, 0x2a, 0xd6, 0x90, 0x46, 0x8f, 0xe0, 0x81,
+	0x64, 0x90, 0x22, 0x64, 0xd0, 0xeb, 0x91, 0x7e, 0xaf, 0x67, 0xa9, 0x19, 0xb4, 0x0d, 0x65, 0xd3,
+	0x7a, 0xdf, 0xe8, 0x98, 0x2d, 0x82, 0x8d, 0x46, 0xa7, 0xab, 0x66, 0xd1, 0x0e, 0x54, 0xd7, 0xf9,
+	0x72, 0x5c, 0x45, 0xcc, 0xd7, 0xb3, 0xcc, 0x9e, 0x45, 0xde, 0x1b, 0xb8, 0x6f, 0xf6, 0x2c, 0x35,
+	0x8f, 0xf6, 0x00, 0xad, 0x92, 0xce, 0xba, 0x8d, 0xa6, 0x5a, 0x40, 0x0f, 0x60, 0x7b, 0x15, 0x7f,
+	0x6b, 0x7c, 0x50, 0x01, 0x69, 0xb0, 0x2b, 0x37, 0x46, 0x4e, 0x8d, 0x4e, 0xef, 0x7b, 0xd2, 0x35,
+	0x2d, 0xb3, 0x3b, 0xec, 0xaa, 0x45, 0xb4, 0x0b, 0x6a, 0xdb, 0x30, 0x88, 0x69, 0xf5, 0x87, 0xed,
+	0xb6, 0xd9, 0x34, 0x0d, 0x6b, 0xa0, 0x96, 0xa4, 0xe5, 0x4d, 0x07, 0x2f, 0x73, 0x81, 0xe6, 0x59,
+	0xc3, 0xb2, 0x8c, 0x0e, 0x69, 0x99, 0xfd, 0xc6, 0x69, 0xc7, 0x68, 0xa9, 0x15, 0x74, 0x00, 0x8f,
+	0x06, 0x46, 0xf7, 0xa2, 0x87, 0x1b, 0xf8, 0x03, 0x89, 0xe9, 0xed, 0x86, 0xd9, 0x19, 0x62, 0x43,
+	0xad, 0xa2, 0xaf, 0xe0, 0x00, 0x1b, 0xef, 0x86, 0x26, 0x36, 0x5a, 0xc4, 0xea, 0xb5, 0x0c, 0xd2,
+	0x36, 0x1a, 0x83, 0x21, 0x36, 0x48, 0xd7, 0xec, 0xf7, 0x4d, 0xeb, 0x3b, 0x55, 0x45, 0xcf, 0xe0,
+	0x70, 0xc1, 0xb2, 0x50, 0xb0, 0xc6, 0xb5, 0xcd, 0xcf, 0x17, 0xbb, 0xd4, 0x32, 0x7e, 0x18, 0x90,
+	0x0b, 0xc3, 0xc0, 0x2a, 0x42, 0x35, 0xd8, 0x5b, 0x9a, 0x97, 0x06, 0x22, 0xdb, 0x3b, 0x9c, 0x76,
+	0x61, 0xe0, 0x6e, 0xc3, 0xe2, 0x0e, 0x5e, 0xa1, 0xed, 0xf2, 0x6d, 0x2f, 0x69, 0xeb, 0xdb, 0x7e,
+	0x80, 0x10, 0x54, 0x12, 0x5e, 0x69, 0x37, 0xb0, 0xba, 0x87, 0xaa, 0x50, 0xec, 0x5e, 0x5c, 0x90,
+	0x81, 0xd9, 0x35, 0x7a, 0xc3, 0x81, 0xfa, 0x10, 0xed, 0x42, 0x35, 0xde, 0x52, 0x2c, 0xf9, 0xaf,
+	0x1c, 0x7a, 0x08, 0x68, 0x68, 0x61, 0xa3, 0xd1, 0xe2, 0x37, 0xb4, 0x20, 0xfc, 0x3b, 0x77, 0x9e,
+	0xce, 0x6f, 0xa9, 0x29, 0xfd, 0x1f, 0x29, 0x28, 0xaf, 0x04, 0x2a, 0x7a, 0x0c, 0x85, 0xd0, 0xb9,
+	0xf6, 0x44, 0xdd, 0x8a, 0xb2, 0xcc, 0x12, 0x10, 0x65, 0x7e, 0x62, 0x3b, 0x9e, 0x4c, 0x6f, 0xb2,
+	0x10, 0x14, 0x04, 0x22, 0x92, 0xdb, 0x3e, 0xe4, 0xe2, 0x96, 0x22, 0xb5, 0x68, 0x29, 0xb2, 0x23,
+	0xd9, 0x4a, 0x3c, 0x86, 0x02, 0xcf, 0xa1, 0x21, 0xb3, 0xa7, 0x33, 0x11, 0xf3, 0x65, 0xbc, 0x04,
+	0xd0, 0x53, 0x28, 0x4f, 0x69, 0x18, 0xda, 0xd7, 0x94, 0xc8, 0xb8, 0x05, 0xc1, 0x51, 0x8a, 0xc0,
+	0xb6, 0x08, 0xdf, 0xa7, 0x10, 0xe7, 0x91, 0x88, 0x29, 0x23, 0x99, 0x22, 0x50, 0x32, 0xad, 0xa7,
+	0x70, 0x66, 0x47, 0xe9, 0x21, 0x99, 0xc2, 0x99, 0x8d, 0x5e, 0xc0, 0xb6, 0xcc, 0x41, 0x8e, 0xe7,
+	0x4c, 0xe7, 0x53, 0x99, 0x8b, 0x72, 0x22, 0x17, 0x55, 0x45, 0x2e, 0x92, 0xb8, 0x48, 0x49, 0x8f,
+	0x20, 0x7f, 0x69, 0x87, 0x94, 0x57, 0x8f, 0x28, 0x57, 0xe4, 0xf8, 0xba, 0x4d, 0x29, 0x27, 0xf1,
+	0x9a, 0x12, 0xf0, 0x2c, 0x28, 0x53, 0x44, 0xee, 0x8a, 0x52, 0xcc, 0xef, 0x72, 0x61, 0xc1, 0xfe,
+	0xb4, 0xb4, 0x50, 0x4c, 0x58, 0x90, 0xb8, 0xb0, 0xf0, 0x02, 0xb6, 0xe9, 0x27, 0x16, 0xd8, 0xc4,
+	0x9f, 0xd9, 0x3f, 0xcd, 0x29, 0x19, 0xdb, 0xcc, 0x16, 0x3d, 0x6a, 0x09, 0x57, 0x05, 0xa1, 0x27,
+	0xf0, 0x96, 0xcd, 0x6c, 0xfd, 0x31, 0xd4, 0x30, 0x0d, 0x29, 0xeb, 0x3a, 0x61, 0xe8, 0xf8, 0x5e,
+	0xd3, 0xf7, 0x58, 0xe0, 0xbb, 0x51, 0x11, 0xd2, 0x0f, 0x60, 0x7f, 0x23, 0x55, 0x56, 0x11, 0x2e,
+	0xfc, 0x6e, 0x4e, 0x83, 0xdb, 0xcd, 0xc2, 0xef, 0x60, 0x7f, 0x23, 0x35, 0x2a, 0x41, 0x2f, 0x21,
+	0x33, 0xb3, 0x9d, 0x20, 0xd4, 0xb6, 0x44, 0x19, 0xdf, 0x5b, 0xe9, 0x1a, 0x9c, 0xe0, 0xcc, 0x09,
+	0x99, 0x1f, 0xdc, 0x62, 0xc9, 0x74, 0x9e, 0xce, 0x2b, 0xea, 0x96, 0xfe, 0x67, 0x05, 0x8a, 0x09,
+	0x22, 0x7f, 0x07, 0x9e, 0x3f, 0xa6, 0xe4, 0x2a, 0xf0, 0xa7, 0xf1, 0x0b, 0x5b, 0x00, 0x48, 0x83,
+	0x9c, 0x58, 0x30, 0x3f, 0x7a, 0x5e, 0xf1, 0x12, 0x7d, 0x03, 0xb9, 0x89, 0x54, 0x21, 0xbc, 0x54,
+	0x3c, 0xd9, 0x59, 0xb3, 0xce, 0xef, 0x06, 0xc7, 0x3c, 0xe7, 0xe9, 0x7c, 0x4a, 0x4d, 0x9f, 0xa7,
+	0xf3, 0x69, 0x35, 0x73, 0x9e, 0xce, 0x67, 0xd4, 0xec, 0x79, 0x3a, 0x9f, 0x55, 0x73, 0xfa, 0x7f,
+	0x14, 0xc8, 0xc7, 0xdc, 0x7c, 0x2f, 0x3c, 0xe7, 0x13, 0xfe, 0x32, 0xa2, 0x8e, 0x60, 0x09, 0x20,
+	0x1d, 0x4a, 0x62, 0xb1, 0xda, 0x68, 0xac, 0x60, 0xe8, 0x19, 0x94, 0x17, 0xeb, 0x45, 0x35, 0x4b,
+	0xe1, 0x55, 0x90, 0x6b, 0x0a, 0xe7, 0xa3, 0x11, 0x0d, 0x43, 0x69, 0x2a, 0x23, 0x35, 0x25, 0x31,
+	0x54, 0x87, 0x6a, 0xbc, 0x8e, 0x0d, 0x66, 0x05, 0xdb, 0x3a, 0x8c, 0x5e, 0x80, 0x9a, 0x84, 0xa6,
+	0xcb, 0x79, 0xe0, 0x0e, 0x2e, 0xaf, 0x41, 0x9f, 0xc2, 0x43, 0xe1, 0xd6, 0x8b, 0xc0, 0xbf, 0xb4,
+	0x2f, 0x1d, 0xd7, 0x61, 0xb7, 0x71, 0xcf, 0xc2, 0xaf, 0x20, 0xf0, 0xa7, 0xc4, 0x8b, 0x9b, 0x80,
+	0x12, 0x5e, 0x02, 0xdc, 0x1d, 0xcc, 0x97, 0xb4, 0xc8, 0x1d, 0xd1, 0x92, 0x77, 0x23, 0x0b, 0xe3,
+	0x29, 0x61, 0x7c, 0xb1, 0xd6, 0x6f, 0x40, 0xbb, 0x6b, 0x2e, 0x7a, 0x42, 0x87, 0x50, 0x9c, 0x2d,
+	0x61, 0x61, 0x51, 0xc1, 0x49, 0x28, 0xe9, 0xe8, 0xad, 0x5f, 0x76, 0xb4, 0xfe, 0x17, 0x05, 0xb6,
+	0x4f, 0xe7, 0x8e, 0x3b, 0x5e, 0x69, 0xc5, 0x92, 0xa3, 0x9e, 0xb2, 0x3a, 0xea, 0x6d, 0x9a, 0xe3,
+	0xb6, 0x36, 0xce, 0x71, 0x9b, 0x66, 0xa5, 0xd4, 0xbd, 0xb3, 0xd2, 0x13, 0x28, 0x2e, 0xc7, 0x24,
+	0xd9, 0xe9, 0x96, 0x30, 0x4c, 0xe2, 0x19, 0x29, 0xd4, 0xdf, 0x00, 0x4a, 0x6e, 0x34, 0xba, 0x90,
+	0x45, 0x47, 0xa8, 0xdc, 0xdb, 0x11, 0xbe, 0xf8, 0xbb, 0x02, 0xa5, 0x64, 0x5b, 0x8e, 0xca, 0x50,
+	0x30, 0x2d, 0xd2, 0xee, 0x98, 0xdf, 0x9d, 0x0d, 0xd4, 0x2f, 0xf8, 0xb2, 0x3f, 0x6c, 0x36, 0x0d,
+	0xa3, 0x65, 0xb4, 0x54, 0x85, 0x17, 0x0c, 0x9e, 0xea, 0x8d, 0xd6, 0xa2, 0x3e, 0x6c, 0xf1, 0xd2,
+	0x1e, 0x61, 0x56, 0x8f, 0xe0, 0xde, 0x70, 0x60, 0xa8, 0x29, 0xa4, 0x42, 0x29, 0x02, 0x0d, 0x8c,
+	0x7b, 0x58, 0x4d, 0xf3, 0xfa, 0x17, 0x21, 0x77, 0xdb, 0x92, 0xb8, 0x6b, 0xc9, 0x88, 0xb6, 0x23,
+	0xe6, 0x5a, 0x56, 0x6c, 0x72, 0xda, 0xe8, 0x34, 0xac, 0xa6, 0xa1, 0x66, 0x4f, 0xfe, 0x96, 0x81,
+	0xac, 0x38, 0x41, 0x80, 0xce, 0xa0, 0x98, 0x98, 0xd0, 0xd0, 0xc1, 0x67, 0x27, 0xb7, 0x9a, 0xb6,
+	0x79, 0x10, 0x99, 0x87, 0xaf, 0x14, 0x74, 0x0e, 0xa5, 0xe4, 0xfc, 0x83, 0x92, 0xcd, 0xea, 0x86,
+	0xc1, 0xe8, 0xb3, 0xba, 0xde, 0x82, 0x6a, 0x84, 0xcc, 0x99, 0xf2, 0xe6, 0x34, 0x1a, 0x17, 0x50,
+	0x2d, 0xc1, 0xbf, 0x36, 0x83, 0xd4, 0xf6, 0x37, 0xd2, 0x22, 0x17, 0x76, 0xe4, 0x11, 0xa3, 0x86,
+	0xfd, 0xce, 0x11, 0x57, 0xa7, 0x84, 0xda, 0x97, 0xf7, 0x91, 0x23, 0x6d, 0x63, 0xd8, 0xd9, 0x90,
+	0xc0, 0xd1, 0xaf, 0x92, 0x3b, 0xb8, 0x37, 0xfd, 0xd7, 0x9e, 0xff, 0x12, 0xdb, 0xd2, 0xca, 0x86,
+	0x4c, 0xbf, 0x62, 0xe5, 0xfe, 0x3a, 0xb1, 0x62, 0xe5, 0x73, 0x05, 0xe3, 0x47, 0x50, 0xd7, 0x33,
+	0x01, 0xd2, 0xd7, 0x65, 0xef, 0x66, 0xa5, 0xda, 0xd3, 0xcf, 0xf2, 0x44, 0xca, 0x4d, 0x80, 0x65,
+	0x3c, 0xa1, 0xc7, 0x09, 0x91, 0x3b, 0xf9, 0xa0, 0x76, 0x70, 0x0f, 0x55, 0xaa, 0x3a, 0xfd, 0xf5,
+	0x1f, 0x8f, 0xaf, 0x1d, 0x36, 0x99, 0x5f, 0x1e, 0x8d, 0xfc, 0xe9, 0xb1, 0xcb, 0x5b, 0x7c, 0xcf,
+	0xf1, 0xae, 0x3d, 0xca, 0x7e, 0xf6, 0x83, 0x9b, 0x63, 0xd7, 0x1b, 0x1f, 0x8b, 0xb0, 0x3c, 0x5e,
+	0x68, 0xb9, 0xcc, 0x8a, 0x3f, 0xa4, 0x7e, 0xf3, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x10, 0xf6,
+	0xab, 0x74, 0xc0, 0x12, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto
index aa0d96760..034ee2c53 100644
--- a/lnrpc/routerrpc/router.proto
+++ b/lnrpc/routerrpc/router.proto
@@ -256,6 +256,7 @@ message Failure {
         PERMANENT_NODE_FAILURE = 20;
         PERMANENT_CHANNEL_FAILURE = 21;
         EXPIRY_TOO_FAR = 22;
+        MPP_TIMEOUT = 23;
 
         /**
         The error source is known, but the failure itself couldn't be decoded.
diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go
index c872afe37..ba23a0799 100644
--- a/lnrpc/routerrpc/router_server.go
+++ b/lnrpc/routerrpc/router_server.go
@@ -415,6 +415,9 @@ func marshallError(sendError error) (*Failure, error) {
 	case *lnwire.FailPermanentChannelFailure:
 		response.Code = Failure_PERMANENT_CHANNEL_FAILURE
 
+	case *lnwire.FailMPPTimeout:
+		response.Code = Failure_MPP_TIMEOUT
+
 	case nil:
 		response.Code = Failure_UNKNOWN_FAILURE
 
diff --git a/lnwire/onion_error.go b/lnwire/onion_error.go
index 961373b93..c6235552e 100644
--- a/lnwire/onion_error.go
+++ b/lnwire/onion_error.go
@@ -80,6 +80,7 @@ const (
 	CodeFinalIncorrectHtlcAmount         FailCode = 19
 	CodeExpiryTooFar                     FailCode = 21
 	CodeInvalidOnionPayload                       = FlagPerm | 22
+	CodeMPPTimeout                       FailCode = 23
 )
 
 // String returns the string representation of the failure code.
@@ -154,6 +155,9 @@ func (c FailCode) String() string {
 	case CodeInvalidOnionPayload:
 		return "InvalidOnionPayload"
 
+	case CodeMPPTimeout:
+		return "MPPTimeout"
+
 	default:
 		return "<unknown>"
 	}
@@ -1182,6 +1186,26 @@ func (f *InvalidOnionPayload) Encode(w io.Writer, pver uint32) error {
 	return WriteElements(w, f.Offset)
 }
 
+// FailMPPTimeout is returned if the complete amount for a multi part payment
+// was not received within a reasonable time.
+//
+// NOTE: May only be returned by the final node in the path.
+type FailMPPTimeout struct{}
+
+// Code returns the failure unique code.
+//
+// NOTE: Part of the FailureMessage interface.
+func (f *FailMPPTimeout) Code() FailCode {
+	return CodeMPPTimeout
+}
+
+// Returns a human readable string describing the target FailureMessage.
+//
+// NOTE: Implements the error interface.
+func (f *FailMPPTimeout) Error() string {
+	return f.Code().String()
+}
+
 // DecodeFailure decodes, validates, and parses the lnwire onion failure, for
 // the provided protocol version.
 func DecodeFailure(r io.Reader, pver uint32) (FailureMessage, error) {
@@ -1366,6 +1390,9 @@ func makeEmptyOnionError(code FailCode) (FailureMessage, error) {
 	case CodeInvalidOnionPayload:
 		return &InvalidOnionPayload{}, nil
 
+	case CodeMPPTimeout:
+		return &FailMPPTimeout{}, nil
+
 	default:
 		return nil, errors.Errorf("unknown error code: %v", code)
 	}
diff --git a/routing/result_interpretation.go b/routing/result_interpretation.go
index 7b3d539af..fae2835a6 100644
--- a/routing/result_interpretation.go
+++ b/routing/result_interpretation.go
@@ -216,6 +216,22 @@ func (i *interpretedResult) processPaymentOutcomeFinal(
 		// deliberately. What to penalize?
 		i.finalFailureReason = &reasonIncorrectDetails
 
+	case *lnwire.FailMPPTimeout:
+		// TODO(carla): decide how to penalize mpp timeout. In the
+		// meantime, attribute success to the hops along the route and
+		// do not penalize the final node.
+
+		i.finalFailureReason = &reasonError
+
+		// If this is a direct payment, take no action.
+		if n == 1 {
+			return
+		}
+
+		// Assign all pairs a success result except the final hop, as
+		// the payment reached the destination correctly.
+		i.successPairRange(route, 0, n-2)
+
 	default:
 		// All other errors are considered terminal if coming from the
 		// final hop. They indicate that something is wrong at the


Test patch:
diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go
index abedcc14e..98e52500a 100644
--- a/contractcourt/htlc_incoming_contest_resolver.go
+++ b/contractcourt/htlc_incoming_contest_resolver.go
@@ -167,10 +167,10 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 	preimageSubscription := h.PreimageDB.SubscribeUpdates()
 	defer preimageSubscription.CancelSubscription()
 
-	// Define closure to process hodl events either direct or triggered by
-	// later notifcation.
-	processHodlEvent := func(e invoices.HodlEvent) (ContractResolver,
-		error) {
+	// Define closure to process htlc resolutions either direct or triggered by
+	// later notification.
+	processHtlcResolution := func(e invoices.HtlcResolution) (
+		ContractResolver, error) {
 
 		if e.Preimage == nil {
 			log.Infof("%T(%v): Exit hop HTLC canceled "+
@@ -201,23 +201,26 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 		HtlcID: h.htlc.HtlcIndex,
 	}
 
-	event, err := h.Registry.NotifyExitHopHtlc(
+	resolution, err := h.Registry.NotifyExitHopHtlc(
 		h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
 		circuitKey, hodlChan, payload,
 	)
-	switch err {
-	case channeldb.ErrInvoiceNotFound:
-	case nil:
-		defer h.Registry.HodlUnsubscribeAll(hodlChan)
-
-		// Resolve the htlc directly if possible.
-		if event != nil {
-			return processHodlEvent(*event)
-		}
-	default:
+	if err != nil {
 		return nil, err
 	}
 
+	defer h.Registry.HodlUnsubscribeAll(hodlChan)
+
+	// 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 {
+
+		return processHtlcResolution(*resolution)
+	}
+
 	// With the epochs and preimage subscriptions initialized, we'll query
 	// to see if we already know the preimage.
 	preimage, ok := h.PreimageDB.LookupPreimage(h.htlc.RHash)
@@ -252,9 +255,9 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
 			return &h.htlcSuccessResolver, nil
 
 		case hodlItem := <-hodlChan:
-			hodlEvent := hodlItem.(invoices.HodlEvent)
+			htlcResolution := hodlItem.(invoices.HtlcResolution)
 
-			return processHodlEvent(hodlEvent)
+			return processHtlcResolution(htlcResolution)
 
 		case newBlock, ok := <-blockEpochs.Epochs:
 			if !ok {
diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go
index 850126c06..773b22c00 100644
--- a/contractcourt/htlc_incoming_resolver_test.go
+++ b/contractcourt/htlc_incoming_resolver_test.go
@@ -21,10 +21,11 @@ const (
 )
 
 var (
-	testResPreimage   = lntypes.Preimage{1, 2, 3}
-	testResHash       = testResPreimage.Hash()
-	testResCircuitKey = channeldb.CircuitKey{}
-	testOnionBlob     = []byte{4, 5, 6}
+	testResPreimage         = lntypes.Preimage{1, 2, 3}
+	testResHash             = testResPreimage.Hash()
+	testResCircuitKey       = channeldb.CircuitKey{}
+	testOnionBlob           = []byte{4, 5, 6}
+	testAcceptHeight  int32 = 1234
 )
 
 // TestHtlcIncomingResolverFwdPreimageKnown tests resolution of a forwarded htlc
@@ -34,7 +35,10 @@ func TestHtlcIncomingResolverFwdPreimageKnown(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
+		testResCircuitKey, testHtlcExpiry,
+		invoices.ResultInvoiceNotFound,
+	)
 	ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
 	ctx.resolve()
 	ctx.waitForResult(true)
@@ -48,7 +52,10 @@ func TestHtlcIncomingResolverFwdContestedSuccess(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
+		testResCircuitKey, testHtlcExpiry,
+		invoices.ResultInvoiceNotFound,
+	)
 	ctx.resolve()
 
 	// Simulate a new block coming in. HTLC is not yet expired.
@@ -65,7 +72,10 @@ func TestHtlcIncomingResolverFwdContestedTimeout(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
+		testResCircuitKey, testHtlcExpiry,
+		invoices.ResultInvoiceNotFound,
+	)
 	ctx.resolve()
 
 	// Simulate a new block coming in. HTLC expires.
@@ -81,8 +91,10 @@ func TestHtlcIncomingResolverFwdTimeout(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-
-	ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
+		testResCircuitKey, testHtlcExpiry,
+		invoices.ResultInvoiceNotFound,
+	)
 	ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
 	ctx.resolver.htlcExpiry = 90
 	ctx.resolve()
@@ -96,10 +108,11 @@ func TestHtlcIncomingResolverExitSettle(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyEvent = &invoices.HodlEvent{
-		CircuitKey: testResCircuitKey,
-		Preimage:   &testResPreimage,
-	}
+	ctx.registry.notifyResolution = invoices.NewSettleResolution(
+		testResPreimage, testResCircuitKey, testAcceptHeight,
+		invoices.ResultReplayToSettled,
+	)
+
 	ctx.resolve()
 
 	data := <-ctx.registry.notifyChan
@@ -126,9 +139,11 @@ func TestHtlcIncomingResolverExitCancel(t *testing.T) {
 	defer timeout(t)()
 
 	ctx := newIncomingResolverTestContext(t)
-	ctx.registry.notifyEvent = &invoices.HodlEvent{
-		CircuitKey: testResCircuitKey,
-	}
+	ctx.registry.notifyResolution = invoices.NewFailureResolution(
+		testResCircuitKey, testAcceptHeight,
+		invoices.ResultInvoiceAlreadyCanceled,
+	)
+
 	ctx.resolve()
 	ctx.waitForResult(false)
 }
@@ -143,10 +158,10 @@ func TestHtlcIncomingResolverExitSettleHodl(t *testing.T) {
 	ctx.resolve()
 
 	notifyData := <-ctx.registry.notifyChan
-	notifyData.hodlChan <- invoices.HodlEvent{
-		CircuitKey: testResCircuitKey,
-		Preimage:   &testResPreimage,
-	}
+	notifyData.hodlChan <- *invoices.NewSettleResolution(
+		testResPreimage, testResCircuitKey, testAcceptHeight,
+		invoices.ResultSettled,
+	)
 
 	ctx.waitForResult(true)
 }
@@ -172,9 +187,10 @@ func TestHtlcIncomingResolverExitCancelHodl(t *testing.T) {
 	ctx := newIncomingResolverTestContext(t)
 	ctx.resolve()
 	notifyData := <-ctx.registry.notifyChan
-	notifyData.hodlChan <- invoices.HodlEvent{
-		CircuitKey: testResCircuitKey,
-	}
+	notifyData.hodlChan <- *invoices.NewFailureResolution(
+		testResCircuitKey, testAcceptHeight, invoices.ResultCanceled,
+	)
+
 	ctx.waitForResult(false)
 }
 
diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go
index 43195249c..dca27d629 100644
--- a/contractcourt/mock_registry_test.go
+++ b/contractcourt/mock_registry_test.go
@@ -16,15 +16,15 @@ type notifyExitHopData struct {
 }
 
 type mockRegistry struct {
-	notifyChan  chan notifyExitHopData
-	notifyErr   error
-	notifyEvent *invoices.HodlEvent
+	notifyChan       chan notifyExitHopData
+	notifyErr        error
+	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.HodlEvent, error) {
+	payload invoices.Payload) (*invoices.HtlcResolution, error) {
 
 	r.notifyChan <- notifyExitHopData{
 		hodlChan:      hodlChan,
@@ -34,7 +34,7 @@ func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
 		currentHeight: currentHeight,
 	}
 
-	return r.notifyEvent, r.notifyErr
+	return r.notifyResolution, r.notifyErr
 }
 
 func (r *mockRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {}
diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go
index e47eaa370..2750e942d 100644
--- a/invoices/invoiceregistry_test.go
+++ b/invoices/invoiceregistry_test.go
@@ -66,7 +66,7 @@ func TestSettleInvoice(t *testing.T) {
 	hodlChan := make(chan interface{}, 1)
 
 	// Try to settle invoice with an htlc that expires too soon.
-	event, err := ctx.registry.NotifyExitHopHtlc(
+	resolution, err := ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, testInvoice.Terms.Value,
 		uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
 		testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
@@ -74,23 +74,30 @@ func TestSettleInvoice(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if event.Preimage != nil {
-		t.Fatal("expected cancel event")
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
 	}
-	if event.AcceptHeight != testCurrentHeight {
+	if resolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, event.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
+	}
+	if resolution.Outcome != ResultExpiryTooSoon {
+		t.Fatalf("expected expiry too soon, got: %v",
+			resolution.Outcome)
 	}
 
 	// Settle invoice with a slightly higher amount.
 	amtPaid := lnwire.MilliSatoshi(100500)
-	_, err = ctx.registry.NotifyExitHopHtlc(
+	resolution, err = ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatal(err)
 	}
+	if resolution.Outcome != ResultSettled {
+		t.Fatalf("expected settled, got: %v", resolution.Outcome)
+	}
 
 	// We expect the settled state to be sent to the single invoice
 	// subscriber.
@@ -120,42 +127,54 @@ func TestSettleInvoice(t *testing.T) {
 
 	// Try to settle again with the same htlc id. We need this idempotent
 	// behaviour after a restart.
-	event, err = ctx.registry.NotifyExitHopHtlc(
+	resolution, err = ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
 	}
-	if event.Preimage == nil {
-		t.Fatal("expected settle event")
+	if resolution.Preimage == nil {
+		t.Fatal("expected settle resolution")
+	}
+	if resolution.Outcome != ResultReplayToSettled {
+		t.Fatalf("expected replay settled, got: %v",
+			resolution.Outcome)
 	}
 
 	// Try to settle again with a new higher-valued htlc. This payment
 	// should also be accepted, to prevent any change in behaviour for a
 	// paid invoice that may open up a probe vector.
-	event, err = ctx.registry.NotifyExitHopHtlc(
+	resolution, err = ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(1), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
 	}
-	if event.Preimage == nil {
-		t.Fatal("expected settle event")
+	if resolution.Preimage == nil {
+		t.Fatal("expected settle resolution")
+	}
+	if resolution.Outcome != ResultDuplicateToSettled {
+		t.Fatalf("expected duplicate settled, got: %v",
+			resolution.Outcome)
 	}
 
 	// Try to settle again with a lower amount. This should fail just as it
 	// would have failed if it were the first payment.
-	event, err = ctx.registry.NotifyExitHopHtlc(
+	resolution, err = ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(2), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
 	}
-	if event.Preimage != nil {
-		t.Fatal("expected cancel event")
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
+	}
+	if resolution.Outcome != ResultAmountTooLow {
+		t.Fatalf("expected amount too low, got: %v",
+			resolution.Outcome)
 	}
 
 	// Check that settled amount is equal to the sum of values of the htlcs
@@ -177,7 +196,7 @@ func TestSettleInvoice(t *testing.T) {
 	// As this is a direct sette, we expect nothing on the hodl chan.
 	select {
 	case <-hodlChan:
-		t.Fatal("unexpected event")
+		t.Fatal("unexpected resolution")
 	default:
 	}
 }
@@ -270,9 +289,9 @@ func TestCancelInvoice(t *testing.T) {
 	}
 
 	// Notify arrival of a new htlc paying to this invoice. This should
-	// result in a cancel event.
+	// result in a cancel resolution.
 	hodlChan := make(chan interface{})
-	event, err := ctx.registry.NotifyExitHopHtlc(
+	resolution, err := ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
@@ -280,12 +299,16 @@ func TestCancelInvoice(t *testing.T) {
 		t.Fatal("expected settlement of a canceled invoice to succeed")
 	}
 
-	if event.Preimage != nil {
-		t.Fatal("expected cancel hodl event")
+	if resolution.Preimage != nil {
+		t.Fatal("expected cancel htlc resolution")
 	}
-	if event.AcceptHeight != testCurrentHeight {
+	if resolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, event.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
+	}
+	if resolution.Outcome != ResultInvoiceAlreadyCanceled {
+		t.Fatalf("expected invoice already canceled, got: %v",
+			resolution.Outcome)
 	}
 }
 
@@ -354,54 +377,58 @@ func TestSettleHoldInvoice(t *testing.T) {
 
 	// NotifyExitHopHtlc without a preimage present in the invoice registry
 	// should be possible.
-	event, err := registry.NotifyExitHopHtlc(
+	resolution, err := registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	if event != nil {
+	if resolution != nil {
 		t.Fatalf("expected htlc to be held")
 	}
 
 	// Test idempotency.
-	event, err = registry.NotifyExitHopHtlc(
+	resolution, err = registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	if event != nil {
+	if resolution != nil {
 		t.Fatalf("expected htlc to be held")
 	}
 
 	// Test replay at a higher height. We expect the same result because it
 	// is a replay.
-	event, err = registry.NotifyExitHopHtlc(
+	resolution, err = registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	if event != nil {
+	if resolution != nil {
 		t.Fatalf("expected htlc to be held")
 	}
 
 	// Test a new htlc coming in that doesn't meet the final cltv delta
 	// requirement. It should be rejected.
-	event, err = registry.NotifyExitHopHtlc(
+	resolution, err = registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, 1, testCurrentHeight,
 		getCircuitKey(1), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	if event == nil || event.Preimage != nil {
+	if resolution == nil || resolution.Preimage != nil {
 		t.Fatalf("expected htlc to be canceled")
 	}
+	if resolution.Outcome != ResultExpiryTooSoon {
+		t.Fatalf("expected expiry too soon, got: %v",
+			resolution.Outcome)
+	}
 
 	// We expect the accepted state to be sent to the single invoice
 	// subscriber. For all invoice subscribers, we don't expect an update.
@@ -421,13 +448,17 @@ func TestSettleHoldInvoice(t *testing.T) {
 		t.Fatal("expected set preimage to succeed")
 	}
 
-	hodlEvent := (<-hodlChan).(HodlEvent)
-	if *hodlEvent.Preimage != testInvoicePreimage {
-		t.Fatal("unexpected preimage in hodl event")
+	htlcResolution := (<-hodlChan).(HtlcResolution)
+	if *htlcResolution.Preimage != testInvoicePreimage {
+		t.Fatal("unexpected preimage in hodl resolution")
 	}
-	if hodlEvent.AcceptHeight != testCurrentHeight {
+	if htlcResolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, event.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
+	}
+	if htlcResolution.Outcome != ResultSettled {
+		t.Fatalf("expected result settled, got: %v",
+			htlcResolution.Outcome)
 	}
 
 	// We expect a settled notification to be sent out for both all and
@@ -496,14 +527,14 @@ func TestCancelHoldInvoice(t *testing.T) {
 
 	// NotifyExitHopHtlc without a preimage present in the invoice registry
 	// should be possible.
-	event, err := registry.NotifyExitHopHtlc(
+	resolution, err := registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	if event != nil {
+	if resolution != nil {
 		t.Fatalf("expected htlc to be held")
 	}
 
@@ -513,35 +544,39 @@ func TestCancelHoldInvoice(t *testing.T) {
 		t.Fatal("cancel invoice failed")
 	}
 
-	hodlEvent := (<-hodlChan).(HodlEvent)
-	if hodlEvent.Preimage != nil {
-		t.Fatal("expected cancel hodl event")
+	htlcResolution := (<-hodlChan).(HtlcResolution)
+	if htlcResolution.Preimage != nil {
+		t.Fatal("expected cancel htlc resolution")
 	}
 
 	// Offering the same htlc again at a higher height should still result
 	// in a rejection. The accept height is expected to be the original
 	// accept height.
-	event, err = registry.NotifyExitHopHtlc(
+	resolution, err = registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight+1,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
 	if err != nil {
 		t.Fatalf("expected settle to succeed but got %v", err)
 	}
-	if event.Preimage != nil {
+	if resolution.Preimage != nil {
 		t.Fatalf("expected htlc to be canceled")
 	}
-	if event.AcceptHeight != testCurrentHeight {
+	if resolution.AcceptHeight != testCurrentHeight {
 		t.Fatalf("expected acceptHeight %v, but got %v",
-			testCurrentHeight, event.AcceptHeight)
+			testCurrentHeight, resolution.AcceptHeight)
+	}
+	if resolution.Outcome != ResultReplayToCanceled {
+		t.Fatalf("expected replay to canceled, got %v",
+			resolution.Outcome)
 	}
 }
 
 // TestUnknownInvoice tests that invoice registry returns an error when the
-// invoice is unknown. This is to guard against returning a cancel hodl event
-// for forwarded htlcs. In the link, NotifyExitHopHtlc is only called if we are
-// the exit hop, but in htlcIncomingContestResolver it is called with forwarded
-// htlc hashes as well.
+// invoice is unknown. This is to guard against returning a cancel htlc
+// resolution for forwarded htlcs. In the link, NotifyExitHopHtlc is only called
+// if we are the exit hop, but in htlcIncomingContestResolver it is called with
+// forwarded htlc hashes as well.
 func TestUnknownInvoice(t *testing.T) {
 	ctx := newTestContext(t)
 	defer ctx.cleanup()
@@ -550,17 +585,23 @@ func TestUnknownInvoice(t *testing.T) {
 	// succeed.
 	hodlChan := make(chan interface{})
 	amt := lnwire.MilliSatoshi(100000)
-	_, err := ctx.registry.NotifyExitHopHtlc(
+	result, err := ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
 		getCircuitKey(0), hodlChan, testPayload,
 	)
-	if err != channeldb.ErrInvoiceNotFound {
-		t.Fatal("expected invoice not found error")
+	if err != nil {
+		t.Fatal("unexpected error")
+	}
+	if result.Outcome != ResultInvoiceNotFound {
+		t.Fatalf("expected ResultInvoiceNotFound, got: %v",
+			result.Outcome)
 	}
 }
 
-// TestSettleMpp tests settling of an invoice with multiple partial payments.
-func TestSettleMpp(t *testing.T) {
+// TestMppPayment tests settling of an invoice with multiple partial payments.
+// It covers the case where there is a mpp timeout before the whole invoice is
+// paid and the case where the invoice is settled in time.
+func TestMppPayment(t *testing.T) {
 	defer timeout()()
 
 	ctx := newTestContext(t)
@@ -578,7 +619,7 @@ func TestSettleMpp(t *testing.T) {
 
 	// Send htlc 1.
 	hodlChan1 := make(chan interface{}, 1)
-	event, err := ctx.registry.NotifyExitHopHtlc(
+	resolution, err := ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, testInvoice.Terms.Value/2,
 		testHtlcExpiry,
 		testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
@@ -586,21 +627,25 @@ func TestSettleMpp(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if event != nil {
+	if resolution != nil {
 		t.Fatal("expected no direct resolution")
 	}
 
 	// Simulate mpp timeout releasing htlc 1.
 	ctx.clock.SetTime(testTime.Add(30 * time.Second))
 
-	hodlEvent := (<-hodlChan1).(HodlEvent)
-	if hodlEvent.Preimage != nil {
-		t.Fatal("expected cancel event")
+	htlcResolution := (<-hodlChan1).(HtlcResolution)
+	if htlcResolution.Preimage != nil {
+		t.Fatal("expected cancel resolution")
+	}
+	if htlcResolution.Outcome != ResultMppTimeout {
+		t.Fatalf("expected mpp timeout, got: %v",
+			htlcResolution.Outcome)
 	}
 
 	// Send htlc 2.
 	hodlChan2 := make(chan interface{}, 1)
-	event, err = ctx.registry.NotifyExitHopHtlc(
+	resolution, err = ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, testInvoice.Terms.Value/2,
 		testHtlcExpiry,
 		testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
@@ -608,13 +653,13 @@ func TestSettleMpp(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if event != nil {
+	if resolution != nil {
 		t.Fatal("expected no direct resolution")
 	}
 
 	// Send htlc 3.
 	hodlChan3 := make(chan interface{}, 1)
-	event, err = ctx.registry.NotifyExitHopHtlc(
+	resolution, err = ctx.registry.NotifyExitHopHtlc(
 		testInvoicePaymentHash, testInvoice.Terms.Value/2,
 		testHtlcExpiry,
 		testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
@@ -622,12 +667,16 @@ func TestSettleMpp(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if event == nil {
-		t.Fatal("expected a settle event")
+	if resolution == nil {
+		t.Fatal("expected a settle resolution")
+	}
+	if resolution.Outcome != ResultSettled {
+		t.Fatalf("expected result settled, got: %v",
+			resolution.Outcome)
 	}
 
 	// Check that settled amount is equal to the sum of values of the htlcs
-	// 0 and 1.
+	// 2 and 3.
 	inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
 	if err != nil {
 		t.Fatal(err)
diff --git a/lnwire/onion_error_test.go b/lnwire/onion_error_test.go
index 5dafceddd..3ec147d1d 100644
--- a/lnwire/onion_error_test.go
+++ b/lnwire/onion_error_test.go
@@ -38,6 +38,7 @@ var onionFailures = []FailureMessage{
 	&FailUnknownNextPeer{},
 	&FailIncorrectPaymentAmount{},
 	&FailFinalExpiryTooSoon{},
+	&FailMPPTimeout{},
 
 	NewFailIncorrectDetails(99, 100),
 	NewInvalidOnionVersion(testOnionHash),
diff --git a/routing/missioncontrol_store_test.go b/routing/missioncontrol_store_test.go
index 9d070f4de..e44c59197 100644
--- a/routing/missioncontrol_store_test.go
+++ b/routing/missioncontrol_store_test.go
@@ -16,8 +16,12 @@ import (
 
 const testMaxRecords = 2
 
+// TestMissionControlStore tests the recording of payment failure events
+// in mission control. It tests encoding and decoding of differing lnwire
+// failures (FailIncorrectDetails and FailMppTimeout), pruning of results
+// and idempotent writes.
 func TestMissionControlStore(t *testing.T) {
-	// Set time zone explictly to keep test deterministic.
+	// Set time zone explicitly to keep test deterministic.
 	time.Local = time.UTC
 
 	file, err := ioutil.TempFile("", "*.db")
@@ -115,11 +119,12 @@ func TestMissionControlStore(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	// Add a newer result.
+	// Add a newer result which failed due to mpp timeout.
 	result3 := result1
 	result3.timeReply = result1.timeReply.Add(2 * time.Hour)
 	result3.timeFwd = result1.timeReply.Add(2 * time.Hour)
 	result3.id = 3
+	result3.failure = &lnwire.FailMPPTimeout{}
 
 	err = store.AddResult(&result3)
 	if err != nil {
diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go
index 42f3ec879..a5ed79895 100644
--- a/routing/result_interpretation_test.go
+++ b/routing/result_interpretation_test.go
@@ -272,6 +272,39 @@ var resultTestCases = []resultTestCase{
 			nodeFailure:        &hops[1],
 		},
 	},
+
+	// Tests a single hop mpp timeout. Test that final node is not
+	// penalized. This is a temporary measure while we decide how to
+	// penalize mpp timeouts.
+	{
+		name:          "one hop mpp timeout",
+		route:         &routeOneHop,
+		failureSrcIdx: 1,
+		failure:       &lnwire.FailMPPTimeout{},
+
+		expectedResult: &interpretedResult{
+			finalFailureReason: &reasonError,
+			nodeFailure:        nil,
+		},
+	},
+
+	// Tests a two hop mpp timeout. Test that final node is not penalized
+	// and the intermediate hop is attributed the success. This is a
+	// temporary measure while we decide how to penalize mpp timeouts.
+	{
+		name:          "two hop mpp timeout",
+		route:         &routeTwoHop,
+		failureSrcIdx: 2,
+		failure:       &lnwire.FailMPPTimeout{},
+
+		expectedResult: &interpretedResult{
+			pairResults: map[DirectedNodePair]pairResult{
+				getTestPair(0, 1): successPairResult(100),
+			},
+			finalFailureReason: &reasonError,
+			nodeFailure:        nil,
+		},
+	},
 }
 
 // TestResultInterpretation executes a list of test cases that test the result