Merge pull request #3844 from carlaKC/htlcnotifier-1-detailedswitcherrors

invoiceregistry+htlcswitch: Introduce resolution types and add link errors
This commit is contained in:
Carla Kirk-Cohen 2020-02-11 09:13:53 +02:00 committed by GitHub
commit 1a3f5f2d6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 861 additions and 513 deletions

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/btcsuite/btcutil"
@ -172,7 +173,23 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
processHtlcResolution := func(e invoices.HtlcResolution) (
ContractResolver, error) {
if e.Preimage == nil {
// 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:
log.Infof("%T(%v): Exit hop HTLC canceled "+
"(expiry=%v, height=%v), abandoning", h,
h.htlcResolution.ClaimOutpoint,
@ -180,13 +197,13 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
h.resolved = true
return nil, h.Checkpoint(h)
}
if err := applyPreimage(*e.Preimage); err != nil {
return nil, err
// Error if the resolution type is unknown, we are only
// expecting settles and fails.
default:
return nil, fmt.Errorf("unknown resolution"+
" type: %v", e)
}
return &h.htlcSuccessResolver, nil
}
// Create a buffered hodl chan to prevent deadlock.
@ -211,14 +228,29 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
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 {
// 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)
}
return processHtlcResolution(*resolution)
// 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)
}
// With the epochs and preimage subscriptions initialized, we'll query
@ -256,7 +288,6 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
case hodlItem := <-hodlChan:
htlcResolution := hodlItem.(invoices.HtlcResolution)
return processHtlcResolution(htlcResolution)
case newBlock, ok := <-blockEpochs.Epochs:

