We now return an error when blinded and non blinded attempts are combined. This was theoretically possible to register a legacy attempt in combination with a blinded payment. This would have been prevented by other checks in the code because legacy payments are not split into shards.
830 lines
25 KiB
Go
830 lines
25 KiB
Go
package paymentsdb
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/davecgh/go-spew/spew"
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
)
|
|
|
|
// FailureReason encodes the reason a payment ultimately failed.
|
|
type FailureReason byte
|
|
|
|
const (
|
|
// FailureReasonTimeout indicates that the payment did timeout before a
|
|
// successful payment attempt was made.
|
|
FailureReasonTimeout FailureReason = 0
|
|
|
|
// FailureReasonNoRoute indicates no successful route to the
|
|
// destination was found during path finding.
|
|
FailureReasonNoRoute FailureReason = 1
|
|
|
|
// FailureReasonError indicates that an unexpected error happened during
|
|
// payment.
|
|
FailureReasonError FailureReason = 2
|
|
|
|
// FailureReasonPaymentDetails indicates that either the hash is unknown
|
|
// or the final cltv delta or amount is incorrect.
|
|
FailureReasonPaymentDetails FailureReason = 3
|
|
|
|
// FailureReasonInsufficientBalance indicates that we didn't have enough
|
|
// balance to complete the payment.
|
|
FailureReasonInsufficientBalance FailureReason = 4
|
|
|
|
// FailureReasonCanceled indicates that the payment was canceled by the
|
|
// user.
|
|
FailureReasonCanceled FailureReason = 5
|
|
|
|
// TODO(joostjager): Add failure reasons for:
|
|
// LocalLiquidityInsufficient, RemoteCapacityInsufficient.
|
|
)
|
|
|
|
// Error returns a human-readable error string for the FailureReason.
|
|
func (r FailureReason) Error() string {
|
|
return r.String()
|
|
}
|
|
|
|
// String returns a human-readable FailureReason.
|
|
func (r FailureReason) String() string {
|
|
switch r {
|
|
case FailureReasonTimeout:
|
|
return "timeout"
|
|
case FailureReasonNoRoute:
|
|
return "no_route"
|
|
case FailureReasonError:
|
|
return "error"
|
|
case FailureReasonPaymentDetails:
|
|
return "incorrect_payment_details"
|
|
case FailureReasonInsufficientBalance:
|
|
return "insufficient_balance"
|
|
case FailureReasonCanceled:
|
|
return "canceled"
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
// PaymentCreationInfo is the information necessary to have ready when
|
|
// initiating a payment, moving it into state InFlight.
|
|
type PaymentCreationInfo struct {
|
|
// PaymentIdentifier is the hash this payment is paying to in case of
|
|
// non-AMP payments, and the SetID for AMP payments.
|
|
PaymentIdentifier lntypes.Hash
|
|
|
|
// Value is the amount we are paying.
|
|
Value lnwire.MilliSatoshi
|
|
|
|
// CreationTime is the time when this payment was initiated.
|
|
CreationTime time.Time
|
|
|
|
// PaymentRequest is the full payment request, if any.
|
|
PaymentRequest []byte
|
|
|
|
// FirstHopCustomRecords are the TLV records that are to be sent to the
|
|
// first hop of this payment. These records will be transmitted via the
|
|
// wire message (UpdateAddHTLC) only and therefore do not affect the
|
|
// onion payload size.
|
|
FirstHopCustomRecords lnwire.CustomRecords
|
|
}
|
|
|
|
// String returns a human-readable description of the payment creation info.
|
|
func (p *PaymentCreationInfo) String() string {
|
|
return fmt.Sprintf("payment_id=%v, amount=%v, created_at=%v",
|
|
p.PaymentIdentifier, p.Value, p.CreationTime)
|
|
}
|
|
|
|
// HTLCAttemptInfo contains static information about a specific HTLC attempt
|
|
// for a payment. This information is used by the router to handle any errors
|
|
// coming back after an attempt is made, and to query the switch about the
|
|
// status of the attempt.
|
|
type HTLCAttemptInfo struct {
|
|
// AttemptID is the unique ID used for this attempt.
|
|
AttemptID uint64
|
|
|
|
// sessionKey is the raw bytes ephemeral key used for this attempt.
|
|
// These bytes are lazily read off disk to save ourselves the expensive
|
|
// EC operations used by btcec.PrivKeyFromBytes.
|
|
sessionKey [btcec.PrivKeyBytesLen]byte
|
|
|
|
// cachedSessionKey is our fully deserialized sesionKey. This value
|
|
// may be nil if the attempt has just been read from disk and its
|
|
// session key has not been used yet.
|
|
cachedSessionKey *btcec.PrivateKey
|
|
|
|
// Route is the route attempted to send the HTLC.
|
|
Route route.Route
|
|
|
|
// AttemptTime is the time at which this HTLC was attempted.
|
|
AttemptTime time.Time
|
|
|
|
// Hash is the hash used for this single HTLC attempt. For AMP payments
|
|
// this will differ across attempts, for non-AMP payments each attempt
|
|
// will use the same hash. This can be nil for older payment attempts,
|
|
// in which the payment's PaymentHash in the PaymentCreationInfo should
|
|
// be used.
|
|
Hash *lntypes.Hash
|
|
|
|
// onionBlob is the cached value for onion blob created from the sphinx
|
|
// construction.
|
|
onionBlob [lnwire.OnionPacketSize]byte
|
|
|
|
// circuit is the cached value for sphinx circuit.
|
|
circuit *sphinx.Circuit
|
|
}
|
|
|
|
// NewHtlcAttempt creates a htlc attempt.
|
|
func NewHtlcAttempt(attemptID uint64, sessionKey *btcec.PrivateKey,
|
|
route route.Route, attemptTime time.Time,
|
|
hash *lntypes.Hash) (*HTLCAttempt, error) {
|
|
|
|
var scratch [btcec.PrivKeyBytesLen]byte
|
|
copy(scratch[:], sessionKey.Serialize())
|
|
|
|
info := HTLCAttemptInfo{
|
|
AttemptID: attemptID,
|
|
sessionKey: scratch,
|
|
cachedSessionKey: sessionKey,
|
|
Route: route,
|
|
AttemptTime: attemptTime,
|
|
Hash: hash,
|
|
}
|
|
|
|
if err := info.attachOnionBlobAndCircuit(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &HTLCAttempt{HTLCAttemptInfo: info}, nil
|
|
}
|
|
|
|
// SessionKey returns the ephemeral key used for a htlc attempt. This function
|
|
// performs expensive ec-ops to obtain the session key if it is not cached.
|
|
func (h *HTLCAttemptInfo) SessionKey() *btcec.PrivateKey {
|
|
if h.cachedSessionKey == nil {
|
|
h.cachedSessionKey, _ = btcec.PrivKeyFromBytes(
|
|
h.sessionKey[:],
|
|
)
|
|
}
|
|
|
|
return h.cachedSessionKey
|
|
}
|
|
|
|
// setSessionKey sets the session key for the htlc attempt.
|
|
//
|
|
// NOTE: Only used for testing.
|
|
//
|
|
//nolint:unused
|
|
func (h *HTLCAttemptInfo) setSessionKey(sessionKey *btcec.PrivateKey) {
|
|
h.cachedSessionKey = sessionKey
|
|
|
|
// Also set the session key as a raw bytes.
|
|
var scratch [btcec.PrivKeyBytesLen]byte
|
|
copy(scratch[:], sessionKey.Serialize())
|
|
h.sessionKey = scratch
|
|
}
|
|
|
|
// OnionBlob returns the onion blob created from the sphinx construction.
|
|
func (h *HTLCAttemptInfo) OnionBlob() ([lnwire.OnionPacketSize]byte, error) {
|
|
var zeroBytes [lnwire.OnionPacketSize]byte
|
|
if h.onionBlob == zeroBytes {
|
|
if err := h.attachOnionBlobAndCircuit(); err != nil {
|
|
return zeroBytes, err
|
|
}
|
|
}
|
|
|
|
return h.onionBlob, nil
|
|
}
|
|
|
|
// Circuit returns the sphinx circuit for this attempt.
|
|
func (h *HTLCAttemptInfo) Circuit() (*sphinx.Circuit, error) {
|
|
if h.circuit == nil {
|
|
if err := h.attachOnionBlobAndCircuit(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return h.circuit, nil
|
|
}
|
|
|
|
// attachOnionBlobAndCircuit creates a sphinx packet and caches the onion blob
|
|
// and circuit for this attempt.
|
|
func (h *HTLCAttemptInfo) attachOnionBlobAndCircuit() error {
|
|
onionBlob, circuit, err := generateSphinxPacket(
|
|
&h.Route, h.Hash[:], h.SessionKey(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
copy(h.onionBlob[:], onionBlob)
|
|
h.circuit = circuit
|
|
|
|
return nil
|
|
}
|
|
|
|
// HTLCAttempt contains information about a specific HTLC attempt for a given
|
|
// payment. It contains the HTLCAttemptInfo used to send the HTLC, as well
|
|
// as a timestamp and any known outcome of the attempt.
|
|
type HTLCAttempt struct {
|
|
HTLCAttemptInfo
|
|
|
|
// Settle is the preimage of a successful payment. This serves as a
|
|
// proof of payment. It will only be non-nil for settled payments.
|
|
//
|
|
// NOTE: Can be nil if payment is not settled.
|
|
Settle *HTLCSettleInfo
|
|
|
|
// Fail is a failure reason code indicating the reason the payment
|
|
// failed. It is only non-nil for failed payments.
|
|
//
|
|
// NOTE: Can be nil if payment is not failed.
|
|
Failure *HTLCFailInfo
|
|
}
|
|
|
|
// HTLCSettleInfo encapsulates the information that augments an HTLCAttempt in
|
|
// the event that the HTLC is successful.
|
|
type HTLCSettleInfo struct {
|
|
// Preimage is the preimage of a successful HTLC. This serves as a proof
|
|
// of payment.
|
|
Preimage lntypes.Preimage
|
|
|
|
// SettleTime is the time at which this HTLC was settled.
|
|
SettleTime time.Time
|
|
}
|
|
|
|
// HTLCFailReason is the reason an htlc failed.
|
|
type HTLCFailReason byte
|
|
|
|
const (
|
|
// HTLCFailUnknown is recorded for htlcs that failed with an unknown
|
|
// reason.
|
|
HTLCFailUnknown HTLCFailReason = 0
|
|
|
|
// HTLCFailUnreadable is recorded for htlcs that had a failure message
|
|
// that couldn't be decrypted.
|
|
HTLCFailUnreadable HTLCFailReason = 1
|
|
|
|
// HTLCFailInternal is recorded for htlcs that failed because of an
|
|
// internal error.
|
|
HTLCFailInternal HTLCFailReason = 2
|
|
|
|
// HTLCFailMessage is recorded for htlcs that failed with a network
|
|
// failure message.
|
|
HTLCFailMessage HTLCFailReason = 3
|
|
)
|
|
|
|
// HTLCFailInfo encapsulates the information that augments an HTLCAttempt in the
|
|
// event that the HTLC fails.
|
|
type HTLCFailInfo struct {
|
|
// FailTime is the time at which this HTLC was failed.
|
|
FailTime time.Time
|
|
|
|
// Message is the wire message that failed this HTLC. This field will be
|
|
// populated when the failure reason is HTLCFailMessage.
|
|
Message lnwire.FailureMessage
|
|
|
|
// Reason is the failure reason for this HTLC.
|
|
Reason HTLCFailReason
|
|
|
|
// The position in the path of the intermediate or final node that
|
|
// generated the failure message. Position zero is the sender node. This
|
|
// field will be populated when the failure reason is either
|
|
// HTLCFailMessage or HTLCFailUnknown.
|
|
FailureSourceIndex uint32
|
|
}
|
|
|
|
// MPPaymentState wraps a series of info needed for a given payment, which is
|
|
// used by both MPP and AMP. This is a memory representation of the payment's
|
|
// current state and is updated whenever the payment is read from disk.
|
|
type MPPaymentState struct {
|
|
// NumAttemptsInFlight specifies the number of HTLCs the payment is
|
|
// waiting results for.
|
|
NumAttemptsInFlight int
|
|
|
|
// RemainingAmt specifies how much more money to be sent.
|
|
RemainingAmt lnwire.MilliSatoshi
|
|
|
|
// FeesPaid specifies the total fees paid so far that can be used to
|
|
// calculate remaining fee budget.
|
|
FeesPaid lnwire.MilliSatoshi
|
|
|
|
// HasSettledHTLC is true if at least one of the payment's HTLCs is
|
|
// settled.
|
|
HasSettledHTLC bool
|
|
|
|
// PaymentFailed is true if the payment has been marked as failed with
|
|
// a reason.
|
|
PaymentFailed bool
|
|
}
|
|
|
|
// MPPayment is a wrapper around a payment's PaymentCreationInfo and
|
|
// HTLCAttempts. All payments will have the PaymentCreationInfo set, any
|
|
// HTLCs made in attempts to be completed will populated in the HTLCs slice.
|
|
// Each populated HTLCAttempt represents an attempted HTLC, each of which may
|
|
// have the associated Settle or Fail struct populated if the HTLC is no longer
|
|
// in-flight.
|
|
type MPPayment struct {
|
|
// SequenceNum is a unique identifier used to sort the payments in
|
|
// order of creation.
|
|
SequenceNum uint64
|
|
|
|
// Info holds all static information about this payment, and is
|
|
// populated when the payment is initiated.
|
|
Info *PaymentCreationInfo
|
|
|
|
// HTLCs holds the information about individual HTLCs that we send in
|
|
// order to make the payment.
|
|
HTLCs []HTLCAttempt
|
|
|
|
// FailureReason is the failure reason code indicating the reason the
|
|
// payment failed.
|
|
//
|
|
// NOTE: Will only be set once the daemon has given up on the payment
|
|
// altogether.
|
|
FailureReason *FailureReason
|
|
|
|
// Status is the current PaymentStatus of this payment.
|
|
Status PaymentStatus
|
|
|
|
// State is the current state of the payment that holds a number of key
|
|
// insights and is used to determine what to do on each payment loop
|
|
// iteration.
|
|
State *MPPaymentState
|
|
}
|
|
|
|
// Terminated returns a bool to specify whether the payment is in a terminal
|
|
// state.
|
|
func (m *MPPayment) Terminated() bool {
|
|
// If the payment is in terminal state, it cannot be updated.
|
|
return m.Status.updatable() != nil
|
|
}
|
|
|
|
// TerminalInfo returns any HTLC settle info recorded. If no settle info is
|
|
// recorded, any payment level failure will be returned. If neither a settle
|
|
// nor a failure is recorded, both return values will be nil.
|
|
func (m *MPPayment) TerminalInfo() (*HTLCAttempt, *FailureReason) {
|
|
for _, h := range m.HTLCs {
|
|
if h.Settle != nil {
|
|
return &h, nil
|
|
}
|
|
}
|
|
|
|
return nil, m.FailureReason
|
|
}
|
|
|
|
// SentAmt returns the sum of sent amount and fees for HTLCs that are either
|
|
// settled or still in flight.
|
|
func (m *MPPayment) SentAmt() (lnwire.MilliSatoshi, lnwire.MilliSatoshi) {
|
|
var sent, fees lnwire.MilliSatoshi
|
|
for _, h := range m.HTLCs {
|
|
if h.Failure != nil {
|
|
continue
|
|
}
|
|
|
|
// The attempt was not failed, meaning the amount was
|
|
// potentially sent to the receiver.
|
|
sent += h.Route.ReceiverAmt()
|
|
fees += h.Route.TotalFees()
|
|
}
|
|
|
|
return sent, fees
|
|
}
|
|
|
|
// InFlightHTLCs returns the HTLCs that are still in-flight, meaning they have
|
|
// not been settled or failed.
|
|
func (m *MPPayment) InFlightHTLCs() []HTLCAttempt {
|
|
var inflights []HTLCAttempt
|
|
for _, h := range m.HTLCs {
|
|
if h.Settle != nil || h.Failure != nil {
|
|
continue
|
|
}
|
|
|
|
inflights = append(inflights, h)
|
|
}
|
|
|
|
return inflights
|
|
}
|
|
|
|
// GetAttempt returns the specified htlc attempt on the payment.
|
|
func (m *MPPayment) GetAttempt(id uint64) (*HTLCAttempt, error) {
|
|
// TODO(yy): iteration can be slow, make it into a tree or use BS.
|
|
for _, htlc := range m.HTLCs {
|
|
htlc := htlc
|
|
if htlc.AttemptID == id {
|
|
return &htlc, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("htlc attempt not found on payment")
|
|
}
|
|
|
|
// Registrable returns an error to specify whether adding more HTLCs to the
|
|
// payment with its current status is allowed. A payment can accept new HTLC
|
|
// registrations when it's newly created, or none of its HTLCs is in a terminal
|
|
// state.
|
|
func (m *MPPayment) Registrable() error {
|
|
// If updating the payment is not allowed, we can't register new HTLCs.
|
|
// Otherwise, the status must be either `StatusInitiated` or
|
|
// `StatusInFlight`.
|
|
if err := m.Status.updatable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Exit early if this is not inflight.
|
|
if m.Status != StatusInFlight {
|
|
return nil
|
|
}
|
|
|
|
// There are still inflight HTLCs and we need to check whether there
|
|
// are settled HTLCs or the payment is failed. If we already have
|
|
// settled HTLCs, we won't allow adding more HTLCs.
|
|
if m.State.HasSettledHTLC {
|
|
return ErrPaymentPendingSettled
|
|
}
|
|
|
|
// If the payment is already failed, we won't allow adding more HTLCs.
|
|
if m.State.PaymentFailed {
|
|
return ErrPaymentPendingFailed
|
|
}
|
|
|
|
// Otherwise we can add more HTLCs.
|
|
return nil
|
|
}
|
|
|
|
// setState creates and attaches a new MPPaymentState to the payment. It also
|
|
// updates the payment's status based on its current state.
|
|
func (m *MPPayment) setState() error {
|
|
// Fetch the total amount and fees that has already been sent in
|
|
// settled and still in-flight shards.
|
|
sentAmt, fees := m.SentAmt()
|
|
|
|
// Sanity check we haven't sent a value larger than the payment amount.
|
|
totalAmt := m.Info.Value
|
|
if sentAmt > totalAmt {
|
|
return fmt.Errorf("%w: sent=%v, total=%v",
|
|
ErrSentExceedsTotal, sentAmt, totalAmt)
|
|
}
|
|
|
|
// Get any terminal info for this payment.
|
|
settle, failure := m.TerminalInfo()
|
|
|
|
// Now determine the payment's status.
|
|
status, err := decidePaymentStatus(m.HTLCs, m.FailureReason)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the payment state and status.
|
|
m.State = &MPPaymentState{
|
|
NumAttemptsInFlight: len(m.InFlightHTLCs()),
|
|
RemainingAmt: totalAmt - sentAmt,
|
|
FeesPaid: fees,
|
|
HasSettledHTLC: settle != nil,
|
|
PaymentFailed: failure != nil,
|
|
}
|
|
m.Status = status
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetState calls the internal method setState. This is a temporary method
|
|
// to be used by the tests in routing. Once the tests are updated to use mocks,
|
|
// this method can be removed.
|
|
//
|
|
// TODO(yy): delete.
|
|
func (m *MPPayment) SetState() error {
|
|
return m.setState()
|
|
}
|
|
|
|
// NeedWaitAttempts decides whether we need to hold creating more HTLC attempts
|
|
// and wait for the results of the payment's inflight HTLCs. Return an error if
|
|
// the payment is in an unexpected state.
|
|
func (m *MPPayment) NeedWaitAttempts() (bool, error) {
|
|
// Check when the remainingAmt is not zero, which means we have more
|
|
// money to be sent.
|
|
if m.State.RemainingAmt != 0 {
|
|
switch m.Status {
|
|
// If the payment is newly created, no need to wait for HTLC
|
|
// results.
|
|
case StatusInitiated:
|
|
return false, nil
|
|
|
|
// If we have inflight HTLCs, we'll check if we have terminal
|
|
// states to decide if we need to wait.
|
|
case StatusInFlight:
|
|
// We still have money to send, and one of the HTLCs is
|
|
// settled. We'd stop sending money and wait for all
|
|
// inflight HTLC attempts to finish.
|
|
if m.State.HasSettledHTLC {
|
|
log.Warnf("payment=%v has remaining amount "+
|
|
"%v, yet at least one of its HTLCs is "+
|
|
"settled", m.Info.PaymentIdentifier,
|
|
m.State.RemainingAmt)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// The payment has a failure reason though we still
|
|
// have money to send, we'd stop sending money and wait
|
|
// for all inflight HTLC attempts to finish.
|
|
if m.State.PaymentFailed {
|
|
return true, nil
|
|
}
|
|
|
|
// Otherwise we don't need to wait for inflight HTLCs
|
|
// since we still have money to be sent.
|
|
return false, nil
|
|
|
|
// We need to send more money, yet the payment is already
|
|
// succeeded. Return an error in this case as the receiver is
|
|
// violating the protocol.
|
|
case StatusSucceeded:
|
|
return false, fmt.Errorf("%w: parts of the payment "+
|
|
"already succeeded but still have remaining "+
|
|
"amount %v", ErrPaymentInternal,
|
|
m.State.RemainingAmt)
|
|
|
|
// The payment is failed and we have no inflight HTLCs, no need
|
|
// to wait.
|
|
case StatusFailed:
|
|
return false, nil
|
|
|
|
// Unknown payment status.
|
|
default:
|
|
return false, fmt.Errorf("%w: %s",
|
|
ErrUnknownPaymentStatus, m.Status)
|
|
}
|
|
}
|
|
|
|
// Now we determine whether we need to wait when the remainingAmt is
|
|
// already zero.
|
|
switch m.Status {
|
|
// When the payment is newly created, yet the payment has no remaining
|
|
// amount, return an error.
|
|
case StatusInitiated:
|
|
return false, fmt.Errorf("%w: %v",
|
|
ErrPaymentInternal, m.Status)
|
|
|
|
// If the payment is inflight, we must wait.
|
|
//
|
|
// NOTE: an edge case is when all HTLCs are failed while the payment is
|
|
// not failed we'd still be in this inflight state. However, since the
|
|
// remainingAmt is zero here, it means we cannot be in that state as
|
|
// otherwise the remainingAmt would not be zero.
|
|
case StatusInFlight:
|
|
return true, nil
|
|
|
|
// If the payment is already succeeded, no need to wait.
|
|
case StatusSucceeded:
|
|
return false, nil
|
|
|
|
// If the payment is already failed, yet the remaining amount is zero,
|
|
// return an error as this indicates an error state. We will only each
|
|
// this status when there are no inflight HTLCs and the payment is
|
|
// marked as failed with a reason, which means the remainingAmt must
|
|
// not be zero because our sentAmt is zero.
|
|
case StatusFailed:
|
|
return false, fmt.Errorf("%w: %v",
|
|
ErrPaymentInternal, m.Status)
|
|
|
|
// Unknown payment status.
|
|
default:
|
|
return false, fmt.Errorf("%w: %s",
|
|
ErrUnknownPaymentStatus, m.Status)
|
|
}
|
|
}
|
|
|
|
// GetState returns the internal state of the payment.
|
|
func (m *MPPayment) GetState() *MPPaymentState {
|
|
return m.State
|
|
}
|
|
|
|
// GetStatus returns the current status of the payment.
|
|
func (m *MPPayment) GetStatus() PaymentStatus {
|
|
return m.Status
|
|
}
|
|
|
|
// GetHTLCs returns all the HTLCs for this payment.
|
|
func (m *MPPayment) GetHTLCs() []HTLCAttempt {
|
|
return m.HTLCs
|
|
}
|
|
|
|
// AllowMoreAttempts is used to decide whether we can safely attempt more HTLCs
|
|
// for a given payment state. Return an error if the payment is in an
|
|
// unexpected state.
|
|
func (m *MPPayment) AllowMoreAttempts() (bool, error) {
|
|
// Now check whether the remainingAmt is zero or not. If we don't have
|
|
// any remainingAmt, no more HTLCs should be made.
|
|
if m.State.RemainingAmt == 0 {
|
|
// If the payment is newly created, yet we don't have any
|
|
// remainingAmt, return an error.
|
|
if m.Status == StatusInitiated {
|
|
return false, fmt.Errorf("%w: initiated payment has "+
|
|
"zero remainingAmt",
|
|
ErrPaymentInternal)
|
|
}
|
|
|
|
// Otherwise, exit early since all other statuses with zero
|
|
// remainingAmt indicate no more HTLCs can be made.
|
|
return false, nil
|
|
}
|
|
|
|
// Otherwise, the remaining amount is not zero, we now decide whether
|
|
// to make more attempts based on the payment's current status.
|
|
//
|
|
// If at least one of the payment's attempts is settled, yet we haven't
|
|
// sent all the amount, it indicates something is wrong with the peer
|
|
// as the preimage is received. In this case, return an error state.
|
|
if m.Status == StatusSucceeded {
|
|
return false, fmt.Errorf("%w: payment already succeeded but "+
|
|
"still have remaining amount %v",
|
|
ErrPaymentInternal, m.State.RemainingAmt)
|
|
}
|
|
|
|
// Now check if we can register a new HTLC.
|
|
err := m.Registrable()
|
|
if err != nil {
|
|
log.Warnf("Payment(%v): cannot register HTLC attempt: %v, "+
|
|
"current status: %s", m.Info.PaymentIdentifier,
|
|
err, m.Status)
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// Now we know we can register new HTLCs.
|
|
return true, nil
|
|
}
|
|
|
|
// generateSphinxPacket generates then encodes a sphinx packet which encodes
|
|
// the onion route specified by the passed layer 3 route. The blob returned
|
|
// from this function can immediately be included within an HTLC add packet to
|
|
// be sent to the first hop within the route.
|
|
func generateSphinxPacket(rt *route.Route, paymentHash []byte,
|
|
sessionKey *btcec.PrivateKey) ([]byte, *sphinx.Circuit, error) {
|
|
|
|
// Now that we know we have an actual route, we'll map the route into a
|
|
// sphinx payment path which includes per-hop payloads for each hop
|
|
// that give each node within the route the necessary information
|
|
// (fees, CLTV value, etc.) to properly forward the payment.
|
|
sphinxPath, err := rt.ToSphinxPath()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
log.Tracef("Constructed per-hop payloads for payment_hash=%x: %v",
|
|
paymentHash, lnutils.NewLogClosure(func() string {
|
|
path := make(
|
|
[]sphinx.OnionHop, sphinxPath.TrueRouteLength(),
|
|
)
|
|
for i := range path {
|
|
hopCopy := sphinxPath[i]
|
|
path[i] = hopCopy
|
|
}
|
|
|
|
return spew.Sdump(path)
|
|
}),
|
|
)
|
|
|
|
// Next generate the onion routing packet which allows us to perform
|
|
// privacy preserving source routing across the network.
|
|
sphinxPacket, err := sphinx.NewOnionPacket(
|
|
sphinxPath, sessionKey, paymentHash,
|
|
sphinx.DeterministicPacketFiller,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Finally, encode Sphinx packet using its wire representation to be
|
|
// included within the HTLC add packet.
|
|
var onionBlob bytes.Buffer
|
|
if err := sphinxPacket.Encode(&onionBlob); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
log.Tracef("Generated sphinx packet: %v",
|
|
lnutils.NewLogClosure(func() string {
|
|
// We make a copy of the ephemeral key and unset the
|
|
// internal curve here in order to keep the logs from
|
|
// getting noisy.
|
|
key := *sphinxPacket.EphemeralKey
|
|
packetCopy := *sphinxPacket
|
|
packetCopy.EphemeralKey = &key
|
|
|
|
return spew.Sdump(packetCopy)
|
|
}),
|
|
)
|
|
|
|
return onionBlob.Bytes(), &sphinx.Circuit{
|
|
SessionKey: sessionKey,
|
|
PaymentPath: sphinxPath.NodeKeys(),
|
|
}, nil
|
|
}
|
|
|
|
// verifyAttempt validates that a new HTLC attempt is compatible with the
|
|
// existing payment and its in-flight HTLCs. This function checks:
|
|
// 1. MPP (Multi-Path Payment) compatibility between attempts
|
|
// 2. Blinded payment consistency
|
|
// 3. Amount validation
|
|
// 4. Total payment amount limits
|
|
func verifyAttempt(payment *MPPayment, attempt *HTLCAttemptInfo) error {
|
|
// If the final hop has encrypted data, then we know this is a
|
|
// blinded payment. In blinded payments, MPP records are not set
|
|
// for split payments and the recipient is responsible for using
|
|
// a consistent PathID across the various encrypted data
|
|
// payloads that we received from them for this payment. All we
|
|
// need to check is that the total amount field for each HTLC
|
|
// in the split payment is correct.
|
|
isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
|
|
|
|
// Make sure any existing shards match the new one with regards
|
|
// to MPP options.
|
|
mpp := attempt.Route.FinalHop().MPP
|
|
|
|
// MPP records should not be set for attempts to blinded paths.
|
|
if isBlinded && mpp != nil {
|
|
return ErrMPPRecordInBlindedPayment
|
|
}
|
|
|
|
for _, h := range payment.InFlightHTLCs() {
|
|
hMpp := h.Route.FinalHop().MPP
|
|
hBlinded := len(h.Route.FinalHop().EncryptedData) != 0
|
|
|
|
// If this is a blinded payment, then no existing HTLCs
|
|
// should have MPP records.
|
|
if isBlinded && hMpp != nil {
|
|
return ErrMPPRecordInBlindedPayment
|
|
}
|
|
|
|
// If the payment is blinded (previous attempts used blinded
|
|
// paths) and the attempt is not, or vice versa, return an
|
|
// error.
|
|
if isBlinded != hBlinded {
|
|
return ErrMixedBlindedAndNonBlindedPayments
|
|
}
|
|
|
|
// If this is a blinded payment, then we just need to
|
|
// check that the TotalAmtMsat field for this shard
|
|
// is equal to that of any other shard in the same
|
|
// payment.
|
|
if isBlinded {
|
|
if attempt.Route.FinalHop().TotalAmtMsat !=
|
|
h.Route.FinalHop().TotalAmtMsat {
|
|
|
|
return ErrBlindedPaymentTotalAmountMismatch
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
// We tried to register a non-MPP attempt for a MPP
|
|
// payment.
|
|
case mpp == nil && hMpp != nil:
|
|
return ErrMPPayment
|
|
|
|
// We tried to register a MPP shard for a non-MPP
|
|
// payment.
|
|
case mpp != nil && hMpp == nil:
|
|
return ErrNonMPPayment
|
|
|
|
// Non-MPP payment, nothing more to validate.
|
|
case mpp == nil:
|
|
continue
|
|
}
|
|
|
|
// Check that MPP options match.
|
|
if mpp.PaymentAddr() != hMpp.PaymentAddr() {
|
|
return ErrMPPPaymentAddrMismatch
|
|
}
|
|
|
|
if mpp.TotalMsat() != hMpp.TotalMsat() {
|
|
return ErrMPPTotalAmountMismatch
|
|
}
|
|
}
|
|
|
|
// If this is a non-MPP attempt, it must match the total amount
|
|
// exactly. Note that a blinded payment is considered an MPP
|
|
// attempt.
|
|
amt := attempt.Route.ReceiverAmt()
|
|
if !isBlinded && mpp == nil && amt != payment.Info.Value {
|
|
return ErrValueMismatch
|
|
}
|
|
|
|
// Ensure we aren't sending more than the total payment amount.
|
|
sentAmt, _ := payment.SentAmt()
|
|
if sentAmt+amt > payment.Info.Value {
|
|
return fmt.Errorf("%w: attempted=%v, payment amount=%v",
|
|
ErrValueExceedsAmt, sentAmt+amt, payment.Info.Value)
|
|
}
|
|
|
|
return nil
|
|
}
|