routing: extend path finding to be TLV-EOB aware, allow dest TLV records
In this commit, we extend the path finding to be able to recognize when a node needs the new TLV format, or the legacy format based on the feature bits they expose. We also extend the `LightningPayment` struct to allow the caller to specify an arbitrary set of TLV records which can be used for a number of use-cases including various variants of spontaneous payments.
This commit is contained in:
parent
5b4c8ac232
commit
4697cfde30
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
|
||||
@ -26,6 +27,7 @@ var (
|
||||
ChannelID: 12345,
|
||||
OutgoingTimeLock: 111,
|
||||
AmtToForward: 555,
|
||||
LegacyPayload: true,
|
||||
}
|
||||
|
||||
testRoute = route.Route{
|
||||
@ -144,7 +146,9 @@ func TestControlTowerSubscribeSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result.Route, &attempt.Route) {
|
||||
t.Fatal("unexpected route")
|
||||
t.Fatalf("unexpected route: %v vs %v",
|
||||
spew.Sdump(result.Route),
|
||||
spew.Sdump(attempt.Route))
|
||||
}
|
||||
|
||||
// After the final event, we expect the channel to be closed.
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
|
||||
"github.com/coreos/bbolt"
|
||||
@ -51,6 +52,7 @@ func TestMissionControlStore(t *testing.T) {
|
||||
Hops: []*route.Hop{
|
||||
{
|
||||
PubKeyBytes: route.Vertex{2},
|
||||
LegacyPayload: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -99,10 +101,12 @@ func TestMissionControlStore(t *testing.T) {
|
||||
|
||||
// Check that results are stored in chronological order.
|
||||
if !reflect.DeepEqual(&result1, results[0]) {
|
||||
t.Fatal()
|
||||
t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result1),
|
||||
spew.Sdump(results[0]))
|
||||
}
|
||||
if !reflect.DeepEqual(&result2, results[1]) {
|
||||
t.Fatal()
|
||||
t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result2),
|
||||
spew.Sdump(results[1]))
|
||||
}
|
||||
|
||||
// Recreate store to test pruning.
|
||||
@ -132,9 +136,11 @@ func TestMissionControlStore(t *testing.T) {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&result2, results[0]) {
|
||||
t.Fatal()
|
||||
t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result2),
|
||||
spew.Sdump(results[0]))
|
||||
}
|
||||
if !reflect.DeepEqual(&result3, results[1]) {
|
||||
t.Fatal()
|
||||
t.Fatalf("the results differ: %v vs %v", spew.Sdump(&result3),
|
||||
spew.Sdump(results[1]))
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,12 @@ var (
|
||||
ChannelID: 1,
|
||||
PubKeyBytes: route.Vertex{11},
|
||||
AmtToForward: 1000,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
{
|
||||
ChannelID: 2,
|
||||
PubKeyBytes: route.Vertex{12},
|
||||
LegacyPayload: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -167,7 +169,8 @@ func TestMissionControl(t *testing.T) {
|
||||
// Check whether history snapshot looks sane.
|
||||
history := ctx.mc.GetHistorySnapshot()
|
||||
if len(history.Nodes) != 1 {
|
||||
t.Fatal("unexpected number of nodes")
|
||||
t.Fatalf("unexpected number of nodes: expected 1 got %v",
|
||||
len(history.Nodes))
|
||||
}
|
||||
|
||||
if len(history.Pairs) != 1 {
|
||||
|
@ -2,6 +2,7 @@ package routing
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/coreos/bbolt"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -98,7 +100,8 @@ func isSamePath(path1, path2 []*channeldb.ChannelEdgePolicy) bool {
|
||||
// the source to the target node of the path finding attempt.
|
||||
func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex route.Vertex,
|
||||
pathEdges []*channeldb.ChannelEdgePolicy, currentHeight uint32,
|
||||
finalCLTVDelta uint16) (*route.Route, error) {
|
||||
finalCLTVDelta uint16,
|
||||
finalDestRecords []tlv.Record) (*route.Route, error) {
|
||||
|
||||
var (
|
||||
hops []*route.Hop
|
||||
@ -179,7 +182,27 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex route.Vertex,
|
||||
ChannelID: edge.ChannelID,
|
||||
AmtToForward: amtToForward,
|
||||
OutgoingTimeLock: outgoingTimeLock,
|
||||
LegacyPayload: true,
|
||||
}
|
||||
|
||||
// We start out above by assuming that this node needs the
|
||||
// legacy payload, as if we don't have the full
|
||||
// NodeAnnouncement information for this node, then we can't
|
||||
// assume it knows the latest features. If we do have a feature
|
||||
// vector for this node, then we'll update the info now.
|
||||
if edge.Node.Features != nil {
|
||||
features := edge.Node.Features
|
||||
currentHop.LegacyPayload = !features.HasFeature(
|
||||
lnwire.TLVOnionPayloadOptional,
|
||||
)
|
||||
}
|
||||
|
||||
// If this is the last hop, then we'll populate any TLV records
|
||||
// destined for it.
|
||||
if i == len(pathEdges)-1 && len(finalDestRecords) != 0 {
|
||||
currentHop.TLVRecords = finalDestRecords
|
||||
}
|
||||
|
||||
hops = append([]*route.Hop{currentHop}, hops...)
|
||||
|
||||
// Finally, we update the amount that needs to flow into the
|
||||
@ -190,7 +213,8 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex route.Vertex,
|
||||
|
||||
// With the base routing data expressed as hops, build the full route
|
||||
newRoute, err := route.NewRouteFromHops(
|
||||
nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex), hops,
|
||||
nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex),
|
||||
hops,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -261,6 +285,11 @@ type RestrictParams struct {
|
||||
// ctlv. After path finding is complete, the caller needs to increase
|
||||
// all cltv expiry heights with the required final cltv delta.
|
||||
CltvLimit *uint32
|
||||
|
||||
// DestPayloadTLV should be set to true if we need to drop off a TLV
|
||||
// payload at the final hop in order to properly complete this payment
|
||||
// attempt.
|
||||
DestPayloadTLV bool
|
||||
}
|
||||
|
||||
// PathFindingConfig defines global parameters that control the trade-off in
|
||||
@ -316,10 +345,34 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
||||
node *channeldb.LightningNode) error {
|
||||
// TODO(roasbeef): with larger graph can just use disk seeks
|
||||
// with a visited map
|
||||
distance[route.Vertex(node.PubKeyBytes)] = nodeWithDist{
|
||||
vertex := route.Vertex(node.PubKeyBytes)
|
||||
distance[vertex] = nodeWithDist{
|
||||
dist: infinity,
|
||||
node: route.Vertex(node.PubKeyBytes),
|
||||
}
|
||||
|
||||
// If we don't have any features for this node, then we can
|
||||
// stop here.
|
||||
if node.Features == nil || !r.DestPayloadTLV {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We only need to perform this check for the final node, so we
|
||||
// can exit here if this isn't them.
|
||||
if vertex != target {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we have any records for the final hop, then we'll check
|
||||
// not to ensure that they are actually able to interpret them.
|
||||
supportsTLV := node.Features.HasFeature(
|
||||
lnwire.TLVOnionPayloadOptional,
|
||||
)
|
||||
if !supportsTLV {
|
||||
return fmt.Errorf("destination hop doesn't " +
|
||||
"understand new TLV paylods")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
@ -670,7 +670,8 @@ func TestFindLowestFeePath(t *testing.T) {
|
||||
}
|
||||
route, err := newRoute(
|
||||
paymentAmt, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV)
|
||||
finalHopCLTV, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create path: %v", err)
|
||||
}
|
||||
@ -819,7 +820,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
|
||||
|
||||
route, err := newRoute(
|
||||
paymentAmt, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV,
|
||||
finalHopCLTV, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create path: %v", err)
|
||||
@ -857,9 +858,15 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
|
||||
for i := 0; i < len(expectedHops)-1; i++ {
|
||||
var expectedHop [8]byte
|
||||
binary.BigEndian.PutUint64(expectedHop[:], route.Hops[i+1].ChannelID)
|
||||
if !bytes.Equal(sphinxPath[i].HopData.NextAddress[:], expectedHop[:]) {
|
||||
|
||||
hopData, err := sphinxPath[i].HopPayload.HopData()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to make hop data: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(hopData.NextAddress[:], expectedHop[:]) {
|
||||
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
|
||||
expectedHop[:], sphinxPath[i].HopData.NextAddress)
|
||||
expectedHop[:], hopData.NextAddress[:])
|
||||
}
|
||||
}
|
||||
|
||||
@ -867,9 +874,15 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
|
||||
// to indicate it's the exit hop.
|
||||
var exitHop [8]byte
|
||||
lastHopIndex := len(expectedHops) - 1
|
||||
if !bytes.Equal(sphinxPath[lastHopIndex].HopData.NextAddress[:], exitHop[:]) {
|
||||
|
||||
hopData, err := sphinxPath[lastHopIndex].HopPayload.HopData()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create hop data: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(hopData.NextAddress[:], exitHop[:]) {
|
||||
t.Fatalf("first hop has incorrect next hop: expected %x, got %x",
|
||||
exitHop[:], sphinxPath[lastHopIndex].HopData.NextAddress)
|
||||
exitHop[:], hopData.NextAddress)
|
||||
}
|
||||
|
||||
var expectedTotalFee lnwire.MilliSatoshi
|
||||
@ -1001,7 +1014,11 @@ func TestNewRoute(t *testing.T) {
|
||||
timeLockDelta uint16) *channeldb.ChannelEdgePolicy {
|
||||
|
||||
return &channeldb.ChannelEdgePolicy{
|
||||
Node: &channeldb.LightningNode{},
|
||||
Node: &channeldb.LightningNode{
|
||||
Features: lnwire.NewFeatureVector(
|
||||
nil, nil,
|
||||
),
|
||||
},
|
||||
FeeProportionalMillionths: feeRate,
|
||||
FeeBaseMSat: baseFee,
|
||||
TimeLockDelta: timeLockDelta,
|
||||
@ -1176,9 +1193,11 @@ func TestNewRoute(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
route, err := newRoute(testCase.paymentAmount,
|
||||
sourceVertex, testCase.hops, startingHeight,
|
||||
finalHopCLTV)
|
||||
route, err := newRoute(
|
||||
testCase.paymentAmount, sourceVertex,
|
||||
testCase.hops, startingHeight, finalHopCLTV,
|
||||
nil,
|
||||
)
|
||||
|
||||
if testCase.expectError {
|
||||
expectedCode := testCase.expectedErrorCode
|
||||
@ -1683,7 +1702,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
carol := ctx.aliases["C"]
|
||||
const amt lnwire.MilliSatoshi = 4999999
|
||||
route, err := ctx.router.FindRoute(
|
||||
bobNode.PubKeyBytes, carol, amt, noRestrictions,
|
||||
bobNode.PubKeyBytes, carol, amt, noRestrictions, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find route: %v", err)
|
||||
@ -1742,7 +1761,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
|
||||
// We'll now request a route from A -> B -> C.
|
||||
route, err = ctx.router.FindRoute(
|
||||
source.PubKeyBytes, carol, amt, noRestrictions,
|
||||
source.PubKeyBytes, carol, amt, noRestrictions, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find routes: %v", err)
|
||||
@ -1925,7 +1944,7 @@ func TestRestrictOutgoingChannel(t *testing.T) {
|
||||
}
|
||||
route, err := newRoute(
|
||||
paymentAmt, sourceVertex, path, startingHeight,
|
||||
finalHopCLTV,
|
||||
finalHopCLTV, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create path: %v", err)
|
||||
@ -2033,6 +2052,7 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) {
|
||||
)
|
||||
route, err := newRoute(
|
||||
paymentAmt, sourceVertex, path, startingHeight, finalHopCLTV,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create path: %v", err)
|
||||
|
@ -127,6 +127,7 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
|
||||
sourceVertex := route.Vertex(ss.SelfNode.PubKeyBytes)
|
||||
route, err := newRoute(
|
||||
payment.Amount, sourceVertex, path, height, finalCltvDelta,
|
||||
payment.FinalDestRecords,
|
||||
)
|
||||
if err != nil {
|
||||
// TODO(roasbeef): return which edge/vertex didn't work
|
||||
|
@ -26,7 +26,11 @@ func TestRequestRoute(t *testing.T) {
|
||||
|
||||
path := []*channeldb.ChannelEdgePolicy{
|
||||
{
|
||||
Node: &channeldb.LightningNode{},
|
||||
Node: &channeldb.LightningNode{
|
||||
Features: lnwire.NewFeatureVector(
|
||||
nil, nil,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/routing/chainview"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/ticker"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
@ -1429,6 +1430,7 @@ type routingMsg struct {
|
||||
// factoring in channel capacities and cumulative fees along the route.
|
||||
func (r *ChannelRouter) FindRoute(source, target route.Vertex,
|
||||
amt lnwire.MilliSatoshi, restrictions *RestrictParams,
|
||||
destTlvRecords []tlv.Record,
|
||||
finalExpiry ...uint16) (*route.Route, error) {
|
||||
|
||||
var finalCLTVDelta uint16
|
||||
@ -1482,6 +1484,7 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex,
|
||||
// Create the route with absolute time lock values.
|
||||
route, err := newRoute(
|
||||
amt, source, path, uint32(currentHeight), finalCLTVDelta,
|
||||
destTlvRecords,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1630,7 +1633,11 @@ type LightningPayment struct {
|
||||
// attempting to complete.
|
||||
PaymentRequest []byte
|
||||
|
||||
// TODO(roasbeef): add e2e message?
|
||||
// FinalDestRecords are TLV records that are to be sent to the final
|
||||
// hop in the new onion payload format. If the destination does not
|
||||
// understand this new onion payload format, then the payment will
|
||||
// fail.
|
||||
FinalDestRecords []tlv.Record
|
||||
}
|
||||
|
||||
// SendPayment attempts to send a payment as described within the passed
|
||||
@ -1694,6 +1701,8 @@ func (r *ChannelRouter) preparePayment(payment *LightningPayment) (
|
||||
|
||||
// Record this payment hash with the ControlTower, ensuring it is not
|
||||
// already in-flight.
|
||||
//
|
||||
// TODO(roasbeef): store records as part of creation info?
|
||||
info := &channeldb.PaymentCreationInfo{
|
||||
PaymentHash: payment.PaymentHash,
|
||||
Value: payment.Amount,
|
||||
|
@ -231,7 +231,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
|
||||
|
||||
route, err := ctx.router.FindRoute(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
target, paymentAmt, restrictions,
|
||||
target, paymentAmt, restrictions, nil,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
@ -392,10 +392,12 @@ func TestChannelUpdateValidation(t *testing.T) {
|
||||
{
|
||||
ChannelID: 1,
|
||||
PubKeyBytes: hop1,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
{
|
||||
ChannelID: 2,
|
||||
PubKeyBytes: hop2,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1074,8 +1076,9 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const startingBlockHeight = 101
|
||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight,
|
||||
basicGraphFilePath)
|
||||
ctx, cleanUp, err := createTestCtxFromFile(
|
||||
startingBlockHeight, basicGraphFilePath,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create router: %v", err)
|
||||
}
|
||||
@ -1108,7 +1111,8 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
fundingTx, _, chanID, err := createChannelEdge(ctx,
|
||||
bitcoinKey1.SerializeCompressed(),
|
||||
bitcoinKey2.SerializeCompressed(),
|
||||
10000, 500)
|
||||
10000, 500,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create channel edge: %v", err)
|
||||
}
|
||||
@ -1266,7 +1270,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
copy(targetPubKeyBytes[:], targetNode.SerializeCompressed())
|
||||
_, err = ctx.router.FindRoute(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions, nil,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
@ -1309,7 +1313,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
// updated.
|
||||
_, err = ctx.router.FindRoute(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions, nil,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
@ -2634,10 +2638,12 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
||||
{
|
||||
ChannelID: 1,
|
||||
PubKeyBytes: hop1,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
{
|
||||
ChannelID: 2,
|
||||
PubKeyBytes: hop2,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -3273,11 +3279,13 @@ func TestSendToRouteStructuredError(t *testing.T) {
|
||||
ChannelID: 1,
|
||||
PubKeyBytes: hop1,
|
||||
AmtToForward: payAmt,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
{
|
||||
ChannelID: 2,
|
||||
PubKeyBytes: hop2,
|
||||
AmtToForward: payAmt,
|
||||
LegacyPayload: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user