routing+channeldb: migrate MC store to use minimal Route encoding

Add a new mcRoute type that houses the data about a route that MC
actually uses. Then add a migration (channeldb/migration32) that
migrates the existing store from its current serialisation to the new,
more minimal serialisation.
This commit is contained in:
Elle Mouton
2024-08-06 14:33:07 +02:00
parent 96445f99b4
commit 383a6d274f
9 changed files with 873 additions and 136 deletions

View File

@@ -26,6 +26,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/migration29"
"github.com/lightningnetwork/lnd/channeldb/migration30"
"github.com/lightningnetwork/lnd/channeldb/migration31"
"github.com/lightningnetwork/lnd/channeldb/migration32"
"github.com/lightningnetwork/lnd/channeldb/migration_01_to_11"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/invoices"
@@ -286,6 +287,10 @@ var (
number: 31,
migration: migration31.DeleteLastPublishedTxTLB,
},
{
number: 32,
migration: migration32.MigrateMCRouteSerialisation,
},
}
// optionalVersions stores all optional migrations that are applied

View File

@@ -0,0 +1,53 @@
package migration32
import (
"bytes"
"fmt"
"github.com/lightningnetwork/lnd/kvdb"
)
// MigrateMCRouteSerialisation reads all the mission control store entries and
// re-serializes them using a minimal route serialisation so that only the parts
// of the route that are actually required for mission control are persisted.
func MigrateMCRouteSerialisation(tx kvdb.RwTx) error {
log.Infof("Migrating Mission Control store to use a more minimal " +
"encoding for routes")
resultBucket := tx.ReadWriteBucket(resultsKey)
// If the results bucket does not exist then there are no entries in
// the mission control store yet and so there is nothing to migrate.
if resultBucket == nil {
return nil
}
// For each entry, read it into memory using the old encoding. Then,
// extract the more minimal route, re-encode and persist the entry.
return resultBucket.ForEach(func(k, v []byte) error {
// Read the entry using the old encoding.
resultOld, err := deserializeOldResult(k, v)
if err != nil {
return err
}
// Convert to the new payment result format with the minimal
// route.
resultNew := convertPaymentResult(resultOld)
// Serialise the new payment result using the new encoding.
key, resultNewBytes, err := serializeNewResult(resultNew)
if err != nil {
return err
}
// Make sure that the derived key is the same.
if !bytes.Equal(key, k) {
return fmt.Errorf("new payment result key (%v) is "+
"not the same as the old key (%v)", key, k)
}
// Finally, overwrite the previous value with the new encoding.
return resultBucket.Put(k, resultNewBytes)
})
}

View File

@@ -0,0 +1,237 @@
package migration32
import (
"encoding/hex"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
lnwire "github.com/lightningnetwork/lnd/channeldb/migration/lnwire21"
"github.com/lightningnetwork/lnd/channeldb/migtest"
"github.com/lightningnetwork/lnd/kvdb"
)
var (
failureIndex = 8
testPub = Vertex{2, 202, 4}
testPub2 = Vertex{22, 202, 4}
pubkeyBytes, _ = hex.DecodeString(
"598ec453728e0ffe0ae2f5e174243cf58f2" +
"a3f2c83d2457b43036db568b11093",
)
pubKeyY = new(btcec.FieldVal)
_ = pubKeyY.SetByteSlice(pubkeyBytes)
pubkey = btcec.NewPublicKey(new(btcec.FieldVal).SetInt(4), pubKeyY)
paymentResultCommon1 = paymentResultCommon{
id: 0,
timeFwd: time.Unix(0, 1),
timeReply: time.Unix(0, 2),
success: false,
failureSourceIdx: &failureIndex,
failure: &lnwire.FailFeeInsufficient{},
}
paymentResultCommon2 = paymentResultCommon{
id: 2,
timeFwd: time.Unix(0, 4),
timeReply: time.Unix(0, 7),
success: true,
}
)
// TestMigrateMCRouteSerialisation tests that the MigrateMCRouteSerialisation
// migration function correctly migrates the MC store from using the old route
// encoding to using the newer, more minimal route encoding.
func TestMigrateMCRouteSerialisation(t *testing.T) {
customRecord := map[uint64][]byte{
65536: {4, 2, 2},
}
resultsOld := []*paymentResultOld{
{
paymentResultCommon: paymentResultCommon1,
route: &Route{
TotalTimeLock: 100,
TotalAmount: 400,
SourcePubKey: testPub,
Hops: []*Hop{
// A hop with MPP, AMP and custom
// records.
{
PubKeyBytes: testPub,
ChannelID: 100,
OutgoingTimeLock: 300,
AmtToForward: 500,
MPP: &MPP{
paymentAddr: [32]byte{
4, 5,
},
totalMsat: 900,
},
AMP: &AMP{
rootShare: [32]byte{
0, 0,
},
setID: [32]byte{
5, 5, 5,
},
childIndex: 90,
},
CustomRecords: customRecord,
Metadata: []byte{6, 7, 7},
},
// A legacy hop.
{
PubKeyBytes: testPub,
ChannelID: 800,
OutgoingTimeLock: 4,
AmtToForward: 4,
LegacyPayload: true,
},
// A hop with a blinding key.
{
PubKeyBytes: testPub,
ChannelID: 800,
OutgoingTimeLock: 4,
AmtToForward: 4,
BlindingPoint: pubkey,
EncryptedData: []byte{
1, 2, 3,
},
TotalAmtMsat: 600,
},
// A hop with a blinding key and custom
// records.
{
PubKeyBytes: testPub,
ChannelID: 800,
OutgoingTimeLock: 4,
AmtToForward: 4,
CustomRecords: customRecord,
BlindingPoint: pubkey,
EncryptedData: []byte{
1, 2, 3,
},
TotalAmtMsat: 600,
},
},
},
},
{
paymentResultCommon: paymentResultCommon2,
route: &Route{
TotalTimeLock: 101,
TotalAmount: 401,
SourcePubKey: testPub2,
Hops: []*Hop{
{
PubKeyBytes: testPub,
ChannelID: 800,
OutgoingTimeLock: 4,
AmtToForward: 4,
BlindingPoint: pubkey,
EncryptedData: []byte{
1, 2, 3,
},
TotalAmtMsat: 600,
},
},
},
},
}
expectedResultsNew := []*paymentResultNew{
{
paymentResultCommon: paymentResultCommon1,
route: &mcRoute{
sourcePubKey: testPub,
totalAmount: 400,
hops: []*mcHop{
{
channelID: 100,
pubKeyBytes: testPub,
amtToFwd: 500,
hasCustomRecords: true,
},
{
channelID: 800,
pubKeyBytes: testPub,
amtToFwd: 4,
},
{
channelID: 800,
pubKeyBytes: testPub,
amtToFwd: 4,
hasBlindingPoint: true,
},
{
channelID: 800,
pubKeyBytes: testPub,
amtToFwd: 4,
hasBlindingPoint: true,
hasCustomRecords: true,
},
},
},
},
{
paymentResultCommon: paymentResultCommon2,
route: &mcRoute{
sourcePubKey: testPub2,
totalAmount: 401,
hops: []*mcHop{
{
channelID: 800,
pubKeyBytes: testPub,
amtToFwd: 4,
hasBlindingPoint: true,
},
},
},
},
}
// Prime the database with some mission control data that uses the
// old route encoding.
before := func(tx kvdb.RwTx) error {
resultBucket, err := tx.CreateTopLevelBucket(resultsKey)
if err != nil {
return err
}
for _, result := range resultsOld {
k, v, err := serializeOldResult(result)
if err != nil {
return err
}
if err := resultBucket.Put(k, v); err != nil {
return err
}
}
return nil
}
// After the migration, ensure that all the relevant info was
// maintained.
after := func(tx kvdb.RwTx) error {
m := make(map[string]interface{})
for _, result := range expectedResultsNew {
k, v, err := serializeNewResult(result)
if err != nil {
return err
}
m[string(k)] = string(v)
}
return migtest.VerifyDB(tx, resultsKey, m)
}
migtest.ApplyMigration(
t, before, after, MigrateMCRouteSerialisation, false,
)
}

