Skip to content

Commit

Permalink
Merge pull request #2640 from joostjager/cltv-limit
Browse files Browse the repository at this point in the history
routing: add cltv limit
  • Loading branch information
halseth committed Mar 27, 2019
2 parents d736454 + ec0d241 commit 4d8100c
Show file tree
Hide file tree
Showing 12 changed files with 842 additions and 592 deletions.
11 changes: 11 additions & 0 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,12 @@ func closedChannels(ctx *cli.Context) error {
return nil
}

var cltvLimitFlag = cli.UintFlag{
Name: "cltv_limit",
Usage: "the maximum time lock that may be used for " +
"this payment",
}

var sendPaymentCommand = cli.Command{
Name: "sendpayment",
Category: "Payments",
Expand Down Expand Up @@ -2000,6 +2006,7 @@ var sendPaymentCommand = cli.Command{
Usage: "percentage of the payment's amount used as the" +
"maximum fee allowed when sending the payment",
},
cltvLimitFlag,
cli.StringFlag{
Name: "payment_hash, r",
Usage: "the hash to use within the payment's HTLC",
Expand Down Expand Up @@ -2119,6 +2126,7 @@ func sendPayment(ctx *cli.Context) error {
Amt: ctx.Int64("amt"),
FeeLimit: feeLimit,
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
CltvLimit: uint32(ctx.Int(cltvLimitFlag.Name)),
}

return sendPaymentRequest(client, req)
Expand Down Expand Up @@ -2266,6 +2274,7 @@ var payInvoiceCommand = cli.Command{
Usage: "percentage of the payment's amount used as the" +
"maximum fee allowed when sending the payment",
},
cltvLimitFlag,
cli.Uint64Flag{
Name: "outgoing_chan_id",
Usage: "short channel id of the outgoing channel to " +
Expand Down Expand Up @@ -2312,7 +2321,9 @@ func payInvoice(ctx *cli.Context) error {
Amt: ctx.Int64("amt"),
FeeLimit: feeLimit,
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
CltvLimit: uint32(ctx.Int(cltvLimitFlag.Name)),
}

return sendPaymentRequest(client, req)
}

Expand Down
1,180 changes: 596 additions & 584 deletions lnrpc/rpc.pb.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions lnrpc/rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,14 @@ message SendRequest {
any channel may be used.
*/
uint64 outgoing_chan_id = 9;

/**
An optional maximum total time lock for the route. If zero, there is no
maximum enforced.
*/
uint32 cltv_limit = 10;
}

message SendResponse {
string payment_error = 1 [json_name = "payment_error"];
bytes payment_preimage = 2 [json_name = "payment_preimage"];
Expand Down
5 changes: 5 additions & 0 deletions lnrpc/rpc.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2977,6 +2977,11 @@
"type": "string",
"format": "uint64",
"description": "*\nThe channel id of the channel that must be taken to the first hop. If zero,\nany channel may be used."
},
"cltv_limit": {
"type": "integer",
"format": "int64",
"description": "* \nAn optional maximum total time lock for the route. If zero, there is no\nmaximum enforced."
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions routing/heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type nodeWithDist struct {
// amount that includes also the fees for subsequent hops.
amountToReceive lnwire.MilliSatoshi

// incomingCltv is the expected cltv value for the incoming htlc of this
// node. This value does not include the final cltv.
incomingCltv uint32

// fee is the fee that this node is charging for forwarding.
fee lnwire.MilliSatoshi
}
Expand Down
2 changes: 2 additions & 0 deletions routing/missioncontrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ func (m *missionControl) NewPaymentSession(routeHints [][]zpay32.HopHint,
bandwidthHints: bandwidthHints,
errFailedPolicyChans: make(map[EdgeLocator]struct{}),
mc: m,
pathFinder: findPath,
}, nil
}

Expand All @@ -240,6 +241,7 @@ func (m *missionControl) NewPaymentSessionFromRoutes(routes []*Route) *paymentSe
preBuiltRoutes: routes,
errFailedPolicyChans: make(map[EdgeLocator]struct{}),
mc: m,
pathFinder: findPath,
}
}

Expand Down
31 changes: 24 additions & 7 deletions routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ const (
RiskFactorBillionths = 15
)

// pathFinder defines the interface of a path finding algorithm.
type pathFinder = func(g *graphParams, r *RestrictParams,
source, target Vertex, amt lnwire.MilliSatoshi) (
[]*channeldb.ChannelEdgePolicy, error)

