diff --git a/channeldb/graph.go b/channeldb/graph.go index 8cd23973..46ba1311 100644 --- a/channeldb/graph.go +++ b/channeldb/graph.go @@ -155,6 +155,9 @@ const ( // would be possible for a node to create a ton of updates and slowly // fill our disk, and also waste bandwidth due to relaying. MaxAllowedExtraOpaqueBytes = 10000 + + // feeRateParts is the total number of parts used to express fee rates. + feeRateParts = 1e6 ) // ChannelGraph is a persistent, on-disk graph representation of the Lightning @@ -2828,6 +2831,31 @@ func (c *ChannelEdgePolicy) IsDisabled() bool { lnwire.ChanUpdateDisabled } +// ComputeFee computes the fee to forward an HTLC of `amt` milli-satoshis over +// the passed active payment channel. This value is currently computed as +// specified in BOLT07, but will likely change in the near future. +func (c *ChannelEdgePolicy) ComputeFee( + amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { + + return c.FeeBaseMSat + (amt*c.FeeProportionalMillionths)/feeRateParts +} + +// divideCeil divides dividend by factor and rounds the result up. +func divideCeil(dividend, factor lnwire.MilliSatoshi) lnwire.MilliSatoshi { + return (dividend + factor - 1) / factor +} + +// ComputeFeeFromIncoming computes the fee to forward an HTLC given the incoming +// amount. +func (c *ChannelEdgePolicy) ComputeFeeFromIncoming( + incomingAmt lnwire.MilliSatoshi) lnwire.MilliSatoshi { + + return incomingAmt - divideCeil( + feeRateParts*(incomingAmt-c.FeeBaseMSat), + feeRateParts+c.FeeProportionalMillionths, + ) +} + // FetchChannelEdgesByOutpoint attempts to lookup the two directed edges for // the channel identified by the funding outpoint. If the channel can't be // found, then ErrEdgeNotFound is returned. A struct which houses the general diff --git a/channeldb/graph_test.go b/channeldb/graph_test.go index 5068228f..de8774a9 100644 --- a/channeldb/graph_test.go +++ b/channeldb/graph_test.go @@ -3173,3 +3173,25 @@ func TestLightningNodeSigVerification(t *testing.T) { t.Fatalf("unable to verify sig") } } + +// TestComputeFee tests fee calculation based on both in- and outgoing amt. +func TestComputeFee(t *testing.T) { + var ( + policy = ChannelEdgePolicy{ + FeeBaseMSat: 10000, + FeeProportionalMillionths: 30000, + } + outgoingAmt = lnwire.MilliSatoshi(1000000) + expectedFee = lnwire.MilliSatoshi(40000) + ) + + fee := policy.ComputeFee(outgoingAmt) + if fee != expectedFee { + t.Fatalf("expected fee %v, got %v", expectedFee, fee) + } + + fwdFee := policy.ComputeFeeFromIncoming(outgoingAmt + fee) + if fwdFee != expectedFee { + t.Fatalf("expected fee %v, but got %v", fee, fwdFee) + } +} diff --git a/cmd/lncli/cmd_build_route.go b/cmd/lncli/cmd_build_route.go new file mode 100644 index 00000000..64cda08f --- /dev/null +++ b/cmd/lncli/cmd_build_route.go @@ -0,0 +1,94 @@ +// +build routerrpc + +package main + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/urfave/cli" +) + +var buildRouteCommand = cli.Command{ + Name: "buildroute", + Category: "Payments", + Usage: "Build a route from a list of hop pubkeys.", + Action: actionDecorator(buildRoute), + Flags: []cli.Flag{ + cli.Int64Flag{ + Name: "amt", + Usage: "the amount to send expressed in satoshis. If" + + "not set, the minimum routable amount is used", + }, + cli.Int64Flag{ + Name: "final_cltv_delta", + Usage: "number of blocks the last hop has to reveal " + + "the preimage", + Value: lnd.DefaultBitcoinTimeLockDelta, + }, + cli.StringFlag{ + Name: "hops", + Usage: "comma separated hex pubkeys", + }, + 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, + }, + }, +} + +func buildRoute(ctx *cli.Context) error { + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + if !ctx.IsSet("hops") { + return errors.New("hops required") + } + + // Build list of hop addresses for the rpc. + hops := strings.Split(ctx.String("hops"), ",") + rpcHops := make([][]byte, 0, len(hops)) + for _, k := range hops { + pubkey, err := route.NewVertexFromStr(k) + if err != nil { + return fmt.Errorf("error parsing %v: %v", k, err) + } + rpcHops = append(rpcHops, pubkey[:]) + } + + var amtMsat int64 + hasAmt := ctx.IsSet("amt") + if hasAmt { + amtMsat = ctx.Int64("amt") * 1000 + if amtMsat == 0 { + return fmt.Errorf("non-zero amount required") + } + } + + // Call BuildRoute rpc. + req := &routerrpc.BuildRouteRequest{ + AmtMsat: amtMsat, + FinalCltvDelta: int32(ctx.Int64("final_cltv_delta")), + HopPubkeys: rpcHops, + OutgoingChanId: ctx.Uint64("outgoing_chan_id"), + } + + rpcCtx := context.Background() + route, err := client.BuildRoute(rpcCtx, req) + if err != nil { + return err + } + + printJSON(route) + + return nil +} diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index 79d0e428..79f15e33 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -21,6 +21,7 @@ import ( "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/walletunlocker" "github.com/urfave/cli" "golang.org/x/crypto/ssh/terminal" @@ -2381,22 +2382,23 @@ var sendToRouteCommand = cli.Command{ Usage: "Send a payment over a predefined route.", Description: ` Send a payment over Lightning using a specific route. One must specify - a list of routes to attempt and the payment hash. This command can even - be chained with the response to queryroutes. This command can be used - to implement channel rebalancing by crafting a self-route, or even - atomic swaps using a self-route that crosses multiple chains. + the route to attempt and the payment hash. This command can even + be chained with the response to queryroutes or buildroute. This command + can be used to implement channel rebalancing by crafting a self-route, + or even atomic swaps using a self-route that crosses multiple chains. - There are three ways to specify routes: + There are three ways to specify a route: * using the --routes parameter to manually specify a JSON encoded - set of routes in the format of the return value of queryroutes: + route in the format of the return value of queryroutes or + buildroute: (lncli sendtoroute --payment_hash= --routes=) - * passing the routes as a positional argument: + * passing the route as a positional argument: (lncli sendtoroute --payment_hash=pay_hash ) - * or reading in the routes from stdin, which can allow chaining the - response from queryroutes, or even read in a file with a set of - pre-computed routes: + * or reading in the route from stdin, which can allow chaining the + response from queryroutes or buildroute, or even read in a file + with a pre-computed route: (lncli queryroutes --args.. | lncli sendtoroute --payment_hash= - notice the '-' at the end, which signals that lncli should read @@ -2474,25 +2476,37 @@ func sendToRoute(ctx *cli.Context) error { jsonRoutes = string(b) } + // Try to parse the provided json both in the legacy QueryRoutes format + // that contains a list of routes and the single route BuildRoute + // format. + var route *lnrpc.Route routes := &lnrpc.QueryRoutesResponse{} err = jsonpb.UnmarshalString(jsonRoutes, routes) - if err != nil { - return fmt.Errorf("unable to unmarshal json string "+ - "from incoming array of routes: %v", err) - } + if err == nil { + if len(routes.Routes) == 0 { + return fmt.Errorf("no routes provided") + } - if len(routes.Routes) == 0 { - return fmt.Errorf("no routes provided") - } + if len(routes.Routes) != 1 { + return fmt.Errorf("expected a single route, but got %v", + len(routes.Routes)) + } - if len(routes.Routes) != 1 { - return fmt.Errorf("expected a single route, but got %v", - len(routes.Routes)) + route = routes.Routes[0] + } else { + routes := &routerrpc.BuildRouteResponse{} + err = jsonpb.UnmarshalString(jsonRoutes, routes) + if err != nil { + return fmt.Errorf("unable to unmarshal json string "+ + "from incoming array of routes: %v", err) + } + + route = routes.Route } req := &lnrpc.SendToRouteRequest{ PaymentHash: rHash, - Route: routes.Routes[0], + Route: route, } return sendToRouteRequest(ctx, req) diff --git a/cmd/lncli/routerrpc_active.go b/cmd/lncli/routerrpc_active.go index f323aed1..4d217267 100644 --- a/cmd/lncli/routerrpc_active.go +++ b/cmd/lncli/routerrpc_active.go @@ -6,5 +6,9 @@ import "github.com/urfave/cli" // routerCommands will return nil for non-routerrpc builds. func routerCommands() []cli.Command { - return []cli.Command{queryMissionControlCommand, resetMissionControlCommand} + return []cli.Command{ + queryMissionControlCommand, + resetMissionControlCommand, + buildRouteCommand, + } } diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index fdb45dfb..99d6ba4a 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -1197,6 +1197,122 @@ func (m *PairHistory) GetLastAttemptSuccessful() bool { return false } +type BuildRouteRequest struct { + //* + //The amount to send expressed in msat. If set to zero, the minimum routable + //amount is used. + AmtMsat int64 `protobuf:"varint,1,opt,name=amt_msat,json=amtMsat,proto3" json:"amt_msat,omitempty"` + //* + //CLTV delta from the current height that should be used for the timelock + //of the final hop + FinalCltvDelta int32 `protobuf:"varint,2,opt,name=final_cltv_delta,json=finalCltvDelta,proto3" json:"final_cltv_delta,omitempty"` + //* + //The channel id of the channel that must be taken to the first hop. If zero, + //any channel may be used. + OutgoingChanId uint64 `protobuf:"varint,3,opt,name=outgoing_chan_id,json=outgoingChanId,proto3" json:"outgoing_chan_id,omitempty"` + //* + //A list of hops that defines the route. This does not include the source hop + //pubkey. + HopPubkeys [][]byte `protobuf:"bytes,4,rep,name=hop_pubkeys,json=hopPubkeys,proto3" json:"hop_pubkeys,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BuildRouteRequest) Reset() { *m = BuildRouteRequest{} } +func (m *BuildRouteRequest) String() string { return proto.CompactTextString(m) } +func (*BuildRouteRequest) ProtoMessage() {} +func (*BuildRouteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{15} +} + +func (m *BuildRouteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_BuildRouteRequest.Unmarshal(m, b) +} +func (m *BuildRouteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_BuildRouteRequest.Marshal(b, m, deterministic) +} +func (m *BuildRouteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_BuildRouteRequest.Merge(m, src) +} +func (m *BuildRouteRequest) XXX_Size() int { + return xxx_messageInfo_BuildRouteRequest.Size(m) +} +func (m *BuildRouteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_BuildRouteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_BuildRouteRequest proto.InternalMessageInfo + +func (m *BuildRouteRequest) GetAmtMsat() int64 { + if m != nil { + return m.AmtMsat + } + return 0 +} + +func (m *BuildRouteRequest) GetFinalCltvDelta() int32 { + if m != nil { + return m.FinalCltvDelta + } + return 0 +} + +func (m *BuildRouteRequest) GetOutgoingChanId() uint64 { + if m != nil { + return m.OutgoingChanId + } + return 0 +} + +func (m *BuildRouteRequest) GetHopPubkeys() [][]byte { + if m != nil { + return m.HopPubkeys + } + return nil +} + +type BuildRouteResponse struct { + //* + //Fully specified route that can be used to execute the payment. + Route *lnrpc.Route `protobuf:"bytes,1,opt,name=route,proto3" json:"route,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BuildRouteResponse) Reset() { *m = BuildRouteResponse{} } +func (m *BuildRouteResponse) String() string { return proto.CompactTextString(m) } +func (*BuildRouteResponse) ProtoMessage() {} +func (*BuildRouteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{16} +} + +func (m *BuildRouteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_BuildRouteResponse.Unmarshal(m, b) +} +func (m *BuildRouteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_BuildRouteResponse.Marshal(b, m, deterministic) +} +func (m *BuildRouteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_BuildRouteResponse.Merge(m, src) +} +func (m *BuildRouteResponse) XXX_Size() int { + return xxx_messageInfo_BuildRouteResponse.Size(m) +} +func (m *BuildRouteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_BuildRouteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_BuildRouteResponse proto.InternalMessageInfo + +func (m *BuildRouteResponse) GetRoute() *lnrpc.Route { + if m != nil { + return m.Route + } + return nil +} + func init() { proto.RegisterEnum("routerrpc.PaymentState", PaymentState_name, PaymentState_value) proto.RegisterEnum("routerrpc.Failure_FailureCode", Failure_FailureCode_name, Failure_FailureCode_value) @@ -1216,123 +1332,131 @@ func init() { proto.RegisterType((*QueryMissionControlResponse)(nil), "routerrpc.QueryMissionControlResponse") proto.RegisterType((*NodeHistory)(nil), "routerrpc.NodeHistory") proto.RegisterType((*PairHistory)(nil), "routerrpc.PairHistory") + proto.RegisterType((*BuildRouteRequest)(nil), "routerrpc.BuildRouteRequest") + proto.RegisterType((*BuildRouteResponse)(nil), "routerrpc.BuildRouteResponse") } func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) } var fileDescriptor_7a0613f69d37b0a5 = []byte{ - // 1769 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x57, 0x41, 0x73, 0x22, 0xb9, - 0x15, 0x5e, 0x0c, 0x18, 0x78, 0x80, 0xdd, 0x96, 0x3d, 0x76, 0x0f, 0x1e, 0xef, 0x7a, 0xd9, 0xcd, - 0xac, 0x6b, 0x6a, 0x63, 0x6f, 0x9c, 0xda, 0xad, 0xa9, 0x3d, 0x24, 0xc5, 0x80, 0x58, 0xf7, 0x0c, - 0x74, 0x7b, 0x05, 0xcc, 0xee, 0x24, 0x07, 0x95, 0x0c, 0xb2, 0xe9, 0x72, 0xd3, 0xcd, 0x74, 0x0b, - 0x67, 0x9c, 0x43, 0x2e, 0xa9, 0x1c, 0x73, 0xcf, 0xbf, 0xc8, 0x6f, 0xca, 0x25, 0xf9, 0x05, 0x39, - 0xa6, 0x2a, 0x25, 0xa9, 0x1b, 0x1a, 0x8c, 0x27, 0x39, 0xd1, 0xfa, 0xde, 0xa7, 0x27, 0xe9, 0x3d, - 0xbd, 0x4f, 0x0f, 0xd8, 0x0f, 0x83, 0x99, 0xe0, 0x61, 0x38, 0x1d, 0x9e, 0xe9, 0xaf, 0xd3, 0x69, - 0x18, 0x88, 0x00, 0x95, 0xe6, 0x78, 0xad, 0x14, 0x4e, 0x87, 0x1a, 0xad, 0xff, 0x27, 0x0b, 0xa8, - 0xc7, 0xfd, 0xd1, 0x25, 0xbb, 0x9f, 0x70, 0x5f, 0x10, 0xfe, 0x7e, 0xc6, 0x23, 0x81, 0x10, 0xe4, - 0x46, 0x3c, 0x12, 0x66, 0xe6, 0x38, 0x73, 0x52, 0x21, 0xea, 0x1b, 0x19, 0x90, 0x65, 0x13, 0x61, - 0x6e, 0x1c, 0x67, 0x4e, 0xb2, 0x44, 0x7e, 0xa2, 0xcf, 0xa1, 0x32, 0xd5, 0xf3, 0xe8, 0x98, 0x45, - 0x63, 0x33, 0xab, 0xd8, 0xe5, 0x18, 0xbb, 0x60, 0xd1, 0x18, 0x9d, 0x80, 0x71, 0xed, 0xfa, 0xcc, - 0xa3, 0x43, 0x4f, 0xdc, 0xd1, 0x11, 0xf7, 0x04, 0x33, 0x73, 0xc7, 0x99, 0x93, 0x3c, 0xd9, 0x52, - 0x78, 0xd3, 0x13, 0x77, 0x2d, 0x89, 0xa2, 0xaf, 0x60, 0x3b, 0x71, 0x16, 0xea, 0x5d, 0x98, 0xf9, - 0xe3, 0xcc, 0x49, 0x89, 0x6c, 0x4d, 0x97, 0xf7, 0xf6, 0x15, 0x6c, 0x0b, 0x77, 0xc2, 0x83, 0x99, - 0xa0, 0x11, 0x1f, 0x06, 0xfe, 0x28, 0x32, 0x37, 0xb5, 0xc7, 0x18, 0xee, 0x69, 0x14, 0xd5, 0xa1, - 0x7a, 0xcd, 0x39, 0xf5, 0xdc, 0x89, 0x2b, 0x68, 0xc4, 0x84, 0x59, 0x50, 0x5b, 0x2f, 0x5f, 0x73, - 0xde, 0x91, 0x58, 0x8f, 0x09, 0xb9, 0xbf, 0x60, 0x26, 0x6e, 0x02, 0xd7, 0xbf, 0xa1, 0xc3, 0x31, - 0xf3, 0xa9, 0x3b, 0x32, 0x8b, 0xc7, 0x99, 0x93, 0x1c, 0xd9, 0x4a, 0xf0, 0xe6, 0x98, 0xf9, 0xd6, - 0x08, 0x1d, 0x01, 0xa8, 0x33, 0x28, 0x77, 0x66, 0x49, 0xad, 0x58, 0x92, 0x88, 0xf2, 0x85, 0xce, - 0xa1, 0xac, 0x02, 0x4c, 0xc7, 0xae, 0x2f, 0x22, 0x13, 0x8e, 0xb3, 0x27, 0xe5, 0x73, 0xe3, 0xd4, - 0xf3, 0x65, 0xac, 0x89, 0xb4, 0x5c, 0xb8, 0xbe, 0x20, 0x69, 0x12, 0xc2, 0x50, 0x94, 0x91, 0xa5, - 0xc2, 0xbb, 0x33, 0xcb, 0x6a, 0xc2, 0x8b, 0xd3, 0x79, 0x96, 0x4e, 0x1f, 0xa6, 0xe5, 0xb4, 0xc5, - 0x23, 0xd1, 0xf7, 0xee, 0xb0, 0x2f, 0xc2, 0x7b, 0x52, 0x18, 0xe9, 0x51, 0xed, 0x7b, 0xa8, 0xa4, - 0x0d, 0x32, 0x51, 0xb7, 0xfc, 0x5e, 0xe5, 0x2e, 0x47, 0xe4, 0x27, 0xda, 0x83, 0xfc, 0x1d, 0xf3, - 0x66, 0x5c, 0x25, 0xaf, 0x42, 0xf4, 0xe0, 0xfb, 0x8d, 0x97, 0x99, 0xfa, 0x4b, 0xd8, 0xed, 0x87, - 0x6c, 0x78, 0xbb, 0x92, 0xff, 0xd5, 0xcc, 0x66, 0x1e, 0x64, 0xb6, 0xfe, 0x27, 0xa8, 0xc6, 0x93, - 0x7a, 0x82, 0x89, 0x59, 0x84, 0x7e, 0x09, 0xf9, 0x48, 0x30, 0xc1, 0x15, 0x79, 0xeb, 0xfc, 0x20, - 0x75, 0x94, 0x14, 0x91, 0x13, 0xcd, 0x42, 0x35, 0x28, 0x4e, 0x43, 0xee, 0x4e, 0xd8, 0x4d, 0xb2, - 0xad, 0xf9, 0x18, 0xd5, 0x21, 0xaf, 0x26, 0xab, 0x1b, 0x55, 0x3e, 0xaf, 0xa4, 0xc3, 0x48, 0xb4, - 0xa9, 0xfe, 0x1b, 0xd8, 0x56, 0xe3, 0x36, 0xe7, 0x1f, 0xbb, 0xb5, 0x07, 0x50, 0x60, 0x13, 0x9d, - 0x7e, 0x7d, 0x73, 0x37, 0xd9, 0x44, 0x66, 0xbe, 0x3e, 0x02, 0x63, 0x31, 0x3f, 0x9a, 0x06, 0x7e, - 0xc4, 0xe5, 0x6d, 0x90, 0xce, 0xe5, 0x65, 0x90, 0x37, 0x67, 0x22, 0x67, 0x65, 0xd4, 0xac, 0xad, - 0x18, 0x6f, 0x73, 0xde, 0x8d, 0x98, 0x40, 0xcf, 0xf5, 0x25, 0xa4, 0x5e, 0x30, 0xbc, 0x95, 0xd7, - 0x9a, 0xdd, 0xc7, 0xee, 0xab, 0x12, 0xee, 0x04, 0xc3, 0xdb, 0x96, 0x04, 0xeb, 0xbf, 0xd7, 0xe5, - 0xd5, 0x0f, 0xf4, 0xde, 0xff, 0xef, 0xf0, 0x2e, 0x42, 0xb0, 0xf1, 0x78, 0x08, 0x28, 0xec, 0x2e, - 0x39, 0x8f, 0x4f, 0x91, 0x8e, 0x6c, 0x66, 0x25, 0xb2, 0x5f, 0x43, 0xe1, 0x9a, 0xb9, 0xde, 0x2c, - 0x4c, 0x1c, 0xa3, 0x54, 0x9a, 0xda, 0xda, 0x42, 0x12, 0x4a, 0xfd, 0x1f, 0x05, 0x28, 0xc4, 0x20, - 0x3a, 0x87, 0xdc, 0x30, 0x18, 0x25, 0xd9, 0xfd, 0xf4, 0xe1, 0xb4, 0xe4, 0xb7, 0x19, 0x8c, 0x38, - 0x51, 0x5c, 0xf4, 0x5b, 0xd8, 0x92, 0x45, 0xe5, 0x73, 0x8f, 0xce, 0xa6, 0x23, 0x36, 0x4f, 0xa8, - 0x99, 0x9a, 0xdd, 0xd4, 0x84, 0x81, 0xb2, 0x93, 0xea, 0x30, 0x3d, 0x44, 0x87, 0x50, 0x1a, 0x0b, - 0x6f, 0xa8, 0x33, 0x91, 0x53, 0x17, 0xba, 0x28, 0x01, 0x95, 0x83, 0x3a, 0x54, 0x03, 0xdf, 0x0d, - 0x7c, 0x1a, 0x8d, 0x19, 0x3d, 0xff, 0xf6, 0x3b, 0xa5, 0x17, 0x15, 0x52, 0x56, 0x60, 0x6f, 0xcc, - 0xce, 0xbf, 0xfd, 0x0e, 0x7d, 0x06, 0x65, 0x55, 0xb5, 0xfc, 0xc3, 0xd4, 0x0d, 0xef, 0x95, 0x50, - 0x54, 0x89, 0x2a, 0x64, 0xac, 0x10, 0x59, 0x1a, 0xd7, 0x1e, 0xbb, 0x89, 0x94, 0x38, 0x54, 0x89, - 0x1e, 0xa0, 0x6f, 0x60, 0x2f, 0x8e, 0x01, 0x8d, 0x82, 0x59, 0x38, 0xe4, 0xd4, 0xf5, 0x47, 0xfc, - 0x83, 0x92, 0x86, 0x2a, 0x41, 0xb1, 0xad, 0xa7, 0x4c, 0x96, 0xb4, 0xa0, 0x7d, 0xd8, 0x1c, 0x73, - 0xf7, 0x66, 0xac, 0xa5, 0xa1, 0x4a, 0xe2, 0x51, 0xfd, 0x6f, 0x79, 0x28, 0xa7, 0x02, 0x83, 0x2a, - 0x50, 0x24, 0xb8, 0x87, 0xc9, 0x5b, 0xdc, 0x32, 0x3e, 0x41, 0x27, 0xf0, 0xa5, 0x65, 0x37, 0x1d, - 0x42, 0x70, 0xb3, 0x4f, 0x1d, 0x42, 0x07, 0xf6, 0x1b, 0xdb, 0xf9, 0xc9, 0xa6, 0x97, 0x8d, 0x77, - 0x5d, 0x6c, 0xf7, 0x69, 0x0b, 0xf7, 0x1b, 0x56, 0xa7, 0x67, 0x64, 0xd0, 0x33, 0x30, 0x17, 0xcc, - 0xc4, 0xdc, 0xe8, 0x3a, 0x03, 0xbb, 0x6f, 0x6c, 0xa0, 0xcf, 0xe0, 0xb0, 0x6d, 0xd9, 0x8d, 0x0e, - 0x5d, 0x70, 0x9a, 0x9d, 0xfe, 0x5b, 0x8a, 0x7f, 0xbe, 0xb4, 0xc8, 0x3b, 0x23, 0xbb, 0x8e, 0x70, - 0xd1, 0xef, 0x34, 0x13, 0x0f, 0x39, 0xf4, 0x14, 0x9e, 0x68, 0x82, 0x9e, 0x42, 0xfb, 0x8e, 0x43, - 0x7b, 0x8e, 0x63, 0x1b, 0x79, 0xb4, 0x03, 0x55, 0xcb, 0x7e, 0xdb, 0xe8, 0x58, 0x2d, 0x4a, 0x70, - 0xa3, 0xd3, 0x35, 0x36, 0xd1, 0x2e, 0x6c, 0xaf, 0xf2, 0x0a, 0xd2, 0x45, 0xc2, 0x73, 0x6c, 0xcb, - 0xb1, 0xe9, 0x5b, 0x4c, 0x7a, 0x96, 0x63, 0x1b, 0x45, 0xb4, 0x0f, 0x68, 0xd9, 0x74, 0xd1, 0x6d, - 0x34, 0x8d, 0x12, 0x7a, 0x02, 0x3b, 0xcb, 0xf8, 0x1b, 0xfc, 0xce, 0x00, 0x64, 0xc2, 0x9e, 0xde, - 0x18, 0x7d, 0x85, 0x3b, 0xce, 0x4f, 0xb4, 0x6b, 0xd9, 0x56, 0x77, 0xd0, 0x35, 0xca, 0x68, 0x0f, - 0x8c, 0x36, 0xc6, 0xd4, 0xb2, 0x7b, 0x83, 0x76, 0xdb, 0x6a, 0x5a, 0xd8, 0xee, 0x1b, 0x15, 0xbd, - 0xf2, 0xba, 0x83, 0x57, 0xe5, 0x84, 0xe6, 0x45, 0xc3, 0xb6, 0x71, 0x87, 0xb6, 0xac, 0x5e, 0xe3, - 0x55, 0x07, 0xb7, 0x8c, 0x2d, 0x74, 0x04, 0x4f, 0xfb, 0xb8, 0x7b, 0xe9, 0x90, 0x06, 0x79, 0x47, - 0x13, 0x7b, 0xbb, 0x61, 0x75, 0x06, 0x04, 0x1b, 0xdb, 0xe8, 0x73, 0x38, 0x22, 0xf8, 0xc7, 0x81, - 0x45, 0x70, 0x8b, 0xda, 0x4e, 0x0b, 0xd3, 0x36, 0x6e, 0xf4, 0x07, 0x04, 0xd3, 0xae, 0xd5, 0xeb, - 0x59, 0xf6, 0x0f, 0x86, 0x81, 0xbe, 0x84, 0xe3, 0x39, 0x65, 0xee, 0x60, 0x85, 0xb5, 0x23, 0xcf, - 0x97, 0xa4, 0xd4, 0xc6, 0x3f, 0xf7, 0xe9, 0x25, 0xc6, 0xc4, 0x40, 0xa8, 0x06, 0xfb, 0x8b, 0xe5, - 0xf5, 0x02, 0xf1, 0xda, 0xbb, 0xd2, 0x76, 0x89, 0x49, 0xb7, 0x61, 0xcb, 0x04, 0x2f, 0xd9, 0xf6, - 0xe4, 0xb6, 0x17, 0xb6, 0xd5, 0x6d, 0x3f, 0x41, 0x7b, 0xb0, 0x9d, 0xac, 0x96, 0x80, 0xff, 0x2c, - 0xa0, 0x03, 0x40, 0x03, 0x9b, 0xe0, 0x46, 0x4b, 0x1e, 0x7e, 0x6e, 0xf8, 0x57, 0xe1, 0x75, 0xae, - 0xb8, 0x61, 0x64, 0xeb, 0x7f, 0xcf, 0x42, 0x75, 0xa9, 0x06, 0xd1, 0x33, 0x28, 0x45, 0xee, 0x8d, - 0xcf, 0x84, 0x54, 0x09, 0x2d, 0x20, 0x0b, 0x40, 0xbd, 0x83, 0x63, 0xe6, 0xfa, 0x5a, 0xb9, 0xb4, - 0x72, 0x97, 0x14, 0xa2, 0x74, 0xeb, 0x00, 0x0a, 0xc9, 0x3b, 0x9a, 0x55, 0xf5, 0xba, 0x39, 0xd4, - 0xef, 0xe7, 0x33, 0x28, 0x49, 0x69, 0x8c, 0x04, 0x9b, 0x4c, 0x55, 0x29, 0x57, 0xc9, 0x02, 0x40, - 0x5f, 0x40, 0x75, 0xc2, 0xa3, 0x88, 0xdd, 0x70, 0xaa, 0xcb, 0x11, 0x14, 0xa3, 0x12, 0x83, 0x6d, - 0x55, 0x95, 0x5f, 0x40, 0x22, 0x0f, 0x31, 0x29, 0xaf, 0x49, 0x31, 0xa8, 0x49, 0xab, 0xca, 0x2c, - 0x58, 0x5c, 0xf5, 0x69, 0x65, 0x16, 0x0c, 0xbd, 0x80, 0x1d, 0x2d, 0x2d, 0xae, 0xef, 0x4e, 0x66, - 0x13, 0x2d, 0x31, 0x05, 0xb5, 0xe5, 0x6d, 0x25, 0x31, 0x1a, 0x57, 0x4a, 0xf3, 0x14, 0x8a, 0x57, - 0x2c, 0xe2, 0xf2, 0x51, 0x88, 0x25, 0xa0, 0x20, 0xc7, 0x6d, 0xce, 0xa5, 0x49, 0x3e, 0x15, 0xa1, - 0x14, 0x37, 0x5d, 0xf9, 0x85, 0x6b, 0xce, 0x89, 0x8c, 0xe3, 0x7c, 0x05, 0xf6, 0x61, 0xb1, 0x42, - 0x39, 0xb5, 0x82, 0xc6, 0xd5, 0x0a, 0x2f, 0x60, 0x87, 0x7f, 0x10, 0x21, 0xa3, 0xc1, 0x94, 0xbd, - 0x9f, 0x71, 0x3a, 0x62, 0x82, 0x99, 0x15, 0x15, 0xdc, 0x6d, 0x65, 0x70, 0x14, 0xde, 0x62, 0x82, - 0xd5, 0x9f, 0x41, 0x8d, 0xf0, 0x88, 0x8b, 0xae, 0x1b, 0x45, 0x6e, 0xe0, 0x37, 0x03, 0x5f, 0x84, - 0x81, 0x17, 0xbf, 0x2d, 0xf5, 0x23, 0x38, 0x5c, 0x6b, 0xd5, 0x8f, 0x83, 0x9c, 0xfc, 0xe3, 0x8c, - 0x87, 0xf7, 0xeb, 0x27, 0xdf, 0xc3, 0xe1, 0x5a, 0x6b, 0xfc, 0xb2, 0x7c, 0x0d, 0x79, 0x3f, 0x18, - 0xf1, 0xc8, 0xcc, 0xa8, 0x6e, 0x65, 0x3f, 0x25, 0xe3, 0x76, 0x30, 0xe2, 0x17, 0x6e, 0x24, 0x82, - 0xf0, 0x9e, 0x68, 0x92, 0x64, 0x4f, 0x99, 0x1b, 0x46, 0xe6, 0xc6, 0x03, 0xf6, 0x25, 0x73, 0xc3, - 0x39, 0x5b, 0x91, 0xea, 0x7f, 0xce, 0x40, 0x39, 0xe5, 0x44, 0x0a, 0xea, 0x74, 0x76, 0x95, 0x34, - 0x32, 0x15, 0x12, 0x8f, 0xd0, 0x73, 0xd8, 0xf2, 0x58, 0x24, 0xa8, 0xd4, 0x60, 0x2a, 0x53, 0x1a, - 0x3f, 0xbc, 0x2b, 0x28, 0x3a, 0x05, 0x14, 0x88, 0x31, 0x0f, 0x69, 0x34, 0x1b, 0x0e, 0x79, 0x14, - 0xd1, 0x69, 0x18, 0x5c, 0xa9, 0x3b, 0xb9, 0x41, 0xd6, 0x58, 0x5e, 0xe7, 0x8a, 0x39, 0x23, 0x5f, - 0xff, 0x77, 0x06, 0xca, 0xa9, 0xcd, 0xc9, 0x5b, 0x2b, 0x0f, 0x43, 0xaf, 0xc3, 0x60, 0x92, 0xd4, - 0xc2, 0x1c, 0x40, 0x26, 0x14, 0xd4, 0x40, 0x04, 0x71, 0x21, 0x24, 0xc3, 0xe5, 0xdb, 0x9e, 0x55, - 0x1b, 0x4c, 0xdd, 0xf6, 0x73, 0xd8, 0x9b, 0xb8, 0x3e, 0x9d, 0x72, 0x9f, 0x79, 0xee, 0x1f, 0x39, - 0x4d, 0x3a, 0x94, 0x9c, 0x22, 0xae, 0xb5, 0xa1, 0x3a, 0x54, 0x96, 0x4e, 0x92, 0x57, 0x27, 0x59, - 0xc2, 0xd0, 0x4b, 0x38, 0x50, 0x51, 0x60, 0x42, 0xf0, 0xc9, 0x54, 0x24, 0x07, 0xbc, 0x9e, 0x79, - 0xaa, 0x06, 0x8a, 0xe4, 0x31, 0xf3, 0x8b, 0xbf, 0x66, 0xa0, 0x92, 0xee, 0xd2, 0x50, 0x15, 0x4a, - 0x96, 0x4d, 0xdb, 0x1d, 0xeb, 0x87, 0x8b, 0xbe, 0xf1, 0x89, 0x1c, 0xf6, 0x06, 0xcd, 0x26, 0xc6, - 0x2d, 0xdc, 0x32, 0x32, 0x08, 0xc1, 0x96, 0x14, 0x12, 0xdc, 0xa2, 0x7d, 0xab, 0x8b, 0x9d, 0x81, - 0x7c, 0x83, 0x76, 0x61, 0x3b, 0xc6, 0x6c, 0x87, 0x12, 0x67, 0xd0, 0xc7, 0x46, 0x16, 0x19, 0x50, - 0x89, 0x41, 0x4c, 0x88, 0x43, 0x8c, 0x9c, 0x14, 0xce, 0x18, 0x79, 0xf8, 0x9e, 0x25, 0xcf, 0x5d, - 0xfe, 0xfc, 0x2f, 0x39, 0xd8, 0x54, 0x5d, 0x4d, 0x88, 0x2e, 0xa0, 0x9c, 0x6a, 0x85, 0xd1, 0xd1, - 0x47, 0x5b, 0xe4, 0x9a, 0xb9, 0xbe, 0xed, 0x9c, 0x45, 0xdf, 0x64, 0xd0, 0x6b, 0xa8, 0xa4, 0x9b, - 0x5d, 0x94, 0x6e, 0x62, 0xd6, 0x74, 0xc1, 0x1f, 0xf5, 0xf5, 0x06, 0x0c, 0x1c, 0x09, 0x77, 0x22, - 0x9b, 0x96, 0xb8, 0x8d, 0x44, 0xb5, 0x14, 0x7f, 0xa5, 0x37, 0xad, 0x1d, 0xae, 0xb5, 0xc5, 0x75, - 0xd5, 0xd1, 0x47, 0x8c, 0x1b, 0xb9, 0x07, 0x47, 0x5c, 0xee, 0x1e, 0x6b, 0x9f, 0x3e, 0x66, 0x8e, - 0xbd, 0x8d, 0x60, 0x77, 0x8d, 0x02, 0xa0, 0x5f, 0xa4, 0x77, 0xf0, 0xa8, 0x7e, 0xd4, 0x9e, 0xff, - 0x2f, 0xda, 0x62, 0x95, 0x35, 0x52, 0xb1, 0xb4, 0xca, 0xe3, 0x42, 0xb3, 0xb4, 0xca, 0x47, 0x14, - 0xe7, 0xd5, 0xaf, 0x7e, 0x77, 0x76, 0xe3, 0x8a, 0xf1, 0xec, 0xea, 0x74, 0x18, 0x4c, 0xce, 0x3c, - 0xd9, 0x52, 0xf9, 0xae, 0x7f, 0xe3, 0x73, 0xf1, 0x87, 0x20, 0xbc, 0x3d, 0xf3, 0xfc, 0xd1, 0x99, - 0x6a, 0x8c, 0xcf, 0xe6, 0xee, 0xae, 0x36, 0xd5, 0x3f, 0xdb, 0x5f, 0xff, 0x37, 0x00, 0x00, 0xff, - 0xff, 0x3c, 0xe4, 0x5c, 0x67, 0x09, 0x0f, 0x00, 0x00, + // 1859 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x58, 0x4f, 0x73, 0x22, 0xc7, + 0x15, 0x37, 0x02, 0x04, 0x3c, 0x40, 0x1a, 0xb5, 0xb4, 0xd2, 0x2c, 0x92, 0xbc, 0x32, 0x76, 0xd6, + 0xaa, 0x2d, 0x47, 0x72, 0x94, 0xb2, 0x6b, 0xcb, 0x87, 0xa4, 0x58, 0x68, 0xac, 0xd9, 0x85, 0x19, + 0xb9, 0x81, 0xb5, 0x37, 0x39, 0x74, 0xb5, 0xa0, 0x25, 0xa6, 0x34, 0xcc, 0xb0, 0x33, 0x8d, 0xb2, + 0xca, 0x21, 0x97, 0x9c, 0x73, 0xcf, 0x3d, 0x1f, 0x20, 0x5f, 0x25, 0x5f, 0x21, 0x97, 0xe4, 0x13, + 0xe4, 0x98, 0xaa, 0x54, 0x77, 0xcf, 0xc0, 0x20, 0xa1, 0x8d, 0x4f, 0x9a, 0xfe, 0xbd, 0xd7, 0xaf, + 0x5f, 0xbf, 0x3f, 0xbf, 0x7e, 0x02, 0x76, 0xc3, 0x60, 0x26, 0x78, 0x18, 0x4e, 0x87, 0xa7, 0xfa, + 0xeb, 0x64, 0x1a, 0x06, 0x22, 0x40, 0xa5, 0x39, 0x5e, 0x2b, 0x85, 0xd3, 0xa1, 0x46, 0xeb, 0xff, + 0xcd, 0x02, 0xea, 0x71, 0x7f, 0x74, 0xc1, 0xee, 0x26, 0xdc, 0x17, 0x84, 0xbf, 0x9f, 0xf1, 0x48, + 0x20, 0x04, 0xb9, 0x11, 0x8f, 0x84, 0x99, 0x39, 0xca, 0x1c, 0x57, 0x88, 0xfa, 0x46, 0x06, 0x64, + 0xd9, 0x44, 0x98, 0x6b, 0x47, 0x99, 0xe3, 0x2c, 0x91, 0x9f, 0xe8, 0x33, 0xa8, 0x4c, 0xf5, 0x3e, + 0x3a, 0x66, 0xd1, 0xd8, 0xcc, 0x2a, 0xed, 0x72, 0x8c, 0x9d, 0xb3, 0x68, 0x8c, 0x8e, 0xc1, 0xb8, + 0x72, 0x7d, 0xe6, 0xd1, 0xa1, 0x27, 0x6e, 0xe9, 0x88, 0x7b, 0x82, 0x99, 0xb9, 0xa3, 0xcc, 0x71, + 0x9e, 0x6c, 0x28, 0xbc, 0xe9, 0x89, 0xdb, 0x96, 0x44, 0xd1, 0x97, 0xb0, 0x99, 0x18, 0x0b, 0xb5, + 0x17, 0x66, 0xfe, 0x28, 0x73, 0x5c, 0x22, 0x1b, 0xd3, 0x65, 0xdf, 0xbe, 0x84, 0x4d, 0xe1, 0x4e, + 0x78, 0x30, 0x13, 0x34, 0xe2, 0xc3, 0xc0, 0x1f, 0x45, 0xe6, 0xba, 0xb6, 0x18, 0xc3, 0x3d, 0x8d, + 0xa2, 0x3a, 0x54, 0xaf, 0x38, 0xa7, 0x9e, 0x3b, 0x71, 0x05, 0x8d, 0x98, 0x30, 0x0b, 0xca, 0xf5, + 0xf2, 0x15, 0xe7, 0x1d, 0x89, 0xf5, 0x98, 0x90, 0xfe, 0x05, 0x33, 0x71, 0x1d, 0xb8, 0xfe, 0x35, + 0x1d, 0x8e, 0x99, 0x4f, 0xdd, 0x91, 0x59, 0x3c, 0xca, 0x1c, 0xe7, 0xc8, 0x46, 0x82, 0x37, 0xc7, + 0xcc, 0xb7, 0x46, 0xe8, 0x10, 0x40, 0xdd, 0x41, 0x99, 0x33, 0x4b, 0xea, 0xc4, 0x92, 0x44, 0x94, + 0x2d, 0x74, 0x06, 0x65, 0x15, 0x60, 0x3a, 0x76, 0x7d, 0x11, 0x99, 0x70, 0x94, 0x3d, 0x2e, 0x9f, + 0x19, 0x27, 0x9e, 0x2f, 0x63, 0x4d, 0xa4, 0xe4, 0xdc, 0xf5, 0x05, 0x49, 0x2b, 0x21, 0x0c, 0x45, + 0x19, 0x59, 0x2a, 0xbc, 0x5b, 0xb3, 0xac, 0x36, 0xbc, 0x38, 0x99, 0x67, 0xe9, 0xe4, 0x61, 0x5a, + 0x4e, 0x5a, 0x3c, 0x12, 0x7d, 0xef, 0x16, 0xfb, 0x22, 0xbc, 0x23, 0x85, 0x91, 0x5e, 0xd5, 0xbe, + 0x83, 0x4a, 0x5a, 0x20, 0x13, 0x75, 0xc3, 0xef, 0x54, 0xee, 0x72, 0x44, 0x7e, 0xa2, 0x1d, 0xc8, + 0xdf, 0x32, 0x6f, 0xc6, 0x55, 0xf2, 0x2a, 0x44, 0x2f, 0xbe, 0x5b, 0x7b, 0x99, 0xa9, 0xbf, 0x84, + 0xed, 0x7e, 0xc8, 0x86, 0x37, 0xf7, 0xf2, 0x7f, 0x3f, 0xb3, 0x99, 0x07, 0x99, 0xad, 0xff, 0x09, + 0xaa, 0xf1, 0xa6, 0x9e, 0x60, 0x62, 0x16, 0xa1, 0x5f, 0x42, 0x3e, 0x12, 0x4c, 0x70, 0xa5, 0xbc, + 0x71, 0xb6, 0x97, 0xba, 0x4a, 0x4a, 0x91, 0x13, 0xad, 0x85, 0x6a, 0x50, 0x9c, 0x86, 0xdc, 0x9d, + 0xb0, 0xeb, 0xc4, 0xad, 0xf9, 0x1a, 0xd5, 0x21, 0xaf, 0x36, 0xab, 0x8a, 0x2a, 0x9f, 0x55, 0xd2, + 0x61, 0x24, 0x5a, 0x54, 0xff, 0x0d, 0x6c, 0xaa, 0x75, 0x9b, 0xf3, 0x8f, 0x55, 0xed, 0x1e, 0x14, + 0xd8, 0x44, 0xa7, 0x5f, 0x57, 0xee, 0x3a, 0x9b, 0xc8, 0xcc, 0xd7, 0x47, 0x60, 0x2c, 0xf6, 0x47, + 0xd3, 0xc0, 0x8f, 0xb8, 0xac, 0x06, 0x69, 0x5c, 0x16, 0x83, 0xac, 0x9c, 0x89, 0xdc, 0x95, 0x51, + 0xbb, 0x36, 0x62, 0xbc, 0xcd, 0x79, 0x37, 0x62, 0x02, 0x3d, 0xd7, 0x45, 0x48, 0xbd, 0x60, 0x78, + 0x23, 0xcb, 0x9a, 0xdd, 0xc5, 0xe6, 0xab, 0x12, 0xee, 0x04, 0xc3, 0x9b, 0x96, 0x04, 0xeb, 0xbf, + 0xd7, 0xed, 0xd5, 0x0f, 0xb4, 0xef, 0x3f, 0x3b, 0xbc, 0x8b, 0x10, 0xac, 0x3d, 0x1e, 0x02, 0x0a, + 0xdb, 0x4b, 0xc6, 0xe3, 0x5b, 0xa4, 0x23, 0x9b, 0xb9, 0x17, 0xd9, 0xaf, 0xa0, 0x70, 0xc5, 0x5c, + 0x6f, 0x16, 0x26, 0x86, 0x51, 0x2a, 0x4d, 0x6d, 0x2d, 0x21, 0x89, 0x4a, 0xfd, 0x9f, 0x05, 0x28, + 0xc4, 0x20, 0x3a, 0x83, 0xdc, 0x30, 0x18, 0x25, 0xd9, 0xfd, 0xf4, 0xe1, 0xb6, 0xe4, 0x6f, 0x33, + 0x18, 0x71, 0xa2, 0x74, 0xd1, 0x6f, 0x61, 0x43, 0x36, 0x95, 0xcf, 0x3d, 0x3a, 0x9b, 0x8e, 0xd8, + 0x3c, 0xa1, 0x66, 0x6a, 0x77, 0x53, 0x2b, 0x0c, 0x94, 0x9c, 0x54, 0x87, 0xe9, 0x25, 0xda, 0x87, + 0xd2, 0x58, 0x78, 0x43, 0x9d, 0x89, 0x9c, 0x2a, 0xe8, 0xa2, 0x04, 0x54, 0x0e, 0xea, 0x50, 0x0d, + 0x7c, 0x37, 0xf0, 0x69, 0x34, 0x66, 0xf4, 0xec, 0x9b, 0x6f, 0x15, 0x5f, 0x54, 0x48, 0x59, 0x81, + 0xbd, 0x31, 0x3b, 0xfb, 0xe6, 0x5b, 0xf4, 0x0c, 0xca, 0xaa, 0x6b, 0xf9, 0x87, 0xa9, 0x1b, 0xde, + 0x29, 0xa2, 0xa8, 0x12, 0xd5, 0xc8, 0x58, 0x21, 0xb2, 0x35, 0xae, 0x3c, 0x76, 0x1d, 0x29, 0x72, + 0xa8, 0x12, 0xbd, 0x40, 0x5f, 0xc3, 0x4e, 0x1c, 0x03, 0x1a, 0x05, 0xb3, 0x70, 0xc8, 0xa9, 0xeb, + 0x8f, 0xf8, 0x07, 0x45, 0x0d, 0x55, 0x82, 0x62, 0x59, 0x4f, 0x89, 0x2c, 0x29, 0x41, 0xbb, 0xb0, + 0x3e, 0xe6, 0xee, 0xf5, 0x58, 0x53, 0x43, 0x95, 0xc4, 0xab, 0xfa, 0x5f, 0xf3, 0x50, 0x4e, 0x05, + 0x06, 0x55, 0xa0, 0x48, 0x70, 0x0f, 0x93, 0xb7, 0xb8, 0x65, 0x7c, 0x82, 0x8e, 0xe1, 0x0b, 0xcb, + 0x6e, 0x3a, 0x84, 0xe0, 0x66, 0x9f, 0x3a, 0x84, 0x0e, 0xec, 0x37, 0xb6, 0xf3, 0xa3, 0x4d, 0x2f, + 0x1a, 0xef, 0xba, 0xd8, 0xee, 0xd3, 0x16, 0xee, 0x37, 0xac, 0x4e, 0xcf, 0xc8, 0xa0, 0x03, 0x30, + 0x17, 0x9a, 0x89, 0xb8, 0xd1, 0x75, 0x06, 0x76, 0xdf, 0x58, 0x43, 0xcf, 0x60, 0xbf, 0x6d, 0xd9, + 0x8d, 0x0e, 0x5d, 0xe8, 0x34, 0x3b, 0xfd, 0xb7, 0x14, 0xff, 0x74, 0x61, 0x91, 0x77, 0x46, 0x76, + 0x95, 0xc2, 0x79, 0xbf, 0xd3, 0x4c, 0x2c, 0xe4, 0xd0, 0x53, 0x78, 0xa2, 0x15, 0xf4, 0x16, 0xda, + 0x77, 0x1c, 0xda, 0x73, 0x1c, 0xdb, 0xc8, 0xa3, 0x2d, 0xa8, 0x5a, 0xf6, 0xdb, 0x46, 0xc7, 0x6a, + 0x51, 0x82, 0x1b, 0x9d, 0xae, 0xb1, 0x8e, 0xb6, 0x61, 0xf3, 0xbe, 0x5e, 0x41, 0x9a, 0x48, 0xf4, + 0x1c, 0xdb, 0x72, 0x6c, 0xfa, 0x16, 0x93, 0x9e, 0xe5, 0xd8, 0x46, 0x11, 0xed, 0x02, 0x5a, 0x16, + 0x9d, 0x77, 0x1b, 0x4d, 0xa3, 0x84, 0x9e, 0xc0, 0xd6, 0x32, 0xfe, 0x06, 0xbf, 0x33, 0x00, 0x99, + 0xb0, 0xa3, 0x1d, 0xa3, 0xaf, 0x70, 0xc7, 0xf9, 0x91, 0x76, 0x2d, 0xdb, 0xea, 0x0e, 0xba, 0x46, + 0x19, 0xed, 0x80, 0xd1, 0xc6, 0x98, 0x5a, 0x76, 0x6f, 0xd0, 0x6e, 0x5b, 0x4d, 0x0b, 0xdb, 0x7d, + 0xa3, 0xa2, 0x4f, 0x5e, 0x75, 0xf1, 0xaa, 0xdc, 0xd0, 0x3c, 0x6f, 0xd8, 0x36, 0xee, 0xd0, 0x96, + 0xd5, 0x6b, 0xbc, 0xea, 0xe0, 0x96, 0xb1, 0x81, 0x0e, 0xe1, 0x69, 0x1f, 0x77, 0x2f, 0x1c, 0xd2, + 0x20, 0xef, 0x68, 0x22, 0x6f, 0x37, 0xac, 0xce, 0x80, 0x60, 0x63, 0x13, 0x7d, 0x06, 0x87, 0x04, + 0xff, 0x30, 0xb0, 0x08, 0x6e, 0x51, 0xdb, 0x69, 0x61, 0xda, 0xc6, 0x8d, 0xfe, 0x80, 0x60, 0xda, + 0xb5, 0x7a, 0x3d, 0xcb, 0xfe, 0xde, 0x30, 0xd0, 0x17, 0x70, 0x34, 0x57, 0x99, 0x1b, 0xb8, 0xa7, + 0xb5, 0x25, 0xef, 0x97, 0xa4, 0xd4, 0xc6, 0x3f, 0xf5, 0xe9, 0x05, 0xc6, 0xc4, 0x40, 0xa8, 0x06, + 0xbb, 0x8b, 0xe3, 0xf5, 0x01, 0xf1, 0xd9, 0xdb, 0x52, 0x76, 0x81, 0x49, 0xb7, 0x61, 0xcb, 0x04, + 0x2f, 0xc9, 0x76, 0xa4, 0xdb, 0x0b, 0xd9, 0x7d, 0xb7, 0x9f, 0xa0, 0x1d, 0xd8, 0x4c, 0x4e, 0x4b, + 0xc0, 0x7f, 0x15, 0xd0, 0x1e, 0xa0, 0x81, 0x4d, 0x70, 0xa3, 0x25, 0x2f, 0x3f, 0x17, 0xfc, 0xbb, + 0xf0, 0x3a, 0x57, 0x5c, 0x33, 0xb2, 0xf5, 0xbf, 0x67, 0xa1, 0xba, 0xd4, 0x83, 0xe8, 0x00, 0x4a, + 0x91, 0x7b, 0xed, 0x33, 0x21, 0x59, 0x42, 0x13, 0xc8, 0x02, 0x50, 0xef, 0xe0, 0x98, 0xb9, 0xbe, + 0x66, 0x2e, 0xcd, 0xdc, 0x25, 0x85, 0x28, 0xde, 0xda, 0x83, 0x42, 0xf2, 0x8e, 0x66, 0x55, 0xbf, + 0xae, 0x0f, 0xf5, 0xfb, 0x79, 0x00, 0x25, 0x49, 0x8d, 0x91, 0x60, 0x93, 0xa9, 0x6a, 0xe5, 0x2a, + 0x59, 0x00, 0xe8, 0x73, 0xa8, 0x4e, 0x78, 0x14, 0xb1, 0x6b, 0x4e, 0x75, 0x3b, 0x82, 0xd2, 0xa8, + 0xc4, 0x60, 0x5b, 0x75, 0xe5, 0xe7, 0x90, 0xd0, 0x43, 0xac, 0x94, 0xd7, 0x4a, 0x31, 0xa8, 0x95, + 0xee, 0x33, 0xb3, 0x60, 0x71, 0xd7, 0xa7, 0x99, 0x59, 0x30, 0xf4, 0x02, 0xb6, 0x34, 0xb5, 0xb8, + 0xbe, 0x3b, 0x99, 0x4d, 0x34, 0xc5, 0x14, 0x94, 0xcb, 0x9b, 0x8a, 0x62, 0x34, 0xae, 0x98, 0xe6, + 0x29, 0x14, 0x2f, 0x59, 0xc4, 0xe5, 0xa3, 0x10, 0x53, 0x40, 0x41, 0xae, 0xdb, 0x9c, 0x4b, 0x91, + 0x7c, 0x2a, 0x42, 0x49, 0x6e, 0xba, 0xf3, 0x0b, 0x57, 0x9c, 0x13, 0x19, 0xc7, 0xf9, 0x09, 0xec, + 0xc3, 0xe2, 0x84, 0x72, 0xea, 0x04, 0x8d, 0xab, 0x13, 0x5e, 0xc0, 0x16, 0xff, 0x20, 0x42, 0x46, + 0x83, 0x29, 0x7b, 0x3f, 0xe3, 0x74, 0xc4, 0x04, 0x33, 0x2b, 0x2a, 0xb8, 0x9b, 0x4a, 0xe0, 0x28, + 0xbc, 0xc5, 0x04, 0xab, 0x1f, 0x40, 0x8d, 0xf0, 0x88, 0x8b, 0xae, 0x1b, 0x45, 0x6e, 0xe0, 0x37, + 0x03, 0x5f, 0x84, 0x81, 0x17, 0xbf, 0x2d, 0xf5, 0x43, 0xd8, 0x5f, 0x29, 0xd5, 0x8f, 0x83, 0xdc, + 0xfc, 0xc3, 0x8c, 0x87, 0x77, 0xab, 0x37, 0xdf, 0xc1, 0xfe, 0x4a, 0x69, 0xfc, 0xb2, 0x7c, 0x05, + 0x79, 0x3f, 0x18, 0xf1, 0xc8, 0xcc, 0xa8, 0x69, 0x65, 0x37, 0x45, 0xe3, 0x76, 0x30, 0xe2, 0xe7, + 0x6e, 0x24, 0x82, 0xf0, 0x8e, 0x68, 0x25, 0xa9, 0x3d, 0x65, 0x6e, 0x18, 0x99, 0x6b, 0x0f, 0xb4, + 0x2f, 0x98, 0x1b, 0xce, 0xb5, 0x95, 0x52, 0xfd, 0xcf, 0x19, 0x28, 0xa7, 0x8c, 0x48, 0x42, 0x9d, + 0xce, 0x2e, 0x93, 0x41, 0xa6, 0x42, 0xe2, 0x15, 0x7a, 0x0e, 0x1b, 0x1e, 0x8b, 0x04, 0x95, 0x1c, + 0x4c, 0x65, 0x4a, 0xe3, 0x87, 0xf7, 0x1e, 0x8a, 0x4e, 0x00, 0x05, 0x62, 0xcc, 0x43, 0x1a, 0xcd, + 0x86, 0x43, 0x1e, 0x45, 0x74, 0x1a, 0x06, 0x97, 0xaa, 0x26, 0xd7, 0xc8, 0x0a, 0xc9, 0xeb, 0x5c, + 0x31, 0x67, 0xe4, 0xeb, 0xff, 0xc9, 0x40, 0x39, 0xe5, 0x9c, 0xac, 0x5a, 0x79, 0x19, 0x7a, 0x15, + 0x06, 0x93, 0xa4, 0x17, 0xe6, 0x00, 0x32, 0xa1, 0xa0, 0x16, 0x22, 0x88, 0x1b, 0x21, 0x59, 0x2e, + 0x57, 0x7b, 0x56, 0x39, 0x98, 0xaa, 0xf6, 0x33, 0xd8, 0x99, 0xb8, 0x3e, 0x9d, 0x72, 0x9f, 0x79, + 0xee, 0x1f, 0x39, 0x4d, 0x26, 0x94, 0x9c, 0x52, 0x5c, 0x29, 0x43, 0x75, 0xa8, 0x2c, 0xdd, 0x24, + 0xaf, 0x6e, 0xb2, 0x84, 0xa1, 0x97, 0xb0, 0xa7, 0xa2, 0xc0, 0x84, 0xe0, 0x93, 0xa9, 0x48, 0x2e, + 0x78, 0x35, 0xf3, 0x54, 0x0f, 0x14, 0xc9, 0x63, 0xe2, 0xfa, 0xdf, 0x32, 0xb0, 0xf5, 0x6a, 0xe6, + 0x7a, 0xa3, 0xa5, 0x39, 0xe5, 0x29, 0x14, 0xe5, 0xf1, 0xa9, 0x39, 0x48, 0x0e, 0x53, 0xaa, 0x60, + 0x57, 0x0d, 0xf6, 0x6b, 0x2b, 0x07, 0xfb, 0x55, 0x23, 0x76, 0x76, 0xe5, 0x88, 0xfd, 0x0c, 0xca, + 0xe3, 0x60, 0x4a, 0x75, 0xa2, 0x23, 0x33, 0x77, 0x94, 0x3d, 0xae, 0x10, 0x18, 0x07, 0xd3, 0x0b, + 0x8d, 0xd4, 0x5f, 0x02, 0x4a, 0x3b, 0x19, 0x57, 0xe5, 0x7c, 0x54, 0xca, 0x3c, 0x3a, 0x2a, 0xbd, + 0xf8, 0x4b, 0x06, 0x2a, 0xe9, 0x29, 0x14, 0x55, 0xa1, 0x64, 0xd9, 0xb4, 0xdd, 0xb1, 0xbe, 0x3f, + 0xef, 0x1b, 0x9f, 0xc8, 0x65, 0x6f, 0xd0, 0x6c, 0x62, 0xdc, 0xc2, 0x2d, 0x23, 0x83, 0x10, 0x6c, + 0x48, 0xa2, 0xc4, 0x2d, 0xda, 0xb7, 0xba, 0xd8, 0x19, 0xc8, 0x37, 0x76, 0x1b, 0x36, 0x63, 0xcc, + 0x76, 0x28, 0x71, 0x06, 0x7d, 0x6c, 0x64, 0x91, 0x01, 0x95, 0x18, 0xc4, 0x84, 0x38, 0xc4, 0xc8, + 0xc9, 0x87, 0x21, 0x46, 0x1e, 0xbe, 0xd7, 0xc9, 0x73, 0x9e, 0x3f, 0xfb, 0x47, 0x0e, 0xd6, 0x95, + 0x83, 0x21, 0x3a, 0x87, 0x72, 0x6a, 0xd4, 0x47, 0x87, 0x1f, 0xfd, 0x17, 0xa0, 0x66, 0xae, 0x1e, + 0xab, 0x67, 0xd1, 0xd7, 0x19, 0xf4, 0x1a, 0x2a, 0xe9, 0x61, 0x1e, 0xa5, 0x87, 0xb4, 0x15, 0x53, + 0xfe, 0x47, 0x6d, 0xbd, 0x01, 0x03, 0x47, 0xc2, 0x9d, 0xc8, 0xa1, 0x2c, 0x1e, 0x93, 0x51, 0x2d, + 0xa5, 0x7f, 0x6f, 0xf6, 0xae, 0xed, 0xaf, 0x94, 0xc5, 0x19, 0xea, 0xe8, 0x2b, 0xc6, 0x83, 0xea, + 0x83, 0x2b, 0x2e, 0x4f, 0xc7, 0xb5, 0x4f, 0x1f, 0x13, 0xc7, 0xd6, 0x46, 0xb0, 0xbd, 0x82, 0xe1, + 0xd0, 0x2f, 0xd2, 0x1e, 0x3c, 0xca, 0x8f, 0xb5, 0xe7, 0xff, 0x4f, 0x6d, 0x71, 0xca, 0x0a, 0x2a, + 0x5c, 0x3a, 0xe5, 0x71, 0x22, 0x5d, 0x3a, 0xe5, 0x63, 0x8c, 0x6a, 0x01, 0x2c, 0x2a, 0x1a, 0x1d, + 0xa4, 0x76, 0x3d, 0xe8, 0xc6, 0xda, 0xe1, 0x23, 0x52, 0x6d, 0xea, 0xd5, 0xaf, 0x7e, 0x77, 0x7a, + 0xed, 0x8a, 0xf1, 0xec, 0xf2, 0x64, 0x18, 0x4c, 0x4e, 0x3d, 0x39, 0x7d, 0xfa, 0xae, 0x7f, 0xed, + 0x73, 0xf1, 0x87, 0x20, 0xbc, 0x39, 0xf5, 0xfc, 0xd1, 0xa9, 0x6a, 0x8c, 0xd3, 0xb9, 0x95, 0xcb, + 0x75, 0xf5, 0x23, 0xc0, 0xaf, 0xff, 0x17, 0x00, 0x00, 0xff, 0xff, 0x85, 0xe2, 0x3f, 0x31, 0x34, + 0x10, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -1373,6 +1497,11 @@ type RouterClient interface { //QueryMissionControl exposes the internal mission control state to callers. //It is a development feature. QueryMissionControl(ctx context.Context, in *QueryMissionControlRequest, opts ...grpc.CallOption) (*QueryMissionControlResponse, error) + //* + //BuildRoute builds a fully specified route based on a list of hop public + //keys. It retrieves the relevant channel policies from the graph in order to + //calculate the correct fees and time locks. + BuildRoute(ctx context.Context, in *BuildRouteRequest, opts ...grpc.CallOption) (*BuildRouteResponse, error) } type routerClient struct { @@ -1483,6 +1612,15 @@ func (c *routerClient) QueryMissionControl(ctx context.Context, in *QueryMission return out, nil } +func (c *routerClient) BuildRoute(ctx context.Context, in *BuildRouteRequest, opts ...grpc.CallOption) (*BuildRouteResponse, error) { + out := new(BuildRouteResponse) + err := c.cc.Invoke(ctx, "/routerrpc.Router/BuildRoute", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RouterServer is the server API for Router service. type RouterServer interface { //* @@ -1511,6 +1649,11 @@ type RouterServer interface { //QueryMissionControl exposes the internal mission control state to callers. //It is a development feature. QueryMissionControl(context.Context, *QueryMissionControlRequest) (*QueryMissionControlResponse, error) + //* + //BuildRoute builds a fully specified route based on a list of hop public + //keys. It retrieves the relevant channel policies from the graph in order to + //calculate the correct fees and time locks. + BuildRoute(context.Context, *BuildRouteRequest) (*BuildRouteResponse, error) } func RegisterRouterServer(s *grpc.Server, srv RouterServer) { @@ -1631,6 +1774,24 @@ func _Router_QueryMissionControl_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _Router_BuildRoute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BuildRouteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouterServer).BuildRoute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/routerrpc.Router/BuildRoute", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouterServer).BuildRoute(ctx, req.(*BuildRouteRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Router_serviceDesc = grpc.ServiceDesc{ ServiceName: "routerrpc.Router", HandlerType: (*RouterServer)(nil), @@ -1651,6 +1812,10 @@ var _Router_serviceDesc = grpc.ServiceDesc{ MethodName: "QueryMissionControl", Handler: _Router_QueryMissionControl_Handler, }, + { + MethodName: "BuildRoute", + Handler: _Router_BuildRoute_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 7299a02b..2fdea3d7 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -380,6 +380,39 @@ message PairHistory { bool last_attempt_successful = 6 [json_name = "last_attempt_successful"]; } +message BuildRouteRequest { + /** + The amount to send expressed in msat. If set to zero, the minimum routable + amount is used. + */ + int64 amt_msat = 1; + + /** + CLTV delta from the current height that should be used for the timelock + of the final hop + */ + int32 final_cltv_delta = 2; + + /** + 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 = 3; + + /** + A list of hops that defines the route. This does not include the source hop + pubkey. + */ + repeated bytes hop_pubkeys = 4; +} + +message BuildRouteResponse { + /** + Fully specified route that can be used to execute the payment. + */ + lnrpc.Route route = 1; +} + service Router { /** SendPayment attempts to route a payment described by the passed @@ -419,4 +452,11 @@ service Router { It is a development feature. */ rpc QueryMissionControl(QueryMissionControlRequest) returns (QueryMissionControlResponse); + + /** + BuildRoute builds a fully specified route based on a list of hop public + keys. It retrieves the relevant channel policies from the graph in order to + calculate the correct fees and time locks. + */ + rpc BuildRoute(BuildRouteRequest) returns (BuildRouteResponse); } diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index cea63be9..fe694d39 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -72,6 +72,10 @@ var ( Entity: "offchain", Action: "write", }}, + "/routerrpc.Router/BuildRoute": {{ + Entity: "offchain", + Action: "read", + }}, } // DefaultRouterMacFilename is the default name of the router macaroon @@ -612,3 +616,49 @@ func marshallFailureReason(reason channeldb.FailureReason) ( return 0, errors.New("unknown failure reason") } + +// BuildRoute builds a route from a list of hop addresses. +func (s *Server) BuildRoute(ctx context.Context, + req *BuildRouteRequest) (*BuildRouteResponse, error) { + + // Unmarshall hop list. + hops := make([]route.Vertex, len(req.HopPubkeys)) + for i, pubkeyBytes := range req.HopPubkeys { + pubkey, err := route.NewVertexFromBytes(pubkeyBytes) + if err != nil { + return nil, err + } + hops[i] = pubkey + } + + // Prepare BuildRoute call parameters from rpc request. + var amt *lnwire.MilliSatoshi + if req.AmtMsat != 0 { + rpcAmt := lnwire.MilliSatoshi(req.AmtMsat) + amt = &rpcAmt + } + + var outgoingChan *uint64 + if req.OutgoingChanId != 0 { + outgoingChan = &req.OutgoingChanId + } + + // Build the route and return it to the caller. + route, err := s.cfg.Router.BuildRoute( + amt, hops, outgoingChan, req.FinalCltvDelta, + ) + if err != nil { + return nil, err + } + + rpcRoute, err := s.cfg.RouterBackend.MarshallRoute(route) + if err != nil { + return nil, err + } + + routeResp := &BuildRouteResponse{ + Route: rpcRoute, + } + + return routeResp, nil +} diff --git a/routing/pathfind.go b/routing/pathfind.go index a38267ff..9f415c84 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -66,15 +66,6 @@ type edgePolicyWithSource struct { edge *channeldb.ChannelEdgePolicy } -// computeFee computes the fee to forward an HTLC of `amt` milli-satoshis over -// the passed active payment channel. This value is currently computed as -// specified in BOLT07, but will likely change in the near future. -func computeFee(amt lnwire.MilliSatoshi, - edge *channeldb.ChannelEdgePolicy) lnwire.MilliSatoshi { - - return edge.FeeBaseMSat + (amt*edge.FeeProportionalMillionths)/1000000 -} - // newRoute returns a fully valid route between the source and target that's // capable of supporting a payment of `amtToSend` after fees are fully // computed. If the route is too long, or the selected path cannot support the @@ -129,7 +120,7 @@ func newRoute(amtToSend lnwire.MilliSatoshi, sourceVertex route.Vertex, // and its policy for the outgoing channel. This policy // is stored as part of the incoming channel of // the next hop. - fee = computeFee(amtToForward, pathEdges[i+1]) + fee = pathEdges[i+1].ComputeFee(amtToForward) } // If this is the last hop, then for verification purposes, the @@ -482,7 +473,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, var fee lnwire.MilliSatoshi var timeLockDelta uint16 if fromVertex != source { - fee = computeFee(amountToSend, edge) + fee = edge.ComputeFee(amountToSend) timeLockDelta = edge.TimeLockDelta } diff --git a/routing/route/route.go b/routing/route/route.go index ef902468..ed51d7d0 100644 --- a/routing/route/route.go +++ b/routing/route/route.go @@ -3,6 +3,7 @@ package route import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" "io" "strconv" @@ -47,6 +48,22 @@ func NewVertexFromBytes(b []byte) (Vertex, error) { return v, nil } +// NewVertexFromStr returns a new Vertex given its hex-encoded string format. +func NewVertexFromStr(v string) (Vertex, error) { + // Return error if hex string is of incorrect length. + if len(v) != VertexSize*2 { + return Vertex{}, fmt.Errorf("invalid vertex string length of "+ + "%v, want %v", len(v), VertexSize*2) + } + + vertex, err := hex.DecodeString(v) + if err != nil { + return Vertex{}, err + } + + return NewVertexFromBytes(vertex) +} + // String returns a human readable version of the Vertex which is the // hex-encoding of the serialized compressed public key. func (v Vertex) String() string { diff --git a/routing/router.go b/routing/router.go index 95982758..d1ecefc0 100644 --- a/routing/router.go +++ b/routing/router.go @@ -2253,3 +2253,297 @@ func generateBandwidthHints(sourceNode *channeldb.LightningNode, return bandwidthHints, nil } + +// runningAmounts keeps running amounts while the route is traversed. +type runningAmounts struct { + // amt is the intended amount to send via the route. + amt lnwire.MilliSatoshi + + // max is the running maximum that the route can carry. + max lnwire.MilliSatoshi +} + +// prependChannel returns a new set of running amounts that would result from +// prepending the given channel to the route. If canIncreaseAmt is set, the +// amount may be increased if it is too small to satisfy the channel's minimum +// htlc amount. +func (r *runningAmounts) prependChannel(policy *channeldb.ChannelEdgePolicy, + capacity btcutil.Amount, localChan bool, canIncreaseAmt bool) ( + runningAmounts, error) { + + // Determine max htlc value. + maxHtlc := lnwire.NewMSatFromSatoshis(capacity) + if policy.MessageFlags.HasMaxHtlc() { + maxHtlc = policy.MaxHTLC + } + + amt := r.amt + + // If we have a specific amount for which we are building the route, + // validate it against the channel constraints and return the new + // running amount. + if !canIncreaseAmt { + if amt < policy.MinHTLC || amt > maxHtlc { + return runningAmounts{}, fmt.Errorf("channel htlc "+ + "constraints [%v - %v] violated with amt %v", + policy.MinHTLC, maxHtlc, amt) + } + + // Update running amount by adding the fee for non-local + // channels. + if !localChan { + amt += policy.ComputeFee(amt) + } + + return runningAmounts{ + amt: amt, + }, nil + } + + // Adapt the minimum amount to what this channel allows. + if policy.MinHTLC > r.amt { + amt = policy.MinHTLC + } + + // Update the maximum amount too to be able to detect incompatible + // channels. + max := r.max + if maxHtlc < r.max { + max = maxHtlc + } + + // If we get in the situation that the minimum amount exceeds the + // maximum amount (enforced further down stream), we have incompatible + // channel policies. + // + // There is possibility with pubkey addressing that we should have + // selected a different channel downstream, but we don't backtrack to + // try to fix that. It would complicate path finding while we expect + // this situation to be rare. The spec recommends to keep all policies + // towards a peer identical. If that is the case, there isn't a better + // channel that we should have selected. + if amt > max { + return runningAmounts{}, + fmt.Errorf("incompatible channel policies: %v "+ + "exceeds %v", amt, max) + } + + // Add fees to the running amounts. Skip the source node fees as + // those do not need to be paid. + if !localChan { + amt += policy.ComputeFee(amt) + max += policy.ComputeFee(max) + } + + return runningAmounts{amt: amt, max: max}, nil +} + +// ErrNoChannel is returned when a route cannot be built because there are no +// channels that satisfy all requirements. +type ErrNoChannel struct { + position int + fromNode route.Vertex +} + +// Error returns a human readable string describing the error. +func (e ErrNoChannel) Error() string { + return fmt.Sprintf("no matching outgoing channel available for "+ + "node %v (%v)", e.position, e.fromNode) +} + +// BuildRoute returns a fully specified route based on a list of pubkeys. If +// amount is nil, the minimum routable amount is used. To force a specific +// outgoing channel, use the outgoingChan parameter. +func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, + hops []route.Vertex, outgoingChan *uint64, + finalCltvDelta int32) (*route.Route, error) { + + log.Tracef("BuildRoute called: hopsCount=%v, amt=%v", + len(hops), amt) + + // If no amount is specified, we need to build a route for the minimum + // amount that this route can carry. + useMinAmt := amt == nil + + // We'll attempt to obtain a set of bandwidth hints that helps us select + // the best outgoing channel to use in case no outgoing channel is set. + bandwidthHints, err := generateBandwidthHints( + r.selfNode, r.cfg.QueryBandwidth, + ) + if err != nil { + return nil, err + } + + // Allocate a list that will contain the selected channels for this + // route. + edges := make([]*channeldb.ChannelEdgePolicy, len(hops)) + + // Keep a running amount and the maximum for this route. + amts := runningAmounts{ + max: lnwire.MilliSatoshi(^uint64(0)), + } + if useMinAmt { + // For minimum amount routes, aim to deliver at least 1 msat to + // the destination. There are nodes in the wild that have a + // min_htlc channel policy of zero, which could lead to a zero + // amount payment being made. + amts.amt = 1 + } else { + // If an amount is specified, we need to build a route that + // delivers exactly this amount to the final destination. + amts.amt = *amt + } + + // Traverse hops backwards to accumulate fees in the running amounts. + source := r.selfNode.PubKeyBytes + for i := len(hops) - 1; i >= 0; i-- { + toNode := hops[i] + + var fromNode route.Vertex + if i == 0 { + fromNode = source + } else { + fromNode = hops[i-1] + } + + localChan := i == 0 + + // Iterate over candidate channels to select the channel + // to use for the final route. + var ( + bestEdge *channeldb.ChannelEdgePolicy + bestAmts *runningAmounts + bestBandwidth lnwire.MilliSatoshi + ) + + cb := func(tx *bbolt.Tx, + edgeInfo *channeldb.ChannelEdgeInfo, + _, inEdge *channeldb.ChannelEdgePolicy) error { + + chanID := edgeInfo.ChannelID + + // Apply outgoing channel restriction is active. + if localChan && outgoingChan != nil && + chanID != *outgoingChan { + + return nil + } + + // No unknown policy channels. + if inEdge == nil { + return nil + } + + // Before we can process the edge, we'll need to + // fetch the node on the _other_ end of this + // channel as we may later need to iterate over + // the incoming edges of this node if we explore + // it further. + chanFromNode, err := edgeInfo.FetchOtherNode( + tx, toNode[:], + ) + if err != nil { + return err + } + + // Continue searching if this channel doesn't + // connect with the previous hop. + if chanFromNode.PubKeyBytes != fromNode { + return nil + } + + // Validate whether this channel's policy is satisfied + // and obtain the new running amounts if this channel + // was to be selected. + newAmts, err := amts.prependChannel( + inEdge, edgeInfo.Capacity, localChan, + useMinAmt, + ) + if err != nil { + log.Tracef("Skipping chan %v: %v", + inEdge.ChannelID, err) + + return nil + } + + // If we already have a best edge, check whether this + // edge is better. + bandwidth := bandwidthHints[chanID] + if bestEdge != nil { + if localChan { + // For local channels, better is defined + // as having more bandwidth. We try to + // maximize the chance that the returned + // route succeeds. + if bandwidth < bestBandwidth { + return nil + } + } else { + // For other channels, better is defined + // as lower fees for the amount to send. + // Normally all channels between two + // nodes should have the same policy, + // but in case not we minimize our cost + // here. Regular path finding would do + // the same. + if newAmts.amt > bestAmts.amt { + return nil + } + } + } + + // If we get here, the current edge is better. Replace + // the best. + bestEdge = inEdge + bestAmts = &newAmts + bestBandwidth = bandwidth + + return nil + } + + err := r.cfg.Graph.ForEachNodeChannel(nil, toNode[:], cb) + if err != nil { + return nil, err + } + + // There is no matching channel. Stop building the route here. + if bestEdge == nil { + return nil, ErrNoChannel{ + fromNode: fromNode, + position: i, + } + } + + log.Tracef("Select channel %v at position %v", bestEdge.ChannelID, i) + + edges[i] = bestEdge + amts = *bestAmts + } + + _, height, err := r.cfg.Chain.GetBestBlock() + if err != nil { + return nil, err + } + + var receiverAmt lnwire.MilliSatoshi + if useMinAmt { + // We've calculated the minimum amount for the htlc that the + // source node hands out. The newRoute call below expects the + // amount that must reach the receiver after subtraction of fees + // along the way. Iterate over all edges to calculate the + // receiver amount. + receiverAmt = amts.amt + for _, edge := range edges[1:] { + receiverAmt -= edge.ComputeFeeFromIncoming(receiverAmt) + } + } else { + // Deliver the specified amount to the receiver. + receiverAmt = *amt + } + + // Build and return the final route. + return newRoute( + receiverAmt, source, edges, uint32(height), + uint16(finalCltvDelta), nil, + ) +} diff --git a/routing/router_test.go b/routing/router_test.go index 56c5ade6..fba79ac7 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -3330,3 +3330,146 @@ func TestSendToRouteStructuredError(t *testing.T) { t.Fatalf("initPayment not called") } } + +// TestBuildRoute tests whether correct routes are built. +func TestBuildRoute(t *testing.T) { + // Setup a three node network. + chanCapSat := btcutil.Amount(100000) + testChannels := []*testChannel{ + // Create two local channels from a. The bandwidth is estimated + // in this test as the channel capacity. For building routes, we + // expected the channel with the largest estimated bandwidth to + // be selected. + symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 20000, + MinHTLC: lnwire.NewMSatFromSatoshis(5), + MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat), + }, 1), + symmetricTestChannel("a", "b", chanCapSat/2, &testChannelPolicy{ + Expiry: 144, + FeeRate: 20000, + MinHTLC: lnwire.NewMSatFromSatoshis(5), + MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat / 2), + }, 6), + + // Create two channels from b to c. For building routes, we + // expect the lowest cost channel to be selected. Note that this + // isn't a situation that we are expecting in reality. Routing + // nodes are recommended to keep their channel policies towards + // the same peer identical. + symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 50000, + MinHTLC: lnwire.NewMSatFromSatoshis(20), + MaxHTLC: lnwire.NewMSatFromSatoshis(120), + }, 2), + symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 60000, + MinHTLC: lnwire.NewMSatFromSatoshis(20), + MaxHTLC: lnwire.NewMSatFromSatoshis(120), + }, 7), + + symmetricTestChannel("a", "e", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 80000, + MinHTLC: lnwire.NewMSatFromSatoshis(5), + MaxHTLC: lnwire.NewMSatFromSatoshis(10), + }, 5), + symmetricTestChannel("e", "c", chanCapSat, &testChannelPolicy{ + Expiry: 144, + FeeRate: 100000, + MinHTLC: lnwire.NewMSatFromSatoshis(20), + MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat), + }, 4), + } + + testGraph, err := createTestGraphFromChannels(testChannels, "a") + if err != nil { + t.Fatalf("unable to create graph: %v", err) + } + defer testGraph.cleanUp() + + const startingBlockHeight = 101 + + ctx, cleanUp, err := createTestCtxFromGraphInstance( + startingBlockHeight, testGraph, + ) + if err != nil { + t.Fatalf("unable to create router: %v", err) + } + defer cleanUp() + + checkHops := func(rt *route.Route, expected []uint64) { + if len(rt.Hops) != len(expected) { + t.Fatal("hop count mismatch") + } + for i, hop := range rt.Hops { + if hop.ChannelID != expected[i] { + t.Fatalf("expected channel %v at pos %v, but "+ + "got channel %v", + expected[i], i, hop.ChannelID) + } + } + } + + // Create hop list from the route node pubkeys. + hops := []route.Vertex{ + ctx.aliases["b"], ctx.aliases["c"], + } + amt := lnwire.NewMSatFromSatoshis(100) + + // Build the route for the given amount. + rt, err := ctx.router.BuildRoute( + &amt, hops, nil, 40, + ) + if err != nil { + t.Fatal(err) + } + + // Check that we get the expected route back. The total amount should be + // the amount to deliver to hop c (100 sats) plus the fee for hop b (5 + // sats). + checkHops(rt, []uint64{1, 2}) + if rt.TotalAmount != 105000 { + t.Fatalf("unexpected total amount %v", rt.TotalAmount) + } + + // Build the route for the minimum amount. + rt, err = ctx.router.BuildRoute( + nil, hops, nil, 40, + ) + if err != nil { + t.Fatal(err) + } + + // Check that we get the expected route back. The minimum that we can + // send from b to c is 20 sats. Hop b charges 1 sat for the forwarding. + // The channel between hop a and b can carry amounts in the range [5, + // 100], so 21 sats is the minimum amount for this route. + checkHops(rt, []uint64{1, 2}) + if rt.TotalAmount != 21000 { + t.Fatalf("unexpected total amount %v", rt.TotalAmount) + } + + // Test a route that contains incompatible channel htlc constraints. + // There is no amount that can pass through both channel 5 and 4. + hops = []route.Vertex{ + ctx.aliases["e"], ctx.aliases["c"], + } + _, err = ctx.router.BuildRoute( + nil, hops, nil, 40, + ) + errNoChannel, ok := err.(ErrNoChannel) + if !ok { + t.Fatalf("expected incompatible policies error, but got %v", + err) + } + if errNoChannel.position != 0 { + t.Fatalf("unexpected no channel error position") + } + if errNoChannel.fromNode != ctx.aliases["a"] { + t.Fatalf("unexpected no channel error node") + } +}