View File

@@ -0,0 +1,320 @@
package migration32
import (
"bytes"
"io"
"math"
"time"
"github.com/btcsuite/btcd/wire"
lnwire "github.com/lightningnetwork/lnd/channeldb/migration/lnwire21"
)
const (
// unknownFailureSourceIdx is the database encoding of an unknown error
// source.
unknownFailureSourceIdx = -1
)
var (
// resultsKey is the fixed key under which the attempt results are
// stored.
resultsKey = []byte("missioncontrol-results")
)
// paymentResultCommon holds the fields that are shared by the old and new
// payment result encoding.
type paymentResultCommon struct {
id uint64
timeFwd, timeReply time.Time
success bool
failureSourceIdx *int
failure lnwire.FailureMessage
}
// paymentResultOld is the information that becomes available when a payment
// attempt completes.
type paymentResultOld struct {
paymentResultCommon
route *Route
}
// deserializeOldResult deserializes a payment result using the old encoding.
func deserializeOldResult(k, v []byte) (*paymentResultOld, error) {
// Parse payment id.
result := paymentResultOld{
paymentResultCommon: paymentResultCommon{
id: byteOrder.Uint64(k[8:]),
},
}
r := bytes.NewReader(v)
// Read timestamps, success status and failure source index.
var (
timeFwd, timeReply uint64
dbFailureSourceIdx int32
)
err := ReadElements(
r, &timeFwd, &timeReply, &result.success, &dbFailureSourceIdx,
)
if err != nil {
return nil, err
}
// Convert time stamps to local time zone for consistent logging.
result.timeFwd = time.Unix(0, int64(timeFwd)).Local()
result.timeReply = time.Unix(0, int64(timeReply)).Local()
// Convert from unknown index magic number to nil value.
if dbFailureSourceIdx != unknownFailureSourceIdx {
failureSourceIdx := int(dbFailureSourceIdx)
result.failureSourceIdx = &failureSourceIdx
}
// Read route.
route, err := DeserializeRoute(r)
if err != nil {
return nil, err
}
result.route = &route
// Read failure.
failureBytes, err := wire.ReadVarBytes(r, 0, math.MaxUint16, "failure")
if err != nil {
return nil, err
}
if len(failureBytes) > 0 {
result.failure, err = lnwire.DecodeFailureMessage(
bytes.NewReader(failureBytes), 0,
)
if err != nil {
return nil, err
}
}
return &result, nil
}
// convertPaymentResult converts a paymentResultOld to a paymentResultNew.
func convertPaymentResult(old *paymentResultOld) *paymentResultNew {
return &paymentResultNew{
paymentResultCommon: old.paymentResultCommon,
route: extractMCRoute(old.route),
}
}
// paymentResultNew is the information that becomes available when a payment
// attempt completes.
type paymentResultNew struct {
paymentResultCommon
route *mcRoute
}
// extractMCRoute extracts the fields required by MC from the Route struct to
// create the more minimal mcRoute struct.
func extractMCRoute(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 []*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 *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 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 Vertex
amtToFwd lnwire.MilliSatoshi
hasBlindingPoint bool
hasCustomRecords bool
}
// serializeOldResult serializes a payment result and returns a key and value
// byte slice to insert into the bucket.
func serializeOldResult(rp *paymentResultOld) ([]byte, []byte, error) {
// Write timestamps, success status, failure source index and route.
var b bytes.Buffer
var dbFailureSourceIdx int32
if rp.failureSourceIdx == nil {
dbFailureSourceIdx = unknownFailureSourceIdx
} else {
dbFailureSourceIdx = int32(*rp.failureSourceIdx)
}
err := WriteElements(
&b,
uint64(rp.timeFwd.UnixNano()),
uint64(rp.timeReply.UnixNano()),
rp.success, dbFailureSourceIdx,
)
if err != nil {
return nil, nil, err
}
if err := SerializeRoute(&b, *rp.route); err != nil {
return nil, nil, err
}
// Write failure. If there is no failure message, write an empty
// byte slice.
var failureBytes bytes.Buffer
if rp.failure != nil {
err := lnwire.EncodeFailureMessage(&failureBytes, rp.failure, 0)
if err != nil {
return nil, nil, err
}
}
err = wire.WriteVarBytes(&b, 0, failureBytes.Bytes())
if err != nil {
return nil, nil, err
}
// Compose key that identifies this result.
key := getResultKeyOld(rp)
return key, b.Bytes(), nil
}
// getResultKeyOld returns a byte slice representing a unique key for this
// payment result.
func getResultKeyOld(rp *paymentResultOld) []byte {
var keyBytes [8 + 8 + 33]byte
// Identify records by a combination of time, payment id and sender pub
// key. This allows importing mission control data from an external
// source without key collisions and keeps the records sorted
// chronologically.
byteOrder.PutUint64(keyBytes[:], uint64(rp.timeReply.UnixNano()))
byteOrder.PutUint64(keyBytes[8:], rp.id)
copy(keyBytes[16:], rp.route.SourcePubKey[:])
return keyBytes[:]
}
// serializeNewResult serializes a payment result and returns a key and value
// byte slice to insert into the bucket.
func serializeNewResult(rp *paymentResultNew) ([]byte, []byte, error) {
// Write timestamps, success status, failure source index and route.
var b bytes.Buffer
var dbFailureSourceIdx int32
if rp.failureSourceIdx == nil {
dbFailureSourceIdx = unknownFailureSourceIdx
} else {
dbFailureSourceIdx = int32(*rp.failureSourceIdx)
}
err := WriteElements(
&b,
uint64(rp.timeFwd.UnixNano()),
uint64(rp.timeReply.UnixNano()),
rp.success, dbFailureSourceIdx,
)
if err != nil {
return nil, nil, err
}
if err := serializeMCRoute(&b, rp.route); err != nil {
return nil, nil, err
}
// Write failure. If there is no failure message, write an empty
// byte slice.
var failureBytes bytes.Buffer
if rp.failure != nil {
err := lnwire.EncodeFailureMessage(&failureBytes, rp.failure, 0)
if err != nil {
return nil, nil, err
}
}
err = wire.WriteVarBytes(&b, 0, failureBytes.Bytes())
if err != nil {
return nil, nil, err
}
// Compose key that identifies this result.
key := getResultKeyNew(rp)
return key, b.Bytes(), nil
}
// getResultKeyNew returns a byte slice representing a unique key for this
// payment result.
func getResultKeyNew(rp *paymentResultNew) []byte {
var keyBytes [8 + 8 + 33]byte
// Identify records by a combination of time, payment id and sender pub
// key. This allows importing mission control data from an external
// source without key collisions and keeps the records sorted
// chronologically.
byteOrder.PutUint64(keyBytes[:], uint64(rp.timeReply.UnixNano()))
byteOrder.PutUint64(keyBytes[8:], rp.id)
copy(keyBytes[16:], rp.route.sourcePubKey[:])
return keyBytes[:]
}
// serializeMCRoute serializes an mcRoute and writes the bytes to the given
// io.Writer.
func serializeMCRoute(w io.Writer, r *mcRoute) error {
if err := WriteElements(
w, r.totalAmount, r.sourcePubKey[:],
); err != nil {
return err
}
if err := WriteElements(w, uint32(len(r.hops))); err != nil {
return err
}
for _, h := range r.hops {
if err := serializeNewHop(w, h); err != nil {
return err
}
}
return nil
}
// serializeMCRoute serializes an mcHop and writes the bytes to the given
// io.Writer.
func serializeNewHop(w io.Writer, h *mcHop) error {
return WriteElements(w,
h.pubKeyBytes[:],
h.channelID,
h.amtToFwd,
h.hasBlindingPoint,
h.hasCustomRecords,
)
}