// Hop represents an intermediate or final node of the route. This naming
// is in line with the definition given in BOLT #4: Onion Routing Protocol.
// The struct houses the channel along which this hop can be reached and
Expand Down Expand Up @@ -393,6 +398,11 @@ type RestrictParams struct {
// OutgoingChannelID is the channel that needs to be taken to the first
// hop. If nil, any channel may be used.
OutgoingChannelID *uint64

// CltvLimit is the maximum time lock of the route excluding the final
// ctlv. After path finding is complete, the caller needs to increase
// all cltv expiry heights with the required final cltv delta.
CltvLimit *uint32
}

// findPath attempts to find a path from the source node within the
Expand Down Expand Up @@ -479,6 +489,7 @@ func findPath(g *graphParams, r *RestrictParams, source, target Vertex,
node: targetNode,
amountToReceive: amt,
fee: 0,
incomingCltv: 0,
}

// We'll use this map as a series of "next" hop pointers. So to get
Expand Down Expand Up @@ -575,6 +586,14 @@ func findPath(g *graphParams, r *RestrictParams, source, target Vertex,
timeLockDelta = edge.TimeLockDelta
}

incomingCltv := toNodeDist.incomingCltv +
uint32(timeLockDelta)

// Check that we have cltv limit and that we are within it.
if r.CltvLimit != nil && incomingCltv > *r.CltvLimit {
return
}

// amountToReceive is the amount that the node that is added to
// the distance map needs to receive from a (to be found)
// previous node in the route. That previous node will need to
Expand Down Expand Up @@ -606,14 +625,11 @@ func findPath(g *graphParams, r *RestrictParams, source, target Vertex,
return
}

// If the edge has no time lock delta, the payment will always
// fail, so return.
//
// TODO(joostjager): Is this really true? Can't it be that
// nodes take this risk in exchange for a extraordinary high
// fee?
// Every edge should have a positive time lock delta. If we
// encounter a zero delta, log a warning line.
if edge.TimeLockDelta == 0 {
return
log.Warnf("Channel %v has zero cltv delta",
edge.ChannelID)
}

// All conditions are met and this new tentative distance is
Expand All @@ -625,6 +641,7 @@ func findPath(g *graphParams, r *RestrictParams, source, target Vertex,
node: fromNode,
amountToReceive: amountToReceive,
fee: fee,
incomingCltv: incomingCltv,
}

next[fromVertex] = edge
Expand Down
108 changes: 108 additions & 0 deletions routing/pathfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2021,3 +2021,111 @@ func TestRestrictOutgoingChannel(t *testing.T) {
"but channel %v was selected instead", route.Hops[0].ChannelID)
}
}

// TestCltvLimit asserts that a cltv limit is obeyed by the path finding
// algorithm.
func TestCltvLimit(t *testing.T) {
t.Run("no limit", func(t *testing.T) { testCltvLimit(t, 0, 1) })
t.Run("no path", func(t *testing.T) { testCltvLimit(t, 50, 0) })
t.Run("force high cost", func(t *testing.T) { testCltvLimit(t, 80, 3) })
}

func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) {
t.Parallel()

// 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,
// but the total time lock is lower (60).
testChannels := []*testChannel{
symmetricTestChannel("roasbeef", "a", 100000, &testChannelPolicy{}, 1),
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
Expiry: 144,
FeeBaseMsat: 10000,
MinHTLC: 1,
}),
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{}, 2),
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
Expiry: 100,
FeeBaseMsat: 20000,
MinHTLC: 1,
}),
symmetricTestChannel("roasbeef", "c", 100000, &testChannelPolicy{}, 3),
symmetricTestChannel("c", "d", 100000, &testChannelPolicy{
Expiry: 30,
FeeBaseMsat: 10000,
MinHTLC: 1,
}),
symmetricTestChannel("d", "target", 100000, &testChannelPolicy{
Expiry: 30,
FeeBaseMsat: 10000,
MinHTLC: 1,
}),
}

testGraphInstance, err := createTestGraphFromChannels(testChannels)
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
defer testGraphInstance.cleanUp()

sourceNode, err := testGraphInstance.graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
sourceVertex := Vertex(sourceNode.PubKeyBytes)

ignoredEdges := make(map[EdgeLocator]struct{})
ignoredVertexes := make(map[Vertex]struct{})

paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := testGraphInstance.aliasMap["target"]

