From 4de8ad829b6cba641a49954ff3df75f9a582e20a Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Thu, 12 Mar 2026 02:08:22 -0400 Subject: [PATCH] lndclient: add FailureMessage and FailureCode to InterceptedHtlcResponse The HTLC interception response type was missing fields for controlling how lnd encodes failure errors back to the sender. Without these fields, all interceptor failures are encoded as TemporaryChannelFailure and attributed to the intercepting node, which causes Mission Control to penalize the forwarding pair. This adds FailureMessage and FailureCode to InterceptedHtlcResponse and wires them through rpcInterceptorResponse. FailureMessage accepts a pre-encrypted onion error blob that lnd relays via IntermediateEncrypt, preserving the error attribution of the original encrypter. This is the path a virtual node behind the interceptor uses to produce errors attributed to itself rather than the intercepting node. FailureCode selects a specific failure code for errors attributed to the interceptor. --- router_client.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/router_client.go b/router_client.go index d603be9..e579bfc 100644 --- a/router_client.go +++ b/router_client.go @@ -395,6 +395,19 @@ type InterceptedHtlcResponse struct { // intercepted. Action InterceptorAction + // FailureMessage is a pre-encrypted onion error to return when the + // action is fail. When set, lnd relays the bytes using + // IntermediateEncrypt, preserving the error attribution of the + // original encrypter. + FailureMessage []byte + + // FailureCode is the failure code to return when the action is + // fail. When non-zero, lnd constructs an onion error attributed to + // the intercepting node using EncryptFirstHop. When zero and + // FailureMessage is also empty, lnd defaults to + // TemporaryChannelFailure. + FailureCode lnrpc.Failure_FailureCode + // IncomingAmount is the amount that should be used to validate the // incoming htlc. This might be different from the actual HTLC amount // for custom channels. @@ -955,6 +968,22 @@ func rpcInterceptorResponse(request InterceptedHtlc, case InterceptorActionFail: rpcResp.Action = routerrpc.ResolveHoldForwardAction_FAIL + if len(response.FailureMessage) > 0 && + response.FailureCode != 0 { + + return nil, errors.New( + "failure_message and failure_code are " + + "mutually exclusive", + ) + } + + if len(response.FailureMessage) > 0 { + rpcResp.FailureMessage = response.FailureMessage + } + if response.FailureCode != 0 { + rpcResp.FailureCode = response.FailureCode + } + case InterceptorActionResume: rpcResp.Action = routerrpc.ResolveHoldForwardAction_RESUME