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 8 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 {
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.
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
// 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)
}
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 @@ -676,7 +681,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 @@ -940,7 +945,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 @@ -989,7 +994,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 @@ -1263,7 +1268,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
19 changes: 11 additions & 8 deletions channeldb/payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ var (
TotalAmount: 1234567,
SourcePubKey: vertex,
Hops: []*route.Hop{
testHop3,
testHop2,
testHop1,
},
Expand Down Expand Up @@ -98,7 +97,7 @@ var (
}
)

func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
func makeFakeInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo) {
var preimg lntypes.Preimage
copy(preimg[:], rev[:])

Expand All @@ -113,17 +112,18 @@ func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
PaymentRequest: []byte("test"),
}

a := NewHtlcAttempt(
a, err := NewHtlcAttempt(
44, priv, testRoute, time.Unix(100, 0), &hash,
)
require.NoError(t, err)

return c, &a.HTLCAttemptInfo
}

func TestSentPaymentSerialization(t *testing.T) {
t.Parallel()

c, s := makeFakeInfo()
c, s := makeFakeInfo(t)

var b bytes.Buffer
require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
Expand Down Expand Up @@ -174,6 +174,9 @@ func TestSentPaymentSerialization(t *testing.T) {
require.NoError(t, err, "deserialize")
require.Equal(t, s.Route, newWireInfo.Route)

err = newWireInfo.attachOnionBlobAndCircuit()
require.NoError(t, err)

// Clear routes to allow DeepEqual to compare the remaining fields.
newWireInfo.Route = route.Route{}
s.Route = route.Route{}
Expand Down Expand Up @@ -517,7 +520,7 @@ func TestQueryPayments(t *testing.T) {

for i := 0; i < nonDuplicatePayments; i++ {
// Generate a test payment.
info, _, preimg, err := genInfo()
info, _, preimg, err := genInfo(t)
if err != nil {
t.Fatalf("unable to create test "+
"payment: %v", err)
Expand Down Expand Up @@ -618,7 +621,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) {
pControl := NewPaymentControl(db)

// Generate a test payment which does not have duplicates.
noDuplicates, _, _, err := genInfo()
noDuplicates, _, _, err := genInfo(t)
require.NoError(t, err)

// Create a new payment entry in the database.
Expand All @@ -632,7 +635,7 @@ func TestFetchPaymentWithSequenceNumber(t *testing.T) {
require.NoError(t, err)

// Generate a test payment which we will add duplicates to.
hasDuplicates, _, preimg, err := genInfo()
hasDuplicates, _, preimg, err := genInfo(t)
require.NoError(t, err)

// Create a new payment entry in the database.
Expand Down Expand Up @@ -783,7 +786,7 @@ func putDuplicatePayment(t *testing.T, duplicateBucket kvdb.RwBucket,
require.NoError(t, err)

// Generate fake information for the duplicate payment.
info, _, _, err := genInfo()
info, _, _, err := genInfo(t)
require.NoError(t, err)

// Write the payment info to disk under the creation info key. This code
Expand Down
Loading
Loading