View File

@@ -208,7 +208,7 @@ type MissionControlPairSnapshot struct {
type paymentResult struct {
id uint64
timeFwd, timeReply time.Time
route *route.Route
route *mcRoute
success bool
failureSourceIdx *int
failure lnwire.FailureMessage
@@ -438,7 +438,7 @@ func (m *MissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route,
id: paymentID,
failureSourceIdx: failureSourceIdx,
failure: failure,
route: rt,
route: extractMCRoute(rt),
}
return m.processPaymentResult(result)
@@ -456,7 +456,7 @@ func (m *MissionControl) ReportPaymentSuccess(paymentID uint64,
timeReply: timestamp,
id: paymentID,
success: true,
route: rt,
route: extractMCRoute(rt),
}
_, err := m.processPaymentResult(result)

View File

@@ -5,6 +5,7 @@ import (
"container/list"
"encoding/binary"
"fmt"
"io"
"math"
"sync"
"time"
@@ -187,7 +188,7 @@ func serializeResult(rp *paymentResult) ([]byte, []byte, error) {
return nil, nil, err
}
if err := channeldb.SerializeRoute(&b, *rp.route); err != nil {
if err := serializeRoute(&b, rp.route); err != nil {
return nil, nil, err
}
@@ -211,6 +212,90 @@ 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.
@@ -244,11 +329,11 @@ func deserializeResult(k, v []byte) (*paymentResult, error) {
}
// Read route.
route, err := channeldb.DeserializeRoute(r)
route, err := deserializeRoute(r)
if err != nil {
return nil, err
}
result.route = &route
result.route = route
// Read failure.
failureBytes, err := wire.ReadVarBytes(
@@ -499,7 +584,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[:]
}

View File

@@ -18,12 +18,16 @@ const testMaxRecords = 2
var (
// mcStoreTestRoute is a test route for the mission control store tests.
mcStoreTestRoute = route.Route{
SourcePubKey: route.Vertex{1},
Hops: []*route.Hop{
mcStoreTestRoute = mcRoute{
totalAmount: lnwire.MilliSatoshi(5),
sourcePubKey: route.Vertex{1},
hops: []*mcHop{
{
PubKeyBytes: route.Vertex{2},
LegacyPayload: true,
pubKeyBytes: route.Vertex{2},
channelID: 4,
amtToFwd: lnwire.MilliSatoshi(7),
hasCustomRecords: true,
hasBlindingPoint: false,
},
},
}

View File

@@ -76,7 +76,7 @@ type interpretedResult struct {
// interpretResult interprets a payment outcome and returns an object that
// contains information required to update mission control.
func interpretResult(rt *route.Route, success bool, failureSrcIdx *int,
func interpretResult(rt *mcRoute, success bool, failureSrcIdx *int,
failure lnwire.FailureMessage) *interpretedResult {
i := &interpretedResult{
@@ -92,15 +92,14 @@ func interpretResult(rt *route.Route, success bool, failureSrcIdx *int,
}
// processSuccess processes a successful payment attempt.
func (i *interpretedResult) processSuccess(route *route.Route) {
func (i *interpretedResult) processSuccess(route *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 *route.Route, errSourceIdx *int,
func (i *interpretedResult) processFail(rt *mcRoute, errSourceIdx *int,
failure lnwire.FailureMessage) {
if errSourceIdx == nil {
@@ -125,10 +124,8 @@ func (i *interpretedResult) processFail(
i.processPaymentOutcomeSelf(rt, failure)
// A failure from the final hop was received.
case len(rt.Hops):
i.processPaymentOutcomeFinal(
rt, failure,
)
case len(rt.hops):
i.processPaymentOutcomeFinal(rt, failure)
// An intermediate hop failed. Interpret the outcome, update reputation
// and try again.
@@ -144,7 +141,7 @@ func (i *interpretedResult) processFail(
// 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 *route.Route,
func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute,
introIdx, errSourceIdx int) {
// We fail the introduction node for not obeying the specification.
@@ -161,14 +158,14 @@ func (i *interpretedResult) processPaymentOutcomeBadIntro(route *route.Route,
// 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 *route.Route, failure lnwire.FailureMessage) {
func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute,
failure lnwire.FailureMessage) {
switch failure.(type) {
@@ -181,7 +178,7 @@ func (i *interpretedResult) processPaymentOutcomeSelf(
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
}
@@ -191,15 +188,15 @@ func (i *interpretedResult) processPaymentOutcomeSelf(
// 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 *route.Route, failure lnwire.FailureMessage) {
func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute,
failure lnwire.FailureMessage) {
n := len(route.Hops)
n := len(route.hops)
failNode := func() {
i.failNode(route, n)
@@ -292,9 +289,10 @@ func (i *interpretedResult) processPaymentOutcomeFinal(
// processPaymentOutcomeIntermediate handles failures sent by an intermediate
// hop.
func (i *interpretedResult) processPaymentOutcomeIntermediate(
route *route.Route, errorSourceIdx int,
failure lnwire.FailureMessage) {
//
//nolint:funlen
func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
errorSourceIdx int, failure lnwire.FailureMessage) {
reportOutgoing := func() {
i.failPair(
@@ -398,8 +396,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
// 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()
@@ -427,8 +425,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
// 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
@@ -502,16 +500,14 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
// 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
// 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
@@ -525,9 +521,9 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
// 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 *route.Route) (int, bool) {
for i, hop := range route.Hops {
if hop.BlindingPoint != nil {
func introductionPointIndex(route *mcRoute) (int, bool) {
for i, hop := range route.hops {
if hop.hasBlindingPoint {
return i + 1, true
}
}
@@ -537,8 +533,8 @@ func introductionPointIndex(route *route.Route) (int, bool) {
// processPaymentOutcomeUnknown processes a payment outcome for which no failure
// message or source is available.
func (i *interpretedResult) processPaymentOutcomeUnknown(route *route.Route) {
n := len(route.Hops)
func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) {
n := len(route.hops)
// If this is a direct payment, the destination must be at fault.
if n == 1 {
@@ -553,12 +549,62 @@ func (i *interpretedResult) processPaymentOutcomeUnknown(route *route.Route) {
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 *route.Route, idx int) {
func (i *interpretedResult) failNode(rt *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
@@ -574,7 +620,7 @@ func (i *interpretedResult) failNode(rt *route.Route, 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)
@@ -584,18 +630,14 @@ func (i *interpretedResult) failNode(rt *route.Route, idx int) {
// failPairRange marks the node pairs from node fromIdx to node toIdx as failed
// in both direction.
func (i *interpretedResult) failPairRange(
rt *route.Route, fromIdx, toIdx int) {
func (i *interpretedResult) failPairRange(rt *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 *route.Route, idx int) {
func (i *interpretedResult) failPair(rt *mcRoute, idx int) {
pair, _ := getPair(rt, idx)
// Report pair in both directions without a minimum penalization amount.
@@ -604,9 +646,7 @@ func (i *interpretedResult) failPair(
}
// failPairBalance marks a pair as failed with a minimum penalization amount.
func (i *interpretedResult) failPairBalance(
rt *route.Route, channelIdx int) {
func (i *interpretedResult) failPairBalance(rt *mcRoute, channelIdx int) {
pair, amt := getPair(rt, channelIdx)
i.pairResults[pair] = failPairResult(amt)
@@ -614,9 +654,7 @@ func (i *interpretedResult) failPairBalance(
// successPairRange marks the node pairs from node fromIdx to node toIdx as
// succeeded.
func (i *interpretedResult) successPairRange(
rt *route.Route, fromIdx, toIdx int) {
func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) {
for idx := fromIdx; idx <= toIdx; idx++ {
pair, amt := getPair(rt, idx)
@@ -626,21 +664,21 @@ func (i *interpretedResult) successPairRange(
// getPair returns a node pair from the route and the amount passed between that
// pair.
func getPair(rt *route.Route, channelIdx int) (DirectedNodePair,
func getPair(rt *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].AmtToForward
nodeFrom = rt.hops[channelIdx-1].pubKeyBytes
amt = rt.hops[channelIdx-1].amtToFwd
}
pair := NewDirectedNodePair(nodeFrom, nodeTo)

View File

@@ -4,7 +4,6 @@ import (
"reflect"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
@@ -15,109 +14,105 @@ var (
{1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4},
}
// blindingPoint provides a non-nil blinding point (value is never
// used).
blindingPoint = &btcec.PublicKey{}
routeOneHop = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 99},
routeOneHop = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{pubKeyBytes: hops[1], amtToFwd: 99},
},
}
routeTwoHop = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 99},
{PubKeyBytes: hops[2], AmtToForward: 97},
routeTwoHop = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{pubKeyBytes: hops[1], amtToFwd: 99},
{pubKeyBytes: hops[2], amtToFwd: 97},
},
}
routeThreeHop = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 99},
{PubKeyBytes: hops[2], AmtToForward: 97},
{PubKeyBytes: hops[3], AmtToForward: 94},
routeThreeHop = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{pubKeyBytes: hops[1], amtToFwd: 99},
{pubKeyBytes: hops[2], amtToFwd: 97},
{pubKeyBytes: hops[3], amtToFwd: 94},
},
}
routeFourHop = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 99},
{PubKeyBytes: hops[2], AmtToForward: 97},
{PubKeyBytes: hops[3], AmtToForward: 94},
{PubKeyBytes: hops[4], AmtToForward: 90},
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},
},
}
// 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 = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 99},
blindedMultiHop = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{pubKeyBytes: hops[1], amtToFwd: 99},
{
PubKeyBytes: hops[2],
AmtToForward: 95,
BlindingPoint: blindingPoint,
pubKeyBytes: hops[2],
amtToFwd: 95,
hasBlindingPoint: true,
},
{PubKeyBytes: hops[3], AmtToForward: 88},
{PubKeyBytes: hops[4], AmtToForward: 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 = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 99},
blindedSingleHop = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{pubKeyBytes: hops[1], amtToFwd: 99},
{
PubKeyBytes: hops[2],
AmtToForward: 95,
BlindingPoint: blindingPoint,
pubKeyBytes: hops[2],
amtToFwd: 95,
hasBlindingPoint: true,
},
{PubKeyBytes: hops[3], AmtToForward: 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 = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
blindedMultiToIntroduction = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{
PubKeyBytes: hops[1],
AmtToForward: 90,
BlindingPoint: blindingPoint,
pubKeyBytes: hops[1],
amtToFwd: 90,
hasBlindingPoint: true,
},
{PubKeyBytes: hops[2], AmtToForward: 75},
{PubKeyBytes: hops[3], AmtToForward: 58},
{pubKeyBytes: hops[2], amtToFwd: 75},
{pubKeyBytes: hops[3], amtToFwd: 58},
},
}
// blindedIntroReceiver is a blinded path where the introduction node
// is the recipient.
blindedIntroReceiver = route.Route{
SourcePubKey: hops[0],
TotalAmount: 100,
Hops: []*route.Hop{
{PubKeyBytes: hops[1], AmtToForward: 95},
blindedIntroReceiver = mcRoute{
sourcePubKey: hops[0],
totalAmount: 100,
hops: []*mcHop{
{pubKeyBytes: hops[1], amtToFwd: 95},
{
PubKeyBytes: hops[2],
AmtToForward: 90,
BlindingPoint: blindingPoint,
pubKeyBytes: hops[2],
amtToFwd: 90,
hasBlindingPoint: true,
},
},
}
@@ -134,7 +129,7 @@ func getPolicyFailure(from, to int) *DirectedNodePair {
type resultTestCase struct {
name string
route *route.Route
route *mcRoute
success bool
failureSrcIdx int
failure lnwire.FailureMessage
@@ -159,7 +154,7 @@ var resultTestCases = []resultTestCase{
},
},
// Tests that a expiry too soon failure result is properly interpreted.
// Tests that an expiry too soon failure result is properly interpreted.
{
name: "fail expiry too soon",
route: &routeFourHop,