From 4b9f2f0f6a84db6d16ff010dade5af967a79da07 Mon Sep 17 00:00:00 2001 From: bitromortac Date: Thu, 23 Jul 2026 12:25:29 +0000 Subject: [PATCH] witness beacon: report node-ID next hop to the on-chain HTLC interceptor Extend the on-chain interceptor path in the witness beacon to expose a node-ID next hop, mirroring the off-chain path. A node-ID next hop has no outgoing channel of its own, so the beacon reports hop.Exit as the outgoing channel (via ForwardingInfo.NextHopChannel().UnwrapOr) and the requested next node's public key. The RPC boundary maps that to the NodeIDForwardSCID sentinel so the forward is not misread as a final receive. This is the requested next hop, not the channel eventually selected by non-strict forwarding, so the beacon deliberately does not resolve it against the circuit map. (cherry picked from commit 9c4b8bfec2e35d7fc2810cc3eb9b1162053f79bf) --- witness_beacon.go | 22 +++++++++++++++++----- witness_beacon_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/witness_beacon.go b/witness_beacon.go index 550a38adc..45799da33 100644 --- a/witness_beacon.go +++ b/witness_beacon.go @@ -106,14 +106,26 @@ func (p *preimageBeacon) SubscribeUpdates( }, } + // Report the forwarding next hop to the interceptor. A channel-ID next + // hop is reported directly; a node-ID next hop has no outgoing channel + // of its own, so outgoingChanID is hop.Exit and the requested node ID + // is exposed separately, exactly as the off-chain interceptor does. + // This is the requested next hop, not the channel that non-strict + // forwarding eventually selects, so we deliberately do not resolve it + // against the circuit map. The RPC boundary maps a node-ID hop to the + // NodeIDForwardSCID sentinel for the client. + // // Notify the htlc interceptor. There may be a client connected // and willing to supply a preimage. packet := &htlcswitch.InterceptedPacket{ - Hash: htlc.RHash, - IncomingExpiry: htlc.RefundTimeout, - IncomingAmount: htlc.Amt, - IncomingCircuit: inKey, - OutgoingChanID: payload.FwdInfo.NextHopChannel().UnwrapOr(hop.Exit), + Hash: htlc.RHash, + IncomingExpiry: htlc.RefundTimeout, + IncomingAmount: htlc.Amt, + IncomingCircuit: inKey, + OutgoingChanID: payload.FwdInfo.NextHopChannel().UnwrapOr( + hop.Exit, + ), + OutgoingNodeID: payload.FwdInfo.NextHopNode(), OutgoingExpiry: payload.FwdInfo.OutgoingCLTV, OutgoingAmount: payload.FwdInfo.AmountToForward, InOnionCustomRecords: payload.CustomRecords(), diff --git a/witness_beacon_test.go b/witness_beacon_test.go index 1edbada93..9c7cf5352 100644 --- a/witness_beacon_test.go +++ b/witness_beacon_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch/hop" @@ -97,6 +98,47 @@ func TestWitnessBeaconInterceptErrorCancels(t *testing.T) { p.RUnlock() } +// TestWitnessBeaconInterceptNodeID asserts that for a node-ID next hop the +// on-chain interceptor reports the exit-hop SCID (hop.Exit) together with the +// requested next node's public key, matching the off-chain interceptor. The +// next hop is not resolved against the circuit map; the RPC boundary maps +// hop.Exit to the sentinel. +func TestWitnessBeaconInterceptNodeID(t *testing.T) { + var interceptedFwd htlcswitch.InterceptedForward + interceptor := func(fwd htlcswitch.InterceptedForward) error { + interceptedFwd = fwd + + return nil + } + + p := newPreimageBeacon( + &mockWitnessCache{}, interceptor, + func(models.CircuitKey) error { + return nil + }, + ) + + var nodeID [33]byte + nodeID[0] = 0x02 + + payload := &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + NextHop: hop.NewNodeNextHop(nodeID), + }, + } + + _, err := p.SubscribeUpdates( + lnwire.NewShortChanIDFromInt(1), + &channeldb.HTLC{RHash: lntypes.Hash{1}}, + payload, []byte{2}, + ) + require.NoError(t, err) + + packet := interceptedFwd.Packet() + require.Equal(t, hop.Exit, packet.OutgoingChanID) + require.Equal(t, fn.Some(nodeID), packet.OutgoingNodeID) +} + type mockWitnessCache struct { witnessCache }