diff --git a/cmd/server/coverage_test.go b/cmd/server/coverage_test.go index 530c62e..40460a3 100644 --- a/cmd/server/coverage_test.go +++ b/cmd/server/coverage_test.go @@ -813,6 +813,56 @@ func TestPrefixMapResolve(t *testing.T) { }) } +func TestPrefixMapCap(t *testing.T) { + // 16-char pubkey — longer than maxPrefixLen + nodes := []nodeInfo{ + {PublicKey: "aabbccdd11223344", Name: "LongKey"}, + {PublicKey: "eeff0011", Name: "ShortKey"}, // exactly 8 chars + } + pm := buildPrefixMap(nodes) + + t.Run("short prefixes still work", func(t *testing.T) { + n := pm.resolve("aabb") + if n == nil || n.Name != "LongKey" { + t.Errorf("expected LongKey for short prefix, got %v", n) + } + }) + + t.Run("full pubkey exact match works", func(t *testing.T) { + n := pm.resolve("aabbccdd11223344") + if n == nil || n.Name != "LongKey" { + t.Errorf("expected LongKey for full key, got %v", n) + } + }) + + t.Run("intermediate prefix beyond cap returns nil", func(t *testing.T) { + // 10-char prefix — beyond maxPrefixLen but not full key + n := pm.resolve("aabbccdd11") + if n != nil { + t.Errorf("expected nil for intermediate prefix beyond cap, got %v", n.Name) + } + }) + + t.Run("short key within cap has all prefixes", func(t *testing.T) { + for l := 2; l <= 8; l++ { + pfx := "eeff0011"[:l] + n := pm.resolve(pfx) + if n == nil || n.Name != "ShortKey" { + t.Errorf("prefix %q: expected ShortKey, got %v", pfx, n) + } + } + }) + + t.Run("map size is capped", func(t *testing.T) { + // LongKey: 7 prefix entries (2..8) + 1 full key = 8 + // ShortKey: 7 prefix entries (2..8), no full key entry (len == maxPrefixLen) = 7 + // No overlapping prefixes between the two nodes → 8 + 7 = 15 unique map keys + if len(pm.m) != 15 { + t.Errorf("expected 15 map entries (8 for LongKey + 7 for ShortKey), got %d", len(pm.m)) + } + }) +} + // --- pathLen --- func TestPathLen(t *testing.T) { diff --git a/cmd/server/store.go b/cmd/server/store.go index 08b95cc..c16e2fb 100644 --- a/cmd/server/store.go +++ b/cmd/server/store.go @@ -3543,14 +3543,27 @@ type prefixMap struct { m map[string][]nodeInfo } +// maxPrefixLen caps prefix map entries. MeshCore path hops use 2–6 char +// prefixes; 8 gives comfortable headroom while cutting map size from ~31×N +// entries to ~7×N (+ 1 full-key entry per node for exact-match lookups). +const maxPrefixLen = 8 + func buildPrefixMap(nodes []nodeInfo) *prefixMap { - pm := &prefixMap{m: make(map[string][]nodeInfo, len(nodes)*10)} + pm := &prefixMap{m: make(map[string][]nodeInfo, len(nodes)*(maxPrefixLen+1))} for _, n := range nodes { pk := strings.ToLower(n.PublicKey) - for l := 2; l <= len(pk); l++ { + maxLen := maxPrefixLen + if maxLen > len(pk) { + maxLen = len(pk) + } + for l := 2; l <= maxLen; l++ { pfx := pk[:l] pm.m[pfx] = append(pm.m[pfx], n) } + // Always add full pubkey so exact-match lookups work. + if len(pk) > maxPrefixLen { + pm.m[pk] = append(pm.m[pk], n) + } } return pm }