fix: skip zero-hop adverts when checking node hash size (#493)

Fixes issue router IDs flapping between 1byte and multi-byte as
described in https://github.com/Kpa-clawbot/CoreScope/issues/303 with a
minimal patch + test coverage.

This fix is critical for regions using multi-byte IDs.

Closes https://github.com/Kpa-clawbot/CoreScope/issues/303

---------

Co-authored-by: you <you@example.com>
This commit is contained in:
Jukka Väisänenandyou authored and GitHub committed 2026-04-03 00:33:20 +00:00
1 parent 2755dc3875
commit 4664c90db4
2 files changed
+128

No files matched your search

+118
View File
@@ -2178,6 +2178,124 @@ func TestGetNodeHashSizeInfoLatestWins(t *testing.T) {
}
}
func TestGetNodeHashSizeInfoIgnoreDirectZeroHop(t *testing.T) {
db := setupTestDB(t)
seedTestData(t, db)
store := NewPacketStore(db, nil)
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
pk := "dddd111122223333444455556666777788889999aaaabbbbccccddddeeee3333"
db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, role) VALUES (?, 'DirIgnore', 'repeater')", pk)
decoded := `{"name":"DirIgnore","pubKey":"` + pk + `"}`
rawFlood2B := "11" + "40" + "aabb" // FLOOD advert, hashSize=2
rawDirect0 := "12" + "00" + "aabb" // DIRECT advert, zero-hop (should be ignored)
payloadType := 4
raws := []string{rawFlood2B, rawDirect0, rawFlood2B, rawDirect0, rawFlood2B}
for i, raw := range raws {
tx := &StoreTx{
ID: 9150 + i,
RawHex: raw,
Hash: "dirignore" + strconv.Itoa(i),
FirstSeen: "2024-01-01T0" + strconv.Itoa(i) + ":00:00Z",
PayloadType: &payloadType,
DecodedJSON: decoded,
}
store.packets = append(store.packets, tx)
store.byPayloadType[4] = append(store.byPayloadType[4], tx)
}
info := store.GetNodeHashSizeInfo()
ni := info[pk]
if ni == nil {
t.Fatal("expected hash info for test node")
}
if ni.HashSize != 2 {
t.Errorf("HashSize=%d, want 2 (direct zero-hop adverts should be ignored)", ni.HashSize)
}
if ni.Inconsistent {
t.Error("expected hash_size_inconsistent=false when direct zero-hop adverts are ignored")
}
if len(ni.AllSizes) != 1 || !ni.AllSizes[2] {
t.Errorf("expected only 2-byte size in AllSizes, got %#v", ni.AllSizes)
}
}
func TestGetNodeHashSizeInfoOnlyDirectZeroHopIgnored(t *testing.T) {
db := setupTestDB(t)
seedTestData(t, db)
store := NewPacketStore(db, nil)
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
pk := "eeee111122223333444455556666777788889999aaaabbbbccccddddeeee4444"
db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, role) VALUES (?, 'OnlyDirect', 'repeater')", pk)
decoded := `{"name":"OnlyDirect","pubKey":"` + pk + `"}`
rawDirect0 := "12" + "00" + "aabb"
payloadType := 4
tx := &StoreTx{
ID: 9160,
RawHex: rawDirect0,
Hash: "onlydirect0",
FirstSeen: "2024-01-01T00:00:00Z",
PayloadType: &payloadType,
DecodedJSON: decoded,
}
store.packets = append(store.packets, tx)
store.byPayloadType[4] = append(store.byPayloadType[4], tx)
info := store.GetNodeHashSizeInfo()
if ni := info[pk]; ni != nil {
t.Errorf("expected nil hash info for direct zero-hop only node, got HashSize=%d", ni.HashSize)
}
}
func TestGetNodeHashSizeInfoDirectNonZeroHopCounted(t *testing.T) {
// A DIRECT advert with non-zero hop count should NOT be skipped —
// only zero-hop DIRECT adverts misreport hash size.
db := setupTestDB(t)
seedTestData(t, db)
store := NewPacketStore(db, nil)
if err := store.Load(); err != nil {
t.Fatalf("store.Load failed: %v", err)
}
pk := "ffff111122223333444455556666777788889999aaaabbbbccccddddeeee5555"
db.conn.Exec("INSERT OR IGNORE INTO nodes (public_key, name, role) VALUES (?, 'DirNonZero', 'repeater')", pk)
decoded := `{"name":"DirNonZero","pubKey":"` + pk + `"}`
// DIRECT advert (route type 2 = 0x02 in bits 0-1), path byte 0x41:
// upper 2 bits = 01 → hash_size = 2, lower 6 bits = 0x01 → hop count 1 (non-zero)
rawDirectNonZero := "12" + "41" + "aabb" // header=0x12 (ADVERT|DIRECT), path=0x41
payloadType := 4
tx := &StoreTx{
ID: 9170,
RawHex: rawDirectNonZero,
Hash: "dirnonzero0",
FirstSeen: "2024-01-01T00:00:00Z",
PayloadType: &payloadType,
DecodedJSON: decoded,
}
store.packets = append(store.packets, tx)
store.byPayloadType[4] = append(store.byPayloadType[4], tx)
info := store.GetNodeHashSizeInfo()
ni := info[pk]
if ni == nil {
t.Fatal("expected hash info for DIRECT non-zero-hop node — it should NOT be skipped")
}
if ni.HashSize != 2 {
t.Errorf("HashSize=%d, want 2 (DIRECT with hop count > 0 should be counted)", ni.HashSize)
}
}
func TestGetNodeHashSizeInfoNoAdverts(t *testing.T) {
// A node with no ADVERT packets should not appear in hash size info.
db := setupTestDB(t)
+10
View File
@@ -4537,10 +4537,20 @@ func (s *PacketStore) computeNodeHashSizeInfo() map[string]*hashSizeNodeInfo {
if len(tx.RawHex) < 4 {
continue
}
header, err := strconv.ParseUint(tx.RawHex[:2], 16, 8)
if err != nil {
continue
}
routeType := int(header & 0x03)
pathByte, err := strconv.ParseUint(tx.RawHex[2:4], 16, 8)
if err != nil {
continue
}
// DIRECT zero-hop adverts use path byte 0x00 locally and can misreport
// multibyte repeater hash mode as 1-byte.
if routeType == RouteDirect && (pathByte&0x3F) == 0 {
continue
}
hs := int((pathByte>>6)&0x3) + 1
var d map[string]interface{}