Skip to content

Commit

Permalink
Merge pull request #2572 from joostjager/outgoing-chan-selection
Browse files Browse the repository at this point in the history
routing: add outgoing channel restriction
  • Loading branch information
joostjager committed Feb 9, 2019
2 parents f4dfcc3 + 7c30a8c commit c44d404
Show file tree
Hide file tree
Showing 9 changed files with 735 additions and 579 deletions.
14 changes: 14 additions & 0 deletions cmd/lncli/commands.go
Expand Up @@ -1951,6 +1951,12 @@ var sendPaymentCommand = cli.Command{
Name: "final_cltv_delta",
Usage: "the number of blocks the last hop has to reveal the preimage",
},
cli.Uint64Flag{
Name: "outgoing_chan_id",
Usage: "short channel id of the outgoing channel to " +
"use for the first hop of the payment",
Value: 0,
},
cli.BoolFlag{
Name: "force, f",
Usage: "will skip payment request confirmation",
Expand Down Expand Up @@ -2047,6 +2053,7 @@ func sendPayment(ctx *cli.Context) error {
PaymentRequest: ctx.String("pay_req"),
Amt: ctx.Int64("amt"),
FeeLimit: feeLimit,
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
}

return sendPaymentRequest(client, req)
Expand Down Expand Up @@ -2186,6 +2193,12 @@ var payInvoiceCommand = cli.Command{
Usage: "percentage of the payment's amount used as the" +
"maximum fee allowed when sending the payment",
},
cli.Uint64Flag{
Name: "outgoing_chan_id",
Usage: "short channel id of the outgoing channel to " +
"use for the first hop of the payment",
Value: 0,
},
cli.BoolFlag{
Name: "force, f",
Usage: "will skip payment request confirmation",
Expand Down Expand Up @@ -2225,6 +2238,7 @@ func payInvoice(ctx *cli.Context) error {
PaymentRequest: payReq,
Amt: ctx.Int64("amt"),
FeeLimit: feeLimit,
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
}
return sendPaymentRequest(client, req)
}
Expand Down
1,139 changes: 576 additions & 563 deletions lnrpc/rpc.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lnrpc/rpc.proto
Expand Up @@ -774,6 +774,12 @@ message SendRequest {
send the payment.
*/
FeeLimit fee_limit = 8;

/**
The channel id of the channel that must be taken to the first hop. If zero,
any channel may be used.
*/
uint64 outgoing_chan_id = 9;
}
message SendResponse {
string payment_error = 1 [json_name = "payment_error"];
Expand Down
5 changes: 5 additions & 0 deletions lnrpc/rpc.swagger.json
Expand Up @@ -2903,6 +2903,11 @@
"fee_limit": {
"$ref": "#/definitions/lnrpcFeeLimit",
"description": "*\nThe maximum number of satoshis that will be paid as a fee of the payment.\nThis value can be represented either as a percentage of the amount being\nsent, or as a fixed amount of the maximum fee the user is willing the pay to\nsend the payment."
},
"outgoing_chan_id": {
"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."
}
}
},
Expand Down
15 changes: 14 additions & 1 deletion routing/pathfind.go
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/coreos/bbolt"

"github.com/lightningnetwork/lightning-onion"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
)
Expand Down Expand Up @@ -453,6 +453,10 @@ type restrictParams struct {
// feeLimit is a maximum fee amount allowed to be used on the path from
// the source to the target.
feeLimit lnwire.MilliSatoshi

// outgoingChannelID is the channel that needs to be taken to the first
// hop. If nil, any channel may be used.
outgoingChannelID *uint64
}

// findPath attempts to find a path from the source node within the
Expand Down Expand Up @@ -563,13 +567,22 @@ func findPath(g *graphParams, r *restrictParams,
// TODO(halseth): also ignore disable flags for non-local
// channels if bandwidth hint is set?
isSourceChan := fromVertex == sourceVertex

edgeFlags := edge.ChannelFlags
isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0

if !isSourceChan && isDisabled {
return
}

// If we have an outgoing channel restriction and this is not
// the specified channel, skip it.
if isSourceChan && r.outgoingChannelID != nil &&
*r.outgoingChannelID != edge.ChannelID {

return
}

// If this vertex or edge has been black listed, then we'll
// skip exploring this edge.
if _, ok := r.ignoredNodes[fromVertex]; ok {
Expand Down
92 changes: 92 additions & 0 deletions routing/pathfind_test.go
Expand Up @@ -1918,3 +1918,95 @@ func TestNewRouteFromEmptyHops(t *testing.T) {
t.Fatalf("expected empty hops error: instead got: %v", err)
}
}

// TestRestrictOutgoingChannel asserts that a outgoing channel restriction is
// obeyed by the path finding algorithm.
func TestRestrictOutgoingChannel(t *testing.T) {
t.Parallel()

// Set up a test graph with three possible paths from roasbeef to
// target. The path through channel 2 is the highest cost path.
testChannels := []*testChannel{
symmetricTestChannel("roasbeef", "a", 100000, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
}, 1),
symmetricTestChannel("a", "target", 100000, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
}),
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{
Expiry: 144,
FeeRate: 800,
MinHTLC: 1,
}, 2),
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{
Expiry: 144,
FeeRate: 600,
MinHTLC: 1,
}, 3),
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
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{})

const (
startingHeight = 100
finalHopCLTV = 1
)

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

// Find the best path given the restriction to only use channel 2 as the
// outgoing channel.
path, err := findPath(
&graphParams{
graph: testGraphInstance.graph,
},
&restrictParams{
ignoredNodes: ignoredVertexes,
ignoredEdges: ignoredEdges,
feeLimit: noFeeLimit,
outgoingChannelID: &outgoingChannelID,
},
sourceNode, target, paymentAmt,
)
if err != nil {
t.Fatalf("unable to find path: %v", err)
}
route, err := newRoute(
paymentAmt, infinity, sourceVertex, path, startingHeight,
finalHopCLTV,
)
if err != nil {
t.Fatalf("unable to create path: %v", err)
}

// Assert that the route starts with channel 2, in line with the
// specified restriction.
if route.Hops[0].ChannelID != 2 {
t.Fatalf("expected route to pass through channel 2, "+
"but channel %v was selected instead", route.Hops[0].ChannelID)
}
}
7 changes: 4 additions & 3 deletions routing/payment_session.go
Expand Up @@ -148,9 +148,10 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
bandwidthHints: p.bandwidthHints,
},
&restrictParams{
ignoredNodes: pruneView.vertexes,
ignoredEdges: pruneView.edges,
feeLimit: payment.FeeLimit,
ignoredNodes: pruneView.vertexes,
ignoredEdges: pruneView.edges,
feeLimit: payment.FeeLimit,
outgoingChannelID: payment.OutgoingChannelID,
},
p.mc.selfNode, payment.Target, payment.Amount,
)
Expand Down
6 changes: 5 additions & 1 deletion routing/router.go
Expand Up @@ -17,7 +17,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/go-errors/errors"

"github.com/lightningnetwork/lightning-onion"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/input"
Expand Down Expand Up @@ -1612,6 +1612,10 @@ type LightningPayment struct {
// destination successfully.
RouteHints [][]HopHint

// OutgoingChannelID is the channel that needs to be taken to the first
// hop. If nil, any channel may be used.
OutgoingChannelID *uint64

// TODO(roasbeef): add e2e message?
}

Expand Down
30 changes: 19 additions & 11 deletions rpcserver.go
Expand Up @@ -2732,12 +2732,13 @@ func unmarshallSendToRouteRequest(req *lnrpc.SendToRouteRequest,
// hints), or we'll get a fully populated route from the user that we'll pass
// directly to the channel router for dispatching.
type rpcPaymentIntent struct {
msat lnwire.MilliSatoshi
feeLimit lnwire.MilliSatoshi
dest *btcec.PublicKey
rHash [32]byte
cltvDelta uint16
routeHints [][]routing.HopHint
msat lnwire.MilliSatoshi
feeLimit lnwire.MilliSatoshi
dest *btcec.PublicKey
rHash [32]byte
cltvDelta uint16
routeHints [][]routing.HopHint
outgoingChannelID *uint64

routes []*routing.Route
}
Expand Down Expand Up @@ -2771,6 +2772,12 @@ func extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error
return payIntent, nil
}

// If there are no routes specified, pass along a outgoing channel
// restriction if specified.
if rpcPayReq.OutgoingChanId != 0 {
payIntent.outgoingChannelID = &rpcPayReq.OutgoingChanId
}

// If the payment request field isn't blank, then the details of the
// invoice are encoded entirely within the encoded payReq. So we'll
// attempt to decode it, populating the payment accordingly.
Expand Down Expand Up @@ -2920,11 +2927,12 @@ func (r *rpcServer) dispatchPaymentIntent(
// router, otherwise we'll create a payment session to execute it.
if len(payIntent.routes) == 0 {
payment := &routing.LightningPayment{
Target: payIntent.dest,
Amount: payIntent.msat,
FeeLimit: payIntent.feeLimit,
PaymentHash: payIntent.rHash,
RouteHints: payIntent.routeHints,
Target: payIntent.dest,
Amount: payIntent.msat,
FeeLimit: payIntent.feeLimit,
PaymentHash: payIntent.rHash,
RouteHints: payIntent.routeHints,
OutgoingChannelID: payIntent.outgoingChannelID,
}

// If the final CLTV value was specified, then we'll use that
Expand Down

0 comments on commit c44d404

Please sign in to comment.