Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routing: add outgoing channel restriction #2572

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
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 " +
joostjager marked this conversation as resolved.
Show resolved Hide resolved
"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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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) {
joostjager marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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