Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routing+htlcswitch: fix stuck inflight payments #9150

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
123 changes: 121 additions & 2 deletions channeldb/mp_payment.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"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"
)
Expand Down Expand Up @@ -45,12 +48,19 @@ type HTLCAttemptInfo struct {
// 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 {
hash *lntypes.Hash) (*HTLCAttempt, error) {

var scratch [btcec.PrivKeyBytesLen]byte
copy(scratch[:], sessionKey.Serialize())
Expand All @@ -64,7 +74,11 @@ func NewHtlcAttempt(attemptID uint64, sessionKey *btcec.PrivateKey,
Hash: hash,
}

return &HTLCAttempt{HTLCAttemptInfo: info}
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
Expand All @@ -79,6 +93,45 @@ func (h *HTLCAttemptInfo) SessionKey() *btcec.PrivateKey {
return h.cachedSessionKey
}

// 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 {
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
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 {
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -629,3 +682,69 @@ func serializeTime(w io.Writer, t time.Time) error {
_, err := w.Write(scratch[:])
return err
}

// generateSphinxPacket generates then encodes a sphinx packet which encodes
// the onion route specified by the passed layer 3 route. The blob returned
ziggie1984 marked this conversation as resolved.
Show resolved Hide resolved
// 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
}
22 changes: 22 additions & 0 deletions channeldb/mp_payment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import (
"fmt"
"testing"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)

var (
testHash = [32]byte{
0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
}
)

// TestLazySessionKeyDeserialize tests that we can read htlc attempt session
// keys that were previously serialized as a private key as raw bytes.
func TestLazySessionKeyDeserialize(t *testing.T) {
Expand Down Expand Up @@ -578,3 +588,15 @@ func makeAttemptInfo(total, amtForwarded int) HTLCAttemptInfo {
},
}
}

// TestEmptyRoutesGenerateSphinxPacket tests that the generateSphinxPacket
// function is able to gracefully handle being passed a nil set of hops for the
// route by the caller.
func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) {
t.Parallel()

sessionKey, _ := btcec.NewPrivateKey()
emptyRoute := &route.Route{}
_, _, err := generateSphinxPacket(emptyRoute, testHash[:], sessionKey)
require.ErrorIs(t, err, route.ErrNoRouteHopsProvided)
}
7 changes: 6 additions & 1 deletion channeldb/payment_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ var (
// amount exceed the total amount.
ErrSentExceedsTotal = errors.New("total sent exceeds total amount")

// ErrRegisterAttempt is returned when a new htlc attempt cannot be
// registered.
ErrRegisterAttempt = errors.New("cannot register htlc attempt")

// errNoAttemptInfo is returned when no attempt info is stored yet.
errNoAttemptInfo = errors.New("unable to find attempt info for " +
"inflight payment")
Expand Down Expand Up @@ -342,7 +346,8 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,

// Check if registering a new attempt is allowed.
if err := payment.Registrable(); err != nil {
return err
return fmt.Errorf("%w: %v", ErrRegisterAttempt,
err.Error())
}

// If the final hop has encrypted data, then we know this is a
Expand Down
29 changes: 17 additions & 12 deletions channeldb/payment_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func genPreimage() ([32]byte, error) {
return preimage, nil
}

func genInfo() (*PaymentCreationInfo, *HTLCAttemptInfo,
func genInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo,
lntypes.Preimage, error) {

preimage, err := genPreimage()
Expand All @@ -38,9 +38,14 @@ func genInfo() (*PaymentCreationInfo, *HTLCAttemptInfo,
}

rhash := sha256.Sum256(preimage[:])
attempt := NewHtlcAttempt(
0, priv, *testRoute.Copy(), time.Time{}, nil,
var hash lntypes.Hash
copy(hash[:], rhash[:])

attempt, err := NewHtlcAttempt(
0, priv, *testRoute.Copy(), time.Time{}, &hash,
)
require.NoError(t, err)

return &PaymentCreationInfo{
PaymentIdentifier: rhash,
Value: testRoute.ReceiverAmt(),
Expand All @@ -60,7 +65,7 @@ func TestPaymentControlSwitchFail(t *testing.T) {

pControl := NewPaymentControl(db)

info, attempt, preimg, err := genInfo()
info, attempt, preimg, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

// Sends base htlc message which initiate StatusInFlight.
Expand Down Expand Up @@ -196,7 +201,7 @@ func TestPaymentControlSwitchDoubleSend(t *testing.T) {

pControl := NewPaymentControl(db)

info, attempt, preimg, err := genInfo()
info, attempt, preimg, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

// Sends base htlc message which initiate base status and move it to
Expand Down Expand Up @@ -266,7 +271,7 @@ func TestPaymentControlSuccessesWithoutInFlight(t *testing.T) {

pControl := NewPaymentControl(db)

info, _, preimg, err := genInfo()
info, _, preimg, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

// Attempt to complete the payment should fail.
Expand All @@ -291,7 +296,7 @@ func TestPaymentControlFailsWithoutInFlight(t *testing.T) {

pControl := NewPaymentControl(db)

info, _, _, err := genInfo()
info, _, _, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

// Calling Fail should return an error.
Expand Down Expand Up @@ -346,7 +351,7 @@ func TestPaymentControlDeleteNonInFlight(t *testing.T) {
var numSuccess, numInflight int

for _, p := range payments {
info, attempt, preimg, err := genInfo()
info, attempt, preimg, err := genInfo(t)
if err != nil {
t.Fatalf("unable to generate htlc message: %v", err)
}
Expand Down Expand Up @@ -684,7 +689,7 @@ func TestPaymentControlMultiShard(t *testing.T) {

pControl := NewPaymentControl(db)

info, attempt, preimg, err := genInfo()
info, attempt, preimg, err := genInfo(t)
if err != nil {
t.Fatalf("unable to generate htlc message: %v", err)
}
Expand Down Expand Up @@ -948,7 +953,7 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) {

pControl := NewPaymentControl(db)

info, attempt, _, err := genInfo()
info, attempt, _, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

// Init the payment.
Expand Down Expand Up @@ -997,7 +1002,7 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) {

// Create and init a new payment. This time we'll check that we cannot
// register an MPP attempt if we already registered a non-MPP one.
info, attempt, _, err = genInfo()
info, attempt, _, err = genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

err = pControl.InitPayment(info.PaymentIdentifier, info)
Expand Down Expand Up @@ -1271,7 +1276,7 @@ func createTestPayments(t *testing.T, p *PaymentControl, payments []*payment) {
attemptID := uint64(0)

for i := 0; i < len(payments); i++ {
info, attempt, preimg, err := genInfo()
info, attempt, preimg, err := genInfo(t)
require.NoError(t, err, "unable to generate htlc message")

// Set the payment id accordingly in the payments slice.
Expand Down
Loading
Loading