// Find the best path given the cltv limit.
var cltvLimit *uint32
if limit != 0 {
cltvLimit = &limit
}

path, err := findPath(
&graphParams{
graph: testGraphInstance.graph,
},
&RestrictParams{
IgnoredNodes: ignoredVertexes,
IgnoredEdges: ignoredEdges,
FeeLimit: noFeeLimit,
CltvLimit: cltvLimit,
},
sourceVertex, target, paymentAmt,
)
if expectedChannel == 0 {
// Finish test if we expect no route.
if IsError(err, ErrNoPathFound) {
return
}
t.Fatal("expected no path to be found")
}
if err != nil {
t.Fatalf("unable to find path: %v", err)
}

const (
startingHeight = 100
finalHopCLTV = 1
)
route, err := newRoute(
paymentAmt, sourceVertex, path, startingHeight, finalHopCLTV,
)
if err != nil {
t.Fatalf("unable to create path: %v", err)
}

// Assert that the route starts with the expected channel.
if route.Hops[0].ChannelID != expectedChannel {
t.Fatalf("expected route to pass through channel %v, "+
"but channel %v was selected instead", expectedChannel,
route.Hops[0].ChannelID)
}
}
15 changes: 14 additions & 1 deletion routing/payment_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type paymentSession struct {

haveRoutes bool
preBuiltRoutes []*Route

pathFinder pathFinder
}

// ReportVertexFailure adds a vertex to the graph prune view after a client
Expand Down Expand Up @@ -136,12 +138,22 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
"edges, %v vertexes", len(pruneView.edges),
len(pruneView.vertexes))

// If a route cltv limit was specified, we need to subtract the final
// delta before passing it into path finding. The optimal path is
// independent of the final cltv delta and the path finding algorithm is
// unaware of this value.
var cltvLimit *uint32
if payment.CltvLimit != nil {
limit := *payment.CltvLimit - uint32(finalCltvDelta)
cltvLimit = &limit
}

// TODO(roasbeef): sync logic amongst dist sys

// Taking into account this prune view, we'll attempt to locate a path
// to our destination, respecting the recommendations from
// missionControl.
path, err := findPath(
path, err := p.pathFinder(
&graphParams{
graph: p.mc.graph,
additionalEdges: p.additionalEdges,
Expand All @@ -152,6 +164,7 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
IgnoredEdges: pruneView.edges,
FeeLimit: payment.FeeLimit,
OutgoingChannelID: payment.OutgoingChannelID,
CltvLimit: cltvLimit,
},
p.mc.selfNode.PubKeyBytes, payment.Target,
payment.Amount,
Expand Down
60 changes: 60 additions & 0 deletions routing/payment_session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package routing

import (
"testing"

"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
)

func TestRequestRoute(t *testing.T) {
const (
height = 10
)

findPath := func(g *graphParams, r *RestrictParams,
source, target Vertex, amt lnwire.MilliSatoshi) (
[]*channeldb.ChannelEdgePolicy, error) {

// We expect find path to receive a cltv limit excluding the
// final cltv delta.
if *r.CltvLimit != 22 {
t.Fatal("wrong cltv limit")
}

path := []*channeldb.ChannelEdgePolicy{
{
Node: &channeldb.LightningNode{},
},
}

return path, nil
}

session := &paymentSession{
mc: &missionControl{
selfNode: &channeldb.LightningNode{},
},
pruneViewSnapshot: graphPruneView{},
pathFinder: findPath,
}

cltvLimit := uint32(30)
finalCltvDelta := uint16(8)

payment := &LightningPayment{
CltvLimit: &cltvLimit,
FinalCLTVDelta: &finalCltvDelta,
}

route, err := session.RequestRoute(payment, height, finalCltvDelta)
if err != nil {
t.Fatal(err)
}

// We expect an absolute route lock value of height + finalCltvDelta
if route.TotalTimeLock != 18 {
t.Fatalf("unexpected total time lock of %v",
route.TotalTimeLock)
}
}
4 changes: 4 additions & 0 deletions routing/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,10 @@ type LightningPayment struct {
// if there isn't a route with lower fees than this limit.
FeeLimit lnwire.MilliSatoshi

// CltvLimit is the maximum time lock that is allowed for attempts to
// complete this payment.
CltvLimit *uint32

// PaymentHash is the r-hash value to use within the HTLC extended to
// the first hop.
PaymentHash [32]byte
Expand Down
Loading

0 comments on commit 4d8100c

Please sign in to comment.