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 new file mode 100644 index 0000000000..4e3c45ef0c --- /dev/null +++ b/channeldb/models/blinded_paths.go @@ -0,0 +1,209 @@ +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 +// 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 + +// 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 { + // 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 +} + +// 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/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/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 diff --git a/go.mod b/go.mod index 4aeea38086..cd197c234f 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 @@ -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 59e0c9faf4..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= @@ -447,8 +449,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= @@ -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/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/invoices.go b/invoices/invoices.go index c48629c583..7595b97248 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 @@ -837,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() @@ -852,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 { @@ -862,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..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" @@ -18,6 +19,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" ) @@ -34,6 +36,15 @@ var ( lnwire.Features, ) + blindedPathFeatures = lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector( + lnwire.TLVOnionPayloadOptional, + lnwire.RouteBlindingOptional, + lnwire.Bolt11BlindedPathsRequired, + ), + lnwire.Features, + ) + testNow = time.Unix(1, 0) ) @@ -75,8 +86,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") @@ -187,6 +199,10 @@ func TestInvoices(t *testing.T) { name: "AddInvoiceWithHTLCs", test: testAddInvoiceWithHTLCs, }, + { + name: "AddInvoiceWithBlindedPathInfo", + test: testAddInvoiceWithBlindedPathInfo, + }, { name: "SetIDIndex", test: testSetIDIndex, @@ -2086,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, 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 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/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index dcb1bef71e..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 @@ -549,8 +550,15 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, for _, path := range paths { options = append(options, zpay32.WithBlindedPaymentPath( - path, + 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/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/blindedpath/blinded_path.go b/routing/blindedpath/blinded_path.go index bc14daa40a..1dcd4a596e 100644 --- a/routing/blindedpath/blinded_path.go +++ b/routing/blindedpath/blinded_path.go @@ -109,11 +109,32 @@ 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 + + // 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 // 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,13 +150,13 @@ 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. - 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. @@ -145,16 +166,18 @@ func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) ( 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) @@ -170,7 +193,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 { @@ -283,12 +306,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,19 +320,25 @@ 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{ + zpay32Path := &zpay32.BlindedPaymentPath{ FeeBaseMsat: uint32(baseFee), FeeRate: feeRate, CltvExpiryDelta: cltvDelta, HTLCMinMsat: uint64(minHTLC), HTLCMaxMsat: uint64(maxHTLC), Features: lnwire.EmptyFeatureVector(), - FirstEphemeralBlindingPoint: blindedPath.BlindingPoint, - Hops: blindedPath.BlindedHops, + 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..47a361ed17 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) @@ -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,13 +942,30 @@ 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 // 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 +1026,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 +1045,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 +1060,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 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/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 d848235d88..b329ed49c2 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" @@ -77,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 ( @@ -263,7 +269,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 @@ -584,7 +590,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) { @@ -597,7 +603,7 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route, id: paymentID, failureSourceIdx: failureSourceIdx, failure: failure, - route: extractMCRoute(rt), + route: rt, } return m.processPaymentResult(result) @@ -606,7 +612,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() @@ -615,7 +621,7 @@ func (m *MissionControl) ReportPaymentSuccess(paymentID uint64, timeReply: timestamp, id: paymentID, success: true, - route: extractMCRoute(rt), + route: 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/missioncontrol_test.go b/routing/missioncontrol_test.go index dad033ac44..889f5128bb 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( @@ -153,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/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/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/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/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..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. @@ -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..ad86af7bc3 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 @@ -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. 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 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 8b142f87a4..b987394b49 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( + routing.BlindedPathMissionControlNamespace, + ) + 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),