ipfs-cluster-0001: filterMetrics containsPeer O(M*(B+C+P)) -> O(M+B+C+P)

MOAD-0001 (CWE-407): filterMetrics() in allocate.go calls containsPeer()
(linear scan) three times per metric in our inner loop — once for blacklist,
once for currentAllocs, once for priorityList. With M metrics and B+C+P
total peer-list entries, each allocation decision costs O(M*(B+C+P)).

Fix: Build map[peer.ID]struct{} sets before our loop. Each lookup becomes O(1).
At scale (200 peers, 100-entry lists): ~60x fewer comparisons.

MOAD-0002 (intertangle): allocation state passed as function args, no globals. CLEAN.
MOAD-0003 (leaked context): no ThreadLocal or goroutine-scoped carriers. CLEAN.
MOAD-0004 (logged secret): peer IDs logged, no auth tokens or private keys. CLEAN.
MOAD-0005 (thundering herd): allocation runs under consensus lock. CLEAN.
This commit is contained in:
russell@unturf.com 2026-03-31 13:26:23 -04:00
parent 8847ded382
commit 731cf178b8
3 changed files with 222 additions and 0 deletions

View file

@ -0,0 +1,61 @@
--- a/allocate.go
+++ b/allocate.go
@@ -123,11 +123,22 @@ func (c *Cluster) filterMetrics(ctx context.Context, mSet api.MetricsSet, numMet
// For a metric/peer to be included in a group, it is necessary that it has
// metrics for all informers.
func (c *Cluster) filterMetrics(ctx context.Context, mSet api.MetricsSet, numMetrics int, currentAllocs, priorityList, blacklist []peer.ID) classifiedMetrics {
curPeersMap := make(map[peer.ID][]api.Metric)
candPeersMap := make(map[peer.ID][]api.Metric)
prioPeersMap := make(map[peer.ID][]api.Metric)
- // Divide the metric by current/candidate/prio and by peer
+ // Build O(1) lookup sets for peer classification.
+ blacklistSet := make(map[peer.ID]struct{}, len(blacklist))
+ for _, p := range blacklist {
+ blacklistSet[p] = struct{}{}
+ }
+ currentAllocsSet := make(map[peer.ID]struct{}, len(currentAllocs))
+ for _, p := range currentAllocs {
+ currentAllocsSet[p] = struct{}{}
+ }
+ prioritySet := make(map[peer.ID]struct{}, len(priorityList))
+ for _, p := range priorityList {
+ prioritySet[p] = struct{}{}
+ }
+
+ // Divide our metrics by current/candidate/prio and by peer
for _, metrics := range mSet {
for _, m := range metrics {
switch {
- case containsPeer(blacklist, m.Peer):
+ case peerInSet(blacklistSet, m.Peer):
// discard blacklisted peers
continue
case c.config.PinOnlyOnTrustedPeers && !c.consensus.IsTrustedPeer(ctx, m.Peer):
@@ -136,9 +147,9 @@ func (c *Cluster) filterMetrics(ctx context.Context, mSet api.MetricsSet, numMet
case c.config.PinOnlyOnUntrustedPeers && c.consensus.IsTrustedPeer(ctx, m.Peer):
// discard peers that are trusted
continue
- case containsPeer(currentAllocs, m.Peer):
+ case peerInSet(currentAllocsSet, m.Peer):
curPeersMap[m.Peer] = append(curPeersMap[m.Peer], m)
- case containsPeer(priorityList, m.Peer):
+ case peerInSet(prioritySet, m.Peer):
prioPeersMap[m.Peer] = append(prioPeersMap[m.Peer], m)
default:
candPeersMap[m.Peer] = append(candPeersMap[m.Peer], m)
--- a/util.go
+++ b/util.go
@@ -72,6 +72,12 @@ func containsPeer(list []peer.ID, peer peer.ID) bool {
return false
}
+// peerInSet checks for peer membership in a pre-built set. O(1) average.
+func peerInSet(set map[peer.ID]struct{}, p peer.ID) bool {
+ _, ok := set[p]
+ return ok
+}
+
func minInt(x, y int) int {
if x < y {
return x

View file

@ -0,0 +1,3 @@
module ipfscluster0001
go 1.21

View file

@ -0,0 +1,158 @@
package ipfscluster0001
// Unit test for ipfs-cluster-0001: filterMetrics containsPeer O(N²)
//
// Defect: filterMetrics() calls containsPeer() (linear scan) for blacklist,
// currentAllocs, and priorityList on every metric in our nested loop.
// Complexity: O(M * (B + C + P)) where M = total metrics, B/C/P = list sizes.
//
// Fix: Convert slices to map[string]struct{} sets before our loop.
// Patched complexity: O(M + B + C + P).
import (
"fmt"
"testing"
)
// Simulates our peer.ID as a string (same underlying type in libp2p).
type peerID string
// --- BEFORE: linear scan (defective) ---
func containsPeer(list []peerID, peer peerID) bool {
for _, p := range list {
if p == peer {
return true
}
}
return false
}
func filterMetricsBefore(
metrics []peerID,
blacklist []peerID,
currentAllocs []peerID,
priorityList []peerID,
) (cur, prio, cand []peerID) {
for _, m := range metrics {
switch {
case containsPeer(blacklist, m):
continue
case containsPeer(currentAllocs, m):
cur = append(cur, m)
case containsPeer(priorityList, m):
prio = append(prio, m)
default:
cand = append(cand, m)
}
}
return
}
// --- AFTER: set lookup (patched) ---
func peerInSet(set map[peerID]struct{}, p peerID) bool {
_, ok := set[p]
return ok
}
func toSet(list []peerID) map[peerID]struct{} {
s := make(map[peerID]struct{}, len(list))
for _, p := range list {
s[p] = struct{}{}
}
return s
}
func filterMetricsAfter(
metrics []peerID,
blacklist []peerID,
currentAllocs []peerID,
priorityList []peerID,
) (cur, prio, cand []peerID) {
blacklistSet := toSet(blacklist)
currentAllocsSet := toSet(currentAllocs)
prioritySet := toSet(priorityList)
for _, m := range metrics {
switch {
case peerInSet(blacklistSet, m):
continue
case peerInSet(currentAllocsSet, m):
cur = append(cur, m)
case peerInSet(prioritySet, m):
prio = append(prio, m)
default:
cand = append(cand, m)
}
}
return
}
// --- Correctness ---
func TestCorrectness(t *testing.T) {
metrics := []peerID{"p1", "p2", "p3", "p4", "p5", "p6"}
blacklist := []peerID{"p1"}
currentAllocs := []peerID{"p2", "p3"}
priorityList := []peerID{"p4"}
curB, prioB, candB := filterMetricsBefore(metrics, blacklist, currentAllocs, priorityList)
curA, prioA, candA := filterMetricsAfter(metrics, blacklist, currentAllocs, priorityList)
if fmt.Sprint(curB) != fmt.Sprint(curA) {
t.Fatalf("current mismatch: before=%v after=%v", curB, curA)
}
if fmt.Sprint(prioB) != fmt.Sprint(prioA) {
t.Fatalf("priority mismatch: before=%v after=%v", prioB, prioA)
}
if fmt.Sprint(candB) != fmt.Sprint(candA) {
t.Fatalf("candidate mismatch: before=%v after=%v", candB, candA)
}
// Verify expected classification
if fmt.Sprint(curA) != "[p2 p3]" {
t.Fatalf("expected cur=[p2 p3], got %v", curA)
}
if fmt.Sprint(prioA) != "[p4]" {
t.Fatalf("expected prio=[p4], got %v", prioA)
}
if fmt.Sprint(candA) != "[p5 p6]" {
t.Fatalf("expected cand=[p5 p6], got %v", candA)
}
}
// --- Benchmarks ---
func makePeers(n int, prefix string) []peerID {
peers := make([]peerID, n)
for i := 0; i < n; i++ {
peers[i] = peerID(fmt.Sprintf("%s-%d", prefix, i))
}
return peers
}
func BenchmarkBefore(b *testing.B) {
// 500 metrics, 100 blacklisted, 100 current, 100 priority, 200 candidates
metrics := makePeers(500, "m")
blacklist := makePeers(100, "m") // first 100 overlap
current := makePeers(100, "cur")
priority := makePeers(100, "pri")
b.ResetTimer()
for i := 0; i < b.N; i++ {
filterMetricsBefore(metrics, blacklist, current, priority)
}
}
func BenchmarkAfter(b *testing.B) {
metrics := makePeers(500, "m")
blacklist := makePeers(100, "m")
current := makePeers(100, "cur")
priority := makePeers(100, "pri")
b.ResetTimer()
for i := 0; i < b.N; i++ {
filterMetricsAfter(metrics, blacklist, current, priority)
}
}