From c84d0b4da5b6dbf1027f1e8d60da5d441feb3751 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 10:40:58 +0200 Subject: [PATCH 01/12] go.mod: update lightning-onion dep --- go.mod | 2 +- go.sum | 4 ++-- routing/blindedpath/blinded_path.go | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 4aeea38086..d4a8084051 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/kkdai/bstream v1.0.0 github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd github.com/lightninglabs/neutrino/cache v1.1.2 - github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb + github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn v1.2.1 diff --git a/go.sum b/go.sum index 59e0c9faf4..b869d2d097 100644 --- a/go.sum +++ b/go.sum @@ -447,8 +447,8 @@ github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3 github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY= github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= +github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI= +github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/routing/blindedpath/blinded_path.go b/routing/blindedpath/blinded_path.go index bc14daa40a..e904b22433 100644 --- a/routing/blindedpath/blinded_path.go +++ b/routing/blindedpath/blinded_path.go @@ -283,12 +283,12 @@ func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) ( } // Encrypt the hop info. - blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath) + blindedPathInfo, err := sphinx.BuildBlindedPath(sessionKey, paymentPath) if err != nil { return nil, err } - if len(blindedPath.BlindedHops) < 1 { + if len(blindedPathInfo.Path.BlindedHops) < 1 { return nil, fmt.Errorf("blinded path must have at least one " + "hop") } @@ -297,8 +297,8 @@ func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) ( // pub key since then we can use this more compact format in the // invoice without needing to encode the un-used blinded node pub key of // the intro node. - blindedPath.BlindedHops[0].BlindedNodePub = - blindedPath.IntroductionPoint + blindedPathInfo.Path.BlindedHops[0].BlindedNodePub = + blindedPathInfo.Path.IntroductionPoint // Now construct a z32 blinded path. return &zpay32.BlindedPaymentPath{ @@ -308,8 +308,8 @@ func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) ( HTLCMinMsat: uint64(minHTLC), HTLCMaxMsat: uint64(maxHTLC), Features: lnwire.EmptyFeatureVector(), - FirstEphemeralBlindingPoint: blindedPath.BlindingPoint, - Hops: blindedPath.BlindedHops, + FirstEphemeralBlindingPoint: blindedPathInfo.Path.BlindingPoint, + Hops: blindedPathInfo.Path.BlindedHops, }, nil } From 0c91e9d4b685cf043958269cce1edc3c93d98758 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 10:49:23 +0200 Subject: [PATCH 02/12] blindedpath: introduce PathInfo struct This new struct holds the zpay32.BlindedPaymentPath along with other useful info about the constructed path like: 1) the session key: which we can later use (as the path constructor) to decrypt the blobs we sent to each blinded hop in the path. 2) the last ephemeral pub key which we can use to uniquely identify this path. --- lnrpc/invoicesrpc/addinvoice.go | 2 +- routing/blindedpath/blinded_path.go | 35 ++++++++++++++++++++---- routing/blindedpath/blinded_path_test.go | 22 +++++++-------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index dcb1bef71e..28ca856982 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -549,7 +549,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, for _, path := range paths { options = append(options, zpay32.WithBlindedPaymentPath( - path, + path.Path, )) } } else { diff --git a/routing/blindedpath/blinded_path.go b/routing/blindedpath/blinded_path.go index e904b22433..d25315a1a5 100644 --- a/routing/blindedpath/blinded_path.go +++ b/routing/blindedpath/blinded_path.go @@ -109,11 +109,28 @@ type BuildBlindedPathCfg struct { DefaultDummyHopPolicy *BlindedHopPolicy } +// PathInfo holds a constructed blinded path along with other useful items +// related to the construction of the path which may be useful. +type PathInfo struct { + // Path holds the constructed blinded path which will be encoded within + // an invoice. + Path *zpay32.BlindedPaymentPath + + // SessionKey is the private key used for the first ephemeral blinding + // key of the path. This can be used later on to decrypt the hop + // payloads in the Path that have been encrypted for the various hops + // along the path. + SessionKey *btcec.PrivateKey + + // LastEphemeralKey is the very last public blinding key of the path. + // This can be used to uniquely identify a path when an incoming payment + // is received. + LastEphemeralKey *btcec.PublicKey +} + // BuildBlindedPaymentPaths uses the passed config to construct a set of blinded // payment paths that can be added to the invoice. -func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ( - []*zpay32.BlindedPaymentPath, error) { - +func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ([]*PathInfo, error) { // Find some appropriate routes for the value to be routed. This will // return a set of routes made up of real nodes. routes, err := cfg.FindRoutes(cfg.ValueMsat) @@ -129,7 +146,7 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ( // Not every route returned will necessarily result in a usable blinded // path and so the number of paths returned might be less than the // number of real routes returned by FindRoutes above. - paths := make([]*zpay32.BlindedPaymentPath, 0, len(routes)) + paths := make([]*PathInfo, 0, len(routes)) // For each route returned, we will construct the associated blinded // payment path. @@ -170,7 +187,7 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ( // buildBlindedPaymentPath takes a route from an introduction node to this node // and uses the given config to convert it into a blinded payment path. func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) ( - *zpay32.BlindedPaymentPath, error) { + *PathInfo, error) { hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path) if err != nil { @@ -301,7 +318,7 @@ func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) ( blindedPathInfo.Path.IntroductionPoint // Now construct a z32 blinded path. - return &zpay32.BlindedPaymentPath{ + zpay32Path := &zpay32.BlindedPaymentPath{ FeeBaseMsat: uint32(baseFee), FeeRate: feeRate, CltvExpiryDelta: cltvDelta, @@ -310,6 +327,12 @@ func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) ( Features: lnwire.EmptyFeatureVector(), FirstEphemeralBlindingPoint: blindedPathInfo.Path.BlindingPoint, Hops: blindedPathInfo.Path.BlindedHops, + } + + return &PathInfo{ + Path: zpay32Path, + SessionKey: sessionKey, + LastEphemeralKey: blindedPathInfo.LastEphemeralKey, }, nil } diff --git a/routing/blindedpath/blinded_path_test.go b/routing/blindedpath/blinded_path_test.go index 51d028eafb..06cb403984 100644 --- a/routing/blindedpath/blinded_path_test.go +++ b/routing/blindedpath/blinded_path_test.go @@ -591,7 +591,7 @@ func TestBuildBlindedPath(t *testing.T) { }, } - paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ + pathInfos, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { @@ -625,9 +625,9 @@ func TestBuildBlindedPath(t *testing.T) { BlocksUntilExpiry: 200, }) require.NoError(t, err) - require.Len(t, paths, 1) + require.Len(t, pathInfos, 1) - path := paths[0] + path := pathInfos[0].Path // Check that all the accumulated policy values are correct. require.EqualValues(t, 201, path.FeeBaseMsat) @@ -759,7 +759,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) { }, } - paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ + pathInfos, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { @@ -811,9 +811,9 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) { }, }) require.NoError(t, err) - require.Len(t, paths, 1) + require.Len(t, pathInfos, 1) - path := paths[0] + path := pathInfos[0].Path // Check that all the accumulated policy values are correct. require.EqualValues(t, 403, path.FeeBaseMsat) @@ -929,7 +929,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) { // the first 2 calls. FindRoutes returns 3 routes and so by the end, we // still get 1 valid path. var errCount int - paths, err = BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ + pathInfos, err = BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { @@ -990,7 +990,7 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) { }, }) require.NoError(t, err) - require.Len(t, paths, 1) + require.Len(t, pathInfos, 1) } // TestSingleHopBlindedPath tests that blinded path construction is done @@ -1009,7 +1009,7 @@ func TestSingleHopBlindedPath(t *testing.T) { Hops: []*route.Hop{}, } - paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ + pathInfos, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { @@ -1024,9 +1024,9 @@ func TestSingleHopBlindedPath(t *testing.T) { BlocksUntilExpiry: 200, }) require.NoError(t, err) - require.Len(t, paths, 1) + require.Len(t, pathInfos, 1) - path := paths[0] + path := pathInfos[0].Path // Check that all the accumulated policy values are correct. Since this // is a unique case where the destination node is also the introduction From 7fb998d9058143d697adec30cb91e7e60d34e0ac Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 11:06:29 +0200 Subject: [PATCH 03/12] blindedpath: add test to demo unblinding data as the sender --- routing/blindedpath/blinded_path_test.go | 96 ++++++++++++++++-------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/routing/blindedpath/blinded_path_test.go b/routing/blindedpath/blinded_path_test.go index 06cb403984..47a361ed17 100644 --- a/routing/blindedpath/blinded_path_test.go +++ b/routing/blindedpath/blinded_path_test.go @@ -844,48 +844,67 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) { data *record.BlindedRouteData ) + // checkCarolData is a helper that we can use to check that the blinded + // path record contains the info we expect for Carol. + checkCarolData := func(data *record.BlindedRouteData) { + require.Equal( + t, lnwire.NewShortChanIDFromInt(chanCB), + data.ShortChannelID.UnwrapOrFail(t).Val, + ) + + require.Equal(t, record.PaymentRelayInfo{ + CltvExpiryDelta: 144, + FeeRate: 500, + BaseFee: 100, + }, data.RelayInfo.UnwrapOrFail(t).Val) + + require.Equal(t, record.PaymentConstraints{ + MaxCltvExpiry: 1788, + HtlcMinimumMsat: 1000, + }, data.Constraints.UnwrapOrFail(t).Val) + } + + // We make a copy of the cipher text here because we want to also check + // later that we can do this same decryption from Alice's (the path + // creator's) perspective later on. + carolCipherText := make([]byte, len(hop.CipherText)) + copy(carolCipherText, hop.CipherText) + // Check that Carol's info is correct. data, blindingPoint = decryptAndDecodeHopData( t, privC, blindingPoint, hop.CipherText, ) + checkCarolData(data) - require.Equal( - t, lnwire.NewShortChanIDFromInt(chanCB), - data.ShortChannelID.UnwrapOrFail(t).Val, - ) + // Check that all Bob's info is correct. + checkBobData := func(data *record.BlindedRouteData) { + require.Equal( + t, lnwire.NewShortChanIDFromInt(chanBA), + data.ShortChannelID.UnwrapOrFail(t).Val, + ) - require.Equal(t, record.PaymentRelayInfo{ - CltvExpiryDelta: 144, - FeeRate: 500, - BaseFee: 100, - }, data.RelayInfo.UnwrapOrFail(t).Val) + require.Equal(t, record.PaymentRelayInfo{ + CltvExpiryDelta: 144, + FeeRate: 500, + BaseFee: 100, + }, data.RelayInfo.UnwrapOrFail(t).Val) - require.Equal(t, record.PaymentConstraints{ - MaxCltvExpiry: 1788, - HtlcMinimumMsat: 1000, - }, data.Constraints.UnwrapOrFail(t).Val) + require.Equal(t, record.PaymentConstraints{ + MaxCltvExpiry: 1644, + HtlcMinimumMsat: 1000, + }, data.Constraints.UnwrapOrFail(t).Val) + } - // Check that all Bob's info is correct. hop = path.Hops[1] + + bobCipherText := make([]byte, len(hop.CipherText)) + copy(bobCipherText, hop.CipherText) + + // Decrypt and check the data from Bob's perspective. data, blindingPoint = decryptAndDecodeHopData( t, privB, blindingPoint, hop.CipherText, ) - - require.Equal( - t, lnwire.NewShortChanIDFromInt(chanBA), - data.ShortChannelID.UnwrapOrFail(t).Val, - ) - - require.Equal(t, record.PaymentRelayInfo{ - CltvExpiryDelta: 144, - FeeRate: 500, - BaseFee: 100, - }, data.RelayInfo.UnwrapOrFail(t).Val) - - require.Equal(t, record.PaymentConstraints{ - MaxCltvExpiry: 1644, - HtlcMinimumMsat: 1000, - }, data.Constraints.UnwrapOrFail(t).Val) + checkBobData(data) // Check that all Alice's info is correct. The payload should contain // a next_node_id field that is equal to Alice's public key. This @@ -923,6 +942,23 @@ func TestBuildBlindedPathWithDummyHops(t *testing.T) { }, data.Constraints.UnwrapOrFail(t).Val) require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val) + // Now, we demonstrate that from Alice's perspective, we can also + // decrypt the payloads that were encrypted for Carol and Bob. Alice can + // do this using the session key for the path. + ephemPrivKey := pathInfos[0].SessionKey + data, _ = decryptAndDecodeHopData(t, ephemPrivKey, pkC, carolCipherText) + checkCarolData(data) + + // Let Alice calculate the next ephemeral private key so that she can + // then also decrypt Bob's payload. + ephemPrivKey, err = sphinx.NextEphemeralPriv( + &sphinx.PrivKeyECDH{PrivKey: ephemPrivKey}, pkC, + ) + require.NoError(t, err) + + data, _ = decryptAndDecodeHopData(t, ephemPrivKey, pkB, bobCipherText) + checkBobData(data) + // Demonstrate that BuildBlindedPaymentPaths continues to use any // functioning paths even if some routes cant be used to build a blinded // path. We do this by forcing FetchChannelEdgesByID to error out for From af221498d63855694a0c841b7cc5cd851ba465dc Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 2 Oct 2024 10:14:58 +0200 Subject: [PATCH 04/12] routing+channeldb: export and move MCRoute Move it to the models package to avoid import cycles later on. --- channeldb/models/route.go | 175 ++++++++++++++++++++++++++ routing/additional_edge.go | 2 +- routing/blinding.go | 18 +-- routing/blinding_test.go | 4 +- routing/heap.go | 2 +- routing/missioncontrol.go | 7 +- routing/missioncontrol_store.go | 92 +------------- routing/missioncontrol_store_test.go | 19 +-- routing/pathfind.go | 28 ++--- routing/pathfind_test.go | 26 ++-- routing/probability_apriori.go | 6 +- routing/result_interpretation.go | 140 ++++++++------------- routing/result_interpretation_test.go | 139 ++++++++++---------- routing/router.go | 14 +-- routing/router_test.go | 26 ++-- routing/unified_edges.go | 8 +- 16 files changed, 380 insertions(+), 326 deletions(-) create mode 100644 channeldb/models/route.go diff --git a/channeldb/models/route.go b/channeldb/models/route.go new file mode 100644 index 0000000000..ea1dcdc27d --- /dev/null +++ b/channeldb/models/route.go @@ -0,0 +1,175 @@ +package models + +import ( + "encoding/binary" + "io" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +var byteOrder = binary.BigEndian + +// MCRoute holds the bare minimum info about a payment attempt route that MC +// requires. +type MCRoute struct { + SourcePubKey route.Vertex + TotalAmount lnwire.MilliSatoshi + Hops []*MCHop +} + +// MCHop holds the bare minimum info about a payment attempt route hop that MC +// requires. +type MCHop struct { + ChannelID uint64 + PubKeyBytes route.Vertex + AmtToFwd lnwire.MilliSatoshi + HasBlindingPoint bool + HasCustomRecords bool +} + +// DeserializeRoute deserializes the MCRoute from the given io.Reader. +func DeserializeRoute(r io.Reader) (*MCRoute, error) { + var ( + rt MCRoute + numHops uint32 + ) + + if err := binary.Read(r, byteOrder, &rt.TotalAmount); err != nil { + return nil, err + } + + pub, err := wire.ReadVarBytes(r, 0, 66000, "[]byte") + if err != nil { + return nil, err + } + copy(rt.SourcePubKey[:], pub) + + if err := binary.Read(r, byteOrder, &numHops); err != nil { + return nil, err + } + + var hops []*MCHop + for i := uint32(0); i < numHops; i++ { + hop, err := deserializeHop(r) + if err != nil { + return nil, err + } + hops = append(hops, hop) + } + rt.Hops = hops + + return &rt, nil +} + +// deserializeHop deserializes the MCHop from the given io.Reader. +func deserializeHop(r io.Reader) (*MCHop, error) { + var h MCHop + + pub, err := wire.ReadVarBytes(r, 0, 66000, "[]byte") + if err != nil { + return nil, err + } + copy(h.PubKeyBytes[:], pub) + + if err := binary.Read(r, byteOrder, &h.ChannelID); err != nil { + return nil, err + } + + var a uint64 + if err := binary.Read(r, byteOrder, &a); err != nil { + return nil, err + } + h.AmtToFwd = lnwire.MilliSatoshi(a) + + if err := binary.Read(r, byteOrder, &h.HasBlindingPoint); err != nil { + return nil, err + } + + if err := binary.Read(r, byteOrder, &h.HasCustomRecords); err != nil { + return nil, err + } + + return &h, nil +} + +// Serialize serializes a MCRoute and writes the resulting bytes to the given +// io.Writer. +func (r *MCRoute) Serialize(w io.Writer) error { + err := binary.Write(w, byteOrder, uint64(r.TotalAmount)) + if err != nil { + return err + } + + if err := wire.WriteVarBytes(w, 0, r.SourcePubKey[:]); err != nil { + return err + } + + if err := binary.Write(w, byteOrder, uint32(len(r.Hops))); err != nil { + return err + } + + for _, h := range r.Hops { + if err := serializeHop(w, h); err != nil { + return err + } + } + + return nil +} + +// serializeHop serializes a MCHop and writes the resulting bytes to the given +// io.Writer. +func serializeHop(w io.Writer, h *MCHop) error { + if err := wire.WriteVarBytes(w, 0, h.PubKeyBytes[:]); err != nil { + return err + } + + if err := binary.Write(w, byteOrder, h.ChannelID); err != nil { + return err + } + + if err := binary.Write(w, byteOrder, uint64(h.AmtToFwd)); err != nil { + return err + } + + if err := binary.Write(w, byteOrder, h.HasBlindingPoint); err != nil { + return err + } + + return binary.Write(w, byteOrder, h.HasCustomRecords) + +} + +// ToMCRoute extracts the fields required by MC from the Route struct to create +// the more minima MCRoute struct. +func ToMCRoute(route *route.Route) *MCRoute { + return &MCRoute{ + SourcePubKey: route.SourcePubKey, + TotalAmount: route.TotalAmount, + Hops: extractMCHops(route.Hops), + } +} + +// extractMCHops extracts the Hop fields that MC actually uses from a slice of +// Hops. +func extractMCHops(hops []*route.Hop) []*MCHop { + mcHops := make([]*MCHop, len(hops)) + for i, hop := range hops { + mcHops[i] = extractMCHop(hop) + } + + return mcHops +} + +// extractMCHop extracts the Hop fields that MC actually uses from a Hop. +func extractMCHop(hop *route.Hop) *MCHop { + return &MCHop{ + ChannelID: hop.ChannelID, + PubKeyBytes: hop.PubKeyBytes, + AmtToFwd: hop.AmtToForward, + HasBlindingPoint: hop.BlindingPoint != nil, + HasCustomRecords: len(hop.CustomRecords) > 0, + } +} diff --git a/routing/additional_edge.go b/routing/additional_edge.go index 5f2d42eebc..9dd6b03764 100644 --- a/routing/additional_edge.go +++ b/routing/additional_edge.go @@ -71,7 +71,7 @@ func (p *PrivateEdge) BlindedPayment() *BlindedPayment { return nil } -// BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed +// BlindedEdge implements the AdditionalEdge interface. Blinded Hops are viewed // as additional edges because they are appended at the end of a normal route. type BlindedEdge struct { policy *models.CachedEdgePolicy diff --git a/routing/blinding.go b/routing/blinding.go index 270f998d9f..3a875cc749 100644 --- a/routing/blinding.go +++ b/routing/blinding.go @@ -18,7 +18,7 @@ var ( ErrNoBlindedPath = errors.New("blinded path required") // ErrInsufficientBlindedHops is returned when a blinded path does - // not have enough blinded hops. + // not have enough blinded Hops. ErrInsufficientBlindedHops = errors.New("blinded path requires " + "at least one hop") @@ -243,7 +243,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) { // payment along a blinded path. type BlindedPayment struct { // BlindedPath contains the unblinded introduction point and blinded - // hops for the blinded section of the payment. + // Hops for the blinded section of the payment. BlindedPath *sphinx.BlindedPath // BaseFee is the total base fee to be paid for payments made over the @@ -255,17 +255,17 @@ type BlindedPayment struct { ProportionalFeeRate uint32 // CltvExpiryDelta is the total expiry delta for the blinded path. This - // field includes the CLTV for the blinded hops *and* the final cltv + // field includes the CLTV for the blinded Hops *and* the final cltv // delta for the receiver. CltvExpiryDelta uint16 // HtlcMinimum is the highest HLTC minimum supported along the blinded - // path (while some hops may have lower values, we're effectively + // path (while some Hops may have lower values, we're effectively // bounded by the highest minimum). HtlcMinimum uint64 // HtlcMaximum is the lowest HTLC maximum supported along the blinded - // path (while some hops may have higher values, we're effectively + // path (while some Hops may have higher values, we're effectively // bounded by the lowest maximum). HtlcMaximum uint64 @@ -299,8 +299,8 @@ func (b *BlindedPayment) Validate() error { // to the introduction point), no hints will be returned. In this case callers // *must* account for the blinded route's CLTV delta elsewhere (as this is // effectively the final_cltv_delta for the receiving introduction node). In -// the case of multiple blinded hops, CLTV delta is fully accounted for in the -// hints (both for intermediate hops and the final_cltv_delta for the receiving +// the case of multiple blinded Hops, CLTV delta is fully accounted for in the +// hints (both for intermediate Hops and the final_cltv_delta for the receiving // node). The pseudoTarget, if provided, will be used to override the pub key // of the destination node in the path. func (b *BlindedPayment) toRouteHints( @@ -360,9 +360,9 @@ func (b *BlindedPayment) toRouteHints( hints[fromNode] = []AdditionalEdge{lastEdge} - // Start at an offset of 1 because the first node in our blinded hops + // Start at an offset of 1 because the first node in our blinded Hops // is the introduction node and terminate at the second-last node - // because we're dealing with hops as pairs. + // because we're dealing with Hops as pairs. for i := 1; i < hintCount; i++ { // Set our origin node to the current fromNode = route.NewVertex( diff --git a/routing/blinding_test.go b/routing/blinding_test.go index 950cb02107..916b57df3c 100644 --- a/routing/blinding_test.go +++ b/routing/blinding_test.go @@ -28,7 +28,7 @@ func TestBlindedPathValidation(t *testing.T) { err: ErrNoBlindedPath, }, { - name: "insufficient hops", + name: "insufficient Hops", payment: &BlindedPayment{ BlindedPath: &sphinx.BlindedPath{ BlindedHops: []*sphinx.BlindedHopInfo{}, @@ -133,7 +133,7 @@ func TestBlindedPaymentToHints(t *testing.T) { require.NoError(t, err) require.Nil(t, hints) - // Populate the blinded payment with hops. + // Populate the blinded payment with Hops. blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{ { BlindedNodePub: pkb1, diff --git a/routing/heap.go b/routing/heap.go index a4c0411d56..f9fe53f3f0 100644 --- a/routing/heap.go +++ b/routing/heap.go @@ -20,7 +20,7 @@ type nodeWithDist struct { // netAmountReceived is the amount that should be received by this node. // Either as final payment to the final node or as an intermediate - // amount that includes also the fees for subsequent hops. This node's + // amount that includes also the fees for subsequent Hops. This node's // inbound fee is already subtracted from the htlc amount - if // applicable. netAmountReceived lnwire.MilliSatoshi diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index d848235d88..3776cc1800 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb" @@ -263,7 +264,7 @@ type MissionControlPairSnapshot struct { type paymentResult struct { id uint64 timeFwd, timeReply time.Time - route *mcRoute + route *models.MCRoute success bool failureSourceIdx *int failure lnwire.FailureMessage @@ -597,7 +598,7 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route, id: paymentID, failureSourceIdx: failureSourceIdx, failure: failure, - route: extractMCRoute(rt), + route: models.ToMCRoute(rt), } return m.processPaymentResult(result) @@ -615,7 +616,7 @@ func (m *MissionControl) ReportPaymentSuccess(paymentID uint64, timeReply: timestamp, id: paymentID, success: true, - route: extractMCRoute(rt), + route: models.ToMCRoute(rt), } _, err := m.processPaymentResult(result) diff --git a/routing/missioncontrol_store.go b/routing/missioncontrol_store.go index c40f24697a..6b15c2193f 100644 --- a/routing/missioncontrol_store.go +++ b/routing/missioncontrol_store.go @@ -5,13 +5,13 @@ import ( "container/list" "encoding/binary" "fmt" - "io" "math" "sync" "time" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" ) @@ -188,7 +188,7 @@ func serializeResult(rp *paymentResult) ([]byte, []byte, error) { return nil, nil, err } - if err := serializeRoute(&b, rp.route); err != nil { + if err := rp.route.Serialize(&b); err != nil { return nil, nil, err } @@ -212,90 +212,6 @@ func serializeResult(rp *paymentResult) ([]byte, []byte, error) { return key, b.Bytes(), nil } -// deserializeRoute deserializes the mcRoute from the given io.Reader. -func deserializeRoute(r io.Reader) (*mcRoute, error) { - var rt mcRoute - if err := channeldb.ReadElements(r, &rt.totalAmount); err != nil { - return nil, err - } - - var pub []byte - if err := channeldb.ReadElements(r, &pub); err != nil { - return nil, err - } - copy(rt.sourcePubKey[:], pub) - - var numHops uint32 - if err := channeldb.ReadElements(r, &numHops); err != nil { - return nil, err - } - - var hops []*mcHop - for i := uint32(0); i < numHops; i++ { - hop, err := deserializeHop(r) - if err != nil { - return nil, err - } - hops = append(hops, hop) - } - rt.hops = hops - - return &rt, nil -} - -// deserializeHop deserializes the mcHop from the given io.Reader. -func deserializeHop(r io.Reader) (*mcHop, error) { - var h mcHop - - var pub []byte - if err := channeldb.ReadElements(r, &pub); err != nil { - return nil, err - } - copy(h.pubKeyBytes[:], pub) - - if err := channeldb.ReadElements(r, - &h.channelID, &h.amtToFwd, &h.hasBlindingPoint, - &h.hasCustomRecords, - ); err != nil { - return nil, err - } - - return &h, nil -} - -// serializeRoute serializes a mcRoute and writes the resulting bytes to the -// given io.Writer. -func serializeRoute(w io.Writer, r *mcRoute) error { - err := channeldb.WriteElements(w, r.totalAmount, r.sourcePubKey[:]) - if err != nil { - return err - } - - if err := channeldb.WriteElements(w, uint32(len(r.hops))); err != nil { - return err - } - - for _, h := range r.hops { - if err := serializeHop(w, h); err != nil { - return err - } - } - - return nil -} - -// serializeHop serializes a mcHop and writes the resulting bytes to the given -// io.Writer. -func serializeHop(w io.Writer, h *mcHop) error { - return channeldb.WriteElements(w, - h.pubKeyBytes[:], - h.channelID, - h.amtToFwd, - h.hasBlindingPoint, - h.hasCustomRecords, - ) -} - // deserializeResult deserializes a payment result. func deserializeResult(k, v []byte) (*paymentResult, error) { // Parse payment id. @@ -329,7 +245,7 @@ func deserializeResult(k, v []byte) (*paymentResult, error) { } // Read route. - route, err := deserializeRoute(r) + route, err := models.DeserializeRoute(r) if err != nil { return nil, err } @@ -582,7 +498,7 @@ func getResultKey(rp *paymentResult) []byte { // chronologically. byteOrder.PutUint64(keyBytes[:], uint64(rp.timeReply.UnixNano())) byteOrder.PutUint64(keyBytes[8:], rp.id) - copy(keyBytes[16:], rp.route.sourcePubKey[:]) + copy(keyBytes[16:], rp.route.SourcePubKey[:]) return keyBytes[:] } diff --git a/routing/missioncontrol_store_test.go b/routing/missioncontrol_store_test.go index 2dbfc11214..640baacf00 100644 --- a/routing/missioncontrol_store_test.go +++ b/routing/missioncontrol_store_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/btcsuite/btcwallet/walletdb" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwire" @@ -18,16 +19,16 @@ const testMaxRecords = 2 var ( // mcStoreTestRoute is a test route for the mission control store tests. - mcStoreTestRoute = mcRoute{ - totalAmount: lnwire.MilliSatoshi(5), - sourcePubKey: route.Vertex{1}, - hops: []*mcHop{ + mcStoreTestRoute = models.MCRoute{ + TotalAmount: lnwire.MilliSatoshi(5), + SourcePubKey: route.Vertex{1}, + Hops: []*models.MCHop{ { - pubKeyBytes: route.Vertex{2}, - channelID: 4, - amtToFwd: lnwire.MilliSatoshi(7), - hasCustomRecords: true, - hasBlindingPoint: false, + PubKeyBytes: route.Vertex{2}, + ChannelID: 4, + AmtToFwd: lnwire.MilliSatoshi(7), + HasCustomRecords: true, + HasBlindingPoint: false, }, }, } diff --git a/routing/pathfind.go b/routing/pathfind.go index 43eae71036..7d51b64d6e 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -126,13 +126,13 @@ type finalHopParams struct { // If the route is to a blinded path, the blindedPath parameter is used to // back fill additional fields that are required for a blinded payment. This is // done in a separate pass to keep our route construction simple, as blinded -// paths require zero expiry and amount values for intermediate hops (which +// paths require zero expiry and amount values for intermediate Hops (which // makes calculating the totals during route construction difficult if we // include blinded paths on the first pass). // // NOTE: The passed slice of unified edges MUST be sorted in forward order: from // the source to the target node of the path finding attempt. It is assumed that -// any feature vectors on all hops have been validated for transitive +// any feature vectors on all Hops have been validated for transitive // dependencies. // NOTE: If a non-nil blinded path is provided it is assumed to have been // validated by the caller. @@ -175,7 +175,7 @@ func newRoute(sourceVertex route.Vertex, // We'll calculate the amounts, timelocks, and fees for each hop // in the route. The base case is the final hop which includes // their amount and timelocks. These values will accumulate - // contributions from the preceding hops back to the sender as + // contributions from the preceding Hops back to the sender as // we compute the route in reverse. var ( amtToForward lnwire.MilliSatoshi @@ -276,7 +276,7 @@ func newRoute(sourceVertex route.Vertex, } // Since we're traversing the path backwards atm, we prepend - // each new hop such that, the final slice of hops will be in + // each new hop such that, the final slice of Hops will be in // the forwards order. currentHop := &route.Hop{ PubKeyBytes: edge.ToNodePubKey(), @@ -345,7 +345,7 @@ func newRoute(sourceVertex route.Vertex, payload := blindedPath.BlindedHops[dataIndex].CipherText hop.EncryptedData = payload - // All of the hops in a blinded route *except* the + // All of the Hops in a blinded route *except* the // final hop should have zero amounts / time locks. if i != len(hops)-1 { hop.AmtToForward = 0 @@ -361,7 +361,7 @@ func newRoute(sourceVertex route.Vertex, } } - // With the base routing data expressed as hops, build the full route + // With the base routing data expressed as Hops, build the full route newRoute, err := route.NewRouteFromHops( nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex), hops, @@ -376,7 +376,7 @@ func newRoute(sourceVertex route.Vertex, // edgeWeight computes the weight of an edge. This value is used when searching // for the shortest path within the channel graph between two nodes. Weight is // is the fee itself plus a time lock penalty added to it. This benefits -// channels with shorter time lock deltas and shorter (hops) routes in general. +// channels with shorter time lock deltas and shorter (Hops) routes in general. // RiskFactor controls the influence of time lock on route selection. This is // currently a fixed value, but might be configurable in the future. func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi, @@ -681,7 +681,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, } } - // The payload size of the final hop differ from intermediate hops + // The payload size of the final hop differ from intermediate Hops // and depends on whether the destination is blinded or not. lastHopPayloadSize := lastHopPayloadSize(r, finalHtlcExpiry, amt) @@ -1140,7 +1140,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // findPath, and avoid using ChannelEdgePolicy altogether. pathEdges[len(pathEdges)-1].policy.ToNodeFeatures = features - log.Debugf("Found route: probability=%v, hops=%v, fee=%v", + log.Debugf("Found route: probability=%v, Hops=%v, fee=%v", distance[source].probability, len(pathEdges), distance[source].netAmountReceived-amt) @@ -1150,14 +1150,14 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // blindedPathRestrictions are a set of constraints to adhere to when // choosing a set of blinded paths to this node. type blindedPathRestrictions struct { - // minNumHops is the minimum number of hops to include in a blinded + // minNumHops is the minimum number of Hops to include in a blinded // path. This doesn't include our node, so if the minimum is 1, then // the path will contain at minimum our node along with an introduction // node hop. A minimum of 0 will include paths where this node is the // introduction node and so should be used with caution. minNumHops uint8 - // maxNumHops is the maximum number of hops to include in a blinded + // maxNumHops is the maximum number of Hops to include in a blinded // path. This doesn't include our node, so if the maximum is 1, then // the path will contain our node along with an introduction node hop. maxNumHops uint8 @@ -1188,9 +1188,9 @@ func findBlindedPaths(g Graph, target route.Vertex, // Sanity check the restrictions. if restrictions.minNumHops > restrictions.maxNumHops { - return nil, fmt.Errorf("maximum number of blinded path hops "+ + return nil, fmt.Errorf("maximum number of blinded path Hops "+ "(%d) must be greater than or equal to the minimum "+ - "number of hops (%d)", restrictions.maxNumHops, + "number of Hops (%d)", restrictions.maxNumHops, restrictions.minNumHops) } @@ -1256,7 +1256,7 @@ func processNodeForBlindedPath(g Graph, node route.Vertex, alreadyVisited map[route.Vertex]bool, restrictions *blindedPathRestrictions) ([][]blindedHop, bool, error) { - // If we have already visited the maximum number of hops, then this path + // If we have already visited the maximum number of Hops, then this path // is complete and we can exit now. if len(alreadyVisited) > int(restrictions.maxNumHops) { return nil, false, nil diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 72f71600dd..4d1ef87ded 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -1250,7 +1250,7 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) { path, err := find(noRestrictions) require.NoError(t, err, "unable to find private path to doge") - // The path should represent the following hops: + // The path should represent the following Hops: // roasbeef -> songoku -> doge assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge") @@ -1355,12 +1355,12 @@ func TestNewRoute(t *testing.T) { // name identifies the test case in the test output. name string - // hops is the list of hops (the route) that gets passed into + // Hops is the list of Hops (the route) that gets passed into // the call to newRoute. hops []*models.CachedEdgePolicy // paymentAmount is the amount that is send into the route - // indicated by hops. + // indicated by Hops. paymentAmount lnwire.MilliSatoshi // destFeatures is a feature vector, that if non-nil, will @@ -1384,7 +1384,7 @@ func TestNewRoute(t *testing.T) { // expectedTotalAmount is the total amount that is expected to // be returned from newRoute. This amount should include all - // the fees to be paid to intermediate hops. + // the fees to be paid to intermediate Hops. expectedTotalAmount lnwire.MilliSatoshi // expectedTotalTimeLock is the time lock that is expected to @@ -1617,7 +1617,7 @@ func TestNewRoute(t *testing.T) { func runNewRoutePathTooLong(t *testing.T, useCache bool) { var testChannels []*testChannel - // Setup a linear network of 26 hops. + // Setup a linear network of 26 Hops. fromNode := "start" for i := 0; i < 26; i++ { toNode := fmt.Sprintf("node-%v", i+1) @@ -1820,7 +1820,7 @@ func runMissingFeatureDep(t *testing.T, useCache bool) { // destination features are set properly from the previous assertions, // but conner's feature vector in the graph is still broken. We expect // errNoPathFound and not the missing feature dep err above since - // intermediate hops are simply skipped if they have invalid feature + // intermediate Hops are simply skipped if they have invalid feature // vectors, leaving no possible route to joost. _, err = ctx.findPath(joost, 100) if err != errNoPathFound { @@ -1881,7 +1881,7 @@ func runUnknownRequiredFeatures(t *testing.T, useCache bool) { // Now, try to find a route to joost through conner. The destination // features are valid, but conner's feature vector in the graph still // requires feature 100. We expect errNoPathFound and not the error - // above since intermediate hops are simply skipped if they have invalid + // above since intermediate Hops are simply skipped if they have invalid // feature vectors, leaving no possible route to joost. This asserts // that we don't try to route _through_ nodes with unknown required // features. @@ -1989,7 +1989,7 @@ func runRouteFailMaxHTLC(t *testing.T, useCache bool) { // Set up a test graph: // roasbeef <--> firstHop <--> secondHop <--> target // We will be adjusting the max HTLC of the edge between the first and - // second hops. + // second Hops. var firstToSecondID uint64 = 1 testChannels := []*testChannel{ symmetricTestChannel("roasbeef", "first", 100000, &testChannelPolicy{ @@ -2230,7 +2230,7 @@ func TestPathFindSpecExample(t *testing.T) { // Now we'll examine the route returned for correctness. // // It should be sending the exact payment amount as there are no - // additional hops. + // additional Hops. require.Equal(t, amt, route.TotalAmount) require.Equal(t, amt, route.Hops[0].AmtToForward) require.Zero(t, route.HopFee(0)) @@ -2254,7 +2254,7 @@ func TestPathFindSpecExample(t *testing.T) { route, _, err = ctx.router.FindRoute(req) require.NoError(t, err, "unable to find routes") - // The route should be two hops. + // The route should be two Hops. require.Len(t, route.Hops, 2) // The total amount should factor in a fee of 10199 and also use a CLTV @@ -2265,7 +2265,7 @@ func TestPathFindSpecExample(t *testing.T) { expectedDelta := uint32(20 + MinCLTVDelta) require.Equal(t, startingHeight+expectedDelta, route.TotalTimeLock) - // Ensure that the hops of the route are properly crafted. + // Ensure that the Hops of the route are properly crafted. // // After taking the fee, Bob should be forwarding the remainder which // is the exact payment to Bob. @@ -2451,7 +2451,7 @@ func testCltvLimit(t *testing.T, useCache bool, limit uint32, // Set up a test graph with three possible paths to the target. The path // through a is the lowest cost with a high time lock (144). The path // through b has a higher cost but a lower time lock (100). That path - // through c and d (two hops) has the same case as the path through b, + // through c and d (two Hops) has the same case as the path through b, // but the total time lock is lower (60). testChannels := []*testChannel{ symmetricTestChannel("roasbeef", "a", 100000, &testChannelPolicy{}, 1), @@ -3660,7 +3660,7 @@ func TestFindBlindedPaths(t *testing.T) { "charlie,dave", }) - // 2) Extend the search to include 2 hops other than the destination. + // 2) Extend the search to include 2 Hops other than the destination. paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{ minNumHops: 1, maxNumHops: 2, diff --git a/routing/probability_apriori.go b/routing/probability_apriori.go index d37e3875ec..513b8f163d 100644 --- a/routing/probability_apriori.go +++ b/routing/probability_apriori.go @@ -127,9 +127,9 @@ func DefaultAprioriConfig() AprioriConfig { // AprioriEstimator returns node and pair probabilities based on historical // payment results. It uses a preconfigured success probability value for -// untried hops (AprioriHopProbability) and returns a high success probability -// for hops that could previously conduct a payment (prevSuccessProbability). -// Successful edges are retried until proven otherwise. Recently failed hops are +// untried Hops (AprioriHopProbability) and returns a high success probability +// for Hops that could previously conduct a payment (prevSuccessProbability). +// Successful edges are retried until proven otherwise. Recently failed Hops are // penalized by an exponential time decay (PenaltyHalfLife), after which they // are reconsidered for routing. If information was learned about a forwarding // node, the information is taken into account to estimate a per node diff --git a/routing/result_interpretation.go b/routing/result_interpretation.go index 4da4134214..aa710ef0db 100644 --- a/routing/result_interpretation.go +++ b/routing/result_interpretation.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -76,7 +77,7 @@ type interpretedResult struct { // interpretResult interprets a payment outcome and returns an object that // contains information required to update mission control. -func interpretResult(rt *mcRoute, success bool, failureSrcIdx *int, +func interpretResult(rt *models.MCRoute, success bool, failureSrcIdx *int, failure lnwire.FailureMessage) *interpretedResult { i := &interpretedResult{ @@ -92,14 +93,14 @@ func interpretResult(rt *mcRoute, success bool, failureSrcIdx *int, } // processSuccess processes a successful payment attempt. -func (i *interpretedResult) processSuccess(route *mcRoute) { +func (i *interpretedResult) processSuccess(route *models.MCRoute) { // For successes, all nodes must have acted in the right way. Therefore // we mark all of them with a success result. - i.successPairRange(route, 0, len(route.hops)-1) + i.successPairRange(route, 0, len(route.Hops)-1) } // processFail processes a failed payment attempt. -func (i *interpretedResult) processFail(rt *mcRoute, errSourceIdx *int, +func (i *interpretedResult) processFail(rt *models.MCRoute, errSourceIdx *int, failure lnwire.FailureMessage) { if errSourceIdx == nil { @@ -124,7 +125,7 @@ func (i *interpretedResult) processFail(rt *mcRoute, errSourceIdx *int, i.processPaymentOutcomeSelf(rt, failure) // A failure from the final hop was received. - case len(rt.hops): + case len(rt.Hops): i.processPaymentOutcomeFinal(rt, failure) // An intermediate hop failed. Interpret the outcome, update reputation @@ -141,7 +142,7 @@ func (i *interpretedResult) processFail(rt *mcRoute, errSourceIdx *int, // node. This indicates that the introduction node is not obeying the route // blinding specification, as we expect all errors from the introduction node // to be source from it. -func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute, +func (i *interpretedResult) processPaymentOutcomeBadIntro(route *models.MCRoute, introIdx, errSourceIdx int) { // We fail the introduction node for not obeying the specification. @@ -158,13 +159,13 @@ func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute, // a final failure reason because the recipient can't process the // payment (independent of the introduction failing to convert the // error, we can't complete the payment if the last hop fails). - if errSourceIdx == len(route.hops) { + if errSourceIdx == len(route.Hops) { i.finalFailureReason = &reasonError } } // processPaymentOutcomeSelf handles failures sent by ourselves. -func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute, +func (i *interpretedResult) processPaymentOutcomeSelf(rt *models.MCRoute, failure lnwire.FailureMessage) { switch failure.(type) { @@ -178,7 +179,7 @@ func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute, i.failNode(rt, 1) // If this was a payment to a direct peer, we can stop trying. - if len(rt.hops) == 1 { + if len(rt.Hops) == 1 { i.finalFailureReason = &reasonError } @@ -188,15 +189,15 @@ func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute, // available in the link has been updated. default: log.Warnf("Routing failure for local channel %v occurred", - rt.hops[0].channelID) + rt.Hops[0].ChannelID) } } // processPaymentOutcomeFinal handles failures sent by the final hop. -func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute, +func (i *interpretedResult) processPaymentOutcomeFinal(route *models.MCRoute, failure lnwire.FailureMessage) { - n := len(route.hops) + n := len(route.Hops) failNode := func() { i.failNode(route, n) @@ -235,7 +236,7 @@ func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute, // from its predecessor. i.failPair(route, n-1) - // The other hops relayed correctly, so assign those pairs a + // The other Hops relayed correctly, so assign those pairs a // success result. At this point, n >= 2. i.successPairRange(route, 0, n-2) @@ -291,8 +292,9 @@ func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute, // hop. // //nolint:funlen -func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute, - errorSourceIdx int, failure lnwire.FailureMessage) { +func (i *interpretedResult) processPaymentOutcomeIntermediate( + route *models.MCRoute, errorSourceIdx int, + failure lnwire.FailureMessage) { reportOutgoing := func() { i.failPair( @@ -396,8 +398,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute, // Set the node pair for which a channel update may be out of // date. The second chance logic uses the policyFailure field. i.policyFailure = &DirectedNodePair{ - From: route.hops[errorSourceIdx-1].pubKeyBytes, - To: route.hops[errorSourceIdx].pubKeyBytes, + From: route.Hops[errorSourceIdx-1].PubKeyBytes, + To: route.Hops[errorSourceIdx].PubKeyBytes, } reportOutgoing() @@ -425,8 +427,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute, // Set the node pair for which a channel update may be out of // date. The second chance logic uses the policyFailure field. i.policyFailure = &DirectedNodePair{ - From: route.hops[errorSourceIdx-1].pubKeyBytes, - To: route.hops[errorSourceIdx].pubKeyBytes, + From: route.Hops[errorSourceIdx-1].PubKeyBytes, + To: route.Hops[errorSourceIdx].PubKeyBytes, } // We report incoming channel. If a second pair is granted in @@ -500,14 +502,14 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute, // Note that if LND is extended to support multiple blinded // routes, this will terminate the payment without re-trying // the other routes. - if introIdx == len(route.hops)-1 { + if introIdx == len(route.Hops)-1 { i.finalFailureReason = &reasonError } else { - // If there are other hops between the recipient and + // If there are other Hops between the recipient and // introduction node, then we just penalize the last // hop in the blinded route to minimize the storage of // results for ephemeral keys. - i.failPairBalance(route, len(route.hops)-1) + i.failPairBalance(route, len(route.Hops)-1) } // In all other cases, we penalize the reporting node. These are all @@ -521,9 +523,9 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute, // route, using the same indexing in the route that we use for errorSourceIdx // (i.e., that we consider our own node to be at index zero). A boolean is // returned to indicate whether the route contains a blinded portion at all. -func introductionPointIndex(route *mcRoute) (int, bool) { - for i, hop := range route.hops { - if hop.hasBlindingPoint { +func introductionPointIndex(route *models.MCRoute) (int, bool) { + for i, hop := range route.Hops { + if hop.HasBlindingPoint { return i + 1, true } } @@ -533,8 +535,10 @@ func introductionPointIndex(route *mcRoute) (int, bool) { // processPaymentOutcomeUnknown processes a payment outcome for which no failure // message or source is available. -func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) { - n := len(route.hops) +func (i *interpretedResult) processPaymentOutcomeUnknown( + route *models.MCRoute) { + + n := len(route.Hops) // If this is a direct payment, the destination must be at fault. if n == 1 { @@ -549,62 +553,12 @@ func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) { i.failPairRange(route, 0, n-1) } -// extractMCRoute extracts the fields required by MC from the Route struct to -// create the more minimal mcRoute struct. -func extractMCRoute(route *route.Route) *mcRoute { - return &mcRoute{ - sourcePubKey: route.SourcePubKey, - totalAmount: route.TotalAmount, - hops: extractMCHops(route.Hops), - } -} - -// extractMCHops extracts the Hop fields that MC actually uses from a slice of -// Hops. -func extractMCHops(hops []*route.Hop) []*mcHop { - mcHops := make([]*mcHop, len(hops)) - for i, hop := range hops { - mcHops[i] = extractMCHop(hop) - } - - return mcHops -} - -// extractMCHop extracts the Hop fields that MC actually uses from a Hop. -func extractMCHop(hop *route.Hop) *mcHop { - return &mcHop{ - channelID: hop.ChannelID, - pubKeyBytes: hop.PubKeyBytes, - amtToFwd: hop.AmtToForward, - hasBlindingPoint: hop.BlindingPoint != nil, - hasCustomRecords: len(hop.CustomRecords) > 0, - } -} - -// mcRoute holds the bare minimum info about a payment attempt route that MC -// requires. -type mcRoute struct { - sourcePubKey route.Vertex - totalAmount lnwire.MilliSatoshi - hops []*mcHop -} - -// mcHop holds the bare minimum info about a payment attempt route hop that MC -// requires. -type mcHop struct { - channelID uint64 - pubKeyBytes route.Vertex - amtToFwd lnwire.MilliSatoshi - hasBlindingPoint bool - hasCustomRecords bool -} - // failNode marks the node indicated by idx in the route as failed. It also // marks the incoming and outgoing channels of the node as failed. This function // intentionally panics when the self node is failed. -func (i *interpretedResult) failNode(rt *mcRoute, idx int) { +func (i *interpretedResult) failNode(rt *models.MCRoute, idx int) { // Mark the node as failing. - i.nodeFailure = &rt.hops[idx-1].pubKeyBytes + i.nodeFailure = &rt.Hops[idx-1].PubKeyBytes // Mark the incoming connection as failed for the node. We intent to // penalize as much as we can for a node level failure, including future @@ -620,7 +574,7 @@ func (i *interpretedResult) failNode(rt *mcRoute, idx int) { // If not the ultimate node, mark the outgoing connection as failed for // the node. - if idx < len(rt.hops) { + if idx < len(rt.Hops) { outgoingChannelIdx := idx outPair, _ := getPair(rt, outgoingChannelIdx) i.pairResults[outPair] = failPairResult(0) @@ -630,14 +584,16 @@ func (i *interpretedResult) failNode(rt *mcRoute, idx int) { // failPairRange marks the node pairs from node fromIdx to node toIdx as failed // in both direction. -func (i *interpretedResult) failPairRange(rt *mcRoute, fromIdx, toIdx int) { +func (i *interpretedResult) failPairRange(rt *models.MCRoute, fromIdx, + toIdx int) { + for idx := fromIdx; idx <= toIdx; idx++ { i.failPair(rt, idx) } } // failPair marks a pair as failed in both directions. -func (i *interpretedResult) failPair(rt *mcRoute, idx int) { +func (i *interpretedResult) failPair(rt *models.MCRoute, idx int) { pair, _ := getPair(rt, idx) // Report pair in both directions without a minimum penalization amount. @@ -646,7 +602,9 @@ func (i *interpretedResult) failPair(rt *mcRoute, idx int) { } // failPairBalance marks a pair as failed with a minimum penalization amount. -func (i *interpretedResult) failPairBalance(rt *mcRoute, channelIdx int) { +func (i *interpretedResult) failPairBalance(rt *models.MCRoute, + channelIdx int) { + pair, amt := getPair(rt, channelIdx) i.pairResults[pair] = failPairResult(amt) @@ -654,7 +612,9 @@ func (i *interpretedResult) failPairBalance(rt *mcRoute, channelIdx int) { // successPairRange marks the node pairs from node fromIdx to node toIdx as // succeeded. -func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) { +func (i *interpretedResult) successPairRange(rt *models.MCRoute, fromIdx, + toIdx int) { + for idx := fromIdx; idx <= toIdx; idx++ { pair, amt := getPair(rt, idx) @@ -664,21 +624,21 @@ func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) { // getPair returns a node pair from the route and the amount passed between that // pair. -func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair, +func getPair(rt *models.MCRoute, channelIdx int) (DirectedNodePair, lnwire.MilliSatoshi) { - nodeTo := rt.hops[channelIdx].pubKeyBytes + nodeTo := rt.Hops[channelIdx].PubKeyBytes var ( nodeFrom route.Vertex amt lnwire.MilliSatoshi ) if channelIdx == 0 { - nodeFrom = rt.sourcePubKey - amt = rt.totalAmount + nodeFrom = rt.SourcePubKey + amt = rt.TotalAmount } else { - nodeFrom = rt.hops[channelIdx-1].pubKeyBytes - amt = rt.hops[channelIdx-1].amtToFwd + nodeFrom = rt.Hops[channelIdx-1].PubKeyBytes + amt = rt.Hops[channelIdx-1].AmtToFwd } pair := NewDirectedNodePair(nodeFrom, nodeTo) diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go index 14506e3a14..95bb9ac29a 100644 --- a/routing/result_interpretation_test.go +++ b/routing/result_interpretation_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -14,105 +15,105 @@ var ( {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, } - routeOneHop = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 99}, + routeOneHop = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 99}, }, } - routeTwoHop = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 99}, - {pubKeyBytes: hops[2], amtToFwd: 97}, + routeTwoHop = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 99}, + {PubKeyBytes: hops[2], AmtToFwd: 97}, }, } - routeThreeHop = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 99}, - {pubKeyBytes: hops[2], amtToFwd: 97}, - {pubKeyBytes: hops[3], amtToFwd: 94}, + routeThreeHop = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 99}, + {PubKeyBytes: hops[2], AmtToFwd: 97}, + {PubKeyBytes: hops[3], AmtToFwd: 94}, }, } - routeFourHop = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 99}, - {pubKeyBytes: hops[2], amtToFwd: 97}, - {pubKeyBytes: hops[3], amtToFwd: 94}, - {pubKeyBytes: hops[4], amtToFwd: 90}, + routeFourHop = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 99}, + {PubKeyBytes: hops[2], AmtToFwd: 97}, + {PubKeyBytes: hops[3], AmtToFwd: 94}, + {PubKeyBytes: hops[4], AmtToFwd: 90}, }, } // blindedMultiHop is a blinded path where there are cleartext hops // before the introduction node, and an intermediate blinded hop before // the recipient after it. - blindedMultiHop = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 99}, + blindedMultiHop = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 99}, { - pubKeyBytes: hops[2], - amtToFwd: 95, - hasBlindingPoint: true, + PubKeyBytes: hops[2], + AmtToFwd: 95, + HasBlindingPoint: true, }, - {pubKeyBytes: hops[3], amtToFwd: 88}, - {pubKeyBytes: hops[4], amtToFwd: 77}, + {PubKeyBytes: hops[3], AmtToFwd: 88}, + {PubKeyBytes: hops[4], AmtToFwd: 77}, }, } // blindedSingleHop is a blinded path with a single blinded hop after // the introduction node. - blindedSingleHop = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 99}, + blindedSingleHop = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 99}, { - pubKeyBytes: hops[2], - amtToFwd: 95, - hasBlindingPoint: true, + PubKeyBytes: hops[2], + AmtToFwd: 95, + HasBlindingPoint: true, }, - {pubKeyBytes: hops[3], amtToFwd: 88}, + {PubKeyBytes: hops[3], AmtToFwd: 88}, }, } // blindedMultiToIntroduction is a blinded path which goes directly // to the introduction node, with multiple blinded hops after it. - blindedMultiToIntroduction = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ + blindedMultiToIntroduction = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ { - pubKeyBytes: hops[1], - amtToFwd: 90, - hasBlindingPoint: true, + PubKeyBytes: hops[1], + AmtToFwd: 90, + HasBlindingPoint: true, }, - {pubKeyBytes: hops[2], amtToFwd: 75}, - {pubKeyBytes: hops[3], amtToFwd: 58}, + {PubKeyBytes: hops[2], AmtToFwd: 75}, + {PubKeyBytes: hops[3], AmtToFwd: 58}, }, } // blindedIntroReceiver is a blinded path where the introduction node // is the recipient. - blindedIntroReceiver = mcRoute{ - sourcePubKey: hops[0], - totalAmount: 100, - hops: []*mcHop{ - {pubKeyBytes: hops[1], amtToFwd: 95}, + blindedIntroReceiver = models.MCRoute{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*models.MCHop{ + {PubKeyBytes: hops[1], AmtToFwd: 95}, { - pubKeyBytes: hops[2], - amtToFwd: 90, - hasBlindingPoint: true, + PubKeyBytes: hops[2], + AmtToFwd: 90, + HasBlindingPoint: true, }, }, } @@ -129,7 +130,7 @@ func getPolicyFailure(from, to int) *DirectedNodePair { type resultTestCase struct { name string - route *mcRoute + route *models.MCRoute success bool failureSrcIdx int failure lnwire.FailureMessage @@ -278,7 +279,7 @@ var resultTestCases = []resultTestCase{ }, // Tests an invalid onion payload from a final hop. The final hop should - // be failed while the proceeding hops are reproed as successes. The + // be failed while the proceeding Hops are reproed as successes. The // failure is terminal since the receiver can't process our onion. { name: "fail invalid onion payload final hop four", @@ -415,7 +416,7 @@ var resultTestCases = []resultTestCase{ }, }, - // Test a channel disabled failure from the final hop in two hops. Only the + // Test a channel disabled failure from the final hop in two Hops. Only the // disabled channel should be penalized for any amount. { name: "two hop channel disabled", @@ -535,7 +536,7 @@ var resultTestCases = []resultTestCase{ expectedResult: &interpretedResult{ pairResults: map[DirectedNodePair]pairResult{ - // Failures from failing hops[1]. + // Failures from failing Hops[1]. getTestPair(0, 1): failPairResult(0), getTestPair(1, 0): failPairResult(0), getTestPair(1, 2): failPairResult(0), @@ -556,7 +557,7 @@ var resultTestCases = []resultTestCase{ expectedResult: &interpretedResult{ pairResults: map[DirectedNodePair]pairResult{ getTestPair(0, 1): successPairResult(100), - // Failures from failing hops[2]. + // Failures from failing Hops[2]. getTestPair(1, 2): failPairResult(0), getTestPair(2, 1): failPairResult(0), getTestPair(2, 3): failPairResult(0), @@ -567,7 +568,7 @@ var resultTestCases = []resultTestCase{ }, // Test the case where an intermediate node that is not in a blinded // route returns an invalid blinding error and there were no successful - // hops before the erring incoming link (the erring node if our peer). + // Hops before the erring incoming link (the erring node if our peer). { name: "peer unexpected blinding", route: &routeThreeHop, @@ -576,7 +577,7 @@ var resultTestCases = []resultTestCase{ expectedResult: &interpretedResult{ pairResults: map[DirectedNodePair]pairResult{ - // Failures from failing hops[1]. + // Failures from failing Hops[1]. getTestPair(0, 1): failPairResult(0), getTestPair(1, 0): failPairResult(0), getTestPair(1, 2): failPairResult(0), diff --git a/routing/router.go b/routing/router.go index 1fea60ddbe..c6c2ecaf70 100644 --- a/routing/router.go +++ b/routing/router.go @@ -592,16 +592,16 @@ type probabilitySource func(route.Vertex, route.Vertex, lnwire.MilliSatoshi, // choosing a set of blinded paths to this node. type BlindedPathRestrictions struct { // MinDistanceFromIntroNode is the minimum number of _real_ (non-dummy) - // hops to include in a blinded path. Since we post-fix dummy hops, this + // Hops to include in a blinded path. Since we post-fix dummy Hops, this // is the minimum distance between our node and the introduction node // of the path. This doesn't include our node, so if the minimum is 1, // then the path will contain at minimum our node along with an // introduction node hop. MinDistanceFromIntroNode uint8 - // NumHops is the number of hops that each blinded path should consist - // of. If paths are found with a number of hops less that NumHops, then - // dummy hops will be padded on to the route. This value doesn't + // NumHops is the number of Hops that each blinded path should consist + // of. If paths are found with a number of Hops less that NumHops, then + // dummy Hops will be padded on to the route. This value doesn't // include our node, so if the maximum is 1, then the path will contain // our node along with an introduction node hop. NumHops uint8 @@ -659,7 +659,7 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex, totalRouteProbability = float64(1) ) - // For each set of hops on the path, get the success probability + // For each set of Hops on the path, get the success probability // of a forward between those two vertices and use that to // update the overall route probability. for j := 1; j < len(path); j++ { @@ -829,7 +829,7 @@ type LightningPayment struct { // RouteHints represents the different routing hints that can be used to // assist a payment in reaching its destination successfully. These - // hints will act as intermediate hops along the route. + // hints will act as intermediate Hops along the route. // // NOTE: This is optional unless required by the payment. When providing // multiple routes, ensure the hop hints within each route are chained @@ -1659,7 +1659,7 @@ func getEdgeUnifiers(source route.Vertex, hops []route.Vertex, // Allocate a list that will contain the edge unifiers for this route. unifiers := make([]*edgeUnifier, len(hops)) - // Traverse hops backwards to accumulate fees in the running amounts. + // Traverse Hops backwards to accumulate fees in the running amounts. for i := len(hops) - 1; i >= 0; i-- { toNode := hops[i] diff --git a/routing/router_test.go b/routing/router_test.go index 6f6f2e4342..20c1a21914 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -85,10 +85,10 @@ func (c *testCtx) getChannelIDFromAlias(t *testing.T, a, b string) uint64 { require.True(t, ok, "cannot find aliases for %s", b) channelIDMap, ok := c.channelIDs[vertexA] - require.True(t, ok, "cannot find channelID map %s(%s)", vertexA, a) + require.True(t, ok, "cannot find ChannelID map %s(%s)", vertexA, a) channelID, ok := channelIDMap[vertexB] - require.True(t, ok, "cannot find channelID using %s(%s)", vertexB, b) + require.True(t, ok, "cannot find ChannelID using %s(%s)", vertexB, b) return channelID } @@ -279,7 +279,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) { ) hops := route.Hops - require.Equal(t, 2, len(hops), "expected 2 hops") + require.Equal(t, 2, len(hops), "expected 2 Hops") require.Equalf(t, ctx.aliases["songoku"], hops[0].PubKeyBytes, @@ -337,7 +337,7 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) { require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) - // The route selected should have two hops + // The route selected should have two Hops require.Equal(t, 2, len(route.Hops), "incorrect route length") // The preimage should match up with the once created above. @@ -647,7 +647,7 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { require.NoErrorf(t, err, "unable to send payment: %v", payment.paymentHash) - // The route selected should have two hops + // The route selected should have two Hops require.Equal(t, 2, len(route.Hops), "incorrect route length") // The preimage should match up with the once created above. @@ -758,7 +758,7 @@ func TestSendPaymentErrorFeeInsufficientPrivateEdge(t *testing.T) { "failed to simulate error in the first payment attempt", ) - // The route selected should have two hops. Make sure that, + // The route selected should have two Hops. Make sure that, // path: roasbeef -> son goku -> sophon -> elst // path: roasbeef -> pham nuwen -> sophon -> elst // are not selected instead. @@ -884,7 +884,7 @@ func TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit(t *testing.T) { "failed to simulate error in the first payment attempt", ) - // The route selected should have three hops. Make sure that, + // The route selected should have three Hops. Make sure that, // path1: roasbeef -> son goku -> sophon -> elst // path2: roasbeef -> pham nuwen -> sophon -> elst // path3: roasbeef -> sophon -> (private channel) else @@ -977,7 +977,7 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // route properly routes around the failure we've introduced in the // graph. assertExpectedPath := func(retPreImage [32]byte, route *route.Route) { - // The route selected should have two hops + // The route selected should have two Hops require.Equal(t, 2, len(route.Hops), "incorrect route length") // The preimage should match up with the once created above. @@ -1182,7 +1182,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { payment.paymentHash) // This should succeed finally. The route selected should have two - // hops. + // Hops. require.Equal(t, 2, len(rt.Hops), "incorrect route length") // The preimage should match up with the once created above. @@ -1235,7 +1235,7 @@ func TestFindPathFeeWeighting(t *testing.T) { } // TestEmptyRoutesGenerateSphinxPacket tests that the generateSphinxPacket -// function is able to gracefully handle being passed a nil set of hops for the +// 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() @@ -1471,7 +1471,7 @@ func TestSendToRouteStructuredError(t *testing.T) { } // TestSendToRouteMaxHops asserts that SendToRoute fails when using a route that -// exceeds the maximum number of hops. +// exceeds the maximum number of Hops. func TestSendToRouteMaxHops(t *testing.T) { t.Parallel() @@ -1661,7 +1661,7 @@ func TestBuildRoute(t *testing.T) { noAmt := fn.None[lnwire.MilliSatoshi]() - // Test that we can't build a route when no hops are given. + // Test that we can't build a route when no Hops are given. hops = []route.Vertex{} _, err = ctx.router.BuildRoute( noAmt, hops, nil, 40, fn.None[[32]byte](), fn.None[[]byte](), @@ -1943,7 +1943,7 @@ func TestSenderAmtBackwardPass(t *testing.T) { MinHTLC: minHTLC, }, // In pathfinding, inbound fees are not - // populated for exit hops because the + // populated for exit Hops because the // newNodeEdgeUnifier enforces this. // This is important as otherwise we // would not fail the min HTLC check in diff --git a/routing/unified_edges.go b/routing/unified_edges.go index 6c44372e99..316c0e92a7 100644 --- a/routing/unified_edges.go +++ b/routing/unified_edges.go @@ -82,7 +82,7 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, return } - // Zero inbound fee for exit hops. + // Zero inbound fee for exit Hops. if !u.useInboundFees { inboundFee = models.InboundFee{} } @@ -106,7 +106,7 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g Graph) error { // Add this policy to the corresponding edgeUnifier. We default // to the clear hop payload size function because // `addGraphPolicies` is only used for cleartext intermediate - // hops in a route. + // Hops in a route. inboundFee := models.NewInboundFeeFromWire( channel.InboundFee, ) @@ -132,8 +132,8 @@ type unifiedEdge struct { // hopPayloadSize supplies an edge with the ability to calculate the // exact payload size if this edge would be included in a route. This - // is needed because hops of a blinded path differ in their payload - // structure compared to cleartext hops. + // is needed because Hops of a blinded path differ in their payload + // structure compared to cleartext Hops. hopPayloadSizeFn PayloadSizeFunc // blindedPayment if set, is the BlindedPayment that this edge was From 690380fd9b17a6d4e19194a09cca414bec789d6c Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 15:29:30 +0200 Subject: [PATCH 05/12] routing: update MissionControl API to take MCRoute instead of route.Route. This will be useful for a later commit when we only have access to MCRoute and want to report success to MC. --- routing/integrated_routing_context_test.go | 7 +++++-- routing/missioncontrol.go | 8 ++++---- routing/missioncontrol_test.go | 19 +++++++++---------- routing/mock_test.go | 8 ++++---- routing/payment_lifecycle.go | 5 +++-- routing/payment_lifecycle_test.go | 7 ++++--- routing/router.go | 4 ++-- routing/router_test.go | 11 +++++++---- 8 files changed, 38 insertions(+), 31 deletions(-) diff --git a/routing/integrated_routing_context_test.go b/routing/integrated_routing_context_test.go index 085ac1a9f3..e8db119f35 100644 --- a/routing/integrated_routing_context_test.go +++ b/routing/integrated_routing_context_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -267,7 +268,9 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32, if success { inFlightHtlcs++ - err := mc.ReportPaymentSuccess(pid, route) + err := mc.ReportPaymentSuccess( + pid, models.ToMCRoute(route), + ) if err != nil { c.t.Fatal(err) } @@ -286,7 +289,7 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32, // Failure, update mission control and retry. finalResult, err := mc.ReportPaymentFail( - pid, route, + pid, models.ToMCRoute(route), getNodeIndex(route, htlcResult.failureSource), htlcResult.failure, ) diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 3776cc1800..3e924f57ea 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -585,7 +585,7 @@ func (m *MissionControl) GetPairHistorySnapshot( // failure source. If it is nil, the failure source is unknown. This function // returns a reason if this failure is a final failure. In that case no further // payment attempts need to be made. -func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route, +func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *models.MCRoute, failureSourceIdx *int, failure lnwire.FailureMessage) ( *channeldb.FailureReason, error) { @@ -598,7 +598,7 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route, id: paymentID, failureSourceIdx: failureSourceIdx, failure: failure, - route: models.ToMCRoute(rt), + route: rt, } return m.processPaymentResult(result) @@ -607,7 +607,7 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route, // ReportPaymentSuccess reports a successful payment to mission control as input // for future probability estimates. func (m *MissionControl) ReportPaymentSuccess(paymentID uint64, - rt *route.Route) error { + rt *models.MCRoute) error { timestamp := m.cfg.clock.Now() @@ -616,7 +616,7 @@ func (m *MissionControl) ReportPaymentSuccess(paymentID uint64, timeReply: timestamp, id: paymentID, success: true, - route: models.ToMCRoute(rt), + route: rt, } _, err := m.processPaymentResult(result) diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index dad033ac44..0fbf1bfa6c 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -13,19 +14,17 @@ import ( ) var ( - mcTestRoute = &route.Route{ + mcTestRoute = &models.MCRoute{ SourcePubKey: mcTestSelf, - Hops: []*route.Hop{ + Hops: []*models.MCHop{ { - ChannelID: 1, - PubKeyBytes: route.Vertex{11}, - AmtToForward: 1000, - LegacyPayload: true, + ChannelID: 1, + PubKeyBytes: route.Vertex{11}, + AmtToFwd: 1000, }, { - ChannelID: 2, - PubKeyBytes: route.Vertex{12}, - LegacyPayload: true, + ChannelID: 2, + PubKeyBytes: route.Vertex{12}, }, }, } @@ -143,7 +142,7 @@ func (ctx *mcTestContext) expectP(amt lnwire.MilliSatoshi, expected float64) { func (ctx *mcTestContext) reportFailure(amt lnwire.MilliSatoshi, failure lnwire.FailureMessage) { - mcTestRoute.Hops[0].AmtToForward = amt + mcTestRoute.Hops[0].AmtToFwd = amt errorSourceIdx := 1 ctx.mc.ReportPaymentFail( diff --git a/routing/mock_test.go b/routing/mock_test.go index 35ac3ecd99..aaa28f9c0b 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -131,7 +131,7 @@ type mockMissionControlOld struct { var _ MissionControlQuerier = (*mockMissionControlOld)(nil) func (m *mockMissionControlOld) ReportPaymentFail( - paymentID uint64, rt *route.Route, + paymentID uint64, rt *models.MCRoute, failureSourceIdx *int, failure lnwire.FailureMessage) ( *channeldb.FailureReason, error) { @@ -146,7 +146,7 @@ func (m *mockMissionControlOld) ReportPaymentFail( } func (m *mockMissionControlOld) ReportPaymentSuccess(paymentID uint64, - rt *route.Route) error { + rt *models.MCRoute) error { return nil } @@ -660,7 +660,7 @@ type mockMissionControl struct { var _ MissionControlQuerier = (*mockMissionControl)(nil) func (m *mockMissionControl) ReportPaymentFail( - paymentID uint64, rt *route.Route, + paymentID uint64, rt *models.MCRoute, failureSourceIdx *int, failure lnwire.FailureMessage) ( *channeldb.FailureReason, error) { @@ -675,7 +675,7 @@ func (m *mockMissionControl) ReportPaymentFail( } func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64, - rt *route.Route) error { + rt *models.MCRoute) error { args := m.Called(paymentID, rt) return args.Error(0) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 93b214bdea..6d08cbd690 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -563,7 +563,7 @@ func (p *paymentLifecycle) collectResult(attempt *channeldb.HTLCAttempt) ( // Report success to mission control. err = p.router.cfg.MissionControl.ReportPaymentSuccess( - attempt.AttemptID, &attempt.Route, + attempt.AttemptID, models.ToMCRoute(&attempt.Route), ) if err != nil { log.Errorf("Error reporting payment success to mc: %v", err) @@ -849,7 +849,8 @@ func (p *paymentLifecycle) handleSwitchErr(attempt *channeldb.HTLCAttempt, // Report outcome to mission control. reason, err := p.router.cfg.MissionControl.ReportPaymentFail( - attemptID, &attempt.Route, srcIdx, msg, + attemptID, models.ToMCRoute(&attempt.Route), srcIdx, + msg, ) if err != nil { log.Errorf("Error reporting payment result to mc: %v", diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 315c1bad58..f04ecc60a5 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lnmock" @@ -1514,7 +1515,7 @@ func TestCollectResultExitOnSettleErr(t *testing.T) { // Once the result is received, `ReportPaymentSuccess` should be // called. m.missionControl.On("ReportPaymentSuccess", - attempt.AttemptID, &attempt.Route, + attempt.AttemptID, models.ToMCRoute(&attempt.Route), ).Return(nil).Once() // Now mock an error being returned from `SettleAttempt`. @@ -1561,7 +1562,7 @@ func TestCollectResultSuccess(t *testing.T) { // Once the result is received, `ReportPaymentSuccess` should be // called. m.missionControl.On("ReportPaymentSuccess", - attempt.AttemptID, &attempt.Route, + attempt.AttemptID, models.ToMCRoute(&attempt.Route), ).Return(nil).Once() // Now the settled htlc being returned from `SettleAttempt`. @@ -1609,7 +1610,7 @@ func TestCollectResultAsyncSuccess(t *testing.T) { // Once the result is received, `ReportPaymentSuccess` should be // called. m.missionControl.On("ReportPaymentSuccess", - attempt.AttemptID, &attempt.Route, + attempt.AttemptID, models.ToMCRoute(&attempt.Route), ).Return(nil).Once() // Now the settled htlc being returned from `SettleAttempt`. diff --git a/routing/router.go b/routing/router.go index c6c2ecaf70..2dbf2347f7 100644 --- a/routing/router.go +++ b/routing/router.go @@ -174,13 +174,13 @@ type MissionControlQuerier interface { // input for future probability estimates. It returns a bool indicating // whether this error is a final error and no further payment attempts // need to be made. - ReportPaymentFail(attemptID uint64, rt *route.Route, + ReportPaymentFail(attemptID uint64, rt *models.MCRoute, failureSourceIdx *int, failure lnwire.FailureMessage) ( *channeldb.FailureReason, error) // ReportPaymentSuccess reports a successful payment to mission control // as input for future probability estimates. - ReportPaymentSuccess(attemptID uint64, rt *route.Route) error + ReportPaymentSuccess(attemptID uint64, rt *models.MCRoute) error // GetProbability is expected to return the success probability of a // payment from fromNode along edge. diff --git a/routing/router_test.go b/routing/router_test.go index 20c1a21914..ad86af7bc3 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -2230,7 +2230,7 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) { }) missionControl.On("ReportPaymentSuccess", - mock.Anything, rt, + mock.Anything, models.ToMCRoute(rt), ).Return(nil) // Mock the control tower to return the mocked payment. @@ -2373,7 +2373,8 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) { // Mock the mission control to return a nil reason from reporting the // attempt failure. missionControl.On("ReportPaymentFail", - mock.Anything, rt, mock.Anything, mock.Anything, + mock.Anything, models.ToMCRoute(rt), mock.Anything, + mock.Anything, ).Return(nil, nil) // Mock the payment to return nil failure reason. @@ -2457,7 +2458,8 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) { failureReason := channeldb.FailureReasonPaymentDetails missionControl.On("ReportPaymentFail", - mock.Anything, rt, mock.Anything, mock.Anything, + mock.Anything, models.ToMCRoute(rt), mock.Anything, + mock.Anything, ).Return(&failureReason, nil) // Mock the control tower to return the mocked payment. @@ -2550,7 +2552,8 @@ func TestSendToRouteTempFailure(t *testing.T) { // Return a nil reason to mock a temporary failure. missionControl.On("ReportPaymentFail", - mock.Anything, rt, mock.Anything, mock.Anything, + mock.Anything, models.ToMCRoute(rt), mock.Anything, + mock.Anything, ).Return(nil, nil) // Expect a failed send to route. From df60d66c2a4100283b0c85527d4399a2c62dbb3b Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 12 Aug 2024 13:33:45 +0200 Subject: [PATCH 06/12] multi: start setting BlindedPathsInfo on Invoice struct Note that then new field is not yet persisted. --- channeldb/models/blinded_paths.go | 25 +++++++++++++++++++++++++ invoices/invoices.go | 9 +++++++++ lnrpc/invoicesrpc/addinvoice.go | 11 ++++++++++- routing/blindedpath/blinded_path.go | 14 ++++++++++---- 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 channeldb/models/blinded_paths.go diff --git a/channeldb/models/blinded_paths.go b/channeldb/models/blinded_paths.go new file mode 100644 index 0000000000..162a8f1e72 --- /dev/null +++ b/channeldb/models/blinded_paths.go @@ -0,0 +1,25 @@ +package models + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/routing/route" +) + +// BlindedPathsInfo is a map from an incoming blinding key to the associated +// blinded path info. An invoice may contain multiple blinded paths but each one +// will have a unique session key and thus a unique final ephemeral key. One +// receipt of a payment along a blinded path, we can use the incoming blinding +// key to thus identify which blinded path in the invoice was used. +type BlindedPathsInfo map[route.Vertex]*BlindedPathInfo + +// BlindedPathInfo holds information we may need regarding a blinded path +// included in an invoice. +type BlindedPathInfo struct { + // Route is the real route of the blinded path. + Route *MCRoute + + // SessionKey is the private key used as the first ephemeral key of the + // path. We can use this key to decrypt any data we encrypted for the + // path. + SessionKey *btcec.PrivateKey +} diff --git a/invoices/invoices.go b/invoices/invoices.go index c48629c583..641937324f 100644 --- a/invoices/invoices.go +++ b/invoices/invoices.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/feature" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -419,6 +420,14 @@ type Invoice struct { // HodlInvoice indicates whether the invoice should be held in the // Accepted state or be settled right away. HodlInvoice bool + + // BlindedPaths is a map from an incoming blinding key to the associated + // blinded path info. An invoice may contain multiple blinded paths but + // each one will have a unique session key and thus a unique final + // ephemeral key. One receipt of a payment along a blinded path, we can + // use the incoming blinding key to thus identify which blinded path in + // the invoice was used. + BlindedPaths models.BlindedPathsInfo } // HTLCSet returns the set of HTLCs belonging to setID and in the provided diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index 28ca856982..741163031e 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -500,6 +500,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, return nil, nil, err } + blindedPathMap := make(models.BlindedPathsInfo) if blind { blindCfg := invoice.BlindedPathCfg @@ -551,6 +552,13 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, options = append(options, zpay32.WithBlindedPaymentPath( path.Path, )) + + lastEphem := route.NewVertex(path.LastEphemeralKey) + + blindedPathMap[lastEphem] = &models.BlindedPathInfo{ + Route: path.Route, + SessionKey: path.SessionKey, + } } } else { options = append(options, zpay32.PaymentAddr(paymentAddr)) @@ -606,7 +614,8 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, PaymentAddr: paymentAddr, Features: invoiceFeatures, }, - HodlInvoice: invoice.HodlInvoice, + HodlInvoice: invoice.HodlInvoice, + BlindedPaths: blindedPathMap, } log.Tracef("[addinvoice] adding new invoice %v", diff --git a/routing/blindedpath/blinded_path.go b/routing/blindedpath/blinded_path.go index d25315a1a5..1dcd4a596e 100644 --- a/routing/blindedpath/blinded_path.go +++ b/routing/blindedpath/blinded_path.go @@ -126,6 +126,10 @@ type PathInfo struct { // This can be used to uniquely identify a path when an incoming payment // is received. LastEphemeralKey *btcec.PublicKey + + // Route is the real un-blinded route that was used to construct the + // Path. + Route *models.MCRoute } // BuildBlindedPaymentPaths uses the passed config to construct a set of blinded @@ -150,9 +154,9 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ([]*PathInfo, error) { // For each route returned, we will construct the associated blinded // payment path. - for _, route := range routes { + for _, r := range routes { // Extract the information we need from the route. - candidatePath := extractCandidatePath(route) + candidatePath := extractCandidatePath(r) // Pad the given route with dummy hops until the minimum number // of hops is met. @@ -162,16 +166,18 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ([]*PathInfo, error) { if errors.Is(err, errInvalidBlindedPath) { log.Debugf("Not using route (%s) as a blinded path "+ "since it resulted in an invalid blinded path", - route) + r) continue } else if err != nil { log.Errorf("Not using route (%s) as a blinded path: %v", - route, err) + r, err) continue } + path.Route = models.ToMCRoute(r) + log.Debugf("Route selected for blinded path: %s", candidatePath) paths = append(paths, path) From 952da0aa1c3f9f5346bc5f209c8bb32493616d15 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 12 Aug 2024 14:56:30 +0200 Subject: [PATCH 07/12] channeldb: serialise blinded path info for bbolt invoice --- channeldb/invoice_test.go | 104 +++++++++++++++ channeldb/invoices.go | 69 +++++++--- channeldb/models/blinded_path_test.go | 126 ++++++++++++++++++ channeldb/models/blinded_paths.go | 184 ++++++++++++++++++++++++++ invoices/invoices.go | 14 +- invoices/invoices_test.go | 6 +- 6 files changed, 482 insertions(+), 21 deletions(-) create mode 100644 channeldb/models/blinded_path_test.go diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index 3fe6b668e8..f723204dcf 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -5,9 +5,11 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/channeldb/models" invpkg "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -103,3 +105,105 @@ func TestEncodeDecodeAmpInvoiceState(t *testing.T) { // The two states should match. require.Equal(t, ampState, ampState2) } + +func genPubKey(t *testing.T) route.Vertex { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return route.NewVertex(pk.PubKey()) +} + +func genPrivKey(t *testing.T) *btcec.PrivateKey { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return pk +} + +// TestEncodeDecodeBlindedPathMap asserts that the nested TLV +// encoding+decoding for the BlindedPathsInfo struct works as expected. +func TestEncodeDecodeBlindedPathMap(t *testing.T) { + t.Parallel() + + blindedPath := models.BlindedPathsInfo{ + genPubKey(t): { + Route: &models.MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 10000, + Hops: []*models.MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 60, + ChannelID: 40, + }, + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 50, + ChannelID: 10, + HasBlindingPoint: true, + }, + }, + }, + SessionKey: genPrivKey(t), + }, + genPubKey(t): { + Route: &models.MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 30, + Hops: []*models.MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 50, + ChannelID: 10, + HasBlindingPoint: true, + }, + }, + }, + SessionKey: genPrivKey(t), + }, + } + + // We'll now make a sample invoice stream, and use that to encode the + // amp state we created above. + tlvStream, err := tlv.NewStream( + tlv.MakeDynamicRecord( + blindedPathsInfoType, &blindedPath, + blindedPathMapRecordSize(blindedPath), + models.BlindedPathInfoEncoder, + models.BlindedPathInfoDecoder, + ), + ) + require.Nil(t, err) + + // Next encode the stream into a set of raw bytes. + var b bytes.Buffer + err = tlvStream.Encode(&b) + require.Nil(t, err) + + // Now create a new blank blinded path map, which we'll use to decode + // the bytes into. + blindedPath2 := make(models.BlindedPathsInfo) + + // Decode from the raw stream into this blank mpa. + tlvStream, err = tlv.NewStream( + tlv.MakeDynamicRecord( + blindedPathsInfoType, &blindedPath2, nil, + models.BlindedPathInfoEncoder, + models.BlindedPathInfoDecoder, + ), + ) + require.Nil(t, err) + + err = tlvStream.Decode(&b) + require.Nil(t, err) + + // The two states should match. + require.Len(t, blindedPath2, len(blindedPath)) + + for k, m1 := range blindedPath { + m2, ok := blindedPath2[k] + require.True(t, ok) + + require.Equal(t, m1, m2) + } +} diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 9da504a5d8..c1588caf70 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -109,22 +109,23 @@ const ( // prevents against the database being rolled back to an older // format where the surrounding logic might assume a different set of // fields are known. - memoType tlv.Type = 0 - payReqType tlv.Type = 1 - createTimeType tlv.Type = 2 - settleTimeType tlv.Type = 3 - addIndexType tlv.Type = 4 - settleIndexType tlv.Type = 5 - preimageType tlv.Type = 6 - valueType tlv.Type = 7 - cltvDeltaType tlv.Type = 8 - expiryType tlv.Type = 9 - paymentAddrType tlv.Type = 10 - featuresType tlv.Type = 11 - invStateType tlv.Type = 12 - amtPaidType tlv.Type = 13 - hodlInvoiceType tlv.Type = 14 - invoiceAmpStateType tlv.Type = 15 + memoType tlv.Type = 0 + payReqType tlv.Type = 1 + createTimeType tlv.Type = 2 + settleTimeType tlv.Type = 3 + addIndexType tlv.Type = 4 + settleIndexType tlv.Type = 5 + preimageType tlv.Type = 6 + valueType tlv.Type = 7 + cltvDeltaType tlv.Type = 8 + expiryType tlv.Type = 9 + paymentAddrType tlv.Type = 10 + featuresType tlv.Type = 11 + invStateType tlv.Type = 12 + amtPaidType tlv.Type = 13 + hodlInvoiceType tlv.Type = 14 + invoiceAmpStateType tlv.Type = 15 + blindedPathsInfoType tlv.Type = 16 // A set of tlv type definitions used to serialize the invoice AMP // state along-side the main invoice body. @@ -1182,6 +1183,26 @@ func putInvoice(invoices, invoiceIndex, payAddrIndex, addIndex kvdb.RwBucket, return nextAddSeqNo, nil } +func blindedPathMapRecordSize(a models.BlindedPathsInfo) func() uint64 { + var ( + b bytes.Buffer + buf [8]byte + ) + + // We know that encoding works since the tests pass in the build this + // file is checked into, so we'll simplify things and simply encode it + // ourselves then report the total amount of bytes used. + if err := models.BlindedPathInfoEncoder(&b, a, &buf); err != nil { + // This should never error out, but we log it just in case it + // does. + log.Errorf("encoding the blinded path map failed: %v", err) + } + + return func() uint64 { + return uint64(len(b.Bytes())) + } +} + // recordSize returns the amount of bytes this TLV record will occupy when // encoded. func ampRecordSize(a *invpkg.AMPInvoiceState) func() uint64 { @@ -1277,6 +1298,14 @@ func serializeInvoice(w io.Writer, i *invpkg.Invoice) error { ampRecordSize(&i.AMPState), ampStateEncoder, ampStateDecoder, ), + + // Blinded Path info. + tlv.MakeDynamicRecord( + blindedPathsInfoType, &i.BlindedPaths, + blindedPathMapRecordSize(i.BlindedPaths), + models.BlindedPathInfoEncoder, + models.BlindedPathInfoDecoder, + ), ) if err != nil { return err @@ -1644,6 +1673,7 @@ func deserializeInvoice(r io.Reader) (invpkg.Invoice, error) { var i invpkg.Invoice i.AMPState = make(invpkg.AMPInvoiceState) + i.BlindedPaths = make(models.BlindedPathsInfo) tlvStream, err := tlv.NewStream( // Memo and payreq. tlv.MakePrimitiveRecord(memoType, &i.Memo), @@ -1674,6 +1704,13 @@ func deserializeInvoice(r io.Reader) (invpkg.Invoice, error) { invoiceAmpStateType, &i.AMPState, nil, ampStateEncoder, ampStateDecoder, ), + + // Blinded Path info. + tlv.MakeDynamicRecord( + blindedPathsInfoType, &i.BlindedPaths, nil, + models.BlindedPathInfoEncoder, + models.BlindedPathInfoDecoder, + ), ) if err != nil { return i, err diff --git a/channeldb/models/blinded_path_test.go b/channeldb/models/blinded_path_test.go new file mode 100644 index 0000000000..93c6112158 --- /dev/null +++ b/channeldb/models/blinded_path_test.go @@ -0,0 +1,126 @@ +package models + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +func genPubKey(t *testing.T) route.Vertex { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return route.NewVertex(pk.PubKey()) +} + +func genPrivKey(t *testing.T) *btcec.PrivateKey { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return pk +} + +// TestBlindedPathInfoSerialise tests the BlindedPathInfo TLV encoder and +// decoder. +func TestBlindedPathInfoSerialise(t *testing.T) { + info := &BlindedPathInfo{ + Route: &MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 600, + Hops: []*MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 60, + ChannelID: 40, + }, + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 50, + ChannelID: 10, + HasBlindingPoint: true, + }, + }, + }, + SessionKey: genPrivKey(t), + } + + var b bytes.Buffer + err := info.Serialize(&b) + require.NoError(t, err) + + info2, err := DeserializeBlindedPathInfo(&b) + require.NoError(t, err) + + compareBlindedPathInfo(t, info, info2) + + infoSet := &BlindedPathsInfo{ + genPubKey(t): &BlindedPathInfo{ + Route: &MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 600, + Hops: []*MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 60, + ChannelID: 40, + }, + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 50, + ChannelID: 10, + HasBlindingPoint: true, + }, + }, + }, + SessionKey: genPrivKey(t), + }, + genPubKey(t): &BlindedPathInfo{ + Route: &MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 60, + Hops: []*MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 60, + ChannelID: 400, + }, + }, + }, + SessionKey: genPrivKey(t), + }, + } + + var b1 bytes.Buffer + err = BlindedPathInfoEncoder(&b1, infoSet, nil) + require.NoError(t, err) + + infoSet2 := make(BlindedPathsInfo) + err = BlindedPathInfoDecoder( + &b1, &infoSet2, nil, uint64(len(b1.Bytes())), + ) + require.NoError(t, err) +} + +func compareBlindedPathInfo(t *testing.T, info, info2 *BlindedPathInfo) { + require.EqualValues(t, info.SessionKey, info2.SessionKey) + require.EqualValues( + t, info.Route.SourcePubKey, info2.Route.SourcePubKey, + ) + require.EqualValues( + t, info.Route.TotalAmount, info2.Route.TotalAmount, + ) + require.Len(t, info.Route.Hops, len(info2.Route.Hops)) + + for i, hop := range info.Route.Hops { + hop2 := info2.Route.Hops[i] + require.EqualValues(t, hop.PubKeyBytes, hop2.PubKeyBytes) + require.EqualValues(t, hop.ChannelID, hop2.ChannelID) + require.EqualValues( + t, hop.HasBlindingPoint, hop2.HasBlindingPoint, + ) + require.EqualValues(t, hop.AmtToFwd, hop2.AmtToFwd) + } +} diff --git a/channeldb/models/blinded_paths.go b/channeldb/models/blinded_paths.go index 162a8f1e72..4e3c45ef0c 100644 --- a/channeldb/models/blinded_paths.go +++ b/channeldb/models/blinded_paths.go @@ -1,8 +1,19 @@ package models import ( + "bytes" + "errors" + "io" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + blindedPathInfoRouteType tlv.Type = 0 + blindedPathInfoSessionKeyType tlv.Type = 1 ) // BlindedPathsInfo is a map from an incoming blinding key to the associated @@ -12,6 +23,85 @@ import ( // key to thus identify which blinded path in the invoice was used. type BlindedPathsInfo map[route.Vertex]*BlindedPathInfo +// BlindedPathInfoEncoder is a custom TLV encoder for a BlindedPathInfo +// record. +func BlindedPathInfoEncoder(w io.Writer, val interface{}, _ *[8]byte) error { + if v, ok := val.(*BlindedPathsInfo); ok { + for key, info := range *v { + // Write 33 byte key. + if err := wire.WriteVarBytes(w, 0, key[:]); err != nil { + return err + } + + // Serialise the info. + var infoBytes bytes.Buffer + err := info.Serialize(&infoBytes) + if err != nil { + return err + } + + // Write the length of the serialised info. + err = wire.WriteVarInt(w, 0, uint64(infoBytes.Len())) + if err != nil { + return err + } + + // Finally, write the serialise info. + if _, err := w.Write(infoBytes.Bytes()); err != nil { + return err + } + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "*BlindedPathsInfo") +} + +// BlindedPathInfoDecoder is a custom TLV decoder for a BlindedPathInfo +// record. +func BlindedPathInfoDecoder(r io.Reader, val interface{}, _ *[8]byte, + l uint64) error { + + if v, ok := val.(*BlindedPathsInfo); ok { + for { + key, err := wire.ReadVarBytes(r, 0, 66000, "[]byte") + switch { + // We'll silence an EOF when zero bytes remain, meaning + // the stream was cleanly encoded. + case errors.Is(err, io.EOF): + return nil + + // Other unexpected errors. + case err != nil: + return err + } + + // Read the length of the serialised info. + infoLen, err := wire.ReadVarInt(r, 0) + if err != nil { + return err + } + + // Create a limited reader using the info length. + lr := io.LimitReader(r, int64(infoLen)) + + // Deserialize the path info using the limited reader. + info, err := DeserializeBlindedPathInfo(lr) + if err != nil { + return err + } + + var k route.Vertex + copy(k[:], key) + + (*v)[k] = info + } + } + + return tlv.NewTypeForDecodingErr(val, "*BlindedPathsInfo", l, l) +} + // BlindedPathInfo holds information we may need regarding a blinded path // included in an invoice. type BlindedPathInfo struct { @@ -23,3 +113,97 @@ type BlindedPathInfo struct { // path. SessionKey *btcec.PrivateKey } + +// Copy makes a deep copy of the BlindedPathInfo. +func (i *BlindedPathInfo) Copy() *BlindedPathInfo { + hops := make([]*MCHop, len(i.Route.Hops)) + for i, hop := range i.Route.Hops { + hops[i] = &MCHop{ + ChannelID: hop.ChannelID, + AmtToFwd: hop.AmtToFwd, + HasBlindingPoint: hop.HasBlindingPoint, + } + + copy(hops[i].PubKeyBytes[:], hop.PubKeyBytes[:]) + } + + r := &MCRoute{ + TotalAmount: i.Route.TotalAmount, + Hops: hops, + } + copy(r.SourcePubKey[:], i.Route.SourcePubKey[:]) + + return &BlindedPathInfo{ + Route: r, + SessionKey: btcec.PrivKeyFromScalar(&i.SessionKey.Key), + } +} + +// Serialize serializes the BlindedPathInfo into a TLV stream and writes the +// resulting bytes to the given io.Writer. +func (i *BlindedPathInfo) Serialize(w io.Writer) error { + var routeBuffer bytes.Buffer + if err := i.Route.Serialize(&routeBuffer); err != nil { + return err + } + + var ( + privKeyBytes = i.SessionKey.Serialize() + routeBytes = routeBuffer.Bytes() + ) + + stream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + blindedPathInfoRouteType, &routeBytes, + ), + tlv.MakePrimitiveRecord( + blindedPathInfoSessionKeyType, &privKeyBytes, + ), + ) + if err != nil { + return err + } + + return stream.Encode(w) +} + +// DeserializeBlindedPathInfo attempts to deserialize a BlindedPathInfo from the +// given io.Reader. +func DeserializeBlindedPathInfo(r io.Reader) (*BlindedPathInfo, error) { + var ( + privKeyBytes []byte + routeBytes []byte + ) + + stream, err := tlv.NewStream( + tlv.MakePrimitiveRecord( + blindedPathInfoRouteType, &routeBytes, + ), + tlv.MakePrimitiveRecord( + blindedPathInfoSessionKeyType, &privKeyBytes, + ), + ) + if err != nil { + return nil, err + } + + if err := stream.Decode(r); err != nil { + return nil, err + } + + routeReader := bytes.NewReader(routeBytes) + mcRoute, err := DeserializeRoute(routeReader) + if err != nil { + return nil, err + } + + sessionKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) + if err != nil { + return nil, err + } + + return &BlindedPathInfo{ + Route: mcRoute, + SessionKey: sessionKey, + }, nil +} diff --git a/invoices/invoices.go b/invoices/invoices.go index 641937324f..7595b97248 100644 --- a/invoices/invoices.go +++ b/invoices/invoices.go @@ -846,8 +846,9 @@ func CopyInvoice(src *Invoice) (*Invoice, error) { Htlcs: make( map[CircuitKey]*InvoiceHTLC, len(src.Htlcs), ), - AMPState: make(map[SetID]InvoiceStateAMP), - HodlInvoice: src.HodlInvoice, + AMPState: make(map[SetID]InvoiceStateAMP), + BlindedPaths: make(models.BlindedPathsInfo), + HodlInvoice: src.HodlInvoice, } dest.Terms.Features = src.Terms.Features.Clone() @@ -861,7 +862,7 @@ func CopyInvoice(src *Invoice) (*Invoice, error) { dest.Htlcs[k] = v.Copy() } - // Lastly, copy the amp invoice state. + // Copy the amp invoice state. for k, v := range src.AMPState { ampInvState, err := v.copy() if err != nil { @@ -871,6 +872,13 @@ func CopyInvoice(src *Invoice) (*Invoice, error) { dest.AMPState[k] = ampInvState } + // Lastly, copy the blinded path info. + for k, info := range src.BlindedPaths { + info := info.Copy() + + dest.BlindedPaths[k] = info + } + return &dest, nil } diff --git a/invoices/invoices_test.go b/invoices/invoices_test.go index b6efd6e557..8e7c16039d 100644 --- a/invoices/invoices_test.go +++ b/invoices/invoices_test.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/sqldb" "github.com/stretchr/testify/require" ) @@ -75,8 +76,9 @@ func randInvoice(value lnwire.MilliSatoshi) (*invpkg.Invoice, error) { Value: value, Features: emptyFeatures, }, - Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, - AMPState: map[invpkg.SetID]invpkg.InvoiceStateAMP{}, + Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, + AMPState: map[invpkg.SetID]invpkg.InvoiceStateAMP{}, + BlindedPaths: map[route.Vertex]*models.BlindedPathInfo{}, } i.Memo = []byte("memo") From 098080972465924fbd5b9193f7a83d821227b417 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 16:00:42 +0200 Subject: [PATCH 08/12] go.mod+invoices: update sql_store to use new blinded path tables --- go.mod | 2 + go.sum | 4 +- invoices/sql_store.go | 136 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index d4a8084051..cd197c234f 100644 --- a/go.mod +++ b/go.mod @@ -205,6 +205,8 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display +replace github.com/lightningnetwork/lnd/sqldb => github.com/ellemouton/lnd/sqldb v0.0.0-20240909090537-0ec3cf6accd0 + // If you change this please also update .github/pull_request_template.md, // docs/INSTALL.md and GO_IMAGE in lnrpc/gen_protos_docker.sh. go 1.22.6 diff --git a/go.sum b/go.sum index b869d2d097..4b401abc0a 100644 --- a/go.sum +++ b/go.sum @@ -185,6 +185,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ellemouton/lnd/sqldb v0.0.0-20240909090537-0ec3cf6accd0 h1:GpP23nH1VQJjEE0gZiuexS5q0aydstTlHOaHNZGv5Dw= +github.com/ellemouton/lnd/sqldb v0.0.0-20240909090537-0ec3cf6accd0/go.mod h1:4cQOkdymlZ1znnjuRNvMoatQGJkRneTj2CoPSPaQhWo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -461,8 +463,6 @@ github.com/lightningnetwork/lnd/kvdb v1.4.10 h1:vK89IVv1oVH9ubQWU+EmoCQFeVRaC8kf github.com/lightningnetwork/lnd/kvdb v1.4.10/go.mod h1:J2diNABOoII9UrMnxXS5w7vZwP7CA1CStrl8MnIrb3A= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= -github.com/lightningnetwork/lnd/sqldb v1.0.4 h1:9cMwPxcrLQG8UmyZO4q8SpR7NmxSwBMbj3AispdcwHg= -github.com/lightningnetwork/lnd/sqldb v1.0.4/go.mod h1:4cQOkdymlZ1znnjuRNvMoatQGJkRneTj2CoPSPaQhWo= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.2.6 h1:icvQG2yDr6k3ZuZzfRdG3EJp6pHurcuh3R6dg0gv/Mw= diff --git a/invoices/sql_store.go b/invoices/sql_store.go index eb465eabb4..d134dafcc8 100644 --- a/invoices/sql_store.go +++ b/invoices/sql_store.go @@ -10,12 +10,14 @@ import ( "strconv" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/sqldb" "github.com/lightningnetwork/lnd/sqldb/sqlc" ) @@ -75,6 +77,19 @@ type SQLInvoiceQueries interface { //nolint:interfacebloat DeleteCanceledInvoices(ctx context.Context) (sql.Result, error) + // Blinded path specific methods. + FetchBlindedPathHops(ctx context.Context, blindedPathID int64) ( + []sqlc.BlindedPathHop, error) + + FetchBlindedPaths(ctx context.Context, invoiceID int64) ( + []sqlc.BlindedPath, error) + + InsertBlindedPath(ctx context.Context, + arg sqlc.InsertBlindedPathParams) (int64, error) + + InsertBlindedPathHop(ctx context.Context, + arg sqlc.InsertBlindedPathHopParams) error + // AMP sub invoice specific methods. UpsertAMPSubInvoice(ctx context.Context, arg sqlc.UpsertAMPSubInvoiceParams) (sql.Result, error) @@ -289,6 +304,36 @@ func (i *SQLStore) AddInvoice(ctx context.Context, } } + for lastEphem, path := range newInvoice.BlindedPaths { + pathParams := sqlc.InsertBlindedPathParams{ + InvoiceID: invoiceID, + LastEphemeralPub: lastEphem[:], + SessionKey: path.SessionKey.Serialize(), + IntroductionNode: path.Route.SourcePubKey[:], + AmountMsat: int64(path.Route.TotalAmount), + } + + pathID, err := db.InsertBlindedPath(ctx, pathParams) + if err != nil { + return err + } + + for hopIndex, hop := range path.Route.Hops { + hopParams := sqlc.InsertBlindedPathHopParams{ + BlindedPathID: pathID, + HopIndex: int64(hopIndex), + ChannelID: int64(hop.ChannelID), + NodePubKey: hop.PubKeyBytes[:], + AmountToFwd: int64(hop.AmtToFwd), + } + + err = db.InsertBlindedPathHop(ctx, hopParams) + if err != nil { + return err + } + } + } + // Finally add a new event for this invoice. return db.OnInvoiceCreated(ctx, sqlc.OnInvoiceCreatedParams{ AddedAt: newInvoice.CreationDate.UTC(), @@ -422,6 +467,72 @@ func (i *SQLStore) fetchInvoice(ctx context.Context, return invoice, nil } +func fetchBlindedPaths(ctx context.Context, db SQLInvoiceQueries, + invoiceID int64) (models.BlindedPathsInfo, error) { + + blindedPathRows, err := db.FetchBlindedPaths(ctx, invoiceID) + if err != nil { + return nil, err + } + + if len(blindedPathRows) == 0 { + return nil, err + } + + info := make(models.BlindedPathsInfo, len(blindedPathRows)) + + for _, sqlPath := range blindedPathRows { + lastEphem, err := btcec.ParsePubKey(sqlPath.LastEphemeralPub) + if err != nil { + return nil, err + } + + sessKey, _ := btcec.PrivKeyFromBytes(sqlPath.SessionKey) + if err != nil { + return nil, err + } + + introNode, err := btcec.ParsePubKey(sqlPath.IntroductionNode) + if err != nil { + return nil, err + } + + sqlHops, err := db.FetchBlindedPathHops(ctx, sqlPath.ID) + if err != nil { + return nil, err + } + + hops := make([]*models.MCHop, len(sqlHops)) + for i, sqlHop := range sqlHops { + hopKey, err := btcec.ParsePubKey(sqlHop.NodePubKey) + if err != nil { + return nil, err + } + hops[i] = &models.MCHop{ + ChannelID: uint64(sqlHop.ChannelID), + PubKeyBytes: route.NewVertex(hopKey), + AmtToFwd: lnwire.MilliSatoshi( + sqlHop.AmountToFwd, + ), + HasBlindingPoint: true, + } + } + + info[route.NewVertex(lastEphem)] = &models.BlindedPathInfo{ + Route: &models.MCRoute{ + SourcePubKey: route.NewVertex(introNode), + TotalAmount: lnwire.MilliSatoshi( + sqlPath.AmountMsat, + ), + Hops: hops, + }, + SessionKey: sessKey, + } + } + + return info, err +} + // fetchAmpState fetches the AMP state for the invoice with the given ID. // Optional setID can be provided to fetch the state for a specific AMP HTLC // set. If setID is nil then we'll fetch the state for all AMP sub invoices. If @@ -1498,6 +1609,18 @@ func fetchInvoiceData(ctx context.Context, db SQLInvoiceQueries, return hash, invoice, nil } + // If this is a blinded invoice, we'll fetch the blinded path info if it + // exists. + if invoice.IsBlinded() { + invoiceID := int64(invoice.AddIndex) + blindedPathInfo, err := fetchBlindedPaths(ctx, db, invoiceID) + if err != nil { + return nil, nil, err + } + + invoice.BlindedPaths = blindedPathInfo + } + // Otherwise simply fetch the invoice HTLCs. htlcs, err := getInvoiceHtlcs(ctx, db, row.ID) if err != nil { @@ -1655,12 +1778,13 @@ func unmarshalInvoice(row sqlc.Invoice) (*lntypes.Hash, *Invoice, Value: lnwire.MilliSatoshi(row.AmountMsat), PaymentAddr: paymentAddr, }, - AddIndex: uint64(row.ID), - State: ContractState(row.State), - AmtPaid: lnwire.MilliSatoshi(row.AmountPaidMsat), - Htlcs: make(map[models.CircuitKey]*InvoiceHTLC), - AMPState: AMPInvoiceState{}, - HodlInvoice: row.IsHodl, + AddIndex: uint64(row.ID), + State: ContractState(row.State), + AmtPaid: lnwire.MilliSatoshi(row.AmountPaidMsat), + Htlcs: make(map[models.CircuitKey]*InvoiceHTLC), + AMPState: AMPInvoiceState{}, + HodlInvoice: row.IsHodl, + BlindedPaths: models.BlindedPathsInfo{}, } return &hash, invoice, nil From 18c4155614b43e1acf662c1ea49b2e5efa2078ae Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 11:17:49 +0200 Subject: [PATCH 09/12] invoices: add test to persist and retrieve invoice with blinded path --- invoices/invoices_test.go | 91 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/invoices/invoices_test.go b/invoices/invoices_test.go index 8e7c16039d..45325a058a 100644 --- a/invoices/invoices_test.go +++ b/invoices/invoices_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" @@ -35,6 +36,15 @@ var ( lnwire.Features, ) + blindedPathFeatures = lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector( + lnwire.TLVOnionPayloadOptional, + lnwire.RouteBlindingOptional, + lnwire.Bolt11BlindedPathsRequired, + ), + lnwire.Features, + ) + testNow = time.Unix(1, 0) ) @@ -189,6 +199,10 @@ func TestInvoices(t *testing.T) { name: "AddInvoiceWithHTLCs", test: testAddInvoiceWithHTLCs, }, + { + name: "AddInvoiceWithBlindedPathInfo", + test: testAddInvoiceWithBlindedPathInfo, + }, { name: "SetIDIndex", test: testSetIDIndex, @@ -2088,6 +2102,83 @@ func TestHTLCSet(t *testing.T) { } } +func testAddInvoiceWithBlindedPathInfo(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + testInvoice, err := randInvoice(1000) + require.Nil(t, err) + + testInvoice.Terms.Features = blindedPathFeatures + + path := models.BlindedPathsInfo{ + genPubKey(t): { + Route: &models.MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 10000, + Hops: []*models.MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 60, + ChannelID: 40, + HasBlindingPoint: true, + }, + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 50, + ChannelID: 10, + HasBlindingPoint: true, + }, + }, + }, + SessionKey: genPrivKey(t), + }, + genPubKey(t): { + Route: &models.MCRoute{ + SourcePubKey: genPubKey(t), + TotalAmount: 30, + Hops: []*models.MCHop{ + { + PubKeyBytes: genPubKey(t), + AmtToFwd: 50, + ChannelID: 10, + HasBlindingPoint: true, + }, + }, + }, + SessionKey: genPrivKey(t), + }, + } + testInvoice.BlindedPaths = path + + ctx := context.Background() + + payHash := testInvoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctx, testInvoice, payHash) + require.NoError(t, err) + + inv, err := db.LookupInvoice(ctx, invpkg.InvoiceRefByHash(payHash)) + require.NoError(t, err) + + require.EqualValues(t, path, inv.BlindedPaths) +} + +func genPubKey(t *testing.T) route.Vertex { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return route.NewVertex(pk.PubKey()) +} + +func genPrivKey(t *testing.T) *btcec.PrivateKey { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + + return pk +} + // testAddInvoiceWithHTLCs asserts that you can't insert an invoice that already // has HTLCs. func testAddInvoiceWithHTLCs(t *testing.T, From 59f8babffd6713bcd705aaa64f653980830fe1c4 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 11:44:47 +0200 Subject: [PATCH 10/12] multi: report blinded path receives to Mission Control in the new "blinded-paths" namespace. --- invoices/interface.go | 5 ++++ invoices/invoiceregistry.go | 42 +++++++++++++++++++++++++++++ invoices/test_utils_test.go | 4 +++ invoices/update.go | 12 +++++---- routing/missioncontrol_test.go | 4 ++- server.go | 49 ++++++++++++++++++++-------------- 6 files changed, 90 insertions(+), 26 deletions(-) diff --git a/invoices/interface.go b/invoices/interface.go index 4f5e08e32e..890478884a 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lntypes" @@ -114,6 +115,10 @@ type Payload interface { // TotalAmtMsat returns the total amount sent to the final hop, as set // by the payee. TotalAmtMsat() lnwire.MilliSatoshi + + // BlindingPoint returns the route blinding point parsed from the onion + // payload. + BlindingPoint() *btcec.PublicKey } // InvoiceQuery represents a query to the invoice database. The query allows a diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 517e237ca6..3a8bf6dd33 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -8,11 +8,13 @@ import ( "sync/atomic" "time" + "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/routing/route" ) var ( @@ -78,6 +80,11 @@ type RegistryConfig struct { // HtlcInterceptor is an interface that allows the invoice registry to // let clients intercept invoices before they are settled. HtlcInterceptor HtlcInterceptor + + // ReportBlindedPathReceive can be used to indicate the successful + // receive along a chosen blinded path. + ReportBlindedPathReceive func(invoiceAddIndex uint64, + rt *models.MCRoute) error } // htlcReleaseEvent describes an htlc auto-release event. It is used to release @@ -937,6 +944,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, metadata: payload.Metadata(), pathID: payload.PathID(), totalAmtMsat: payload.TotalAmtMsat(), + blindingPoint: payload.BlindingPoint(), } switch { @@ -1080,8 +1088,12 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( var ( resolution HtlcResolution updateSubscribers bool + blindedPath *models.BlindedPathInfo + addIndex uint64 ) callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) { + addIndex = inv.AddIndex + updateDesc, res, err := updateInvoice(ctx, inv) if err != nil { return nil, err @@ -1094,6 +1106,24 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( // Assign resolution to outer scope variable. resolution = res + if ctx.blindingPoint == nil { + return updateDesc, nil + } + + // If this invoice was created before we started persisting the + // blinded path info for an invoice, exit. + if inv.BlindedPaths == nil { + return updateDesc, nil + } + + path, ok := inv.BlindedPaths[route.NewVertex(ctx.blindingPoint)] + if !ok { + return nil, fmt.Errorf("expected a blinding path " + + "matching the received ephemeral key") + } + + blindedPath = path + return updateDesc, nil } @@ -1131,6 +1161,18 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( return nil, nil, err } + if blindedPath != nil { + // Regardless of what we will do with the HTLC, we want to + // report to mission control that the blinded path route it + // chose successfully reached us. + err = i.cfg.ReportBlindedPathReceive( + addIndex, blindedPath.Route, + ) + if err != nil { + return nil, nil, err + } + } + var invoiceToExpire invoiceExpiry log.Tracef("Settlement resolution: %T %v", resolution, resolution) diff --git a/invoices/test_utils_test.go b/invoices/test_utils_test.go index fe69cad6f1..8bc69c028d 100644 --- a/invoices/test_utils_test.go +++ b/invoices/test_utils_test.go @@ -46,6 +46,10 @@ func (p *mockPayload) PathID() *chainhash.Hash { return p.pathID } +func (p *mockPayload) BlindingPoint() *btcec.PublicKey { + return nil +} + func (p *mockPayload) TotalAmtMsat() lnwire.MilliSatoshi { return p.totalAmtMsat } diff --git a/invoices/update.go b/invoices/update.go index 3137bbbfa7..bcf0d2ecf0 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/amp" "github.com/lightningnetwork/lnd/lntypes" @@ -30,11 +31,12 @@ type invoiceUpdateCtx struct { // HTLC onion payload. customRecords record.CustomSet - mpp *record.MPP - amp *record.AMP - metadata []byte - pathID *chainhash.Hash - totalAmtMsat lnwire.MilliSatoshi + mpp *record.MPP + amp *record.AMP + metadata []byte + pathID *chainhash.Hash + totalAmtMsat lnwire.MilliSatoshi + blindingPoint *btcec.PublicKey } // invoiceRef returns an identifier that can be used to lookup or update the diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index 0fbf1bfa6c..889f5128bb 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -152,7 +152,9 @@ func (ctx *mcTestContext) reportFailure(amt lnwire.MilliSatoshi, // reportSuccess reports a success by using a test route. func (ctx *mcTestContext) reportSuccess() { - err := ctx.mc.ReportPaymentSuccess(ctx.pid, mcTestRoute) + err := ctx.mc.ReportPaymentSuccess( + ctx.pid, mcTestRoute, + ) if err != nil { ctx.t.Fatal(err) } diff --git a/server.go b/server.go index 8b142f87a4..0be17d410d 100644 --- a/server.go +++ b/server.go @@ -565,18 +565,6 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } invoiceHtlcModifier := invoices.NewHtlcModificationInterceptor() - registryConfig := invoices.RegistryConfig{ - FinalCltvRejectDelta: lncfg.DefaultFinalCltvRejectDelta, - HtlcHoldDuration: invoices.DefaultHtlcHoldDuration, - Clock: clock.NewDefaultClock(), - AcceptKeySend: cfg.AcceptKeySend, - AcceptAMP: cfg.AcceptAMP, - GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup, - GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly, - KeysendHoldTime: cfg.KeysendHoldTime, - HtlcInterceptor: invoiceHtlcModifier, - } - s := &server{ cfg: cfg, implCfg: implCfg, @@ -638,14 +626,6 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return nil, err } - expiryWatcher := invoices.NewInvoiceExpiryWatcher( - clock.NewDefaultClock(), cfg.Invoices.HoldExpiryDelta, - uint32(currentHeight), currentHash, cc.ChainNotifier, - ) - s.invoices = invoices.NewRegistry( - dbs.InvoiceDB, expiryWatcher, ®istryConfig, - ) - s.htlcNotifier = htlcswitch.NewHtlcNotifier(time.Now) thresholdSats := btcutil.Amount(cfg.MaxFeeExposure) @@ -972,6 +952,35 @@ func newServer(cfg *Config, listenAddrs []net.Addr, "default namespace: %w", err) } + blindedPathMC, err := s.missionController.GetNamespacedStore( + "blinded-path", + ) + if err != nil { + return nil, fmt.Errorf("can't create mission control in the "+ + "blinded path namespace: %w", err) + } + + registryConfig := invoices.RegistryConfig{ + FinalCltvRejectDelta: lncfg.DefaultFinalCltvRejectDelta, + HtlcHoldDuration: invoices.DefaultHtlcHoldDuration, + Clock: clock.NewDefaultClock(), + AcceptKeySend: cfg.AcceptKeySend, + AcceptAMP: cfg.AcceptAMP, + GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup, + GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly, + KeysendHoldTime: cfg.KeysendHoldTime, + HtlcInterceptor: invoiceHtlcModifier, + ReportBlindedPathReceive: blindedPathMC.ReportPaymentSuccess, + } + + expiryWatcher := invoices.NewInvoiceExpiryWatcher( + clock.NewDefaultClock(), cfg.Invoices.HoldExpiryDelta, + uint32(currentHeight), currentHash, cc.ChainNotifier, + ) + s.invoices = invoices.NewRegistry( + dbs.InvoiceDB, expiryWatcher, ®istryConfig, + ) + srvrLog.Debugf("Instantiating payment session source with config: "+ "AttemptCost=%v + %v%%, MinRouteProbability=%v", int64(routingConfig.AttemptCost), From 262b95cf6070b4115f4209d833311a6bcf1eea94 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 11:48:52 +0200 Subject: [PATCH 11/12] multi: query probabilities for blinded paths from new MC namespace Start querying blinded path probabilites from the new MC namespace where the results of successful blinded path receives will be written to. --- routing/missioncontrol.go | 5 +++++ rpcserver.go | 11 +++++++++-- server.go | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 3e924f57ea..b329ed49c2 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -78,6 +78,11 @@ const ( // control name space. This is used as the sub-bucket key within the // top level DB bucket to store mission control results. DefaultMissionControlNamespace = "default" + + // BlindedPathMissionControlNamespace is the name of the mission control + // namespace where results from successful blinded path receives will + // be stored. + BlindedPathMissionControlNamespace = "blinded-paths" ) var ( diff --git a/rpcserver.go b/rpcserver.go index c9edf852a3..874c486f15 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6032,6 +6032,14 @@ func (r *rpcServer) AddInvoice(ctx context.Context, "be included in each path") } + blindedPathMC, err := r.server.missionController.GetNamespacedStore( + routing.BlindedPathMissionControlNamespace, + ) + if err != nil { + return nil, fmt.Errorf("could not initialise mission control "+ + "in the blinded paths namespace: %w", err) + } + addInvoiceCfg := &invoicesrpc.AddInvoiceConfig{ AddInvoice: r.server.invoices.AddInvoice, IsChannelActive: r.server.htlcSwitch.HasActiveLink, @@ -6070,8 +6078,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context, []*route.Route, error) { return r.server.chanRouter.FindBlindedPaths( - r.selfNode, amt, - r.server.defaultMC.GetProbability, + r.selfNode, amt, blindedPathMC.GetProbability, blindingRestrictions, ) }, diff --git a/server.go b/server.go index 0be17d410d..b987394b49 100644 --- a/server.go +++ b/server.go @@ -953,7 +953,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, } blindedPathMC, err := s.missionController.GetNamespacedStore( - "blinded-path", + routing.BlindedPathMissionControlNamespace, ) if err != nil { return nil, fmt.Errorf("can't create mission control in the "+ From c5f2e11260cbe02dd89f07de66a757b5146a8add Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 14 Aug 2024 16:36:08 +0200 Subject: [PATCH 12/12] docs: add release note --- docs/release-notes/release-notes-0.19.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index a0262b0f57..0104023738 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -42,6 +42,9 @@ * [Allow](https://github.com/lightningnetwork/lnd/pull/9017) the compression of logs during rotation with ZSTD via the `logcompressor` startup argument. +* Start [reporting successful recieves from blinded + paths](https://github.com/lightningnetwork/lnd/pull/9004) to mission control. + ## RPC Updates ## lncli Updates