fix(#1218): include multi-byte prefix repeaters in 1-byte hash usage matrix view (#1591)

## Problem

`/analytics` Hash Usage Matrix 1-byte view excluded repeaters configured
for 2- or 3-byte hash prefixes. In MeshCore, 1-byte path-matching is a
first-byte equality check, so any packet routed by 1-byte hash collides
on that first byte regardless of the downstream repeater's configured
prefix size. Omitting multi-byte prefix repeaters under-reports real
conflicts in the 1-byte hash space.

## Fix

**Data layer — `cmd/server/store.go` (`computeHashCollisions`,
~L7907-L7918 before, L7907-L7941 after):**

Before — `one_byte_cells` was populated only from `prefixMap`, which
only contained repeaters with `hash_size == 1`:

```go
if bytes == 1 {
    oneByteCells = make(map[string][]collisionNode)
    for i := 0; i < 256; i++ {
        hex := strings.ToUpper(fmt.Sprintf("%02x", i))
        oneByteCells[hex] = prefixMap[hex]
        if oneByteCells[hex] == nil {
            oneByteCells[hex] = make([]collisionNode, 0)
        }
    }
} else if bytes == 2 { ... }
```

After — additionally project all `hash_size in {2,3}` repeaters to their
first byte:

```go
if bytes == 1 {
    // ... (same baseline population) ...
    for _, cn := range allCNodes {
        if cn.Role != "repeater" { continue }
        if cn.HashSize != 2 && cn.HashSize != 3 { continue }
        if len(cn.PublicKey) < 2 { continue }
        hex := strings.ToUpper(cn.PublicKey[:2])
        if _, ok := oneByteCells[hex]; !ok { continue }
        oneByteCells[hex] = append(oneByteCells[hex], cn)
    }
}
```

The 2-byte view's bucketing is unchanged — that view continues to count
only repeaters configured for 2-byte prefixes (those semantics differ).

**UI — `public/analytics.js` L1459:** clarified the 1-byte view
description so the inclusion of multi-byte prefix repeaters is explicit.

## API shape

No response-shape change. `one_byte_cells[HEX]` is still
`[]collisionNode`; only the contents now include 2/3-byte prefix
repeaters in the appropriate first-byte buckets. The existing frontend
decoder is unaffected.

## Tests

-
`cmd/server/routes_test.go::TestHashCollisionsOneByteIncludesMultiBytePrefixRepeaters`
— seeds three repeaters with first byte `CC` configured for 1/2/3-byte
prefixes plus an unrelated `DD` repeater, asserts all three appear in
`one_byte_cells["CC"]`, and that the 2-byte view's `nodes_for_byte` is
unchanged.

Red commit `278bdf8d` (test only) fails on assertion ("got 1, want 3");
green commit `9127ea4e` passes.

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ clean.

Closes #1218

---------

Co-authored-by: clawbot <bot@corescope>
This commit is contained in:
Kpa-clawbot
2026-06-04 20:44:19 -07:00
committed by GitHub
parent 373ee81641
commit 3df8924114
3 changed files with 97 additions and 1 deletions
+74
View File
@@ -3610,6 +3610,80 @@ func TestHashCollisionsOnlyRepeaters(t *testing.T) {
}
}
// TestHashCollisionsOneByteIncludesMultiBytePrefixRepeaters verifies that the
// 1-byte Hash Usage Matrix view includes the first byte of repeaters configured
// for 2-byte and 3-byte prefixes. In MeshCore, a multi-byte hash repeater still
// occupies its first byte in the 1-byte hash space, so any 1-byte path-matching
// collides on that first byte regardless of the configured prefix size. Omitting
// these under-reports real conflicts in the 1-byte space. (#1218)
func TestHashCollisionsOneByteIncludesMultiBytePrefixRepeaters(t *testing.T) {
db := setupTestDB(t)
// Three repeaters with first byte "CC":
// - cc11... (hash_size=1) — already counted in 1-byte view
// - cc22aa... (hash_size=2) — must now be counted in 1-byte view's CC cell
// - cc33bbdd... (hash_size=3) — must now be counted in 1-byte view's CC cell
// One unrelated repeater with first byte "DD" must NOT appear in CC cell.
now := time.Now().Format("2006-01-02 15:04:05")
db.conn.Exec(`INSERT INTO nodes (public_key, name, role, last_seen) VALUES
('cc11223344556677', 'Rep1B', 'repeater', ?),
('cc22aabbccddeeff', 'Rep2B', 'repeater', ?),
('cc33bbddeeff0011', 'Rep3B', 'repeater', ?),
('dd44556677889900', 'RepDD', 'repeater', ?)`, now, now, now, now)
cfg := &Config{Port: 3000}
hub := NewHub()
srv := NewServer(db, cfg, hub)
store := NewPacketStore(db, nil)
store.Load()
srv.store = store
store.hashSizeInfoMu.Lock()
store.hashSizeInfoCache = map[string]*hashSizeNodeInfo{
"cc11223344556677": {HashSize: 1, AllSizes: map[int]bool{1: true}},
"cc22aabbccddeeff": {HashSize: 2, AllSizes: map[int]bool{2: true}},
"cc33bbddeeff0011": {HashSize: 3, AllSizes: map[int]bool{3: true}},
"dd44556677889900": {HashSize: 1, AllSizes: map[int]bool{1: true}},
}
store.hashSizeInfoAt = time.Now()
store.hashSizeInfoMu.Unlock()
result := store.computeHashCollisions("", "")
bySize := result["by_size"].(map[string]interface{})
size1 := bySize["1"].(map[string]interface{})
cells, ok := size1["one_byte_cells"].(map[string][]collisionNode)
if !ok {
t.Fatalf("one_byte_cells has unexpected type %T", size1["one_byte_cells"])
}
ccNodes := cells["CC"]
if len(ccNodes) != 3 {
t.Errorf("expected 3 nodes in one_byte_cells[CC] (1B + 2B + 3B repeaters), got %d", len(ccNodes))
}
seen := map[string]bool{}
for _, n := range ccNodes {
seen[strings.ToLower(n.PublicKey)] = true
}
for _, pk := range []string{"cc11223344556677", "cc22aabbccddeeff", "cc33bbddeeff0011"} {
if !seen[pk] {
t.Errorf("expected one_byte_cells[CC] to include %s, missing", pk)
}
}
// Sanity: DD repeater must not be in CC cell.
if seen["dd44556677889900"] {
t.Errorf("one_byte_cells[CC] unexpectedly contains DD repeater")
}
// Sanity: the same multi-byte repeaters must NOT be added to the 2/3-byte
// view's repeater roster (that view continues to bucket by configured size).
size2 := bySize["2"].(map[string]interface{})
stats2 := size2["stats"].(map[string]interface{})
if n, _ := stats2["nodes_for_byte"].(int); n != 1 {
t.Errorf("expected 2-byte view nodes_for_byte=1, got %v", stats2["nodes_for_byte"])
}
}
func TestNodePathsEndpointUsesIndex(t *testing.T) {
srv, router := setupTestServer(t)
+22
View File
@@ -7916,6 +7916,28 @@ func (s *PacketStore) computeHashCollisions(region, area string) map[string]inte
oneByteCells[hex] = make([]collisionNode, 0)
}
}
// Fix #1218: a repeater configured for a 2- or 3-byte hash still
// occupies its first byte in the 1-byte hash space — any packet
// routed by 1-byte path-matching collides on that first byte
// regardless of the configured prefix size. Project all repeater
// hashes to their first byte so the 1-byte view reflects real
// conflicts in the 1-byte hash space.
for _, cn := range allCNodes {
if cn.Role != "repeater" {
continue
}
if cn.HashSize != 2 && cn.HashSize != 3 {
continue
}
if len(cn.PublicKey) < 2 {
continue
}
hex := strings.ToUpper(cn.PublicKey[:2])
if _, ok := oneByteCells[hex]; !ok {
continue
}
oneByteCells[hex] = append(oneByteCells[hex], cn)
}
} else if bytes == 2 {
twoByteCells = make(map[string]*twoByteCellInfo)
for i := 0; i < 256; i++ {
+1 -1
View File
@@ -1456,7 +1456,7 @@
if (matrixTitle) matrixTitle.textContent = bytes === 3 ? '🔢 Hash Usage Matrix' : `🔢 ${bytes}-Byte Hash Usage Matrix`;
if (riskTitle) riskTitle.textContent = `💥 ${bytes}-Byte Collision Risk`;
if (matrixDesc) {
if (bytes === 1) matrixDesc.textContent = 'Click a cell to see which nodes share that 1-byte prefix.';
if (bytes === 1) matrixDesc.textContent = 'Cells include the first byte of all repeaters — including those using 2- or 3-byte prefixes — so this reflects real conflicts in the 1-byte hash space. Click a cell to see the nodes.';
else if (bytes === 2) matrixDesc.textContent = 'Each cell = first-byte group. Color shows worst 2-byte collision within. Click a cell to see the breakdown.';
else matrixDesc.textContent = '3-byte prefix space is too large to visualize as a matrix — collision table is shown below.';
}