mirror of
https://github.com/aljazceru/breez-lnd.git
synced 2025-12-18 14:44:22 +01:00
routing: add outgoing channel restriction
This commit is contained in:
@@ -1951,6 +1951,12 @@ var sendPaymentCommand = cli.Command{
|
|||||||
Name: "final_cltv_delta",
|
Name: "final_cltv_delta",
|
||||||
Usage: "the number of blocks the last hop has to reveal the preimage",
|
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{
|
cli.BoolFlag{
|
||||||
Name: "force, f",
|
Name: "force, f",
|
||||||
Usage: "will skip payment request confirmation",
|
Usage: "will skip payment request confirmation",
|
||||||
@@ -2047,6 +2053,7 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
PaymentRequest: ctx.String("pay_req"),
|
PaymentRequest: ctx.String("pay_req"),
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
FeeLimit: feeLimit,
|
FeeLimit: feeLimit,
|
||||||
|
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPaymentRequest(client, req)
|
return sendPaymentRequest(client, req)
|
||||||
@@ -2186,6 +2193,12 @@ var payInvoiceCommand = cli.Command{
|
|||||||
Usage: "percentage of the payment's amount used as the" +
|
Usage: "percentage of the payment's amount used as the" +
|
||||||
"maximum fee allowed when sending the payment",
|
"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{
|
cli.BoolFlag{
|
||||||
Name: "force, f",
|
Name: "force, f",
|
||||||
Usage: "will skip payment request confirmation",
|
Usage: "will skip payment request confirmation",
|
||||||
@@ -2225,6 +2238,7 @@ func payInvoice(ctx *cli.Context) error {
|
|||||||
PaymentRequest: payReq,
|
PaymentRequest: payReq,
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
FeeLimit: feeLimit,
|
FeeLimit: feeLimit,
|
||||||
|
OutgoingChanId: ctx.Uint64("outgoing_chan_id"),
|
||||||
}
|
}
|
||||||
return sendPaymentRequest(client, req)
|
return sendPaymentRequest(client, req)
|
||||||
}
|
}
|
||||||
|
|||||||
1139
lnrpc/rpc.pb.go
1139
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@@ -774,6 +774,12 @@ message SendRequest {
|
|||||||
send the payment.
|
send the payment.
|
||||||
*/
|
*/
|
||||||
FeeLimit fee_limit = 8;
|
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 {
|
message SendResponse {
|
||||||
string payment_error = 1 [json_name = "payment_error"];
|
string payment_error = 1 [json_name = "payment_error"];
|
||||||
|
|||||||
@@ -2903,6 +2903,11 @@
|
|||||||
"fee_limit": {
|
"fee_limit": {
|
||||||
"$ref": "#/definitions/lnrpcFeeLimit",
|
"$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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/coreos/bbolt"
|
"github.com/coreos/bbolt"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
@@ -453,6 +453,10 @@ type restrictParams struct {
|
|||||||
// feeLimit is a maximum fee amount allowed to be used on the path from
|
// feeLimit is a maximum fee amount allowed to be used on the path from
|
||||||
// the source to the target.
|
// the source to the target.
|
||||||
feeLimit lnwire.MilliSatoshi
|
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
|
// findPath attempts to find a path from the source node within the
|
||||||
@@ -563,6 +567,7 @@ func findPath(g *graphParams, r *restrictParams,
|
|||||||
// TODO(halseth): also ignore disable flags for non-local
|
// TODO(halseth): also ignore disable flags for non-local
|
||||||
// channels if bandwidth hint is set?
|
// channels if bandwidth hint is set?
|
||||||
isSourceChan := fromVertex == sourceVertex
|
isSourceChan := fromVertex == sourceVertex
|
||||||
|
|
||||||
edgeFlags := edge.ChannelFlags
|
edgeFlags := edge.ChannelFlags
|
||||||
isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0
|
isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0
|
||||||
|
|
||||||
@@ -570,6 +575,14 @@ func findPath(g *graphParams, r *restrictParams,
|
|||||||
return
|
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
|
// If this vertex or edge has been black listed, then we'll
|
||||||
// skip exploring this edge.
|
// skip exploring this edge.
|
||||||
if _, ok := r.ignoredNodes[fromVertex]; ok {
|
if _, ok := r.ignoredNodes[fromVertex]; ok {
|
||||||
|
|||||||
@@ -1918,3 +1918,95 @@ func TestNewRouteFromEmptyHops(t *testing.T) {
|
|||||||
t.Fatalf("expected empty hops error: instead got: %v", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,9 +148,10 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
|
|||||||
bandwidthHints: p.bandwidthHints,
|
bandwidthHints: p.bandwidthHints,
|
||||||
},
|
},
|
||||||
&restrictParams{
|
&restrictParams{
|
||||||
ignoredNodes: pruneView.vertexes,
|
ignoredNodes: pruneView.vertexes,
|
||||||
ignoredEdges: pruneView.edges,
|
ignoredEdges: pruneView.edges,
|
||||||
feeLimit: payment.FeeLimit,
|
feeLimit: payment.FeeLimit,
|
||||||
|
outgoingChannelID: payment.OutgoingChannelID,
|
||||||
},
|
},
|
||||||
p.mc.selfNode, payment.Target, payment.Amount,
|
p.mc.selfNode, payment.Target, payment.Amount,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/go-errors/errors"
|
"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/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
@@ -1612,6 +1612,10 @@ type LightningPayment struct {
|
|||||||
// destination successfully.
|
// destination successfully.
|
||||||
RouteHints [][]HopHint
|
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?
|
// TODO(roasbeef): add e2e message?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
rpcserver.go
30
rpcserver.go
@@ -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
|
// hints), or we'll get a fully populated route from the user that we'll pass
|
||||||
// directly to the channel router for dispatching.
|
// directly to the channel router for dispatching.
|
||||||
type rpcPaymentIntent struct {
|
type rpcPaymentIntent struct {
|
||||||
msat lnwire.MilliSatoshi
|
msat lnwire.MilliSatoshi
|
||||||
feeLimit lnwire.MilliSatoshi
|
feeLimit lnwire.MilliSatoshi
|
||||||
dest *btcec.PublicKey
|
dest *btcec.PublicKey
|
||||||
rHash [32]byte
|
rHash [32]byte
|
||||||
cltvDelta uint16
|
cltvDelta uint16
|
||||||
routeHints [][]routing.HopHint
|
routeHints [][]routing.HopHint
|
||||||
|
outgoingChannelID *uint64
|
||||||
|
|
||||||
routes []*routing.Route
|
routes []*routing.Route
|
||||||
}
|
}
|
||||||
@@ -2771,6 +2772,12 @@ func extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPaymentIntent, error
|
|||||||
return payIntent, nil
|
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
|
// If the payment request field isn't blank, then the details of the
|
||||||
// invoice are encoded entirely within the encoded payReq. So we'll
|
// invoice are encoded entirely within the encoded payReq. So we'll
|
||||||
// attempt to decode it, populating the payment accordingly.
|
// attempt to decode it, populating the payment accordingly.
|
||||||
@@ -2920,11 +2927,12 @@ func (r *rpcServer) dispatchPaymentIntent(
|
|||||||
// router, otherwise we'll create a payment session to execute it.
|
// router, otherwise we'll create a payment session to execute it.
|
||||||
if len(payIntent.routes) == 0 {
|
if len(payIntent.routes) == 0 {
|
||||||
payment := &routing.LightningPayment{
|
payment := &routing.LightningPayment{
|
||||||
Target: payIntent.dest,
|
Target: payIntent.dest,
|
||||||
Amount: payIntent.msat,
|
Amount: payIntent.msat,
|
||||||
FeeLimit: payIntent.feeLimit,
|
FeeLimit: payIntent.feeLimit,
|
||||||
PaymentHash: payIntent.rHash,
|
PaymentHash: payIntent.rHash,
|
||||||
RouteHints: payIntent.routeHints,
|
RouteHints: payIntent.routeHints,
|
||||||
|
OutgoingChannelID: payIntent.outgoingChannelID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the final CLTV value was specified, then we'll use that
|
// If the final CLTV value was specified, then we'll use that
|
||||||
|
|||||||
Reference in New Issue
Block a user