diff --git a/htlcswitch/interceptable_switch.go b/htlcswitch/interceptable_switch.go index 5e379d0a4..9ef686a65 100644 --- a/htlcswitch/interceptable_switch.go +++ b/htlcswitch/interceptable_switch.go @@ -705,6 +705,7 @@ func (f *interceptedForward) Packet() InterceptedPacket { HtlcID: f.packet.incomingHTLCID, }, OutgoingChanID: f.packet.outgoingChanID, + OutgoingNodeID: f.packet.outgoingHop.RightToSome(), Hash: f.htlc.PaymentHash, OutgoingExpiry: f.htlc.Expiry, OutgoingAmount: f.htlc.Amount, diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index ef62eb71d..2a2b834cb 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -382,6 +382,14 @@ type InterceptableHtlcForwarder interface { // and resolve it later or let the switch execute its default behavior. type ForwardInterceptor func(InterceptedPacket) error +// NodeIDForwardSCID is the sentinel outgoing SCID reported to HTLC interceptor +// clients (at the RPC boundary) for a next hop identified by node ID (BOLT 4 +// next_node_id) rather than by channel. All bits are set, an out-of-range value +// that can never match a real or alias channel, so a client switching on a zero +// SCID to detect the exit hop does not read the forward as a final receive. The +// pubkey is in InterceptedPacket.OutgoingNodeID. +const NodeIDForwardSCID uint64 = ^uint64(0) + // InterceptedPacket contains the relevant information for the interceptor about // an HTLC. type InterceptedPacket struct { @@ -389,9 +397,17 @@ type InterceptedPacket struct { // packet. IncomingCircuit models.CircuitKey - // OutgoingChanID is the destination channel for this packet. + // OutgoingChanID is the destination channel for this packet. For a + // node-ID next hop with no concrete channel known yet it is hop.Exit + // and OutgoingNodeID holds the pubkey; the RPC layer maps that to the + // NodeIDForwardSCID sentinel before reporting it to a client. OutgoingChanID lnwire.ShortChannelID + // OutgoingNodeID is the next hop's compressed pubkey for a blinded + // route that identifies it by node ID (next_node_id). None in the + // common channel-ID case. + OutgoingNodeID fn.Option[[33]byte] + // Hash is the payment hash of the htlc. Hash lntypes.Hash diff --git a/lnrpc/routerrpc/forward_interceptor.go b/lnrpc/routerrpc/forward_interceptor.go index 61adf8f2b..a1a065ff5 100644 --- a/lnrpc/routerrpc/forward_interceptor.go +++ b/lnrpc/routerrpc/forward_interceptor.go @@ -100,6 +100,17 @@ func (r *forwardInterceptor) onIntercept( InWireCustomRecords: htlc.InWireCustomRecords, } + // A node-ID forward has no requested outgoing channel. Expose the + // requested pubkey and report the reserved NodeIDForwardSCID sentinel + // rather than a zero SCID. Older un-upgraded protobuf clients do not + // know about outgoing_requested_node_id and would otherwise interpret + // a zero SCID as an exit hop. + htlc.OutgoingNodeID.WhenSome(func(nodeID [33]byte) { + interceptionRequest.OutgoingRequestedNodeId = nodeID[:] + interceptionRequest.OutgoingRequestedChanId = + htlcswitch.NodeIDForwardSCID + }) + return r.stream.Send(interceptionRequest) }