mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-03 01:11:37 +00:00
## 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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user