channeldb+routing: cache circuit and onion blob for htlc attempt

This commit caches the creation of sphinx circuit and onion blob to
avoid re-creating them again.
This commit is contained in:
yyforyongyu 2024-09-29 09:24:29 +09:00
parent 46eb811543
commit f96eb50ca8
No known key found for this signature in database
GPG Key ID: 9BCD95C4FF296868
8 changed files with 187 additions and 121 deletions

View File

@ -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"
)
@ -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())
@ -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
@ -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.
@ -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
}

View File

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

View File

@ -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()
@ -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(),
@ -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.
@ -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
@ -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.
@ -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.
@ -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)
}
@ -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)
}
@ -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.
@ -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)
@ -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.

View File

@ -64,7 +64,6 @@ var (
TotalAmount: 1234567,
SourcePubKey: vertex,
Hops: []*route.Hop{
testHop3,
testHop2,
testHop1,
},
@ -98,7 +97,7 @@ var (
}
)
func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
func makeFakeInfo(t *testing.T) (*PaymentCreationInfo, *HTLCAttemptInfo) {
var preimg lntypes.Preimage
copy(preimg[:], rev[:])
@ -113,9 +112,10 @@ 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
}
@ -123,7 +123,7 @@ func makeFakeInfo() (*PaymentCreationInfo, *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")
@ -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{}
@ -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)
@ -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.
@ -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.
@ -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

View File

@ -535,15 +535,23 @@ func genInfo() (*channeldb.PaymentCreationInfo, *channeldb.HTLCAttemptInfo,
}
rhash := sha256.Sum256(preimage[:])
var hash lntypes.Hash
copy(hash[:], rhash[:])
attempt, err := channeldb.NewHtlcAttempt(
1, priv, testRoute, time.Time{}, &hash,
)
if err != nil {
return nil, nil, lntypes.Preimage{}, err
}
return &channeldb.PaymentCreationInfo{
PaymentIdentifier: rhash,
Value: testRoute.ReceiverAmt(),
CreationTime: time.Unix(time.Now().Unix(), 0),
PaymentRequest: []byte("hola"),
},
&channeldb.NewHtlcAttempt(
1, priv, testRoute, time.Time{}, nil,
).HTLCAttemptInfo, preimage, nil
&attempt.HTLCAttemptInfo, preimage, nil
}
func genPreimage() ([32]byte, error) {

View File

@ -489,9 +489,8 @@ func (p *paymentLifecycle) collectResult(attempt *channeldb.HTLCAttempt) (
log.Tracef("Collecting result for attempt %v", spew.Sdump(attempt))
// Regenerate the circuit for this attempt.
_, circuit, err := generateSphinxPacket(
&attempt.Route, attempt.Hash[:], attempt.SessionKey(),
)
circuit, err := attempt.Circuit()
// TODO(yy): We generate this circuit to create the error decryptor,
// which is then used in htlcswitch as the deobfuscator to decode the
// error from `UpdateFailHTLC`. However, suppose it's an
@ -667,11 +666,9 @@ func (p *paymentLifecycle) createNewPaymentAttempt(rt *route.Route,
// We now have all the information needed to populate the current
// attempt information.
attempt := channeldb.NewHtlcAttempt(
return channeldb.NewHtlcAttempt(
attemptID, sessionKey, *rt, p.router.cfg.Clock.Now(), &hash,
)
return attempt, nil
}
// sendAttempt attempts to send the current attempt to the switch to complete
@ -703,9 +700,7 @@ func (p *paymentLifecycle) sendAttempt(
// Generate the raw encoded sphinx packet to be included along
// with the htlcAdd message that we send directly to the
// switch.
onionBlob, _, err := generateSphinxPacket(
&rt, attempt.Hash[:], attempt.SessionKey(),
)
onionBlob, err := attempt.OnionBlob()
if err != nil {
log.Errorf("Failed to create onion blob: attempt=%d in "+
"payment=%v, err:%v", attempt.AttemptID,
@ -714,7 +709,7 @@ func (p *paymentLifecycle) sendAttempt(
return p.failAttempt(attempt.AttemptID, err)
}
copy(htlcAdd.OnionBlob[:], onionBlob)
htlcAdd.OnionBlob = onionBlob
// Send it to the Switch. When this method returns we assume
// the Switch successfully has persisted the payment attempt,

View File

@ -1,7 +1,6 @@
package routing
import (
"bytes"
"context"
"fmt"
"math"
@ -15,7 +14,6 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/go-errors/errors"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/amp"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/clock"
@ -722,71 +720,6 @@ func generateNewSessionKey() (*btcec.PrivateKey, error) {
return btcec.NewPrivateKey()
}
// 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
}
// LightningPayment describes a payment to be sent through the network to the
// final destination.
type LightningPayment struct {

View File

@ -48,13 +48,6 @@ var (
testFeatures = lnwire.NewFeatureVector(nil, lnwire.Features)
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,
}
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
priv1, _ = btcec.NewPrivateKey()
@ -1235,18 +1228,6 @@ func TestFindPathFeeWeighting(t *testing.T) {
require.Equal(t, ctx.aliases["luoji"], path[0].policy.ToNodePubKey())
}
// 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)
}
// TestUnknownErrorSource tests that if the source of an error is unknown, all
// edges along the route will be pruned.
func TestUnknownErrorSource(t *testing.T) {