lnrpc/routerrpc: add outgoing_node_id to HTLC intercept request
What changed, and why it matters
This commit only updates the API definition and auto-generated code for LND's HTLC interceptor. It adds a new optional field, outgoing_requested_node_id, and documents that when a blinded route uses a node ID instead of a channel ID, the existing outgoing_requested_chan_id field will be set to a special reserved value (the maximum unsigned 64-bit integer). The commit explicitly states that the new field is not yet populated by real logic; that will come in later commits. There is no runtime behavior change here, so it does not by itself create a security vulnerability or fix one.
No immediate action required. Treat this as a non-security API/schema change. When reviewing later commits that populate outgoing_requested_node_id, verify that the sentinel value is set consistently and that interceptor clients handle it correctly to avoid misrouting or misclassification of blinded forwards.
Security signals we found
No runtime logic changed; only protobuf schema and generated stubs
New field is explicitly documented as unpopulated until later commits
Sentinel value (MaxUint64) introduced to prevent misclassification of final receive vs node-addressed forward
Documentation change is defensive: warns clients not to interpret sentinel channel ID as real channel
Evidence from the diff
The diff modifies lnrpc/routerrpc/router.proto, the regenerated router.pb.go, and router.swagger.json. It adds bytes outgoing_requested_node_id = 12 to ForwardHtlcInterceptRequest and updates comments to clarify that outgoing_requested_chan_id becomes 0xffffffffffffffff for node-addressed blinded forwards. The Go stubs gain a getter for the new field. The commit message says ‘This commit only adds the schema and regenerated stubs; the fields are populated by later commits.’ Therefore no actual forwarding or interceptor logic is changed, and no security boundary is crossed in this commit alone.
Changed components
lnrpc/routerrpc/router.protolnrpc/routerrpc/router.pb.golnrpc/routerrpc/router.swagger.jsonForwardHtlcInterceptRequest messageInspect captured patch +48 / −7
diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go
index 78f21e2..a1ada9c 100644
--- a/lnrpc/routerrpc/router.pb.go
+++ b/lnrpc/routerrpc/router.pb.go
@@ -2827,7 +2827,8 @@ type ForwardHtlcInterceptRequest struct {
// The requested outgoing channel id for this forwarded htlc. Because of
// non-strict forwarding, this isn't necessarily the channel over which the
// packet will be forwarded eventually. A different channel to the same peer
- // may be selected as well.
+ // may be selected as well. This is set to a sentinel value (all bits set)
+ // if the outgoing_requested_node_id is specified for blinded routes.
OutgoingRequestedChanId uint64 `protobuf:"varint,7,opt,name=outgoing_requested_chan_id,json=outgoingRequestedChanId,proto3" json:"outgoing_requested_chan_id,omitempty"`
// The outgoing htlc amount.
OutgoingAmountMsat uint64 `protobuf:"varint,3,opt,name=outgoing_amount_msat,json=outgoingAmountMsat,proto3" json:"outgoing_amount_msat,omitempty"`
@@ -2843,8 +2844,21 @@ type ForwardHtlcInterceptRequest struct {
AutoFailHeight int32 `protobuf:"varint,10,opt,name=auto_fail_height,json=autoFailHeight,proto3" json:"auto_fail_height,omitempty"`
// The custom records of the peer's incoming p2p wire message.
InWireCustomRecords map[uint64][]byte `protobuf:"bytes,11,rep,name=in_wire_custom_records,json=inWireCustomRecords,proto3" json:"in_wire_custom_records,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ // The requested outgoing node for a blinded forward. When non-empty, this
+ // field contains exactly one 33-byte compressed public key and
+ // outgoing_requested_chan_id is set to 18446744073709551615
+ // (0xffffffffffffffff). Clients MUST NOT interpret that value as an actual
+ // channel ID; the presence of this field identifies a node-addressed
+ // forward.
+ //
+ // The possible next-hop representations are:
+ //
+ // node ID empty, channel ID 0: final receive;
+ // node ID empty, ordinary channel ID: channel-addressed forward;
+ // node ID present, channel ID MaxUint64: node-addressed forward.
+ OutgoingRequestedNodeId []byte `protobuf:"bytes,12,opt,name=outgoing_requested_node_id,json=outgoingRequestedNodeId,proto3" json:"outgoing_requested_node_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *ForwardHtlcInterceptRequest) Reset() {
@@ -2954,6 +2968,13 @@ func (x *ForwardHtlcInterceptRequest) GetInWireCustomRecords() map[uint64][]byte
return nil
}
+func (x *ForwardHtlcInterceptRequest) GetOutgoingRequestedNodeId() []byte {
+ if x != nil {
+ return x.OutgoingRequestedNodeId
+ }
+ return nil
+}
+
// *
// ForwardHtlcInterceptResponse enables the caller to resolve a previously hold
// forward. The caller can choose either to:
@@ -3792,7 +3813,7 @@ const file_routerrpc_router_proto_rawDesc = "" +
"\n" +
"CircuitKey\x12\x17\n" +
"\achan_id\x18\x01 \x01(\x04R\x06chanId\x12\x17\n" +
- "\ahtlc_id\x18\x02 \x01(\x04R\x06htlcId\"\xa7\x06\n" +
+ "\ahtlc_id\x18\x02 \x01(\x04R\x06htlcId\"\xe4\x06\n" +
"\x1bForwardHtlcInterceptRequest\x12G\n" +
"\x14incoming_circuit_key\x18\x01 \x01(\v2\x15.routerrpc.CircuitKeyR\x12incomingCircuitKey\x120\n" +
"\x14incoming_amount_msat\x18\x05 \x01(\x04R\x12incomingAmountMsat\x12'\n" +
@@ -3806,7 +3827,8 @@ const file_routerrpc_router_proto_rawDesc = "" +
"onion_blob\x18\t \x01(\fR\tonionBlob\x12(\n" +
"\x10auto_fail_height\x18\n" +
" \x01(\x05R\x0eautoFailHeight\x12t\n" +
- "\x16in_wire_custom_records\x18\v \x03(\v2?.routerrpc.ForwardHtlcInterceptRequest.InWireCustomRecordsEntryR\x13inWireCustomRecords\x1a@\n" +
+ "\x16in_wire_custom_records\x18\v \x03(\v2?.routerrpc.ForwardHtlcInterceptRequest.InWireCustomRecordsEntryR\x13inWireCustomRecords\x12;\n" +
+ "\x1aoutgoing_requested_node_id\x18\f \x01(\fR\x17outgoingRequestedNodeId\x1a@\n" +
"\x12CustomRecordsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\x1aF\n" +
diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto
index 7b422bd..8060b17 100644
--- a/lnrpc/routerrpc/router.proto
+++ b/lnrpc/routerrpc/router.proto
@@ -932,7 +932,8 @@ message ForwardHtlcInterceptRequest {
// The requested outgoing channel id for this forwarded htlc. Because of
// non-strict forwarding, this isn't necessarily the channel over which the
// packet will be forwarded eventually. A different channel to the same peer
- // may be selected as well.
+ // may be selected as well. This is set to a sentinel value (all bits set)
+ // if the outgoing_requested_node_id is specified for blinded routes.
uint64 outgoing_requested_chan_id = 7;
// The outgoing htlc amount.
@@ -954,6 +955,19 @@ message ForwardHtlcInterceptRequest {
// The custom records of the peer's incoming p2p wire message.
map<uint64, bytes> in_wire_custom_records = 11;
+
+ // The requested outgoing node for a blinded forward. When non-empty, this
+ // field contains exactly one 33-byte compressed public key and
+ // outgoing_requested_chan_id is set to 18446744073709551615
+ // (0xffffffffffffffff). Clients MUST NOT interpret that value as an actual
+ // channel ID; the presence of this field identifies a node-addressed
+ // forward.
+ //
+ // The possible next-hop representations are:
+ // node ID empty, channel ID 0: final receive;
+ // node ID empty, ordinary channel ID: channel-addressed forward;
+ // node ID present, channel ID MaxUint64: node-addressed forward.
+ bytes outgoing_requested_node_id = 12;
}
/**
diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json
index 88b4761..6f89703 100644
--- a/lnrpc/routerrpc/router.swagger.json
+++ b/lnrpc/routerrpc/router.swagger.json
@@ -1572,7 +1572,7 @@
"outgoing_requested_chan_id": {
"type": "string",
"format": "uint64",
- "description": "The requested outgoing channel id for this forwarded htlc. Because of\nnon-strict forwarding, this isn't necessarily the channel over which the\npacket will be forwarded eventually. A different channel to the same peer\nmay be selected as well."
+ "description": "The requested outgoing channel id for this forwarded htlc. Because of\nnon-strict forwarding, this isn't necessarily the channel over which the\npacket will be forwarded eventually. A different channel to the same peer\nmay be selected as well. This is set to a sentinel value (all bits set)\nif the outgoing_requested_node_id is specified for blinded routes."
},
"outgoing_amount_msat": {
"type": "string",
@@ -1609,6 +1609,11 @@
"format": "byte"
},
"description": "The custom records of the peer's incoming p2p wire message."
+ },
+ "outgoing_requested_node_id": {
+ "type": "string",
+ "format": "byte",
+ "description": "The requested outgoing node for a blinded forward. When non-empty, this\nfield contains exactly one 33-byte compressed public key and\noutgoing_requested_chan_id is set to 18446744073709551615\n(0xffffffffffffffff). Clients MUST NOT interpret that value as an actual\nchannel ID; the presence of this field identifies a node-addressed\nforward.\n\nThe possible next-hop representations are:\n node ID empty, channel ID 0: final receive;\n node ID empty, ordinary channel ID: channel-addressed forward;\n node ID present, channel ID MaxUint64: node-addressed forward."
}
}
},
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.