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:
Olaoluwa Osuntokun 2019-07-30 21:41:58 -07:00
parent 5b4c8ac232
commit 4697cfde30
No known key found for this signature in database
GPG Key ID: CE58F7F8E20FD9A2
9 changed files with 158 additions and 50 deletions

@ -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,
},
}