diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index aa0fbe2a337..15dc176623d 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -232,6 +232,10 @@ map is returned. When `include_log` is set to `true`, the log file content is also included in the response. +* [Add `source_pub_key` to `Route` proto message](https://github.com/lightningnetwork/lnd/pull/9153) + so that routes can be constructed and unmarshalled from the perspective of + different nodes. Defaults to the node's own public key. + ## lncli Updates * The `getdebuginfo` command now supports an `--include_log` flag. By default, diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 86305faf378..6ab522ef0ed 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -10719,8 +10719,11 @@ type Route struct { FirstHopAmountMsat int64 `protobuf:"varint,7,opt,name=first_hop_amount_msat,json=firstHopAmountMsat,proto3" json:"first_hop_amount_msat,omitempty"` // Custom channel data that might be populated in custom channels. CustomChannelData []byte `protobuf:"bytes,8,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The source node from whose perspective the route was built. Defaults to + // the node's own public key. + SourcePubKey string `protobuf:"bytes,9,opt,name=source_pub_key,json=sourcePubKey,proto3" json:"source_pub_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Route) Reset() { @@ -10811,6 +10814,13 @@ func (x *Route) GetCustomChannelData() []byte { return nil } +func (x *Route) GetSourcePubKey() string { + if x != nil { + return x.SourcePubKey + } + return "" +} + type NodeInfoRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The 33-byte hex-encoded compressed public of the target node @@ -19622,7 +19632,7 @@ const file_lightning_proto_rawDesc = "" + "root_share\x18\x01 \x01(\fR\trootShare\x12\x15\n" + "\x06set_id\x18\x02 \x01(\fR\x05setId\x12\x1f\n" + "\vchild_index\x18\x03 \x01(\rR\n" + - "childIndex\"\xc4\x02\n" + + "childIndex\"\xea\x02\n" + "\x05Route\x12&\n" + "\x0ftotal_time_lock\x18\x01 \x01(\rR\rtotalTimeLock\x12!\n" + "\n" + @@ -19633,7 +19643,8 @@ const file_lightning_proto_rawDesc = "" + "\x0ftotal_fees_msat\x18\x05 \x01(\x03R\rtotalFeesMsat\x12$\n" + "\x0etotal_amt_msat\x18\x06 \x01(\x03R\ftotalAmtMsat\x121\n" + "\x15first_hop_amount_msat\x18\a \x01(\x03R\x12firstHopAmountMsat\x12.\n" + - "\x13custom_channel_data\x18\b \x01(\fR\x11customChannelData\"\x83\x01\n" + + "\x13custom_channel_data\x18\b \x01(\fR\x11customChannelData\x12$\n" + + "\x0esource_pub_key\x18\t \x01(\tR\fsourcePubKey\"\x83\x01\n" + "\x0fNodeInfoRequest\x12\x17\n" + "\apub_key\x18\x01 \x01(\tR\x06pubKey\x12)\n" + "\x10include_channels\x18\x02 \x01(\bR\x0fincludeChannels\x12,\n" + diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 92ea1526074..127001eefc0 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -3554,6 +3554,12 @@ message Route { Custom channel data that might be populated in custom channels. */ bytes custom_channel_data = 8; + + /* + The source node from whose perspective the route was built. Defaults to + the node's own public key. + */ + string source_pub_key = 9; } message NodeInfoRequest { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index 75b05525d49..983e844dbd8 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -7529,6 +7529,10 @@ "type": "string", "format": "byte", "description": "Custom channel data that might be populated in custom channels." + }, + "source_pub_key": { + "type": "string", + "description": "The source node from whose perspective the route was built. Defaults to\nthe node's own public key." } }, "description": "A path through the channel graph which runs over one or more channels in\nsuccession. This struct carries all the information required to craft the\nSphinx onion packet, and send the payment along the first hop in the path. A\nroute is only selected as valid if all the channels have sufficient capacity to\ncarry the initial payment amount after fees are accounted for." diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index ff449656757..db21b46411c 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -2151,8 +2151,11 @@ type BuildRouteRequest struct { // the custom range >= 65536. When using REST, the values must be encoded as // base64. FirstHopCustomRecords map[uint64][]byte `protobuf:"bytes,6,rep,name=first_hop_custom_records,json=firstHopCustomRecords,proto3" json:"first_hop_custom_records,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // An optional source node public key from whose perspective the route is to be + // built. If empty, the router's identity key is assumed. + SourcePubKey []byte `protobuf:"bytes,7,opt,name=source_pub_key,json=sourcePubKey,proto3" json:"source_pub_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BuildRouteRequest) Reset() { @@ -2227,6 +2230,13 @@ func (x *BuildRouteRequest) GetFirstHopCustomRecords() map[uint64][]byte { return nil } +func (x *BuildRouteRequest) GetSourcePubKey() []byte { + if x != nil { + return x.SourcePubKey + } + return nil +} + type BuildRouteResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Fully specified route that can be used to execute the payment. @@ -3752,7 +3762,7 @@ const file_routerrpc_router_proto_rawDesc = "" + "\bamt_msat\x18\x03 \x01(\x03R\aamtMsat\"k\n" + "\x18QueryProbabilityResponse\x12 \n" + "\vprobability\x18\x01 \x01(\x01R\vprobability\x12-\n" + - "\ahistory\x18\x02 \x01(\v2\x13.routerrpc.PairDataR\ahistory\"\x86\x03\n" + + "\ahistory\x18\x02 \x01(\v2\x13.routerrpc.PairDataR\ahistory\"\xac\x03\n" + "\x11BuildRouteRequest\x12\x19\n" + "\bamt_msat\x18\x01 \x01(\x03R\aamtMsat\x12(\n" + "\x10final_cltv_delta\x18\x02 \x01(\x05R\x0efinalCltvDelta\x12,\n" + @@ -3760,7 +3770,8 @@ const file_routerrpc_router_proto_rawDesc = "" + "\vhop_pubkeys\x18\x04 \x03(\fR\n" + "hopPubkeys\x12!\n" + "\fpayment_addr\x18\x05 \x01(\fR\vpaymentAddr\x12p\n" + - "\x18first_hop_custom_records\x18\x06 \x03(\v27.routerrpc.BuildRouteRequest.FirstHopCustomRecordsEntryR\x15firstHopCustomRecords\x1aH\n" + + "\x18first_hop_custom_records\x18\x06 \x03(\v27.routerrpc.BuildRouteRequest.FirstHopCustomRecordsEntryR\x15firstHopCustomRecords\x12$\n" + + "\x0esource_pub_key\x18\a \x01(\fR\fsourcePubKey\x1aH\n" + "\x1aFirstHopCustomRecordsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"8\n" + diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 72fe02059e6..180bc91dcdc 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -761,6 +761,12 @@ message BuildRouteRequest { base64. */ map first_hop_custom_records = 6; + + /* + An optional source node public key from whose perspective the route is to be + built. If empty, the router's identity key is assumed. + */ + bytes source_pub_key = 7; } message BuildRouteResponse { diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json index 59aa426dd1b..cf6e1483075 100644 --- a/lnrpc/routerrpc/router.swagger.json +++ b/lnrpc/routerrpc/router.swagger.json @@ -1211,6 +1211,10 @@ "type": "string", "format": "byte", "description": "Custom channel data that might be populated in custom channels." + }, + "source_pub_key": { + "type": "string", + "description": "The source node from whose perspective the route was built. Defaults to\nthe node's own public key." } }, "description": "A path through the channel graph which runs over one or more channels in\nsuccession. This struct carries all the information required to craft the\nSphinx onion packet, and send the payment along the first hop in the path. A\nroute is only selected as valid if all the channels have sufficient capacity to\ncarry the initial payment amount after fees are accounted for." @@ -1344,6 +1348,11 @@ "format": "byte" }, "description": "An optional field that can be used to pass an arbitrary set of TLV records\nto the first hop peer of this payment. This can be used to pass application\nspecific data during the payment attempt. Record types are required to be in\nthe custom range \u003e= 65536. When using REST, the values must be encoded as\nbase64." + }, + "source_pub_key": { + "type": "string", + "format": "byte", + "description": "An optional source node public key from whose perspective the route is to be\nbuilt. If empty, the router's identity key is assumed." } } }, diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 62e98d5595b..d4f979cd48d 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -614,6 +614,7 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error) TotalAmtMsat: int64(route.TotalAmount), Hops: make([]*lnrpc.Hop, len(route.Hops)), FirstHopAmountMsat: int64(route.FirstHopAmount.Val.Int()), + SourcePubKey: route.SourcePubKey.String(), } // Encode the route's custom channel data (if available). @@ -808,7 +809,25 @@ func (r *RouterBackend) UnmarshallHop(rpcHop *lnrpc.Hop, func (r *RouterBackend) UnmarshallRoute(rpcroute *lnrpc.Route) ( *route.Route, error) { - prevNodePubKey := r.SelfNode + var err error + + // Most routes we construct are from our node's perspective. + sourcePubKey := r.SelfNode + + // Optionally override with supplied public key. + if rpcroute.SourcePubKey != "" { + sourcePubKey, err = route.NewVertexFromStr( + rpcroute.SourcePubKey, + ) + if err != nil { + return nil, fmt.Errorf("invalid source pubkey: %w", err) + } + } + + // The previous node starts as the source of the route. This is + // used for implicit hop pubkey resolution when hops only specify + // a channel ID. + prevNodePubKey := sourcePubKey hops := make([]*route.Hop, len(rpcroute.Hops)) for i, hop := range rpcroute.Hops { @@ -818,14 +837,13 @@ func (r *RouterBackend) UnmarshallRoute(rpcroute *lnrpc.Route) ( } hops[i] = routeHop - prevNodePubKey = routeHop.PubKeyBytes } route, err := route.NewRouteFromHops( lnwire.MilliSatoshi(rpcroute.TotalAmtMsat), rpcroute.TotalTimeLock, - r.SelfNode, + sourcePubKey, hops, ) if err != nil { diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go index e572f8066f0..a267da40cdd 100644 --- a/lnrpc/routerrpc/router_backend_test.go +++ b/lnrpc/routerrpc/router_backend_test.go @@ -3,6 +3,7 @@ package routerrpc import ( "bytes" "encoding/hex" + "fmt" "testing" "time" @@ -989,3 +990,133 @@ func TestMarshallRouteChanCapacity(t *testing.T) { t, hop1Forward.ToSatoshis(), rpcRoute.Hops[1].ChanCapacity, ) } + +// TestUnmarshallRouteSourcePubKey tests that UnmarshallRoute correctly handles +// the source_pub_key field on the Route proto message. +func TestUnmarshallRouteSourcePubKey(t *testing.T) { + t.Parallel() + + // Define test vertices. The self node is the default source used + // when source_pub_key is empty. + selfNode := route.Vertex{1, 2, 3} + overrideSource := route.Vertex{4, 5, 6} + + // A hop target that will appear in the route. + hopTarget := route.Vertex{7, 8, 9} + + backend := &RouterBackend{ + SelfNode: selfNode, + FetchChannelEndpoints: func(chanID uint64) (route.Vertex, + route.Vertex, error) { + + // For channel 12345, return overrideSource and + // hopTarget as the two endpoints. + if chanID == 12345 { + return overrideSource, hopTarget, nil + } + + return route.Vertex{}, route.Vertex{}, + fmt.Errorf("unknown channel: %d", chanID) + }, + } + + t.Run("default source when empty", func(t *testing.T) { + t.Parallel() + + rpcRoute := &lnrpc.Route{ + TotalAmtMsat: 1000, + TotalTimeLock: 144, + Hops: []*lnrpc.Hop{ + { + PubKey: hex.EncodeToString( + hopTarget[:], + ), + ChanId: 12345, + AmtToForward: 1000, + }, + }, + } + + r, err := backend.UnmarshallRoute(rpcRoute) + require.NoError(t, err) + + // With no SourcePubKey set, the route source should + // default to the backend's SelfNode. + require.Equal(t, selfNode, r.SourcePubKey) + }) + + t.Run("override source with valid pubkey", func(t *testing.T) { + t.Parallel() + + rpcRoute := &lnrpc.Route{ + TotalAmtMsat: 1000, + TotalTimeLock: 144, + SourcePubKey: hex.EncodeToString(overrideSource[:]), + Hops: []*lnrpc.Hop{ + { + PubKey: hex.EncodeToString( + hopTarget[:], + ), + ChanId: 12345, + AmtToForward: 1000, + }, + }, + } + + r, err := backend.UnmarshallRoute(rpcRoute) + require.NoError(t, err) + + // The route source should match the override. + require.Equal(t, overrideSource, r.SourcePubKey) + }) + + t.Run("invalid source pubkey", func(t *testing.T) { + t.Parallel() + + rpcRoute := &lnrpc.Route{ + TotalAmtMsat: 1000, + TotalTimeLock: 144, + SourcePubKey: "not-a-valid-hex-pubkey", + Hops: []*lnrpc.Hop{ + { + PubKey: hex.EncodeToString( + hopTarget[:], + ), + ChanId: 12345, + AmtToForward: 1000, + }, + }, + } + + _, err := backend.UnmarshallRoute(rpcRoute) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid source pubkey") + }) + + t.Run("implicit hop resolution with src override ", func(t *testing.T) { + t.Parallel() + + // Hop omits PubKey, forcing implicit resolution via + // FetchChannelEndpoints using the override source. + rpcRoute := &lnrpc.Route{ + TotalAmtMsat: 1000, + TotalTimeLock: 144, + SourcePubKey: hex.EncodeToString(overrideSource[:]), + Hops: []*lnrpc.Hop{ + { + ChanId: 12345, + AmtToForward: 1000, + }, + }, + } + + r, err := backend.UnmarshallRoute(rpcRoute) + require.NoError(t, err) + + // The hop should resolve to hopTarget (the other + // endpoint of channel 12345, since the source is + // overrideSource). + require.Equal(t, hopTarget, r.Hops[0].PubKeyBytes) + require.Equal(t, overrideSource, r.SourcePubKey) + }) +} diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 7f2514aee51..91336bd2257 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -1701,10 +1701,20 @@ func (s *Server) BuildRoute(_ context.Context, firstHopBlob = fn.Some(firstHopData) } + // Default to the router's own identity as the route source. + sourceNode := s.cfg.RouterBackend.SelfNode + if len(req.SourcePubKey) != 0 { + src, err := route.NewVertexFromBytes(req.SourcePubKey) + if err != nil { + return nil, err + } + sourceNode = src + } + // Build the route and return it to the caller. route, err := s.cfg.Router.BuildRoute( - amt, hops, outgoingChan, req.FinalCltvDelta, payAddr, - firstHopBlob, + sourceNode, amt, hops, outgoingChan, req.FinalCltvDelta, + payAddr, firstHopBlob, ) if err != nil { return nil, err diff --git a/lnrpc/switchrpc/switch.swagger.json b/lnrpc/switchrpc/switch.swagger.json index 71fc3086e07..a6dd9c3f20a 100644 --- a/lnrpc/switchrpc/switch.swagger.json +++ b/lnrpc/switchrpc/switch.swagger.json @@ -342,6 +342,10 @@ "type": "string", "format": "byte", "description": "Custom channel data that might be populated in custom channels." + }, + "source_pub_key": { + "type": "string", + "description": "The source node from whose perspective the route was built. Defaults to\nthe node's own public key." } }, "description": "A path through the channel graph which runs over one or more channels in\nsuccession. This struct carries all the information required to craft the\nSphinx onion packet, and send the payment along the first hop in the path. A\nroute is only selected as valid if all the channels have sufficient capacity to\ncarry the initial payment amount after fees are accounted for." diff --git a/routing/router.go b/routing/router.go index 0f9288fde12..e1bed82f5bc 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1389,15 +1389,18 @@ func (e ErrNoChannel) Error() string { "node index %v", e.position) } -// BuildRoute returns a fully specified route based on a list of pubkeys. If -// amount is nil, the minimum routable amount is used. To force a specific -// outgoing channel, use the outgoingChan parameter. -func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi], - hops []route.Vertex, outgoingChan *uint64, finalCltvDelta int32, +// BuildRoute builds a fully specified route based on a list of pubkeys from +// the perspective of the provided source node. If amount is nil, the minimum +// routable amount is used. To force a specific outgoing channel, use the +// outgoingChan parameter. +func (r *ChannelRouter) BuildRoute(sourceNode route.Vertex, + amt fn.Option[lnwire.MilliSatoshi], hops []route.Vertex, + outgoingChan *uint64, finalCltvDelta int32, payAddr fn.Option[[32]byte], firstHopBlob fn.Option[[]byte]) ( *route.Route, error) { - log.Tracef("BuildRoute called: hopsCount=%v, amt=%v", len(hops), amt) + log.Tracef("BuildRoute called: sourceNode=%v, hopsCount=%v, amt=%v", + sourceNode, len(hops), amt) var outgoingChans map[uint64]struct{} if outgoingChan != nil { @@ -1416,12 +1419,10 @@ func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi], return nil, err } - sourceNode := r.cfg.SelfNode - // We check that each node in the route has a connection to others that // can forward in principle. unifiers, err := getEdgeUnifiers( - r.cfg.SelfNode, hops, outgoingChans, r.cfg.RoutingGraph, + sourceNode, hops, outgoingChans, r.cfg.RoutingGraph, ) if err != nil { return nil, err diff --git a/routing/router_test.go b/routing/router_test.go index 1f00c6c6e4e..882ab3dae91 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -1668,17 +1668,21 @@ func TestBuildRoute(t *testing.T) { noAmt := fn.None[lnwire.MilliSatoshi]() + selfNode := ctx.router.cfg.SelfNode + // Test that we can't build a route when no hops are given. hops = []route.Vertex{} _, err = ctx.router.BuildRoute( - noAmt, hops, nil, 40, fn.None[[32]byte](), fn.None[[]byte](), + selfNode, noAmt, hops, nil, 40, + fn.None[[32]byte](), fn.None[[]byte](), ) require.Error(t, err) // Create hop list for an unknown destination. hops := []route.Vertex{ctx.aliases["b"], ctx.aliases["y"]} _, err = ctx.router.BuildRoute( - noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](), + selfNode, noAmt, hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), ) noChanErr := ErrNoChannel{} require.ErrorAs(t, err, &noChanErr) @@ -1690,8 +1694,8 @@ func TestBuildRoute(t *testing.T) { // Build the route for the given amount. rt, err := ctx.router.BuildRoute( - fn.Some(amt), hops, nil, 40, fn.Some(payAddr), - fn.None[[]byte](), + selfNode, fn.Some(amt), hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), ) require.NoError(t, err) @@ -1703,7 +1707,8 @@ func TestBuildRoute(t *testing.T) { // Build the route for the minimum amount. rt, err = ctx.router.BuildRoute( - noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](), + selfNode, noAmt, hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), ) require.NoError(t, err) @@ -1721,7 +1726,8 @@ func TestBuildRoute(t *testing.T) { // There is no amount that can pass through both channel 5 and 4. hops = []route.Vertex{ctx.aliases["e"], ctx.aliases["c"]} _, err = ctx.router.BuildRoute( - noAmt, hops, nil, 40, fn.None[[32]byte](), fn.None[[]byte](), + selfNode, noAmt, hops, nil, 40, + fn.None[[32]byte](), fn.None[[]byte](), ) require.Error(t, err) noChanErr = ErrNoChannel{} @@ -1741,7 +1747,8 @@ func TestBuildRoute(t *testing.T) { // policy of channel 3. hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["z"]} rt, err = ctx.router.BuildRoute( - noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](), + selfNode, noAmt, hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), ) require.NoError(t, err) checkHops(rt, []uint64{1, 8}, payAddr) @@ -1755,8 +1762,8 @@ func TestBuildRoute(t *testing.T) { hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]} amt = lnwire.NewMSatFromSatoshis(100) rt, err = ctx.router.BuildRoute( - fn.Some(amt), hops, nil, 40, fn.Some(payAddr), - fn.None[[]byte](), + selfNode, fn.Some(amt), hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), ) require.NoError(t, err) checkHops(rt, []uint64{9, 10}, payAddr) @@ -1772,11 +1779,24 @@ func TestBuildRoute(t *testing.T) { // is a third pass through newRoute in which this gets corrected to end hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]} rt, err = ctx.router.BuildRoute( - noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](), + selfNode, noAmt, hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), ) require.NoError(t, err) checkHops(rt, []uint64{9, 10}, payAddr) require.EqualValues(t, 20180, rt.TotalAmount, "%v", rt.TotalAmount) + + // Test a route built from an alternate source node (d --> f). + hops = []route.Vertex{ctx.aliases["f"]} + rt, err = ctx.router.BuildRoute( + ctx.aliases["d"], fn.Some(amt), hops, nil, 40, + fn.Some(payAddr), fn.None[[]byte](), + ) + require.NoError(t, err) + require.Equal(t, ctx.aliases["d"], rt.SourcePubKey, + "expected 'd' as source") + checkHops(rt, []uint64{10}, payAddr) + } // TestReceiverAmtForwardPass tests that the forward pass returns the expected