chanstate: match active htlcs by identity

ActiveHtlcs previously matched HTLCs across the local and remote
commitment snapshots by hashing the onion blob. The onion blob is
routing payload data and can be duplicated by buggy or malicious
senders, so it is not a reliable key for identifying the same HTLC on
both commitments.

Match on the HTLC's channel identity instead: the channel-level HTLC
index combined with the direction of the offer uniquely identifies an
offered HTLC within the channel state. A test is added to lock in the
new matching behavior.
This commit is contained in:
ziggie 2026-07-07 08:58:44 -03:00
parent 04b7486fde
commit 489a6dabd9
No known key found for this signature in database
GPG key ID: 1AFF9C4DCED6D666
2 changed files with 83 additions and 7 deletions

View file

@ -1,7 +1,6 @@
package chanstate
import (
"crypto/sha256"
"errors"
"fmt"
"net"
@ -822,17 +821,33 @@ func (c *OpenChannel) ActiveHtlcs() []HTLC {
c.RLock()
defer c.RUnlock()
// htlcKey uniquely identifies an HTLC within the channel state by its
// channel-level HTLC index and the direction of the offer. This is used
// to match the same HTLC across the local and remote commitment
// snapshots.
type htlcKey struct {
index uint64
incoming bool
}
// We'll only return HTLC's that are locked into *both* commitment
// transactions. So we'll iterate through their set of HTLC's to note
// which ones are present on their commitment.
remoteHtlcs := make(map[[32]byte]struct{})
//
// HTLC identity is defined by the channel-level HTLC index plus the
// direction of the offer. The onion blob is routing payload data and
// can be duplicated by buggy or malicious senders, so it is not a
// robust key for matching the same HTLC across commitment snapshots.
remoteHtlcs := make(map[htlcKey]struct{})
for _, htlc := range c.RemoteCommitment.Htlcs {
log.Tracef("RemoteCommitment has htlc: id=%v, update=%v "+
"incoming=%v", htlc.HtlcIndex, htlc.LogIndex,
htlc.Incoming)
onionHash := sha256.Sum256(htlc.OnionBlob[:])
remoteHtlcs[onionHash] = struct{}{}
remoteHtlcs[htlcKey{
index: htlc.HtlcIndex,
incoming: htlc.Incoming,
}] = struct{}{}
}
// Now that we know which HTLC's they have, we'll only mark the HTLC's
@ -843,9 +858,12 @@ func (c *OpenChannel) ActiveHtlcs() []HTLC {
"incoming=%v", htlc.HtlcIndex, htlc.LogIndex,
htlc.Incoming)
onionHash := sha256.Sum256(htlc.OnionBlob[:])
if _, ok := remoteHtlcs[onionHash]; !ok {
log.Tracef("Skipped htlc due to onion mismatched: "+
_, ok := remoteHtlcs[htlcKey{
index: htlc.HtlcIndex,
incoming: htlc.Incoming,
}]
if !ok {
log.Tracef("Skipped htlc due to identity mismatch: "+
"id=%v, update=%v incoming=%v",
htlc.HtlcIndex, htlc.LogIndex, htlc.Incoming)

View file

@ -0,0 +1,58 @@
package chanstate
import (
"testing"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// TestActiveHtlcsMatchesByHTLCIdentity asserts that ActiveHtlcs matches HTLCs
// by their channel identity, not by their onion blob. Onion blobs are routing
// payload data and can be duplicated, while the HTLC index plus direction
// identifies an offered HTLC within the channel state.
func TestActiveHtlcsMatchesByHTLCIdentity(t *testing.T) {
t.Parallel()
var onionBlob [lnwire.OnionPacketSize]byte
onionBlob[0] = 1
matchingHTLC := HTLC{
HtlcIndex: 7,
LogIndex: 10,
Incoming: false,
OnionBlob: onionBlob,
}
duplicateOnionHTLC := HTLC{
HtlcIndex: 8,
LogIndex: 11,
Incoming: false,
OnionBlob: onionBlob,
}
oppositeDirectionHTLC := HTLC{
HtlcIndex: 7,
LogIndex: 12,
Incoming: true,
OnionBlob: onionBlob,
}
channel := &OpenChannel{
LocalCommitment: ChannelCommitment{
Htlcs: []HTLC{
matchingHTLC,
duplicateOnionHTLC,
oppositeDirectionHTLC,
},
},
RemoteCommitment: ChannelCommitment{
Htlcs: []HTLC{
matchingHTLC,
},
},
}
activeHtlcs := channel.ActiveHtlcs()
require.Len(t, activeHtlcs, 1)
require.Equal(t, matchingHTLC.HtlcIndex, activeHtlcs[0].HtlcIndex)
require.Equal(t, matchingHTLC.Incoming, activeHtlcs[0].Incoming)
}