@ -35,7 +35,7 @@ func TestHtlcIncomingResolverFwdPreimageKnown(t *testing.T) {
defer timeout(t)()
ctx := newIncomingResolverTestContext(t)
ctx.registry.notifyResolution = invoices.NewFailureResolution(
ctx.registry.notifyResolution = invoices.NewFailResolution(
testResCircuitKey, testHtlcExpiry,
invoices.ResultInvoiceNotFound,
)
@ -52,7 +52,7 @@ func TestHtlcIncomingResolverFwdContestedSuccess(t *testing.T) {
defer timeout(t)()
ctx := newIncomingResolverTestContext(t)
ctx.registry.notifyResolution = invoices.NewFailureResolution(
ctx.registry.notifyResolution = invoices.NewFailResolution(
testResCircuitKey, testHtlcExpiry,
invoices.ResultInvoiceNotFound,
)
@ -72,7 +72,7 @@ func TestHtlcIncomingResolverFwdContestedTimeout(t *testing.T) {
defer timeout(t)()
ctx := newIncomingResolverTestContext(t)
ctx.registry.notifyResolution = invoices.NewFailureResolution(
ctx.registry.notifyResolution = invoices.NewFailResolution(
testResCircuitKey, testHtlcExpiry,
invoices.ResultInvoiceNotFound,
)
@ -91,7 +91,7 @@ func TestHtlcIncomingResolverFwdTimeout(t *testing.T) {
defer timeout(t)()
ctx := newIncomingResolverTestContext(t)
ctx.registry.notifyResolution = invoices.NewFailureResolution(
ctx.registry.notifyResolution = invoices.NewFailResolution(
testResCircuitKey, testHtlcExpiry,
invoices.ResultInvoiceNotFound,
)
@ -139,7 +139,7 @@ func TestHtlcIncomingResolverExitCancel(t *testing.T) {
defer timeout(t)()
ctx := newIncomingResolverTestContext(t)
ctx.registry.notifyResolution = invoices.NewFailureResolution(
ctx.registry.notifyResolution = invoices.NewFailResolution(
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.NewFailureResolution(
notifyData.hodlChan <- invoices.NewFailResolution(
testResCircuitKey, testAcceptHeight, invoices.ResultCanceled,
)

@ -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{})

@ -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,

@ -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 == FailureDetailNone {
if l.FailureDetail == nil {
return l.msg.Error()
}
return fmt.Sprintf("%v: %v", l.msg.Error(), l.FailureDetail)
return l.FailureDetail.FailureString()
}
// ForwardingError wraps an lnwire.FailureMessage in a struct that also

@ -1,65 +1,96 @@
package htlcswitch
// FailureDetail is an enum which is used to enrich failures with
// additional information.
type FailureDetail int
// 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
const (
// FailureDetailNone is returned when the wire message contains
// OutgoingFailureNone is returned when the wire message contains
// sufficient information.
FailureDetailNone = iota
OutgoingFailureNone OutgoingFailure = iota
// FailureDetailOnionDecode indicates that we could not decode an
// onion error.
FailureDetailOnionDecode
// OutgoingFailureDecodeError indicates that we could not decode the
// failure reason provided for a failed payment.
OutgoingFailureDecodeError
// FailureDetailLinkNotEligible indicates that a routing attempt was
// OutgoingFailureLinkNotEligible indicates that a routing attempt was
// made over a link that is not eligible for routing.
FailureDetailLinkNotEligible
OutgoingFailureLinkNotEligible
// 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
// 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
// FailureDetailHTLCExceedsMax is returned when a htlc exceeds our
// OutgoingFailureHTLCExceedsMax is returned when a htlc exceeds our
// policy's maximum htlc amount.
FailureDetailHTLCExceedsMax
OutgoingFailureHTLCExceedsMax
// FailureDetailInsufficientBalance is returned when we cannot route a
// OutgoingFailureInsufficientBalance is returned when we cannot route a
// htlc due to insufficient outgoing capacity.
FailureDetailInsufficientBalance
OutgoingFailureInsufficientBalance
// FailureDetailCircularRoute is returned when an attempt is made
// OutgoingFailureCircularRoute is returned when an attempt is made
// to forward a htlc through our node which arrives and leaves on the
// same channel.
FailureDetailCircularRoute
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
)
// String returns the string representation of a failure detail.
func (fd FailureDetail) String() string {
// FailureString returns the string representation of a failure detail.
//
// Note: it is part of the FailureDetail interface.
func (fd OutgoingFailure) FailureString() string {
switch fd {
case FailureDetailNone:
case OutgoingFailureNone:
return "no failure detail"
case FailureDetailOnionDecode:
return "could not decode onion"
case OutgoingFailureDecodeError:
return "could not decode wire failure"
case FailureDetailLinkNotEligible:
case OutgoingFailureLinkNotEligible:
return "link not eligible"
case FailureDetailOnChainTimeout:
case OutgoingFailureOnChainTimeout:
return "payment was resolved on-chain, then canceled back"
case FailureDetailHTLCExceedsMax:
case OutgoingFailureHTLCExceedsMax:
return "htlc exceeds maximum policy amount"
case FailureDetailInsufficientBalance:
case OutgoingFailureInsufficientBalance:
return "insufficient bandwidth to route htlc"
case FailureDetailCircularRoute:
case OutgoingFailureCircularRoute:
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"
}

@ -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.

@ -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,49 +1193,68 @@ loop:
func (l *channelLink) processHtlcResolution(resolution invoices.HtlcResolution,
htlc hodlHtlc) error {
circuitKey := resolution.CircuitKey
circuitKey := resolution.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)
// 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)
return l.settleHTLC(
*resolution.Preimage, htlc.pd.HtlcIndex,
res.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.sendHTLCError(
htlc.pd.HtlcIndex, failure,
htlc.obfuscator, htlc.pd.SourceRef,
)
return nil
// 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.log.Debugf("received cancel resolution for %v with outcome: %v",
circuitKey, resolution.Outcome)
// Get the lnwire failure message based on the resolution result.
failure := getResolutionFailure(resolution, htlc.pd.Amount)
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.HtlcResolution,
amount lnwire.MilliSatoshi) lnwire.FailureMessage {
func getResolutionFailure(resolution *invoices.HtlcFailResolution,
amount lnwire.MilliSatoshi) *LinkError {
// 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 &lnwire.FailMPPTimeout{}
return NewDetailedLinkError(
&lnwire.FailMPPTimeout{}, resolution.Outcome,
)
}
// 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(
// 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(
amount, uint32(resolution.AcceptHeight),
)
return NewDetailedLinkError(incorrectDetails, resolution.Outcome)
}
// randomFeeUpdateTimeout returns a random timeout between the bounds defined
@ -1299,6 +1318,10 @@ 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(
@ -1307,28 +1330,43 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
},
)
// Encrypt the error back to the source unless
// the payment was generated locally.
// If the payment was locally initiated (which
// is indicated by a nil obfuscator), we do
// not need to encrypt it back to the sender.
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,
@ -1336,6 +1374,7 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
sourceRef: pkt.sourceRef,
hasSource: true,
localFailure: localFailure,
linkFailure: linkError,
htlc: &lnwire.UpdateFailHTLC{
Reason: reason,
},
@ -2269,7 +2308,7 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
return lnwire.NewTemporaryChannelFailure(upd)
},
)
return NewDetailedLinkError(failure, FailureDetailHTLCExceedsMax)
return NewDetailedLinkError(failure, OutgoingFailureHTLCExceedsMax)
}
// We want to avoid offering an HTLC which will expire in the near
@ -2304,7 +2343,7 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
},
)
return NewDetailedLinkError(
failure, FailureDetailInsufficientBalance,
failure, OutgoingFailureInsufficientBalance,
)
}
@ -2446,7 +2485,9 @@ func (l *channelLink) processRemoteSettleFails(fwdPkg *channeldb.FwdPkg,
}
// Fetch the reason the HTLC was canceled so we can
// continue to propagate it.
// continue to propagate it. This failure originated
// from another node, so the linkFailure field is not
// set on the packet.
failPacket := &htlcPacket{
outgoingChanID: l.ShortChanID(),
outgoingHTLCID: pd.ParentIndex,
@ -2613,9 +2654,10 @@ 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,
lnwire.NewInvalidOnionPayload(failedType, 0),
NewLinkError(failure),
obfuscator, pd.SourceRef,
)
@ -2729,7 +2771,10 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
)
l.sendHTLCError(
pd.HtlcIndex, failure, obfuscator, pd.SourceRef,
pd.HtlcIndex,
NewLinkError(failure),
obfuscator,
pd.SourceRef,
)
continue
}
@ -2812,7 +2857,9 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
"value: expected %v, got %v", pd.RHash,
pd.Amount, fwdInfo.AmountToForward)
failure := lnwire.NewFinalIncorrectHtlcAmount(pd.Amount)
failure := NewLinkError(
lnwire.NewFinalIncorrectHtlcAmount(pd.Amount),
)
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
return nil
@ -2825,7 +2872,9 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
"time-lock: expected %v, got %v",
pd.RHash[:], pd.Timeout, fwdInfo.OutgoingCTLV)
failure := lnwire.NewFinalIncorrectCltvExpiry(pd.Timeout)
failure := NewLinkError(
lnwire.NewFinalIncorrectCltvExpiry(pd.Timeout),
)
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
return nil
@ -2863,7 +2912,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.
@ -2942,10 +2991,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 lnwire.FailureMessage,
func (l *channelLink) sendHTLCError(htlcIndex uint64, failure *LinkError,
e hop.ErrorEncrypter, sourceRef *channeldb.AddRef) {
reason, err := e.EncryptFirstHop(failure)
reason, err := e.EncryptFirstHop(failure.WireMessage())
if err != nil {
l.log.Errorf("unable to obfuscate error: %v", err)
return

@ -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,

@ -54,6 +54,11 @@ 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

@ -45,11 +45,6 @@ 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.
@ -496,9 +491,12 @@ func (s *Switch) forward(packet *htlcPacket) error {
} else {
failure = lnwire.NewTemporaryChannelFailure(update)
}
addErr := ErrIncompleteForward
return s.failAddPacket(packet, failure, addErr)
linkError := NewDetailedLinkError(
failure, OutgoingFailureIncompleteForward,
)
return s.failAddPacket(packet, linkError)
}
packet.circuit = circuit
@ -647,14 +645,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, failure, addErr)
_ = s.failAddPacket(packet, linkError)
}
}
@ -771,7 +769,7 @@ func (s *Switch) handleLocalDispatch(pkt *htlcPacket) error {
// will be returned back to the router.
return NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailLinkNotEligible,
OutgoingFailureLinkNotEligible,
)
}
@ -919,12 +917,12 @@ func (s *Switch) parseFailedPayment(deobfuscator ErrorDecrypter,
// need to apply an update here since it goes
// directly to the router.
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailOnionDecode,
OutgoingFailureDecodeError,
)
log.Errorf("%v: (hash=%v, pid=%d): %v",
linkError.FailureDetail, paymentHash, paymentID,
err)
linkError.FailureDetail.FailureString(),
paymentHash, paymentID, err)
return linkError
}
@ -939,10 +937,11 @@ func (s *Switch) parseFailedPayment(deobfuscator ErrorDecrypter,
case isResolution && htlc.Reason == nil:
linkError := NewDetailedLinkError(
&lnwire.FailPermanentChannelFailure{},
FailureDetailOnChainTimeout,
OutgoingFailureOnChainTimeout,
)
log.Info("%v: hash=%v, pid=%d", linkError.FailureDetail,
log.Info("%v: hash=%v, pid=%d",
linkError.FailureDetail.FailureString(),
paymentHash, paymentID)
return linkError
@ -978,10 +977,12 @@ 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 := &lnwire.FailChannelDisabled{}
addErr := fmt.Errorf("unable to forward any htlcs")
failure := NewDetailedLinkError(
&lnwire.FailChannelDisabled{},
OutgoingFailureForwardsDisabled,
)
return s.failAddPacket(packet, failure, addErr)
return s.failAddPacket(packet, failure)
}
if packet.incomingChanID == hop.Source {
@ -1001,9 +1002,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
s.cfg.AllowCircularRoute, htlc.PaymentHash,
)
if linkErr != nil {
return s.failAddPacket(
packet, linkErr.WireMessage(), linkErr,
)
return s.failAddPacket(packet, linkErr)
}
s.indexMtx.RLock()
@ -1011,14 +1010,17 @@ 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.
failure := &lnwire.FailUnknownNextPeer{}
addErr := fmt.Errorf("unable to find link with "+
"destination %v", packet.outgoingChanID)
linkError := NewLinkError(
&lnwire.FailUnknownNextPeer{},
)
return s.failAddPacket(packet, failure, addErr)
return s.failAddPacket(packet, linkError)
}
targetPeerKey := targetLink.Peer().PubKey()
interfaceLinks, _ := s.getLinks(targetPeerKey)
@ -1041,7 +1043,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
if !link.EligibleToForward() {
failure = NewDetailedLinkError(
&lnwire.FailUnknownNextPeer{},
FailureDetailLinkNotEligible,
OutgoingFailureLinkNotEligible,
)
} else {
// We'll ensure that the HTLC satisfies the
@ -1087,12 +1089,12 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
}))
}
addErr := fmt.Errorf("incoming HTLC(%x) violated "+
log.Tracef("incoming HTLC(%x) violated "+
"target outgoing link (id=%v) policy: %v",
htlc.PaymentHash[:], packet.outgoingChanID,
linkErr)
return s.failAddPacket(packet, linkErr.WireMessage(), addErr)
return s.failAddPacket(packet, linkErr)
}
// Send the packet to the destination channel link which
@ -1217,20 +1219,18 @@ func checkCircularForward(incoming, outgoing lnwire.ShortChannelID,
// node, so we do not include a channel update.
return NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailCircularRoute,
OutgoingFailureCircularRoute,
)
}
// 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 lnwire.FailureMessage, failErr error) error {
func (s *Switch) failAddPacket(packet *htlcPacket, failure *LinkError) 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)
reason, err := packet.obfuscator.EncryptFirstHop(failure.WireMessage())
if err != nil {
err := fmt.Errorf("unable to obfuscate "+
"error: %v", err)
@ -1238,13 +1238,14 @@ func (s *Switch) failAddPacket(packet *htlcPacket,
return err
}
log.Error(failErr)
log.Error(failure.Error())
failPkt := &htlcPacket{
sourceRef: packet.sourceRef,
incomingChanID: packet.incomingChanID,
incomingHTLCID: packet.incomingHTLCID,
circuit: packet.circuit,
linkFailure: failure,
htlc: &lnwire.UpdateFailHTLC{
Reason: reason,
},
@ -1260,7 +1261,7 @@ func (s *Switch) failAddPacket(packet *htlcPacket,
return err
}
return failErr
return failure
}
// closeCircuit accepts a settle or fail htlc and the associated htlc packet and
@ -1868,8 +1869,11 @@ 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.
// 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.
failPacket := &htlcPacket{
outgoingChanID: fwdPkg.Source,
outgoingHTLCID: pd.ParentIndex,

@ -211,10 +211,13 @@ 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)
expErr := fmt.Sprintf("unable to find link with destination %v",
aliceChanID)
if err != nil && err.Error() != expErr {
t.Fatalf("expected forward failure: %v", err)
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())
}
// No message should be sent, since the packet was failed.
@ -1070,9 +1073,13 @@ 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)
if err != ErrIncompleteForward {
t.Fatal("unexpected error when reforwarding a "+
"failed packet", err)
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)
}
// After detecting an incomplete forward, the fail packet should have
@ -1348,7 +1355,7 @@ func TestCircularForwards(t *testing.T) {
allowCircularPayment: false,
expectedErr: NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailCircularRoute,
OutgoingFailureCircularRoute,
),
},
}
@ -1465,7 +1472,7 @@ func TestCheckCircularForward(t *testing.T) {
outgoingLink: lnwire.NewShortChanIDFromInt(123),
expectedErr: NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailCircularRoute,
OutgoingFailureCircularRoute,
),
},
}
@ -1527,7 +1534,7 @@ func TestSkipIneligibleLinksMultiHopForward(t *testing.T) {
eligible1: true,
failure1: NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailInsufficientBalance,
OutgoingFailureInsufficientBalance,
),
eligible2: true,
failure2: NewLinkError(

@ -36,48 +36,6 @@ 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
@ -607,7 +565,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 ResolutionResult) error {
key channeldb.CircuitKey, result FailResolutionResult) error {
i.Lock()
defer i.Unlock()
@ -683,7 +641,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
return fmt.Errorf("htlc %v not found", key)
}
if htlc.State == channeldb.HtlcStateCanceled {
resolution := *NewFailureResolution(
resolution := NewFailResolution(
key, int32(htlc.AcceptHeight), result,
)
@ -777,7 +735,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()
@ -802,7 +760,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
if err != nil {
updateCtx.log(fmt.Sprintf("keysend error: %v", err))
return NewFailureResolution(
return NewFailResolution(
circuitKey, currentHeight, ResultKeySendError,
), nil
}
@ -817,15 +775,10 @@ 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 *acceptResolution:
case *htlcAcceptResolution:
if r.autoRelease {
err := i.startHtlcTimer(rHash, circuitKey, r.acceptTime)
if err != nil {
@ -833,33 +786,31 @@ 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{}) (
interface{}, error) {
HtlcResolution, 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 (
result ResolutionResult
resolution HtlcResolution
updateSubscribers bool
)
invoice, err := i.cdb.UpdateInvoice(
@ -876,8 +827,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
updateSubscribers = updateDesc != nil &&
updateDesc.State != nil
// Assign result to outer scope variable.
result = res
// Assign resolution to outer scope variable.
resolution = res
return updateDesc, nil
},
@ -886,7 +837,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 NewFailureResolution(
return NewFailResolution(
ctx.circuitKey, ctx.currentHeight,
ResultInvoiceNotFound,
), nil
@ -898,38 +849,40 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
return nil, err
}
ctx.log(result.String())
if updateSubscribers {
i.notifyClients(ctx.hash, invoice, invoice.State)
}
// Inspect latest htlc state on the invoice.
invoiceHtlc, ok := invoice.Htlcs[ctx.circuitKey]
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)
}
// If it isn't recorded, cancel htlc.
if !ok {
return NewFailureResolution(
ctx.circuitKey, ctx.currentHeight, result,
), nil
}
ctx.log(fmt.Sprintf("failure resolution result "+
"outcome: %v, at accept height: %v",
res.Outcome, res.AcceptHeight))
// 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)
return res, nil
switch invoiceHtlc.State {
case channeldb.HtlcStateCanceled:
return NewFailureResolution(
ctx.circuitKey, acceptHeight, result,
), nil
// 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))
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.
// Also settle any previously accepted htlcs. If a 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
@ -940,34 +893,49 @@ 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.
resolution := *NewSettleResolution(
invoice.Terms.PaymentPreimage, key,
acceptHeight, result,
htlcSettleResolution := NewSettleResolution(
res.Preimage, key,
int32(htlc.AcceptHeight), res.Outcome,
)
i.notifyHodlSubscribers(resolution)
// Notify subscribers that the htlc should be settled
// with our peer.
i.notifyHodlSubscribers(htlcSettleResolution)
}
resolution := NewSettleResolution(
invoice.Terms.PaymentPreimage, ctx.circuitKey,
acceptHeight, result,
)
return resolution, nil
case channeldb.HtlcStateAccepted:
var resolution acceptResolution
// 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))
// 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 {
resolution.acceptTime = invoiceHtlc.AcceptTime
resolution.autoRelease = true
res.acceptTime = invoiceHtlc.AcceptTime
res.autoRelease = true
}
i.hodlSubscribe(hodlChan, ctx.circuitKey)
return &resolution, nil
return res, nil
default:
panic("unknown action")
@ -1022,7 +990,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
continue
}
resolution := *NewSettleResolution(
resolution := NewSettleResolution(
preimage, key, int32(htlc.AcceptHeight), ResultSettled,
)
@ -1102,7 +1070,7 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
}
i.notifyHodlSubscribers(
*NewFailureResolution(
NewFailResolution(
key, int32(htlc.AcceptHeight), ResultCanceled,
),
)
@ -1374,7 +1342,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
}
@ -1391,11 +1359,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.

@ -74,29 +74,38 @@ func TestSettleInvoice(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if resolution.Preimage != nil {
t.Fatal("expected cancel resolution")
failResolution, ok := resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if resolution.AcceptHeight != testCurrentHeight {
if failResolution.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, resolution.AcceptHeight)
testCurrentHeight, failResolution.AcceptHeight)
}
if resolution.Outcome != ResultExpiryTooSoon {
if failResolution.Outcome != ResultExpiryTooSoon {
t.Fatalf("expected expiry too soon, got: %v",
resolution.Outcome)
failResolution.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)
}
if resolution.Outcome != ResultSettled {
t.Fatalf("expected settled, got: %v", resolution.Outcome)
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)
}
// We expect the settled state to be sent to the single invoice
@ -134,12 +143,14 @@ func TestSettleInvoice(t *testing.T) {
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if resolution.Preimage == nil {
t.Fatal("expected settle resolution")
settleResolution, ok = resolution.(*HtlcSettleResolution)
if !ok {
t.Fatalf("expected settle resolution, got: %T",
resolution)
}
if resolution.Outcome != ResultReplayToSettled {
if settleResolution.Outcome != ResultReplayToSettled {
t.Fatalf("expected replay settled, got: %v",
resolution.Outcome)
settleResolution.Outcome)
}
// Try to settle again with a new higher-valued htlc. This payment
@ -152,12 +163,14 @@ func TestSettleInvoice(t *testing.T) {
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if resolution.Preimage == nil {
t.Fatal("expected settle resolution")
settleResolution, ok = resolution.(*HtlcSettleResolution)
if !ok {
t.Fatalf("expected settle resolution, got: %T",
resolution)
}
if resolution.Outcome != ResultDuplicateToSettled {
if settleResolution.Outcome != ResultDuplicateToSettled {
t.Fatalf("expected duplicate settled, got: %v",
resolution.Outcome)
settleResolution.Outcome)
}
// Try to settle again with a lower amount. This should fail just as it
@ -169,12 +182,14 @@ func TestSettleInvoice(t *testing.T) {
if err != nil {
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
}
if resolution.Preimage != nil {
t.Fatal("expected cancel resolution")
failResolution, ok = resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if resolution.Outcome != ResultAmountTooLow {
if failResolution.Outcome != ResultAmountTooLow {
t.Fatalf("expected amount too low, got: %v",
resolution.Outcome)
failResolution.Outcome)
}
// Check that settled amount is equal to the sum of values of the htlcs
@ -298,17 +313,18 @@ func TestCancelInvoice(t *testing.T) {
if err != nil {
t.Fatal("expected settlement of a canceled invoice to succeed")
}
if resolution.Preimage != nil {
t.Fatal("expected cancel htlc resolution")
failResolution, ok := resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if resolution.AcceptHeight != testCurrentHeight {
if failResolution.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, resolution.AcceptHeight)
testCurrentHeight, failResolution.AcceptHeight)
}
if resolution.Outcome != ResultInvoiceAlreadyCanceled {
t.Fatalf("expected invoice already canceled, got: %v",
resolution.Outcome)
if failResolution.Outcome != ResultInvoiceAlreadyCanceled {
t.Fatalf("expected expiry too soon, got: %v",
failResolution.Outcome)
}
}
@ -422,12 +438,14 @@ func TestSettleHoldInvoice(t *testing.T) {
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if resolution == nil || resolution.Preimage != nil {
t.Fatalf("expected htlc to be canceled")
failResolution, ok := resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if resolution.Outcome != ResultExpiryTooSoon {
if failResolution.Outcome != ResultExpiryTooSoon {
t.Fatalf("expected expiry too soon, got: %v",
resolution.Outcome)
failResolution.Outcome)
}
// We expect the accepted state to be sent to the single invoice
@ -449,16 +467,21 @@ func TestSettleHoldInvoice(t *testing.T) {
}
htlcResolution := (<-hodlChan).(HtlcResolution)
if *htlcResolution.Preimage != testInvoicePreimage {
settleResolution, ok := htlcResolution.(*HtlcSettleResolution)
if !ok {
t.Fatalf("expected settle resolution, got: %T",
htlcResolution)
}
if settleResolution.Preimage != testInvoicePreimage {
t.Fatal("unexpected preimage in hodl resolution")
}
if htlcResolution.AcceptHeight != testCurrentHeight {
if settleResolution.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, resolution.AcceptHeight)
testCurrentHeight, settleResolution.AcceptHeight)
}
if htlcResolution.Outcome != ResultSettled {
if settleResolution.Outcome != ResultSettled {
t.Fatalf("expected result settled, got: %v",
htlcResolution.Outcome)
settleResolution.Outcome)
}
// We expect a settled notification to be sent out for both all and
@ -545,8 +568,10 @@ func TestCancelHoldInvoice(t *testing.T) {
}
htlcResolution := (<-hodlChan).(HtlcResolution)
if htlcResolution.Preimage != nil {
t.Fatal("expected cancel htlc resolution")
_, ok := htlcResolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
htlcResolution)
}
// Offering the same htlc again at a higher height should still result
@ -559,16 +584,18 @@ func TestCancelHoldInvoice(t *testing.T) {
if err != nil {
t.Fatalf("expected settle to succeed but got %v", err)
}
if resolution.Preimage != nil {
t.Fatalf("expected htlc to be canceled")
failResolution, ok := resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if resolution.AcceptHeight != testCurrentHeight {
if failResolution.AcceptHeight != testCurrentHeight {
t.Fatalf("expected acceptHeight %v, but got %v",
testCurrentHeight, resolution.AcceptHeight)
testCurrentHeight, failResolution.AcceptHeight)
}
if resolution.Outcome != ResultReplayToCanceled {
if failResolution.Outcome != ResultReplayToCanceled {
t.Fatalf("expected replay to canceled, got %v",
resolution.Outcome)
failResolution.Outcome)
}
}
@ -585,16 +612,21 @@ func TestUnknownInvoice(t *testing.T) {
// succeed.
hodlChan := make(chan interface{})
amt := lnwire.MilliSatoshi(100000)
result, err := ctx.registry.NotifyExitHopHtlc(
resolution, err := ctx.registry.NotifyExitHopHtlc(
testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
getCircuitKey(0), hodlChan, testPayload,
)
if err != nil {
t.Fatal("unexpected error")
}
if result.Outcome != ResultInvoiceNotFound {
failResolution, ok := resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if failResolution.Outcome != ResultInvoiceNotFound {
t.Fatalf("expected ResultInvoiceNotFound, got: %v",
result.Outcome)
failResolution.Outcome)
}
}
@ -646,16 +678,17 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
if err != nil {
t.Fatal(err)
}
// Expect a cancel resolution with the correct outcome.
if resolution.Preimage != nil {
t.Fatal("expected cancel resolution")
failResolution, ok := resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
switch {
case !keySendEnabled && resolution.Outcome != ResultInvoiceNotFound:
case !keySendEnabled && failResolution.Outcome != ResultInvoiceNotFound:
t.Fatal("expected invoice not found outcome")
case keySendEnabled && resolution.Outcome != ResultKeySendError:
case keySendEnabled && failResolution.Outcome != ResultKeySendError:
t.Fatal("expected keysend error")
}
@ -676,15 +709,25 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
// Expect a cancel resolution if keysend is disabled.
if !keySendEnabled {
if resolution.Outcome != ResultInvoiceNotFound {
failResolution, ok = resolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if failResolution.Outcome != ResultInvoiceNotFound {
t.Fatal("expected keysend payment not to be accepted")
}
return
}
// Otherwise we expect no error and a settle resolution for the htlc.
if resolution.Preimage == nil || *resolution.Preimage != preimage {
t.Fatal("expected valid settle event")
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")
}
// We expect a new invoice notification to be sent out.
@ -739,12 +782,14 @@ func TestMppPayment(t *testing.T) {
ctx.clock.SetTime(testTime.Add(30 * time.Second))
htlcResolution := (<-hodlChan1).(HtlcResolution)
if htlcResolution.Preimage != nil {
t.Fatal("expected cancel resolution")
failResolution, ok := htlcResolution.(*HtlcFailResolution)
if !ok {
t.Fatalf("expected fail resolution, got: %T",
resolution)
}
if htlcResolution.Outcome != ResultMppTimeout {
if failResolution.Outcome != ResultMppTimeout {
t.Fatalf("expected mpp timeout, got: %v",
htlcResolution.Outcome)
failResolution.Outcome)
}
// Send htlc 2.
@ -771,12 +816,14 @@ func TestMppPayment(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if resolution == nil {
t.Fatal("expected a settle resolution")
settleResolution, ok := resolution.(*HtlcSettleResolution)
if !ok {
t.Fatalf("expected settle resolution, got: %T",
htlcResolution)
}
if resolution.Outcome != ResultSettled {
if settleResolution.Outcome != ResultSettled {
t.Fatalf("expected result settled, got: %v",
resolution.Outcome)
settleResolution.Outcome)
}
// Check that settled amount is equal to the sum of values of the htlcs

125
invoices/resolution.go Normal file

@ -0,0 +1,125 @@
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
}

@ -0,0 +1,202 @@
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"
}
}

@ -9,164 +9,6 @@ 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 {
@ -186,26 +28,55 @@ 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.
// settlement logic. It returns a hltc resolution that indicates what the
// outcome of the update was.
func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
*channeldb.InvoiceUpdateDesc, HtlcResolution, 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, ctx.failRes(ResultReplayToCanceled), nil
case channeldb.HtlcStateAccepted:
return nil, ResultReplayToAccepted, nil
return nil, ctx.acceptRes(resultReplayToAccepted), nil
case channeldb.HtlcStateSettled:
return nil, ResultReplayToSettled, nil
return nil, ctx.settleRes(
inv.Terms.PaymentPreimage,
ResultReplayToSettled,
), nil
default:
return nil, 0, errors.New("unknown htlc state")
return nil, nil, errors.New("unknown htlc state")
}
}
@ -218,8 +89,9 @@ 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, ResolutionResult, error) {
func updateMpp(ctx *invoiceUpdateCtx,
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
HtlcResolution, error) {
// Start building the accept descriptor.
acceptDesc := &channeldb.HtlcAcceptDesc{
@ -235,23 +107,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, ctx.failRes(ResultInvoiceNotOpen), nil
}
// Check the payment address that authorizes the payment.
if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
return nil, ResultAddressMismatch, nil
return nil, ctx.failRes(ResultAddressMismatch), nil
}
// Don't accept zero-valued sets.
if ctx.mpp.TotalMsat() == 0 {
return nil, ResultHtlcSetTotalTooLow, nil
return nil, ctx.failRes(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, ctx.failRes(ResultHtlcSetTotalTooLow), nil
}
// Check whether total amt matches other htlcs in the set.
@ -265,7 +137,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
}
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
return nil, ResultHtlcSetTotalMismatch, nil
return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil
}
newSetTotal += htlc.Amt
@ -276,16 +148,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, ctx.failRes(ResultHtlcSetOverpayment), nil
}
// The invoice is still open. Check the expiry.
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
return nil, ResultExpiryTooSoon, nil
return nil, ctx.failRes(ResultExpiryTooSoon), nil
}
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
return nil, ResultExpiryTooSoon, nil
return nil, ctx.failRes(ResultExpiryTooSoon), nil
}
// Record HTLC in the invoice database.
@ -300,7 +172,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, ctx.acceptRes(resultPartialAccepted), nil
}
// Check to see if we can settle or this is an hold invoice and
@ -310,7 +182,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
update.State = &channeldb.InvoiceStateUpdateDesc{
NewState: channeldb.ContractAccepted,
}
return &update, ResultAccepted, nil
return &update, ctx.acceptRes(resultAccepted), nil
}
update.State = &channeldb.InvoiceStateUpdateDesc{
@ -318,18 +190,20 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
Preimage: inv.Terms.PaymentPreimage,
}
return &update, ResultSettled, nil
return &update, ctx.settleRes(
inv.Terms.PaymentPreimage, 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, ResolutionResult, error) {
func updateLegacy(ctx *invoiceUpdateCtx,
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc, HtlcResolution, 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, ctx.failRes(ResultInvoiceAlreadyCanceled), nil
}
// If an invoice amount is specified, check that enough is paid. Also
@ -337,7 +211,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, ctx.failRes(ResultAmountTooLow), nil
}
// TODO(joostjager): Check invoice mpp required feature
@ -350,17 +224,17 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
if htlc.State == channeldb.HtlcStateAccepted &&
htlc.MppTotalAmt > 0 {
return nil, ResultMppInProgress, nil
return nil, ctx.failRes(ResultMppInProgress), nil
}
}
// The invoice is still open. Check the expiry.
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
return nil, ResultExpiryTooSoon, nil
return nil, ctx.failRes(ResultExpiryTooSoon), nil
}
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
return nil, ResultExpiryTooSoon, nil
return nil, ctx.failRes(ResultExpiryTooSoon), nil
}
// Record HTLC in the invoice database.
@ -381,10 +255,12 @@ 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, ctx.acceptRes(resultDuplicateToAccepted), nil
case channeldb.ContractSettled:
return &update, ResultDuplicateToSettled, nil
return &update, ctx.settleRes(
inv.Terms.PaymentPreimage, ResultDuplicateToSettled,
), nil
}
// Check to see if we can settle or this is an hold invoice and we need
@ -394,7 +270,8 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
update.State = &channeldb.InvoiceStateUpdateDesc{
NewState: channeldb.ContractAccepted,
}
return &update, ResultAccepted, nil
return &update, ctx.acceptRes(resultAccepted), nil
}
update.State = &channeldb.InvoiceStateUpdateDesc{
@ -402,5 +279,7 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
Preimage: inv.Terms.PaymentPreimage,
}
return &update, ResultSettled, nil
return &update, ctx.settleRes(
inv.Terms.PaymentPreimage, ResultSettled,
), nil
}