From f96eb50ca88c0efb2e39589556f3bea7139595da Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 29 Sep 2024 09:24:29 +0900 Subject: [PATCH] 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. --- channeldb/mp_payment.go | 123 +++++++++++++++++++++++++++++- channeldb/mp_payment_test.go | 22 ++++++ channeldb/payment_control_test.go | 29 ++++--- channeldb/payments_test.go | 19 +++-- routing/control_tower_test.go | 14 +++- routing/payment_lifecycle.go | 15 ++-- routing/router.go | 67 ---------------- routing/router_test.go | 19 ----- 8 files changed, 187 insertions(+), 121 deletions(-) diff --git a/channeldb/mp_payment.go b/channeldb/mp_payment.go index cf5669a50..b94bfd418 100644 --- a/channeldb/mp_payment.go +++ b/channeldb/mp_payment.go @@ -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 +} diff --git a/channeldb/mp_payment_test.go b/channeldb/mp_payment_test.go index 51eda72bb..13e39871a 100644 --- a/channeldb/mp_payment_test.go +++ b/channeldb/mp_payment_test.go @@ -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) +} diff --git a/channeldb/payment_control_test.go b/channeldb/payment_control_test.go index fb965bb32..9bc486a83 100644 --- a/channeldb/payment_control_test.go +++ b/channeldb/payment_control_test.go @@ -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. diff --git a/channeldb/payments_test.go b/channeldb/payments_test.go index 0c3753e66..b2a0292a4 100644 --- a/channeldb/payments_test.go +++ b/channeldb/payments_test.go @@ -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 diff --git a/routing/control_tower_test.go b/routing/control_tower_test.go index 5fb271afa..532e639d6 100644 --- a/routing/control_tower_test.go +++ b/routing/control_tower_test.go @@ -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) { diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index a4ca0a0fb..b4d883840 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -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, diff --git a/routing/router.go b/routing/router.go index 468510a6c..bd6ca8c0a 100644 --- a/routing/router.go +++ b/routing/router.go @@ -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 { diff --git a/routing/router_test.go b/routing/router_test.go index 22c9d14e5..543f3b000 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -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) {