diff --git a/cmd/lncli/cmd_payments.go b/cmd/lncli/cmd_payments.go index ec6a04b37c..228dd3415f 100644 --- a/cmd/lncli/cmd_payments.go +++ b/cmd/lncli/cmd_payments.go @@ -1099,8 +1099,13 @@ var queryRoutesCommand = cli.Command{ }, cli.Int64Flag{ Name: "final_cltv_delta", - Usage: "(optional) number of blocks the last hop has to reveal " + - "the preimage", + Usage: "(optional) number of blocks the last hop has " + + "to reveal the preimage. Note that this " + + "should not be set in the case where the " + + "path includes a blinded path since in " + + "that case, the receiver will already have " + + "accounted for this value in the " + + "blinded_cltv value", }, cli.BoolFlag{ Name: "use_mc", @@ -1238,6 +1243,13 @@ func parseBlindedPaymentParameters(ctx *cli.Context) ( return nil, nil } + // If a blinded path has been provided, then the final_cltv_delta flag + // should not be provided since this value will be ignored. + if ctx.IsSet("final_cltv_delta") { + return nil, fmt.Errorf("`final_cltv_delta` should not be " + + "provided if a blinded path is provided") + } + // If any one of our blinding related flags is set, we expect the // full set to be set and we'll error out accordingly. introNode, err := route.NewVertexFromStr( diff --git a/docs/release-notes/release-notes-0.18.3.md b/docs/release-notes/release-notes-0.18.3.md index 3017b1ffd2..9245903954 100644 --- a/docs/release-notes/release-notes-0.18.3.md +++ b/docs/release-notes/release-notes-0.18.3.md @@ -89,6 +89,9 @@ channel. We will still wait for the channel to have at least one confirmation and so the main change here is that we don't error out for such a case. +* [Groundwork](https://github.com/lightningnetwork/lnd/pull/8752) in preparation + for implementing route blinding receives. + ## Testing ## Database diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index a89f1b7347..ddfbe5934f 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -360,6 +360,7 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, if err != nil { return nil, err } + // Validate the data in the blinded route against our incoming htlc's // information. if err := ValidateBlindedRouteData( @@ -368,9 +369,31 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, return nil, err } + // Exit early if this onion is for the exit hop of the route since + // route blinding receives are not yet supported. + if isFinalHop { + return nil, fmt.Errorf("being the final hop in a blinded " + + "path is not yet supported") + } + + // At this point, we know we are a forwarding node for this onion + // and so we expect the relay info and next SCID fields to be set. + relayInfo, err := routeData.RelayInfo.UnwrapOrErr( + fmt.Errorf("relay info not set for non-final blinded hop"), + ) + if err != nil { + return nil, err + } + + nextSCID, err := routeData.ShortChannelID.UnwrapOrErr( + fmt.Errorf("next SCID not set for non-final blinded hop"), + ) + if err != nil { + return nil, err + } + fwdAmt, err := calculateForwardingAmount( - b.IncomingAmount, routeData.RelayInfo.Val.BaseFee, - routeData.RelayInfo.Val.FeeRate, + b.IncomingAmount, relayInfo.Val.BaseFee, relayInfo.Val.FeeRate, ) if err != nil { return nil, err @@ -400,10 +423,10 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, } return &ForwardingInfo{ - NextHop: routeData.ShortChannelID.Val, + NextHop: nextSCID.Val, AmountToForward: fwdAmt, OutgoingCTLV: b.IncomingCltv - uint32( - routeData.RelayInfo.Val.CltvExpiryDelta, + relayInfo.Val.CltvExpiryDelta, ), // Remap from blinding override type to blinding point type. NextBlinding: tlv.SomeRecordT( diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go index e69361b0a4..9995c71baf 100644 --- a/htlcswitch/hop/iterator_test.go +++ b/htlcswitch/hop/iterator_test.go @@ -186,7 +186,7 @@ func TestDecryptAndValidateFwdInfo(t *testing.T) { // Encode valid blinding data that we'll fake decrypting for our test. maxCltv := 1000 - blindedData := record.NewBlindedRouteData( + blindedData := record.NewNonFinalBlindedRouteData( lnwire.NewShortChanIDFromInt(1500), nil, record.PaymentRelayInfo{ CltvExpiryDelta: 10, diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go index c22144d7b8..7398813a3e 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -646,7 +646,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }{ { name: "max cltv expired", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -663,7 +663,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "zero max cltv", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -682,7 +682,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "amount below minimum", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -699,7 +699,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "valid, no features", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -714,7 +714,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "unknown features", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -738,7 +738,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "valid data", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{ diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 5c32a85a39..514e856d89 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -676,7 +676,7 @@ func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge, // Encode the route's blinded data and include it in the // blinded hop. - payload := record.NewBlindedRouteData( + payload := record.NewNonFinalBlindedRouteData( scid, nil, *relayInfo, constraints, nil, ) payloadBytes, err := record.EncodeBlindedRouteData(payload) @@ -739,7 +739,7 @@ func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge, // node ID here so that it _looks like_ a valid // forwarding hop (though in reality it's the last // hop). - record.NewBlindedRouteData( + record.NewNonFinalBlindedRouteData( lnwire.NewShortChanIDFromInt(100), nil, record.PaymentRelayInfo{}, nil, nil, ), diff --git a/lnrpc/invoicesrpc/utils.go b/lnrpc/invoicesrpc/utils.go index 70d7740828..2d1507267b 100644 --- a/lnrpc/invoicesrpc/utils.go +++ b/lnrpc/invoicesrpc/utils.go @@ -266,6 +266,60 @@ func CreateRPCRouteHints(routeHints [][]zpay32.HopHint) []*lnrpc.RouteHint { return res } +// CreateRPCBlindedPayments takes a set of zpay32.BlindedPaymentPath and +// converts them into a set of lnrpc.BlindedPaymentPaths. +func CreateRPCBlindedPayments(blindedPaths []*zpay32.BlindedPaymentPath) ( + []*lnrpc.BlindedPaymentPath, error) { + + var res []*lnrpc.BlindedPaymentPath + for _, path := range blindedPaths { + features := path.Features.Features() + var featuresSlice []lnrpc.FeatureBit + for feature := range features { + featuresSlice = append( + featuresSlice, lnrpc.FeatureBit(feature), + ) + } + + if len(path.Hops) == 0 { + return nil, fmt.Errorf("each blinded path must " + + "contain at least one hop") + } + + var hops []*lnrpc.BlindedHop + for _, hop := range path.Hops { + blindedNodeID := hop.BlindedNodePub. + SerializeCompressed() + hops = append(hops, &lnrpc.BlindedHop{ + BlindedNode: blindedNodeID, + EncryptedData: hop.CipherText, + }) + } + + introNode := path.Hops[0].BlindedNodePub + firstBlindingPoint := path.FirstEphemeralBlindingPoint + + blindedPath := &lnrpc.BlindedPath{ + IntroductionNode: introNode.SerializeCompressed(), + BlindingPoint: firstBlindingPoint. + SerializeCompressed(), + BlindedHops: hops, + } + + res = append(res, &lnrpc.BlindedPaymentPath{ + BlindedPath: blindedPath, + BaseFeeMsat: uint64(path.FeeBaseMsat), + ProportionalFeeRate: path.FeeRate, + TotalCltvDelta: uint32(path.CltvExpiryDelta), + HtlcMinMsat: path.HTLCMinMsat, + HtlcMaxMsat: path.HTLCMaxMsat, + Features: featuresSlice, + }) + } + + return res, nil +} + // CreateZpay32HopHints takes in the lnrpc form of route hints and converts them // into an invoice decoded form. func CreateZpay32HopHints(routeHints []*lnrpc.RouteHint) ([][]zpay32.HopHint, error) { diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 305e624124..3cd5d9767d 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -14304,19 +14304,20 @@ type PayReq struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` - PaymentHash string `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` - NumSatoshis int64 `protobuf:"varint,3,opt,name=num_satoshis,json=numSatoshis,proto3" json:"num_satoshis,omitempty"` - Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Expiry int64 `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"` - Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` - DescriptionHash string `protobuf:"bytes,7,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"` - FallbackAddr string `protobuf:"bytes,8,opt,name=fallback_addr,json=fallbackAddr,proto3" json:"fallback_addr,omitempty"` - CltvExpiry int64 `protobuf:"varint,9,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"` - RouteHints []*RouteHint `protobuf:"bytes,10,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"` - PaymentAddr []byte `protobuf:"bytes,11,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"` - NumMsat int64 `protobuf:"varint,12,opt,name=num_msat,json=numMsat,proto3" json:"num_msat,omitempty"` - Features map[uint32]*Feature `protobuf:"bytes,13,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` + PaymentHash string `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` + NumSatoshis int64 `protobuf:"varint,3,opt,name=num_satoshis,json=numSatoshis,proto3" json:"num_satoshis,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Expiry int64 `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + DescriptionHash string `protobuf:"bytes,7,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"` + FallbackAddr string `protobuf:"bytes,8,opt,name=fallback_addr,json=fallbackAddr,proto3" json:"fallback_addr,omitempty"` + CltvExpiry int64 `protobuf:"varint,9,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"` + RouteHints []*RouteHint `protobuf:"bytes,10,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"` + PaymentAddr []byte `protobuf:"bytes,11,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"` + NumMsat int64 `protobuf:"varint,12,opt,name=num_msat,json=numMsat,proto3" json:"num_msat,omitempty"` + Features map[uint32]*Feature `protobuf:"bytes,13,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + BlindedPaths []*BlindedPaymentPath `protobuf:"bytes,14,rep,name=blinded_paths,json=blindedPaths,proto3" json:"blinded_paths,omitempty"` } func (x *PayReq) Reset() { @@ -14442,6 +14443,13 @@ func (x *PayReq) GetFeatures() map[uint32]*Feature { return nil } +func (x *PayReq) GetBlindedPaths() []*BlindedPaymentPath { + if x != nil { + return x.BlindedPaths + } + return nil +} + type Feature struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -20202,7 +20210,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x75, 0x62, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x27, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52, - 0x65, 0x71, 0x22, 0xb0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, + 0x65, 0x71, 0x22, 0xf0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, @@ -20232,7 +20240,11 @@ var file_lightning_proto_rawDesc = []byte{ 0x37, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, - 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, + 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x62, 0x6c, 0x69, 0x6e, + 0x64, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x50, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0c, 0x62, 0x6c, 0x69, 0x6e, + 0x64, 0x65, 0x64, 0x50, 0x61, 0x74, 0x68, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, @@ -21520,192 +21532,193 @@ var file_lightning_proto_depIdxs = []int32{ 38, // 138: lnrpc.AbandonChannelRequest.channel_point:type_name -> lnrpc.ChannelPoint 150, // 139: lnrpc.PayReq.route_hints:type_name -> lnrpc.RouteHint 244, // 140: lnrpc.PayReq.features:type_name -> lnrpc.PayReq.FeaturesEntry - 179, // 141: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport - 38, // 142: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint - 181, // 143: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee - 39, // 144: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint - 11, // 145: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure - 183, // 146: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate - 186, // 147: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent - 38, // 148: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint - 38, // 149: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint - 38, // 150: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint - 193, // 151: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups - 190, // 152: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup - 189, // 153: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup - 193, // 154: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups - 198, // 155: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission - 198, // 156: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission - 245, // 157: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry - 20, // 158: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode - 209, // 159: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate - 211, // 160: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op - 198, // 161: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission - 215, // 162: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth - 216, // 163: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage - 216, // 164: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage - 218, // 165: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration - 219, // 166: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback - 177, // 167: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature - 177, // 168: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature - 4, // 169: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator - 3, // 170: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType - 226, // 171: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 226, // 172: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 229, // 173: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments - 226, // 174: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 226, // 175: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 108, // 176: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC - 15, // 177: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState - 113, // 178: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance - 177, // 179: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature - 137, // 180: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric - 177, // 181: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature - 177, // 182: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature - 154, // 183: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState - 177, // 184: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature - 205, // 185: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList - 114, // 186: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest - 117, // 187: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest - 30, // 188: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest - 42, // 189: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest - 46, // 190: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest - 48, // 191: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest - 30, // 192: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest - 44, // 193: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest - 50, // 194: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest - 52, // 195: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest - 54, // 196: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest - 56, // 197: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest - 58, // 198: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest - 74, // 199: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest - 76, // 200: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription - 78, // 201: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest - 80, // 202: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest - 82, // 203: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest - 109, // 204: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest - 63, // 205: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest - 111, // 206: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription - 70, // 207: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest - 96, // 208: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest - 96, // 209: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest - 93, // 210: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest - 106, // 211: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg - 37, // 212: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse - 88, // 213: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest - 171, // 214: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest - 33, // 215: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest - 33, // 216: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest - 35, // 217: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest - 35, // 218: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest - 155, // 219: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice - 160, // 220: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest - 159, // 221: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash - 162, // 222: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription - 175, // 223: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString - 165, // 224: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest - 167, // 225: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest - 168, // 226: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest - 133, // 227: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest - 135, // 228: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest - 138, // 229: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest - 127, // 230: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest - 119, // 231: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest - 139, // 232: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest - 141, // 233: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest - 143, // 234: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription - 173, // 235: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest - 178, // 236: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest - 182, // 237: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest - 185, // 238: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest - 188, // 239: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest - 191, // 240: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest - 192, // 241: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot - 194, // 242: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest - 196, // 243: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription - 199, // 244: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest - 201, // 245: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest - 203, // 246: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest - 206, // 247: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest - 212, // 248: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest - 217, // 249: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse - 25, // 250: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest - 23, // 251: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest - 66, // 252: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest - 21, // 253: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest - 115, // 254: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse - 118, // 255: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse - 31, // 256: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails - 43, // 257: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse - 47, // 258: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse - 49, // 259: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse - 29, // 260: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction - 45, // 261: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse - 51, // 262: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse - 53, // 263: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse - 55, // 264: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse - 57, // 265: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse - 59, // 266: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse - 75, // 267: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse - 77, // 268: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent - 79, // 269: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse - 81, // 270: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse - 83, // 271: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse - 110, // 272: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse - 64, // 273: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse - 112, // 274: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate - 71, // 275: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse - 38, // 276: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint - 97, // 277: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate - 95, // 278: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse - 107, // 279: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp - 36, // 280: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest - 89, // 281: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate - 172, // 282: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse - 34, // 283: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse - 34, // 284: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse - 34, // 285: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse - 34, // 286: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse - 158, // 287: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse - 161, // 288: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse - 155, // 289: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice - 155, // 290: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice - 176, // 291: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq - 166, // 292: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse - 169, // 293: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse - 170, // 294: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse - 134, // 295: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph - 136, // 296: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse - 132, // 297: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge - 128, // 298: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo - 122, // 299: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse - 140, // 300: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo - 142, // 301: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse - 144, // 302: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate - 174, // 303: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse - 180, // 304: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse - 184, // 305: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse - 187, // 306: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse - 189, // 307: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup - 192, // 308: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot - 197, // 309: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse - 195, // 310: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse - 192, // 311: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot - 200, // 312: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse - 202, // 313: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse - 204, // 314: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse - 207, // 315: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse - 213, // 316: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse - 214, // 317: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest - 26, // 318: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse - 24, // 319: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage - 67, // 320: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse - 22, // 321: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse - 254, // [254:322] is the sub-list for method output_type - 186, // [186:254] is the sub-list for method input_type - 186, // [186:186] is the sub-list for extension type_name - 186, // [186:186] is the sub-list for extension extendee - 0, // [0:186] is the sub-list for field type_name + 151, // 141: lnrpc.PayReq.blinded_paths:type_name -> lnrpc.BlindedPaymentPath + 179, // 142: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport + 38, // 143: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint + 181, // 144: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee + 39, // 145: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint + 11, // 146: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure + 183, // 147: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate + 186, // 148: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent + 38, // 149: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint + 38, // 150: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint + 38, // 151: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint + 193, // 152: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups + 190, // 153: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup + 189, // 154: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup + 193, // 155: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups + 198, // 156: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission + 198, // 157: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission + 245, // 158: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry + 20, // 159: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode + 209, // 160: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate + 211, // 161: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op + 198, // 162: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission + 215, // 163: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth + 216, // 164: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage + 216, // 165: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage + 218, // 166: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration + 219, // 167: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback + 177, // 168: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature + 177, // 169: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature + 4, // 170: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator + 3, // 171: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType + 226, // 172: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 226, // 173: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 229, // 174: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments + 226, // 175: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 226, // 176: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 108, // 177: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC + 15, // 178: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState + 113, // 179: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance + 177, // 180: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature + 137, // 181: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric + 177, // 182: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature + 177, // 183: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature + 154, // 184: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState + 177, // 185: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature + 205, // 186: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList + 114, // 187: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest + 117, // 188: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest + 30, // 189: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest + 42, // 190: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest + 46, // 191: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest + 48, // 192: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest + 30, // 193: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest + 44, // 194: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest + 50, // 195: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest + 52, // 196: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest + 54, // 197: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest + 56, // 198: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest + 58, // 199: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest + 74, // 200: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest + 76, // 201: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription + 78, // 202: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest + 80, // 203: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest + 82, // 204: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest + 109, // 205: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest + 63, // 206: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest + 111, // 207: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription + 70, // 208: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest + 96, // 209: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest + 96, // 210: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest + 93, // 211: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest + 106, // 212: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg + 37, // 213: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse + 88, // 214: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest + 171, // 215: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest + 33, // 216: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest + 33, // 217: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest + 35, // 218: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest + 35, // 219: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest + 155, // 220: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice + 160, // 221: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest + 159, // 222: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash + 162, // 223: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription + 175, // 224: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString + 165, // 225: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest + 167, // 226: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest + 168, // 227: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest + 133, // 228: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest + 135, // 229: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest + 138, // 230: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest + 127, // 231: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest + 119, // 232: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest + 139, // 233: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest + 141, // 234: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest + 143, // 235: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription + 173, // 236: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest + 178, // 237: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest + 182, // 238: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest + 185, // 239: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest + 188, // 240: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest + 191, // 241: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest + 192, // 242: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot + 194, // 243: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest + 196, // 244: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription + 199, // 245: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest + 201, // 246: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest + 203, // 247: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest + 206, // 248: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest + 212, // 249: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest + 217, // 250: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse + 25, // 251: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest + 23, // 252: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest + 66, // 253: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest + 21, // 254: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest + 115, // 255: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse + 118, // 256: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse + 31, // 257: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails + 43, // 258: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse + 47, // 259: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse + 49, // 260: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse + 29, // 261: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction + 45, // 262: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse + 51, // 263: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse + 53, // 264: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse + 55, // 265: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse + 57, // 266: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse + 59, // 267: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse + 75, // 268: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse + 77, // 269: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent + 79, // 270: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse + 81, // 271: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse + 83, // 272: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse + 110, // 273: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse + 64, // 274: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse + 112, // 275: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate + 71, // 276: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse + 38, // 277: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint + 97, // 278: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate + 95, // 279: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse + 107, // 280: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp + 36, // 281: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest + 89, // 282: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate + 172, // 283: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse + 34, // 284: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse + 34, // 285: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse + 34, // 286: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse + 34, // 287: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse + 158, // 288: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse + 161, // 289: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse + 155, // 290: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice + 155, // 291: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice + 176, // 292: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq + 166, // 293: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse + 169, // 294: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse + 170, // 295: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse + 134, // 296: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph + 136, // 297: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse + 132, // 298: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge + 128, // 299: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo + 122, // 300: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse + 140, // 301: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo + 142, // 302: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse + 144, // 303: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate + 174, // 304: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse + 180, // 305: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse + 184, // 306: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse + 187, // 307: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse + 189, // 308: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup + 192, // 309: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot + 197, // 310: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse + 195, // 311: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse + 192, // 312: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot + 200, // 313: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse + 202, // 314: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse + 204, // 315: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse + 207, // 316: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse + 213, // 317: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse + 214, // 318: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest + 26, // 319: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse + 24, // 320: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage + 67, // 321: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse + 22, // 322: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse + 255, // [255:323] is the sub-list for method output_type + 187, // [187:255] is the sub-list for method input_type + 187, // [187:187] is the sub-list for extension type_name + 187, // [187:187] is the sub-list for extension extendee + 0, // [0:187] is the sub-list for field type_name } func init() { file_lightning_proto_init() } diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index a13ca95c5f..5b912e9c00 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -4289,6 +4289,7 @@ message PayReq { bytes payment_addr = 11; int64 num_msat = 12; map features = 13; + repeated BlindedPaymentPath blinded_paths = 14; } enum FeatureBit { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index ae59f4a196..a0c6761ffd 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -6299,6 +6299,12 @@ "additionalProperties": { "$ref": "#/definitions/lnrpcFeature" } + }, + "blinded_paths": { + "type": "array", + "items": { + "$ref": "#/definitions/lnrpcBlindedPaymentPath" + } } } }, diff --git a/record/blinded_data.go b/record/blinded_data.go index 7990fa7388..7b7081b3be 100644 --- a/record/blinded_data.go +++ b/record/blinded_data.go @@ -14,8 +14,18 @@ import ( // route encrypted data blob that is created by the recipient to provide // forwarding information. type BlindedRouteData struct { + // Padding is an optional set of bytes that a recipient can use to pad + // the data so that the encrypted recipient data blobs are all the same + // length. + Padding tlv.OptionalRecordT[tlv.TlvType1, []byte] + // ShortChannelID is the channel ID of the next hop. - ShortChannelID tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID] + ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID] + + // PathID is a secret set of bytes that the blinded path creator will + // set so that they can check the value on decryption to ensure that the + // path they created was used for the intended purpose. + PathID tlv.OptionalRecordT[tlv.TlvType6, []byte] // NextBlindingOverride is a blinding point that should be switched // in for the next hop. This is used to combine two blinded paths into @@ -24,7 +34,7 @@ type BlindedRouteData struct { NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey] // RelayInfo provides the relay parameters for the hop. - RelayInfo tlv.RecordT[tlv.TlvType10, PaymentRelayInfo] + RelayInfo tlv.OptionalRecordT[tlv.TlvType10, PaymentRelayInfo] // Constraints provides the payment relay constraints for the hop. Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints] @@ -33,16 +43,20 @@ type BlindedRouteData struct { Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector] } -// NewBlindedRouteData creates the data that's provided for hops within a -// blinded route. -func NewBlindedRouteData(chanID lnwire.ShortChannelID, +// NewNonFinalBlindedRouteData creates the data that's provided for hops within +// a blinded route. +func NewNonFinalBlindedRouteData(chanID lnwire.ShortChannelID, blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo, constraints *PaymentConstraints, features *lnwire.FeatureVector) *BlindedRouteData { info := &BlindedRouteData{ - ShortChannelID: tlv.NewRecordT[tlv.TlvType2](chanID), - RelayInfo: tlv.NewRecordT[tlv.TlvType10](relayInfo), + ShortChannelID: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType2](chanID), + ), + RelayInfo: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType10](relayInfo), + ), } if blindingOverride != nil { @@ -64,12 +78,36 @@ func NewBlindedRouteData(chanID lnwire.ShortChannelID, return info } +// NewFinalHopBlindedRouteData creates the data that's provided for the final +// hop in a blinded route. +func NewFinalHopBlindedRouteData(constraints *PaymentConstraints, + pathID []byte) *BlindedRouteData { + + var data BlindedRouteData + if pathID != nil { + data.PathID = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6](pathID), + ) + } + + if constraints != nil { + data.Constraints = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType12](*constraints)) + } + + return &data +} + // DecodeBlindedRouteData decodes the data provided within a blinded route. func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { var ( d BlindedRouteData + padding = d.Padding.Zero() + scid = d.ShortChannelID.Zero() + pathID = d.PathID.Zero() blindingOverride = d.NextBlindingOverride.Zero() + relayInfo = d.RelayInfo.Zero() constraints = d.Constraints.Zero() features = d.Features.Zero() ) @@ -80,19 +118,35 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { } typeMap, err := tlvRecords.ExtractRecords( - &d.ShortChannelID, - &blindingOverride, &d.RelayInfo.Val, &constraints, - &features, + &padding, &scid, &pathID, &blindingOverride, &relayInfo, + &constraints, &features, ) if err != nil { return nil, err } - val, ok := typeMap[d.NextBlindingOverride.TlvType()] + val, ok := typeMap[d.Padding.TlvType()] + if ok && val == nil { + d.Padding = tlv.SomeRecordT(padding) + } + + if val, ok := typeMap[d.ShortChannelID.TlvType()]; ok && val == nil { + d.ShortChannelID = tlv.SomeRecordT(scid) + } + + if val, ok := typeMap[d.PathID.TlvType()]; ok && val == nil { + d.PathID = tlv.SomeRecordT(pathID) + } + + val, ok = typeMap[d.NextBlindingOverride.TlvType()] if ok && val == nil { d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride) } + if val, ok := typeMap[d.RelayInfo.TlvType()]; ok && val == nil { + d.RelayInfo = tlv.SomeRecordT(relayInfo) + } + if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil { d.Constraints = tlv.SomeRecordT(constraints) } @@ -111,7 +165,19 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { recordProducers = make([]tlv.RecordProducer, 0, 5) ) - recordProducers = append(recordProducers, &data.ShortChannelID) + data.Padding.WhenSome(func(p tlv.RecordT[tlv.TlvType1, []byte]) { + recordProducers = append(recordProducers, &p) + }) + + data.ShortChannelID.WhenSome(func(scid tlv.RecordT[tlv.TlvType2, + lnwire.ShortChannelID]) { + + recordProducers = append(recordProducers, &scid) + }) + + data.PathID.WhenSome(func(pathID tlv.RecordT[tlv.TlvType6, []byte]) { + recordProducers = append(recordProducers, &pathID) + }) data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) { @@ -119,7 +185,11 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { recordProducers = append(recordProducers, &pk) }) - recordProducers = append(recordProducers, &data.RelayInfo.Val) + data.RelayInfo.WhenSome(func(r tlv.RecordT[tlv.TlvType10, + PaymentRelayInfo]) { + + recordProducers = append(recordProducers, &r) + }) data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12, PaymentConstraints]) { @@ -140,6 +210,19 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { return e[:], nil } +// PadBy adds "n" padding bytes to the BlindedRouteData using the Padding field. +// Callers should be aware that the total payload size will change by more than +// "n" since the "n" bytes will be prefixed by BigSize type and length fields. +// Callers may need to call PadBy iteratively until each encrypted data packet +// is the same size and so each call will overwrite the Padding record. +// Note that calling PadBy with an n value of 0 will still result in a zero +// length TLV entry being added. +func (b *BlindedRouteData) PadBy(n int) { + b.Padding = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType1](make([]byte, n)), + ) +} + // PaymentRelayInfo describes the relay policy for a blinded path. type PaymentRelayInfo struct { // CltvExpiryDelta is the expiry delta for the payment. @@ -153,8 +236,8 @@ type PaymentRelayInfo struct { BaseFee uint32 } -// newPaymentRelayRecord creates a tlv.Record that encodes the payment relay -// (type 10) type for an encrypted blob payload. +// Record creates a tlv.Record that encodes the payment relay (type 10) type for +// an encrypted blob payload. func (i *PaymentRelayInfo) Record() tlv.Record { return tlv.MakeDynamicRecord( 10, &i, func() uint64 { diff --git a/record/blinded_data_test.go b/record/blinded_data_test.go index f8e95cdcc0..88ab9cddd6 100644 --- a/record/blinded_data_test.go +++ b/record/blinded_data_test.go @@ -101,7 +101,7 @@ func TestBlindedDataEncoding(t *testing.T) { } } - encodedData := NewBlindedRouteData( + encodedData := NewNonFinalBlindedRouteData( channelID, pubkey(t), info, constraints, testCase.features, ) @@ -118,6 +118,127 @@ func TestBlindedDataEncoding(t *testing.T) { } } +// TestBlindedDataFinalHopEncoding tests the encoding and decoding of a blinded +// data blob intended for the final hop of a blinded path where only the pathID +// will potentially be set. +func TestBlindedDataFinalHopEncoding(t *testing.T) { + tests := []struct { + name string + pathID []byte + constraints bool + }{ + { + name: "with path ID", + pathID: []byte{1, 2, 3, 4, 5, 6}, + }, + { + name: "with no path ID", + pathID: nil, + }, + { + name: "with path ID and constraints", + pathID: []byte{1, 2, 3, 4, 5, 6}, + constraints: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + var constraints *PaymentConstraints + if test.constraints { + constraints = &PaymentConstraints{ + MaxCltvExpiry: 4, + HtlcMinimumMsat: 5, + } + } + + encodedData := NewFinalHopBlindedRouteData( + constraints, test.pathID, + ) + + encoded, err := EncodeBlindedRouteData(encodedData) + require.NoError(t, err) + + b := bytes.NewBuffer(encoded) + decodedData, err := DecodeBlindedRouteData(b) + require.NoError(t, err) + + require.Equal(t, encodedData, decodedData) + }) + } +} + +// TestBlindedRouteDataPadding tests the PadBy method of BlindedRouteData. +func TestBlindedRouteDataPadding(t *testing.T) { + newBlindedRouteData := func() *BlindedRouteData { + channelID := lnwire.NewShortChanIDFromInt(1) + info := PaymentRelayInfo{ + FeeRate: 2, + CltvExpiryDelta: 3, + BaseFee: 30, + } + + constraints := &PaymentConstraints{ + MaxCltvExpiry: 4, + HtlcMinimumMsat: 100, + } + + return NewNonFinalBlindedRouteData( + channelID, pubkey(t), info, constraints, nil, + ) + } + + tests := []struct { + name string + paddingSize int + expectedSizeIncrease uint64 + }{ + { + // Calling PadBy with an n value of 0 in the case where + // there is not yet a padding field will result in a + // zero length TLV entry being added. This will add 2 + // bytes for the type and length fields. + name: "no extra padding", + expectedSizeIncrease: 2, + }, + { + name: "small padding (length " + + "field of 1 byte)", + paddingSize: 200, + expectedSizeIncrease: 202, + }, + { + name: "medium padding (length field " + + "of 3 bytes)", + paddingSize: 256, + expectedSizeIncrease: 260, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + data := newBlindedRouteData() + + prePaddingEncoding, err := EncodeBlindedRouteData(data) + require.NoError(t, err) + + data.PadBy(test.paddingSize) + + postPaddingEncoding, err := EncodeBlindedRouteData(data) + require.NoError(t, err) + + require.EqualValues( + t, test.expectedSizeIncrease, + len(postPaddingEncoding)- + len(prePaddingEncoding), + ) + }) + } +} + // TestBlindedRouteVectors tests encoding/decoding of the test vectors for // blinded route data provided in the specification. // @@ -131,10 +252,11 @@ func TestBlindingSpecTestVectors(t *testing.T) { tests := []struct { encoded string expectedPaymentData *BlindedRouteData + expectedPadding int }{ { encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", - expectedPaymentData: NewBlindedRouteData( + expectedPaymentData: NewNonFinalBlindedRouteData( lnwire.ShortChannelID{ BlockHeight: 0, TxIndex: 0, @@ -155,10 +277,11 @@ func TestBlindingSpecTestVectors(t *testing.T) { lnwire.Features, ), ), + expectedPadding: 26, }, { encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", - expectedPaymentData: NewBlindedRouteData( + expectedPaymentData: NewNonFinalBlindedRouteData( lnwire.ShortChannelID{ TxPosition: 1105, }, @@ -175,7 +298,8 @@ func TestBlindingSpecTestVectors(t *testing.T) { lnwire.NewFeatureVector( lnwire.NewRawFeatureVector(), lnwire.Features, - )), + ), + ), }, } @@ -189,6 +313,12 @@ func TestBlindingSpecTestVectors(t *testing.T) { decodedRoute, err := DecodeBlindedRouteData(buff) require.NoError(t, err) + if test.expectedPadding != 0 { + test.expectedPaymentData.PadBy( + test.expectedPadding, + ) + } + require.Equal( t, test.expectedPaymentData, decodedRoute, ) diff --git a/routing/additional_edge.go b/routing/additional_edge.go index eee17cce1a..5f2d42eebc 100644 --- a/routing/additional_edge.go +++ b/routing/additional_edge.go @@ -2,8 +2,8 @@ package routing import ( "errors" + "fmt" - "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -29,6 +29,11 @@ type AdditionalEdge interface { // EdgePolicy returns the policy of the additional edge. EdgePolicy() *models.CachedEdgePolicy + + // BlindedPayment returns the BlindedPayment that this additional edge + // info was derived from. It will return nil if this edge was not + // derived from a blinded route. + BlindedPayment() *BlindedPayment } // PayloadSizeFunc defines the interface for the payload size function. @@ -60,12 +65,49 @@ func (p *PrivateEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi, return hop.PayloadSize(channelID) } +// BlindedPayment is a no-op for a PrivateEdge since it is not associated with +// a blinded payment. This will thus return nil. +func (p *PrivateEdge) BlindedPayment() *BlindedPayment { + return nil +} + // BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed -// as additional edges because they are appened at the end of a normal route. +// as additional edges because they are appended at the end of a normal route. type BlindedEdge struct { - policy *models.CachedEdgePolicy - cipherText []byte - blindingPoint *btcec.PublicKey + policy *models.CachedEdgePolicy + + // blindedPayment is the BlindedPayment that this blinded edge was + // derived from. + blindedPayment *BlindedPayment + + // hopIndex is the index of the hop in the blinded payment path that + // this edge is associated with. + hopIndex int +} + +// NewBlindedEdge constructs a new BlindedEdge which packages the policy info +// for a specific hop within the given blinded payment path. The hop index +// should correspond to the hop within the blinded payment that this edge is +// associated with. +func NewBlindedEdge(policy *models.CachedEdgePolicy, payment *BlindedPayment, + hopIndex int) (*BlindedEdge, error) { + + if payment == nil { + return nil, fmt.Errorf("blinded payment cannot be nil for " + + "blinded edge") + } + + if hopIndex < 0 || hopIndex >= len(payment.BlindedPath.BlindedHops) { + return nil, fmt.Errorf("the hop index %d is outside the "+ + "valid range between 0 and %d", hopIndex, + len(payment.BlindedPath.BlindedHops)-1) + } + + return &BlindedEdge{ + policy: policy, + hopIndex: hopIndex, + blindedPayment: payment, + }, nil } // EdgePolicy return the policy of the BlindedEdge. @@ -78,15 +120,23 @@ func (b *BlindedEdge) EdgePolicy() *models.CachedEdgePolicy { func (b *BlindedEdge) IntermediatePayloadSize(_ lnwire.MilliSatoshi, _ uint32, _ uint64) uint64 { + blindedPath := b.blindedPayment.BlindedPath + hop := route.Hop{ - BlindingPoint: b.blindingPoint, - EncryptedData: b.cipherText, + BlindingPoint: blindedPath.BlindingPoint, + EncryptedData: blindedPath.BlindedHops[b.hopIndex].CipherText, } // For blinded paths the next chanID is in the encrypted data tlv. return hop.PayloadSize(0) } +// BlindedPayment returns the blinded payment that this edge is associated +// with. +func (b *BlindedEdge) BlindedPayment() *BlindedPayment { + return b.blindedPayment +} + // Compile-time constraints to ensure the PrivateEdge and the BlindedEdge // implement the AdditionalEdge interface. var _ AdditionalEdge = (*PrivateEdge)(nil) diff --git a/routing/additional_edge_test.go b/routing/additional_edge_test.go index b5628c6b7f..0324e2e106 100644 --- a/routing/additional_edge_test.go +++ b/routing/additional_edge_test.go @@ -42,9 +42,13 @@ func TestIntermediatePayloadSize(t *testing.T) { hop: route.Hop{ EncryptedData: []byte{12, 13}, }, - edge: &BlindedEdge{ - cipherText: []byte{12, 13}, - }, + edge: &BlindedEdge{blindedPayment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindedHops: []*sphinx.BlindedHopInfo{ + {CipherText: []byte{12, 13}}, + }, + }, + }}, }, { name: "Blinded edge - introduction point", @@ -52,10 +56,14 @@ func TestIntermediatePayloadSize(t *testing.T) { EncryptedData: []byte{12, 13}, BlindingPoint: blindedPoint, }, - edge: &BlindedEdge{ - cipherText: []byte{12, 13}, - blindingPoint: blindedPoint, - }, + edge: &BlindedEdge{blindedPayment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindingPoint: blindedPoint, + BlindedHops: []*sphinx.BlindedHopInfo{ + {CipherText: []byte{12, 13}}, + }, + }, + }}, }, } diff --git a/routing/blinding.go b/routing/blinding.go index d2d64aa5dd..788fb7b77e 100644 --- a/routing/blinding.go +++ b/routing/blinding.go @@ -88,13 +88,13 @@ func (b *BlindedPayment) Validate() error { // the case of multiple blinded hops, CLTV delta is fully accounted for in the // hints (both for intermediate hops and the final_cltv_delta for the receiving // node). -func (b *BlindedPayment) toRouteHints() RouteHints { +func (b *BlindedPayment) toRouteHints() (RouteHints, error) { // If we just have a single hop in our blinded route, it just contains // an introduction node (this is a valid path according to the spec). // Since we have the un-blinded node ID for the introduction node, we // don't need to add any route hints. if len(b.BlindedPath.BlindedHops) == 1 { - return nil + return nil, nil } hintCount := len(b.BlindedPath.BlindedHops) - 1 @@ -136,14 +136,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints { ToNodeFeatures: features, } - hints[fromNode] = []AdditionalEdge{ - &BlindedEdge{ - policy: edgePolicy, - cipherText: b.BlindedPath.BlindedHops[0].CipherText, - blindingPoint: b.BlindedPath.BlindingPoint, - }, + edge, err := NewBlindedEdge(edgePolicy, b, 0) + if err != nil { + return nil, err } + hints[fromNode] = []AdditionalEdge{edge} + // Start at an offset of 1 because the first node in our blinded hops // is the introduction node and terminate at the second-last node // because we're dealing with hops as pairs. @@ -169,14 +168,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints { ToNodeFeatures: features, } - hints[fromNode] = []AdditionalEdge{ - &BlindedEdge{ - policy: edgePolicy, - cipherText: b.BlindedPath.BlindedHops[i]. - CipherText, - }, + edge, err := NewBlindedEdge(edgePolicy, b, i) + if err != nil { + return nil, err } + + hints[fromNode] = []AdditionalEdge{edge} } - return hints + return hints, nil } diff --git a/routing/blinding_test.go b/routing/blinding_test.go index f6327ecd5d..58ad565949 100644 --- a/routing/blinding_test.go +++ b/routing/blinding_test.go @@ -128,7 +128,9 @@ func TestBlindedPaymentToHints(t *testing.T) { HtlcMaximum: htlcMax, Features: features, } - require.Nil(t, blindedPayment.toRouteHints()) + hints, err := blindedPayment.toRouteHints() + require.NoError(t, err) + require.Nil(t, hints) // Populate the blinded payment with hops. blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{ @@ -146,41 +148,43 @@ func TestBlindedPaymentToHints(t *testing.T) { }, } + policy1 := &models.CachedEdgePolicy{ + TimeLockDelta: cltvDelta, + MinHTLC: lnwire.MilliSatoshi(htlcMin), + MaxHTLC: lnwire.MilliSatoshi(htlcMax), + FeeBaseMSat: lnwire.MilliSatoshi(baseFee), + FeeProportionalMillionths: lnwire.MilliSatoshi( + ppmFee, + ), + ToNodePubKey: func() route.Vertex { + return vb2 + }, + ToNodeFeatures: features, + } + policy2 := &models.CachedEdgePolicy{ + ToNodePubKey: func() route.Vertex { + return vb3 + }, + ToNodeFeatures: features, + } + + blindedEdge1, err := NewBlindedEdge(policy1, blindedPayment, 0) + require.NoError(t, err) + + blindedEdge2, err := NewBlindedEdge(policy2, blindedPayment, 1) + require.NoError(t, err) + expected := RouteHints{ v1: { - //nolint:lll - &BlindedEdge{ - policy: &models.CachedEdgePolicy{ - TimeLockDelta: cltvDelta, - MinHTLC: lnwire.MilliSatoshi(htlcMin), - MaxHTLC: lnwire.MilliSatoshi(htlcMax), - FeeBaseMSat: lnwire.MilliSatoshi(baseFee), - FeeProportionalMillionths: lnwire.MilliSatoshi( - ppmFee, - ), - ToNodePubKey: func() route.Vertex { - return vb2 - }, - ToNodeFeatures: features, - }, - blindingPoint: blindedPoint, - cipherText: cipherText, - }, + blindedEdge1, }, vb2: { - &BlindedEdge{ - policy: &models.CachedEdgePolicy{ - ToNodePubKey: func() route.Vertex { - return vb3 - }, - ToNodeFeatures: features, - }, - cipherText: cipherText, - }, + blindedEdge2, }, } - actual := blindedPayment.toRouteHints() + actual, err := blindedPayment.toRouteHints() + require.NoError(t, err) require.Equal(t, len(expected), len(actual)) for vertex, expectedHint := range expected { diff --git a/routing/pathfind.go b/routing/pathfind.go index fbe4b58308..208a550858 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -96,9 +96,17 @@ type edgePolicyWithSource struct { // such as amounts and cltvs, as well as more complex features like destination // custom records and payment address. type finalHopParams struct { - amt lnwire.MilliSatoshi - totalAmt lnwire.MilliSatoshi - cltvDelta uint16 + amt lnwire.MilliSatoshi + totalAmt lnwire.MilliSatoshi + + // cltvDelta is the final hop's minimum CLTV expiry delta. + // + // NOTE that in the case of paying to a blinded path, this value will + // be set to a duplicate of the blinded path's accumulated CLTV value. + // We would then only need to use this value in the case where the + // introduction node of the path is also the destination node. + cltvDelta uint16 + records record.CustomSet paymentAddr *[32]byte @@ -190,10 +198,21 @@ func newRoute(sourceVertex route.Vertex, // reporting through RPC. Set to zero for the final hop. fee = 0 - // As this is the last hop, we'll use the specified - // final CLTV delta value instead of the value from the - // last link in the route. - totalTimeLock += uint32(finalHop.cltvDelta) + // Only include the final hop CLTV delta in the total + // time lock value if this is not a route to a blinded + // path. For blinded paths, the total time-lock from the + // whole path will be deduced from the introduction + // node's CLTV delta. The exception is for the case + // where the final hop is the blinded path introduction + // node. + if blindedPath == nil || + len(blindedPath.BlindedHops) == 1 { + + // As this is the last hop, we'll use the + // specified final CLTV delta value instead of + // the value from the last link in the route. + totalTimeLock += uint32(finalHop.cltvDelta) + } outgoingTimeLock = totalTimeLock // Attach any custom records to the final hop. @@ -968,6 +987,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, inboundFee, fakeHopHintCapacity, reverseEdge.edge.IntermediatePayloadSize, + reverseEdge.edge.BlindedPayment(), ) } diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 06005716f5..0f2a2659b1 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -3322,16 +3322,19 @@ func TestBlindedRouteConstruction(t *testing.T) { ToNodeFeatures: tlvFeatures, } - // Create final hop parameters for payment amount = 110. Note - // that final cltv delta is not set because blinded paths - // include this final delta in their aggregate delta. A - // sender-set delta may be added to account for block arrival - // during payment, but we do not set it in this test. + // Create final hop parameters for payment amount = 110. totalAmt lnwire.MilliSatoshi = 110 finalHopParams = finalHopParams{ amt: totalAmt, totalAmt: totalAmt, metadata: metadata, + + // We set a CLTV delta here just to test that this will + // be ignored by newRoute since this is a blinded path + // where the accumulated CLTV delta for the route + // communicated in the blinded path should be assumed to + // include the CLTV delta of the final hop. + cltvDelta: MaxCLTVDelta, } ) @@ -3341,7 +3344,9 @@ func TestBlindedRouteConstruction(t *testing.T) { // that make up the graph we'll give to route construction. The hints // map is keyed by source node, so we can retrieve our blinded edges // accordingly. - blindedEdges := blindedPayment.toRouteHints() + blindedEdges, err := blindedPayment.toRouteHints() + require.NoError(t, err) + carolDaveEdge := blindedEdges[carolVertex][0] daveEveEdge := blindedEdges[daveBlindedVertex][0] diff --git a/routing/router.go b/routing/router.go index 851db4af0b..149cd34156 100644 --- a/routing/router.go +++ b/routing/router.go @@ -2001,6 +2001,7 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex, // Assume that we're starting off with a regular payment. requestHints = routeHints requestExpiry = finalExpiry + err error ) if blindedPayment != nil { @@ -2038,7 +2039,10 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex, requestExpiry = blindedPayment.CltvExpiryDelta } - requestHints = blindedPayment.toRouteHints() + requestHints, err = blindedPayment.toRouteHints() + if err != nil { + return nil, err + } } requestTarget, err := getTargetNode(target, blindedPayment) diff --git a/routing/unified_edges.go b/routing/unified_edges.go index 232c92e1c6..d39eda1efd 100644 --- a/routing/unified_edges.go +++ b/routing/unified_edges.go @@ -51,7 +51,8 @@ func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, useInboundFees bool, // incorrectly specified. func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, edge *models.CachedEdgePolicy, inboundFee models.InboundFee, - capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc) { + capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc, + blindedPayment *BlindedPayment) { localChan := fromNode == u.sourceNode @@ -86,12 +87,9 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, inboundFee = models.InboundFee{} } - unifier.edges = append(unifier.edges, &unifiedEdge{ - policy: edge, - capacity: capacity, - hopPayloadSizeFn: hopPayloadSizeFn, - inboundFees: inboundFee, - }) + unifier.edges = append(unifier.edges, newUnifiedEdge( + edge, capacity, inboundFee, hopPayloadSizeFn, blindedPayment, + )) } // addGraphPolicies adds all policies that are known for the toNode in the @@ -115,7 +113,7 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error { u.addPolicy( channel.OtherNode, channel.InPolicy, inboundFee, - channel.Capacity, defaultHopPayloadSize, + channel.Capacity, defaultHopPayloadSize, nil, ) return nil @@ -137,6 +135,24 @@ type unifiedEdge struct { // is needed because hops of a blinded path differ in their payload // structure compared to cleartext hops. hopPayloadSizeFn PayloadSizeFunc + + // blindedPayment if set, is the BlindedPayment that this edge was + // derived from originally. + blindedPayment *BlindedPayment +} + +// newUnifiedEdge constructs a new unifiedEdge. +func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount, + inboundFees models.InboundFee, hopPayloadSizeFn PayloadSizeFunc, + blindedPayment *BlindedPayment) *unifiedEdge { + + return &unifiedEdge{ + policy: policy, + capacity: capacity, + inboundFees: inboundFees, + hopPayloadSizeFn: hopPayloadSizeFn, + blindedPayment: blindedPayment, + } } // amtInRange checks whether an amount falls within the valid range for a @@ -292,12 +308,10 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi, maxBandwidth = bandwidth // Update best edge. - bestEdge = &unifiedEdge{ - policy: edge.policy, - capacity: edge.capacity, - hopPayloadSizeFn: edge.hopPayloadSizeFn, - inboundFees: edge.inboundFees, - } + bestEdge = newUnifiedEdge( + edge.policy, edge.capacity, edge.inboundFees, + edge.hopPayloadSizeFn, edge.blindedPayment, + ) } return bestEdge @@ -376,10 +390,10 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, } maxFee = fee - bestPolicy = &unifiedEdge{ - policy: edge.policy, - inboundFees: edge.inboundFees, - } + bestPolicy = newUnifiedEdge( + edge.policy, 0, edge.inboundFees, nil, + edge.blindedPayment, + ) // The payload size function for edges to a connected peer is // always the same hence there is not need to find the maximum. @@ -404,15 +418,13 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, // chance for this node pair. But this is all only needed for nodes that // have distinct policies for channels to the same peer. policyCopy := *bestPolicy.policy - modifiedEdge := unifiedEdge{ - policy: &policyCopy, - inboundFees: bestPolicy.inboundFees, - } - modifiedEdge.policy.TimeLockDelta = maxTimelock - modifiedEdge.capacity = maxCapMsat.ToSatoshis() - modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn + policyCopy.TimeLockDelta = maxTimelock + modifiedEdge := newUnifiedEdge( + &policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees, + hopPayloadSizeFn, bestPolicy.blindedPayment, + ) - return &modifiedEdge + return modifiedEdge } // minAmt returns the minimum amount that can be forwarded on this connection. diff --git a/routing/unified_edges_test.go b/routing/unified_edges_test.go index 7b1650c025..82605e9b37 100644 --- a/routing/unified_edges_test.go +++ b/routing/unified_edges_test.go @@ -59,37 +59,37 @@ func TestNodeEdgeUnifier(t *testing.T) { unifierFilled := newNodeEdgeUnifier(source, toNode, false, nil) unifierFilled.addPolicy( - fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil, ) unifierFilled.addPolicy( - fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, + fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, nil, ) unifierNoCapacity := newNodeEdgeUnifier(source, toNode, false, nil) unifierNoCapacity.addPolicy( - fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize, nil, ) unifierNoCapacity.addPolicy( - fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize, + fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize, nil, ) unifierNoInfo := newNodeEdgeUnifier(source, toNode, false, nil) unifierNoInfo.addPolicy( fromNode, &models.CachedEdgePolicy{}, models.InboundFee{}, - 0, defaultHopPayloadSize, + 0, defaultHopPayloadSize, nil, ) unifierInboundFee := newNodeEdgeUnifier(source, toNode, true, nil) unifierInboundFee.addPolicy( - fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil, ) unifierInboundFee.addPolicy( - fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, + fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, nil, ) unifierLocal := newNodeEdgeUnifier(fromNode, toNode, true, nil) unifierLocal.addPolicy( - fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil, ) inboundFeeZero := models.InboundFee{} @@ -98,10 +98,11 @@ func TestNodeEdgeUnifier(t *testing.T) { } unifierNegInboundFee := newNodeEdgeUnifier(source, toNode, true, nil) unifierNegInboundFee.addPolicy( - fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize, nil, ) unifierNegInboundFee.addPolicy( fromNode, &p2, inboundFeeNegative, c2, defaultHopPayloadSize, + nil, ) tests := []struct { diff --git a/rpcserver.go b/rpcserver.go index 4cd0fc4587..a306d4fd29 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6971,6 +6971,13 @@ func (r *rpcServer) DecodePayReq(ctx context.Context, // Convert between the `lnrpc` and `routing` types. routeHints := invoicesrpc.CreateRPCRouteHints(payReq.RouteHints) + blindedPaymentPaths, err := invoicesrpc.CreateRPCBlindedPayments( + payReq.BlindedPaymentPaths, + ) + if err != nil { + return nil, err + } + var amtSat, amtMsat int64 if payReq.MilliSat != nil { amtSat = int64(payReq.MilliSat.ToSatoshis()) @@ -6996,6 +7003,7 @@ func (r *rpcServer) DecodePayReq(ctx context.Context, Expiry: expiry, CltvExpiry: int64(payReq.MinFinalCLTVExpiry()), RouteHints: routeHints, + BlindedPaths: blindedPaymentPaths, PaymentAddr: paymentAddr, Features: invoicesrpc.CreateRPCFeatures(payReq.Features), }, nil diff --git a/zpay32/blinded_path.go b/zpay32/blinded_path.go new file mode 100644 index 0000000000..128a05e4ba --- /dev/null +++ b/zpay32/blinded_path.go @@ -0,0 +1,246 @@ +package zpay32 + +import ( + "encoding/binary" + "fmt" + "io" + "math" + + "github.com/btcsuite/btcd/btcec/v2" + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // relayInfoSize is the number of bytes that the relay info of a blinded + // payment will occupy. + // base fee: 4 bytes + // prop fee: 4 bytes + // cltv delta: 2 bytes + // min htlc: 8 bytes + // max htlc: 8 bytes + relayInfoSize = 26 + + // maxNumHopsPerPath is the maximum number of blinded path hops that can + // be included in a single encoded blinded path. This is calculated + // based on the `data_length` limit of 638 bytes for any tagged field in + // a BOLT 11 invoice along with the estimated number of bytes required + // for encoding the most minimal blinded path hop. See the [bLIP + // proposal](https://github.com/lightning/blips/pull/39) for a detailed + // calculation. + maxNumHopsPerPath = 7 +) + +// BlindedPaymentPath holds all the information a payer needs to know about a +// blinded path to a receiver of a payment. +type BlindedPaymentPath struct { + // FeeBaseMsat is the total base fee for the path in milli-satoshis. + FeeBaseMsat uint32 + + // FeeRate is the total fee rate for the path in parts per million. + FeeRate uint32 + + // CltvExpiryDelta is the total CLTV delta to apply to the path. + CltvExpiryDelta uint16 + + // HTLCMinMsat is the minimum number of milli-satoshis that any hop in + // the path will route. + HTLCMinMsat uint64 + + // HTLCMaxMsat is the maximum number of milli-satoshis that a hop in the + // path will route. + HTLCMaxMsat uint64 + + // Features is the feature bit vector for the path. + Features *lnwire.FeatureVector + + // FirstEphemeralBlindingPoint is the blinding point to send to the + // introduction node. It will be used by the introduction node to derive + // a shared secret with the receiver which can then be used to decode + // the encrypted payload from the receiver. + FirstEphemeralBlindingPoint *btcec.PublicKey + + // Hops is the blinded path. The first hop is the introduction node and + // so the BlindedNodeID of this hop will be the real node ID. + Hops []*sphinx.BlindedHopInfo +} + +// DecodeBlindedPayment attempts to parse a BlindedPaymentPath from the passed +// reader. +func DecodeBlindedPayment(r io.Reader) (*BlindedPaymentPath, error) { + var relayInfo [relayInfoSize]byte + n, err := r.Read(relayInfo[:]) + if err != nil { + return nil, err + } + if n != relayInfoSize { + return nil, fmt.Errorf("unable to read %d relay info bytes "+ + "off of the given stream: %w", relayInfoSize, err) + } + + var payment BlindedPaymentPath + + // Parse the relay info fields. + payment.FeeBaseMsat = binary.BigEndian.Uint32(relayInfo[:4]) + payment.FeeRate = binary.BigEndian.Uint32(relayInfo[4:8]) + payment.CltvExpiryDelta = binary.BigEndian.Uint16(relayInfo[8:10]) + payment.HTLCMinMsat = binary.BigEndian.Uint64(relayInfo[10:18]) + payment.HTLCMaxMsat = binary.BigEndian.Uint64(relayInfo[18:]) + + // Parse the feature bit vector. + f := lnwire.EmptyFeatureVector() + err = f.Decode(r) + if err != nil { + return nil, err + } + payment.Features = f + + // Parse the first ephemeral blinding point. + var blindingPointBytes [btcec.PubKeyBytesLenCompressed]byte + _, err = r.Read(blindingPointBytes[:]) + if err != nil { + return nil, err + } + + blinding, err := btcec.ParsePubKey(blindingPointBytes[:]) + if err != nil { + return nil, err + } + payment.FirstEphemeralBlindingPoint = blinding + + // Read the one byte hop number. + var numHops [1]byte + _, err = r.Read(numHops[:]) + if err != nil { + return nil, err + } + + payment.Hops = make([]*sphinx.BlindedHopInfo, int(numHops[0])) + + // Parse each hop. + for i := 0; i < len(payment.Hops); i++ { + hop, err := DecodeBlindedHop(r) + if err != nil { + return nil, err + } + + payment.Hops[i] = hop + } + + return &payment, nil +} + +// Encode serialises the BlindedPaymentPath and writes the bytes to the passed +// writer. +// 1) The first 26 bytes contain the relay info: +// - Base Fee in msat: uint32 (4 bytes). +// - Proportional Fee in PPM: uint32 (4 bytes). +// - CLTV expiry delta: uint16 (2 bytes). +// - HTLC min msat: uint64 (8 bytes). +// - HTLC max msat: uint64 (8 bytes). +// +// 2) Feature bit vector length (2 bytes). +// 3) Feature bit vector (can be zero length). +// 4) First blinding point: 33 bytes. +// 5) Number of hops: 1 byte. +// 6) Encoded BlindedHops. +func (p *BlindedPaymentPath) Encode(w io.Writer) error { + var relayInfo [26]byte + binary.BigEndian.PutUint32(relayInfo[:4], p.FeeBaseMsat) + binary.BigEndian.PutUint32(relayInfo[4:8], p.FeeRate) + binary.BigEndian.PutUint16(relayInfo[8:10], p.CltvExpiryDelta) + binary.BigEndian.PutUint64(relayInfo[10:18], p.HTLCMinMsat) + binary.BigEndian.PutUint64(relayInfo[18:], p.HTLCMaxMsat) + + _, err := w.Write(relayInfo[:]) + if err != nil { + return err + } + + err = p.Features.Encode(w) + if err != nil { + return err + } + + _, err = w.Write(p.FirstEphemeralBlindingPoint.SerializeCompressed()) + if err != nil { + return err + } + + numHops := len(p.Hops) + if numHops > maxNumHopsPerPath { + return fmt.Errorf("the number of hops, %d, exceeds the "+ + "maximum of %d", numHops, maxNumHopsPerPath) + } + + _, err = w.Write([]byte{byte(numHops)}) + if err != nil { + return err + } + + for _, hop := range p.Hops { + err = EncodeBlindedHop(w, hop) + if err != nil { + return err + } + } + + return nil +} + +// DecodeBlindedHop reads a sphinx.BlindedHopInfo from the passed reader. +func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) { + var nodeIDBytes [btcec.PubKeyBytesLenCompressed]byte + _, err := r.Read(nodeIDBytes[:]) + if err != nil { + return nil, err + } + + nodeID, err := btcec.ParsePubKey(nodeIDBytes[:]) + if err != nil { + return nil, err + } + + dataLen, err := tlv.ReadVarInt(r, &[8]byte{}) + if err != nil { + return nil, err + } + + encryptedData := make([]byte, dataLen) + _, err = r.Read(encryptedData) + if err != nil { + return nil, err + } + + return &sphinx.BlindedHopInfo{ + BlindedNodePub: nodeID, + CipherText: encryptedData, + }, nil +} + +// EncodeBlindedHop writes the passed BlindedHopInfo to the given writer. +// +// 1) Blinded node pub key: 33 bytes +// 2) Cipher text length: BigSize +// 3) Cipher text. +func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error { + _, err := w.Write(hop.BlindedNodePub.SerializeCompressed()) + if err != nil { + return err + } + + if len(hop.CipherText) > math.MaxUint16 { + return fmt.Errorf("encrypted recipient data can not exceed a "+ + "length of %d bytes", math.MaxUint16) + } + + err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{}) + if err != nil { + return err + } + + _, err = w.Write(hop.CipherText) + + return err +} diff --git a/zpay32/decode.go b/zpay32/decode.go index d37a34cf9d..59bfccf001 100644 --- a/zpay32/decode.go +++ b/zpay32/decode.go @@ -215,6 +215,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.PaymentHash, err = parse32Bytes(base32Data) + case fieldTypeS: if invoice.PaymentAddr != nil { // We skip the field if we have already seen a @@ -223,6 +224,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.PaymentAddr, err = parse32Bytes(base32Data) + case fieldTypeD: if invoice.Description != nil { // We skip the field if we have already seen a @@ -231,6 +233,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.Description, err = parseDescription(base32Data) + case fieldTypeM: if invoice.Metadata != nil { // We skip the field if we have already seen a @@ -248,6 +251,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.Destination, err = parseDestination(base32Data) + case fieldTypeH: if invoice.DescriptionHash != nil { // We skip the field if we have already seen a @@ -256,6 +260,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.DescriptionHash, err = parse32Bytes(base32Data) + case fieldTypeX: if invoice.expiry != nil { // We skip the field if we have already seen a @@ -264,6 +269,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.expiry, err = parseExpiry(base32Data) + case fieldTypeC: if invoice.minFinalCLTVExpiry != nil { // We skip the field if we have already seen a @@ -271,7 +277,9 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er continue } - invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data) + invoice.minFinalCLTVExpiry, err = + parseMinFinalCLTVExpiry(base32Data) + case fieldTypeF: if invoice.FallbackAddr != nil { // We skip the field if we have already seen a @@ -279,7 +287,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er continue } - invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net) + invoice.FallbackAddr, err = parseFallbackAddr( + base32Data, net, + ) + case fieldTypeR: // An `r` field can be included in an invoice multiple // times, so we won't skip it if we have already seen @@ -289,7 +300,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er return err } - invoice.RouteHints = append(invoice.RouteHints, routeHint) + invoice.RouteHints = append( + invoice.RouteHints, routeHint, + ) + case fieldType9: if invoice.Features != nil { // We skip the field if we have already seen a @@ -298,6 +312,19 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.Features, err = parseFeatures(base32Data) + + case fieldTypeB: + blindedPaymentPath, err := parseBlindedPaymentPath( + base32Data, + ) + if err != nil { + return err + } + + invoice.BlindedPaymentPaths = append( + invoice.BlindedPaymentPaths, blindedPaymentPath, + ) + default: // Ignore unknown type. } @@ -495,6 +522,17 @@ func parseRouteHint(data []byte) ([]HopHint, error) { return routeHint, nil } +// parseBlindedPaymentPath attempts to parse a BlindedPaymentPath from the given +// byte slice. +func parseBlindedPaymentPath(data []byte) (*BlindedPaymentPath, error) { + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + return DecodeBlindedPayment(bytes.NewReader(base256Data)) +} + // parseFeatures decodes any feature bits directly from the base32 // representation. func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { diff --git a/zpay32/encode.go b/zpay32/encode.go index bf544d062e..130abdcfd5 100644 --- a/zpay32/encode.go +++ b/zpay32/encode.go @@ -260,6 +260,29 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { } } + for _, path := range invoice.BlindedPaymentPaths { + var buf bytes.Buffer + + err := path.Encode(&buf) + if err != nil { + return err + } + + blindedPathBase32, err := bech32.ConvertBits( + buf.Bytes(), 8, 5, true, + ) + if err != nil { + return err + } + + err = writeTaggedField( + bufferBase32, fieldTypeB, blindedPathBase32, + ) + if err != nil { + return err + } + } + if invoice.Destination != nil { // Convert 33 byte pubkey to 53 5-bit groups. pubKeyBase32, err := bech32.ConvertBits( diff --git a/zpay32/invoice.go b/zpay32/invoice.go index f23992ec9b..8b1457ab43 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -76,6 +76,10 @@ const ( // probing the recipient. fieldTypeS = 16 + // fieldTypeB contains blinded payment path information. This field may + // be repeated to include multiple blinded payment paths in the invoice. + fieldTypeB = 20 + // maxInvoiceLength is the maximum total length an invoice can have. // This is chosen to be the maximum number of bytes that can fit into a // single QR code: https://en.wikipedia.org/wiki/QR_code#Storage @@ -152,6 +156,10 @@ type Invoice struct { // This field is un-exported and can only be read by the // MinFinalCLTVExpiry() method. By forcing callers to read via this // method, we can easily enforce the default if not specified. + // + // NOTE: this field is ignored in the case that the invoice contains + // blinded paths since then the final minimum cltv expiry delta is + // expected to be included in the route's accumulated CLTV delta value. minFinalCLTVExpiry *uint64 // Description is a short description of the purpose of this invoice. @@ -180,9 +188,17 @@ type Invoice struct { // hint can be individually used to reach the destination. These usually // represent private routes. // - // NOTE: This is optional. + // NOTE: This is optional and should not be set at the same time as + // BlindedPaymentPaths. RouteHints [][]HopHint + // BlindedPaymentPaths is a set of blinded payment paths that can be + // used to find the payment receiver. + // + // NOTE: This is optional and should not be set at the same time as + // RouteHints. + BlindedPaymentPaths []*BlindedPaymentPath + // Features represents an optional field used to signal optional or // required support for features by the receiver. Features *lnwire.FeatureVector @@ -263,6 +279,15 @@ func RouteHint(routeHint []HopHint) func(*Invoice) { } } +// WithBlindedPaymentPath is a functional option that allows a caller of +// NewInvoice to attach a blinded payment path to the invoice. The option can +// be used multiple times to attach multiple paths. +func WithBlindedPaymentPath(p *BlindedPaymentPath) func(*Invoice) { + return func(i *Invoice) { + i.BlindedPaymentPaths = append(i.BlindedPaymentPaths, p) + } +} + // Features is a functional option that allows callers of NewInvoice to set the // desired feature bits that are advertised on the invoice. If this option is // not used, an empty feature vector will automatically be populated. @@ -355,6 +380,13 @@ func validateInvoice(invoice *Invoice) error { return fmt.Errorf("no payment hash found") } + if len(invoice.RouteHints) != 0 && + len(invoice.BlindedPaymentPaths) != 0 { + + return fmt.Errorf("cannot have both route hints and blinded " + + "payment paths") + } + // Either Description or DescriptionHash must be set, not both. if invoice.Description != nil && invoice.DescriptionHash != nil { return fmt.Errorf("both description and description hash set") diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index a360ed2a07..006b4fb6d7 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -7,7 +7,6 @@ import ( "bytes" "encoding/hex" "fmt" - "reflect" "strings" "testing" "time" @@ -17,7 +16,9 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" ) var ( @@ -116,6 +117,62 @@ var ( // Must be initialized in init(). testDescriptionHash [32]byte + + testBlindedPK1Bytes, _ = hex.DecodeString("03f3311e948feb5115242c4e39" + + "6c81c448ab7ee5fd24c4e24e66c73533cc4f98b8") + testBlindedHopPK1, _ = btcec.ParsePubKey(testBlindedPK1Bytes) + testBlindedPK2Bytes, _ = hex.DecodeString("03a8c97ed5cd40d474e4ef18c8" + + "99854b25e5070106504cb225e6d2c112d61a805e") + testBlindedHopPK2, _ = btcec.ParsePubKey(testBlindedPK2Bytes) + testBlindedPK3Bytes, _ = hex.DecodeString("0220293926219d8efe733336e2" + + "b674570dd96aa763acb3564e6e367b384d861a0a") + testBlindedHopPK3, _ = btcec.ParsePubKey(testBlindedPK3Bytes) + testBlindedPK4Bytes, _ = hex.DecodeString("02c75eb336a038294eaaf76015" + + "8b2e851c3c0937262e35401ae64a1bee71a2e40c") + testBlindedHopPK4, _ = btcec.ParsePubKey(testBlindedPK4Bytes) + + blindedPath1 = &BlindedPaymentPath{ + FeeBaseMsat: 40, + FeeRate: 20, + CltvExpiryDelta: 130, + HTLCMinMsat: 2, + HTLCMaxMsat: 100, + Features: lnwire.EmptyFeatureVector(), + FirstEphemeralBlindingPoint: testBlindedHopPK1, + Hops: []*sphinx.BlindedHopInfo{ + { + BlindedNodePub: testBlindedHopPK2, + CipherText: []byte{1, 2, 3, 4, 5}, + }, + { + BlindedNodePub: testBlindedHopPK3, + CipherText: []byte{5, 4, 3, 2, 1}, + }, + { + BlindedNodePub: testBlindedHopPK4, + CipherText: []byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, + }, + }, + }, + } + + blindedPath2 = &BlindedPaymentPath{ + FeeBaseMsat: 4, + FeeRate: 2, + CltvExpiryDelta: 10, + HTLCMinMsat: 0, + HTLCMaxMsat: 10, + Features: lnwire.EmptyFeatureVector(), + FirstEphemeralBlindingPoint: testBlindedHopPK4, + Hops: []*sphinx.BlindedHopInfo{ + { + BlindedNodePub: testBlindedHopPK3, + CipherText: []byte{1, 2, 3, 4, 5}, + }, + }, + } ) func init() { @@ -125,6 +182,8 @@ func init() { // TestDecodeEncode tests that an encoded invoice gets decoded into the expected // Invoice object, and that reencoding the decoded invoice gets us back to the // original encoded string. +// +//nolint:lll func TestDecodeEncode(t *testing.T) { t.Parallel() @@ -673,52 +732,77 @@ func TestDecodeEncode(t *testing.T) { i.Destination = nil }, }, + { + // Invoice with blinded payment paths. + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f", + valid: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + MilliSat: &testMillisat20mBTC, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCupOfCoffee, + Destination: testPubKey, + Features: emptyFeatures, + BlindedPaymentPaths: []*BlindedPaymentPath{ + blindedPath1, + blindedPath2, + }, + } + }, + beforeEncoding: func(i *Invoice) { + // Since this destination pubkey was recovered + // from the signature, we must set it nil before + // encoding to get back the same invoice string. + i.Destination = nil + }, + }, } for i, test := range tests { - var decodedInvoice *Invoice - net := &chaincfg.MainNetParams - if test.decodedInvoice != nil { - decodedInvoice = test.decodedInvoice() - net = decodedInvoice.Net - } + test := test - invoice, err := Decode(test.encodedInvoice, net) - if (err == nil) != test.valid { - t.Errorf("Decoding test %d failed: %v", i, err) - return - } + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() - if test.valid { - if err := compareInvoices(decodedInvoice, invoice); err != nil { - t.Errorf("Invoice decoding result %d not as expected: %v", i, err) + var decodedInvoice *Invoice + net := &chaincfg.MainNetParams + if test.decodedInvoice != nil { + decodedInvoice = test.decodedInvoice() + net = decodedInvoice.Net + } + + invoice, err := Decode(test.encodedInvoice, net) + if !test.valid { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, decodedInvoice, invoice) + } + + if test.skipEncoding { return } - } - if test.skipEncoding { - continue - } + if test.beforeEncoding != nil { + test.beforeEncoding(decodedInvoice) + } - if test.beforeEncoding != nil { - test.beforeEncoding(decodedInvoice) - } + if decodedInvoice == nil { + return + } - if decodedInvoice != nil { reencoded, err := decodedInvoice.Encode( testMessageSigner, ) - if (err == nil) != test.valid { - t.Errorf("Encoding test %d failed: %v", i, err) + if !test.valid { + require.Error(t, err) return } - - if test.valid && test.encodedInvoice != reencoded { - t.Errorf("Encoding %d failed, expected %v, got %v", - i, test.encodedInvoice, reencoded) - return - } - } + require.NoError(t, err) + require.Equal(t, test.encodedInvoice, reencoded) + }) } } @@ -805,25 +889,42 @@ func TestNewInvoice(t *testing.T) { valid: true, encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66df5c8pqjjt4z4ymmuaxfx8eh5v7hmzs3wrfas8m2sz5qz56rw2lxy8mmgm4xln0ha26qkw6u3vhu22pss2udugr9g74c3x20slpcqjgq0el4h6", }, + { + // Mainnet invoice with two blinded paths. + newInvoice: func() (*Invoice, error) { + return NewInvoice(&chaincfg.MainNetParams, + testPaymentHash, + time.Unix(1496314658, 0), + Amount(testMillisat20mBTC), + Description(testCupOfCoffee), + WithBlindedPaymentPath(blindedPath1), + WithBlindedPaymentPath(blindedPath2), + ) + }, + valid: true, + //nolint:lll + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f", + }, } for i, test := range tests { + test := test - invoice, err := test.newInvoice() - if err != nil && !test.valid { - continue - } - encoded, err := invoice.Encode(testMessageSigner) - if (err == nil) != test.valid { - t.Errorf("NewInvoice test %d failed: %v", i, err) - return - } + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() - if test.valid && test.encodedInvoice != encoded { - t.Errorf("Encoding %d failed, expected %v, got %v", - i, test.encodedInvoice, encoded) - return - } + invoice, err := test.newInvoice() + if !test.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + + encoded, err := invoice.Encode(testMessageSigner) + require.NoError(t, err) + + require.Equal(t, test.encodedInvoice, encoded) + }) } } @@ -909,73 +1010,6 @@ func TestInvoiceChecksumMalleability(t *testing.T) { } } -func compareInvoices(expected, actual *Invoice) error { - if !reflect.DeepEqual(expected.Net, actual.Net) { - return fmt.Errorf("expected net %v, got %v", - expected.Net, actual.Net) - } - - if !reflect.DeepEqual(expected.MilliSat, actual.MilliSat) { - return fmt.Errorf("expected milli sat %d, got %d", - *expected.MilliSat, *actual.MilliSat) - } - - if expected.Timestamp != actual.Timestamp { - return fmt.Errorf("expected timestamp %v, got %v", - expected.Timestamp, actual.Timestamp) - } - - if !compareHashes(expected.PaymentHash, actual.PaymentHash) { - return fmt.Errorf("expected payment hash %x, got %x", - *expected.PaymentHash, *actual.PaymentHash) - } - - if !reflect.DeepEqual(expected.Description, actual.Description) { - return fmt.Errorf("expected description \"%s\", got \"%s\"", - *expected.Description, *actual.Description) - } - - if !comparePubkeys(expected.Destination, actual.Destination) { - return fmt.Errorf("expected destination pubkey %x, got %x", - expected.Destination.SerializeCompressed(), - actual.Destination.SerializeCompressed()) - } - - if !compareHashes(expected.DescriptionHash, actual.DescriptionHash) { - return fmt.Errorf("expected description hash %x, got %x", - *expected.DescriptionHash, *actual.DescriptionHash) - } - - if expected.Expiry() != actual.Expiry() { - return fmt.Errorf("expected expiry %d, got %d", - expected.Expiry(), actual.Expiry()) - } - - if !reflect.DeepEqual(expected.FallbackAddr, actual.FallbackAddr) { - return fmt.Errorf("expected FallbackAddr %v, got %v", - expected.FallbackAddr, actual.FallbackAddr) - } - - if len(expected.RouteHints) != len(actual.RouteHints) { - return fmt.Errorf("expected %d RouteHints, got %d", - len(expected.RouteHints), len(actual.RouteHints)) - } - - for i, routeHint := range expected.RouteHints { - err := compareRouteHints(routeHint, actual.RouteHints[i]) - if err != nil { - return err - } - } - - if !reflect.DeepEqual(expected.Features, actual.Features) { - return fmt.Errorf("expected features %v, got %v", - expected.Features, actual.Features) - } - - return nil -} - func comparePubkeys(a, b *btcec.PublicKey) bool { if a == b { return true