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:
parent
8847ded382
commit
731cf178b8
3 changed files with 222 additions and 0 deletions
61
defects/ipfs-cluster-0001/patch/ipfs-cluster-0001.patch
Normal file
61
defects/ipfs-cluster-0001/patch/ipfs-cluster-0001.patch
Normal 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
|
||||
3
defects/ipfs-cluster-0001/test/go.mod
Normal file
3
defects/ipfs-cluster-0001/test/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module ipfscluster0001
|
||||
|
||||
go 1.21
|
||||
158
defects/ipfs-cluster-0001/test/ipfs_cluster_0001_test.go
Normal file
158
defects/ipfs-cluster-0001/test/ipfs_cluster_0001_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue