Repository: libp2p/go-libp2p
Pull request #3266 (2025-04-03 13:04:30)

identify: rate limit id push protocol
The rate limits id pushes from peers to one every five second with an allowed burst of 10 pushes. This should be enough for all but malfunctioning and malicious peers. 

We can use the exact same code for autonat, autonatv2, circuit v2, etc. 

Introducing limits to identify separately to get some feedback for #3265. For this PR, I'd like to ignore issues regarding where should this piece of code go, and focus on how specifically it should behave. See the long comment in `rateLimiter.allow` for example. We should probably modify `x/time/rate` for our usecase.

Part of: #3265


Patch:
diff --git a/.github/workflows/go-check.yml b/.github/workflows/go-check.yml
index e2ff65ca..c82b34a9 100644
--- a/.github/workflows/go-check.yml
+++ b/.github/workflows/go-check.yml
@@ -19,42 +19,3 @@ jobs:
     with:
       go-version: "1.24.x"
       go-generate-ignore-protoc-version-comments: true
-  
-  golangci-lint:
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        go: [  "1.23.x", "1.24.x" ]
-    env:
-      GOLANGCI_LINT_VERSION: v2.0.2
-    name: golangci-lint (Go ${{ matrix.go }})
-    steps:
-      - uses: actions/checkout@v4
-      - uses: actions/setup-go@v5
-        with:
-          go-version: ${{ matrix.go }}
-      - name: golangci-lint (Linux)
-        uses: golangci/golangci-lint-action@v7
-        with:
-          args: --timeout=5m
-          version: ${{ env.GOLANGCI_LINT_VERSION }}
-          only-new-issues: true
-      - name: golangci-lint (Windows)
-        if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v7
-        env:
-          GOOS: "windows"
-        with:
-          args: --timeout=5m
-          version: ${{ env.GOLANGCI_LINT_VERSION }}
-          only-new-issues: true
-      - name: golangci-lint (OSX)
-        if: success() || failure() # run this step even if the previous one failed
-        uses: golangci/golangci-lint-action@v7
-        env:
-          GOOS: "darwin"
-        with:
-          args: --timeout=5m
-          version: ${{ env.GOLANGCI_LINT_VERSION }}
-          only-new-issues: true
diff --git a/.golangci.yml b/.golangci.yml
deleted file mode 100644
index 645266d5..00000000
--- a/.golangci.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-version: "2"
-
-run:
-  timeout: 5m
-
-linters:
-  enable:
-    - revive
-    - unused
-    - prealloc
-
-  settings:
-    revive:
-      severity: warning
-      rules:
-        - name: unused-parameter
-          severity: warning
-
-severity:
-    default: warning
-
diff --git a/go.mod b/go.mod
index 690e96c4..f34023fc 100644
--- a/go.mod
+++ b/go.mod
@@ -63,6 +63,7 @@ require (
 	golang.org/x/crypto v0.35.0
 	golang.org/x/sync v0.11.0
 	golang.org/x/sys v0.30.0
+	golang.org/x/time v0.11.0
 	golang.org/x/tools v0.30.0
 	google.golang.org/protobuf v1.36.5
 )
diff --git a/go.sum b/go.sum
index e895b5bf..cbfa0b84 100644
--- a/go.sum
+++ b/go.sum
@@ -429,8 +429,8 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/p2p/host/peerstore/pstoreds/addr_book_gc.go b/p2p/host/peerstore/pstoreds/addr_book_gc.go
index cc048918..2e35e7ae 100644
--- a/p2p/host/peerstore/pstoreds/addr_book_gc.go
+++ b/p2p/host/peerstore/pstoreds/addr_book_gc.go
@@ -278,7 +278,7 @@ func (gc *dsAddrBookGc) purgeStore() {
 	for result := range results.Next() {
 		record.Reset()
 		if err = proto.Unmarshal(result.Value, record); err != nil {
-			log.Warnf("failed to unmarshal record during GC purge: key=%s, err=%v", result.Key, err)
+			// TODO log
 			continue
 		}
 
diff --git a/p2p/host/resource-manager/conn_limiter.go b/p2p/host/resource-manager/conn_limiter.go
index 73c288f0..717249e8 100644
--- a/p2p/host/resource-manager/conn_limiter.go
+++ b/p2p/host/resource-manager/conn_limiter.go
@@ -114,8 +114,8 @@ type connLimiter struct {
 	// Subnet limits.
 	connLimitPerSubnetV4 []ConnLimitPerSubnet
 	connLimitPerSubnetV6 []ConnLimitPerSubnet
-	ip4connsPerLimit     []map[netip.Prefix]int
-	ip6connsPerLimit     []map[netip.Prefix]int
+	ip4connsPerLimit     []map[string]int
+	ip6connsPerLimit     []map[string]int
 }
 
 func newConnLimiter() *connLimiter {
@@ -180,7 +180,7 @@ func (cl *connLimiter) addConn(ip netip.Addr) bool {
 	}
 
 	if len(connsPerLimit) == 0 && len(limits) > 0 {
-		connsPerLimit = make([]map[netip.Prefix]int, len(limits))
+		connsPerLimit = make([]map[string]int, len(limits))
 		if isIP6 {
 			cl.ip6connsPerLimit = connsPerLimit
 		} else {
@@ -193,12 +193,13 @@ func (cl *connLimiter) addConn(ip netip.Addr) bool {
 		if err != nil {
 			return false
 		}
-		counts, ok := connsPerLimit[i][prefix]
+		masked := prefix.String()
+		counts, ok := connsPerLimit[i][masked]
 		if !ok {
 			if connsPerLimit[i] == nil {
-				connsPerLimit[i] = make(map[netip.Prefix]int)
+				connsPerLimit[i] = make(map[string]int)
 			}
-			connsPerLimit[i][prefix] = 0
+			connsPerLimit[i][masked] = 0
 		}
 		if counts+1 > limit.ConnCount {
 			return false
@@ -208,7 +209,8 @@ func (cl *connLimiter) addConn(ip netip.Addr) bool {
 	// All limit checks passed, now we update the counts
 	for i, limit := range limits {
 		prefix, _ := ip.Prefix(limit.PrefixLength)
-		connsPerLimit[i][prefix]++
+		masked := prefix.String()
+		connsPerLimit[i][masked]++
 	}
 
 	return true
@@ -256,7 +258,7 @@ func (cl *connLimiter) rmConn(ip netip.Addr) {
 	if len(connsPerLimit) == 0 && len(limits) > 0 {
 		// Initialize just in case. We should have already initialized in
 		// addConn, but if the callers calls rmConn first we don't want to panic
-		connsPerLimit = make([]map[netip.Prefix]int, len(limits))
+		connsPerLimit = make([]map[string]int, len(limits))
 		if isIP6 {
 			cl.ip6connsPerLimit = connsPerLimit
 		} else {
@@ -271,15 +273,16 @@ func (cl *connLimiter) rmConn(ip netip.Addr) {
 			log.Errorf("unexpected error getting prefix: %v", err)
 			continue
 		}
-		counts, ok := connsPerLimit[i][prefix]
+		masked := prefix.String()
+		counts, ok := connsPerLimit[i][masked]
 		if !ok || counts == 0 {
 			// Unexpected, but don't panic
-			log.Errorf("unexpected conn count for %s ok=%v count=%v", prefix, ok, counts)
+			log.Errorf("unexpected conn count for %s ok=%v count=%v", masked, ok, counts)
 			continue
 		}
-		connsPerLimit[i][prefix]--
-		if connsPerLimit[i][prefix] <= 0 {
-			delete(connsPerLimit[i], prefix)
+		connsPerLimit[i][masked]--
+		if connsPerLimit[i][masked] <= 0 {
+			delete(connsPerLimit[i], masked)
 		}
 	}
 }
diff --git a/p2p/internal/rate/limiter.go b/p2p/internal/rate/limiter.go
new file mode 100644
index 00000000..d3cf1e22
--- /dev/null
+++ b/p2p/internal/rate/limiter.go
@@ -0,0 +1,319 @@
+// Package rate provides rate limiting functionality at a global, network, and subnet level.
+package rate
+
+import (
+	"container/heap"
+	"net/netip"
+	"slices"
+	"sync"
+	"time"
+
+	"github.com/libp2p/go-libp2p/core/network"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+	"golang.org/x/time/rate"
+)
+
+// Limit is the configuration for a token bucket rate limiter.
+// The bucket has a capacity of Burst, and is refilled at a rate of RPS tokens per second.
+// Initially, buckets are completley full, i.e. tokens in the bucket is equal to `Burst`.
+// In any given time interval T seconds, maximum events allowed will be `T*RPS + Burst`.
+type Limit struct {
+	// RPS is the rate of requests per second in steady state.
+	RPS float64
+	// Burst is the number of requests allowed over the RPS.
+	Burst int
+}
+
+// PrefixLimit is a rate limit configuration that applies to a specific network prefix.
+type PrefixLimit struct {
+	Prefix netip.Prefix
+	Limit
+}
+
+// SubnetLimit is a rate limit configuration that applies to a specific subnet.
+type SubnetLimit struct {
+	PrefixLength int
+	Limit
+}
+
+// Limiter rate limits new streams for a service. It allows setting NetworkPrefix specific,
+// global, and subnet specific limits. Use 0 for no rate limiting.
+// The limiter maintains state that must be periodically cleaned up using Cleanup
+type Limiter struct {
+	// NetworkPrefixLimits are limits for streams with peer IPs belonging to specific subnets.
+	// It can be used to increase the limit for trusted networks and decrease the limit for specific networks.
+	NetworkPrefixLimits []PrefixLimit
+	// GlobalLimit is the limit for all streams where the peer IP doesn't fall within any
+	// of the `NetworkPrefixLimits`
+	GlobalLimit Limit
+	// SubnetRateLimiter is a rate limiter for subnets.
+	SubnetRateLimiter SubnetLimiter
+
+	initOnce             sync.Once
+	globalBucket         *rate.Limiter
+	networkPrefixBuckets []*rate.Limiter // ith element ratelimits ith NetworkPrefixLimits
+}
+
+func (r *Limiter) init() {
+	r.initOnce.Do(func() {
+		if r.GlobalLimit.RPS == 0 {
+			r.globalBucket = rate.NewLimiter(rate.Inf, 0)
+		} else {
+			r.globalBucket = rate.NewLimiter(rate.Limit(r.GlobalLimit.RPS), r.GlobalLimit.Burst)
+		}
+		// sort such that the widest prefix (smallest bit count) is last.
+		slices.SortFunc(r.NetworkPrefixLimits, func(a, b PrefixLimit) int { return b.Prefix.Bits() - a.Prefix.Bits() })
+		r.networkPrefixBuckets = make([]*rate.Limiter, 0, len(r.NetworkPrefixLimits))
+		for _, limit := range r.NetworkPrefixLimits {
+			if limit.RPS == 0 {
+				r.networkPrefixBuckets = append(r.networkPrefixBuckets, rate.NewLimiter(rate.Inf, 0))
+			} else {
+				r.networkPrefixBuckets = append(r.networkPrefixBuckets, rate.NewLimiter(rate.Limit(limit.RPS), limit.Burst))
+			}
+		}
+	})
+}
+
+// Limit rate limits a StreamHandler function.
+func (r *Limiter) Limit(f func(s network.Stream)) func(s network.Stream) {
+	r.init()
+	return func(s network.Stream) {
+		if !r.allow(s.Conn().RemoteMultiaddr()) {
+			_ = s.ResetWithError(network.StreamRateLimited)
+			return
+		}
+		f(s)
+	}
+}
+
+func (r *Limiter) allow(addr ma.Multiaddr) bool {
+	r.init()
+	// Check buckets from the most specific to the least.
+	//
+	// This ensures that a single peer cannot take up all the tokens in the global
+	// rate limiting bucket. We *MUST* follow this order because the rate limiter
+	// implementation doesn't have a `ReturnToken` method. If we checked the global
+	// bucket before the specific bucket, and the specific bucket rejected the
+	// request, there's no way to return the token to the global bucket. So all
+	// rejected requests from the specific bucket would take up tokens from the global bucket.
+	ip, err := manet.ToIP(addr)
+	if err != nil {
+		return r.globalBucket.Allow()
+	}
+	ipAddr, ok := netip.AddrFromSlice(ip)
+	if !ok {
+		return r.globalBucket.Allow()
+	}
+
+	// prefixs have been sorted from most to least specific so rejected requests for more
+	// specific prefixes don't take up tokens from the less specific prefixes.
+	isWithinNetworkPrefix := false
+	for i, limit := range r.NetworkPrefixLimits {
+		if limit.Prefix.Contains(ipAddr) {
+			if !r.networkPrefixBuckets[i].Allow() {
+				return false
+			}
+			isWithinNetworkPrefix = true
+		}
+	}
+	if isWithinNetworkPrefix {
+		return true
+	}
+
+	if !r.SubnetRateLimiter.Allow(ipAddr, time.Now()) {
+		return false
+	}
+	return r.globalBucket.Allow()
+}
+
+// SubnetLimiter rate limits requests per ip subnet.
+type SubnetLimiter struct {
+	// IPv4SubnetLimits are the per subnet limits for streams with IPv4 Peers.
+	IPv4SubnetLimits []SubnetLimit
+	// IPv6SubnetLimits are the per subnet limits for streams with IPv6 Peers.
+	IPv6SubnetLimits []SubnetLimit
+	// GracePeriod is the time to wait to remove a full capacity bucket.
+	// Keeping a bucket around helps prevent allocations
+	GracePeriod time.Duration
+
+	initOnce  sync.Once
+	mx        sync.Mutex
+	ipv4Heaps []*bucketHeap
+	ipv6Heaps []*bucketHeap
+}
+
+func (s *SubnetLimiter) init() {
+	s.initOnce.Do(func() {
+		// smaller prefix length, i.e. largest subnet, last
+		slices.SortFunc(s.IPv4SubnetLimits, func(a, b SubnetLimit) int { return b.PrefixLength - a.PrefixLength })
+		slices.SortFunc(s.IPv6SubnetLimits, func(a, b SubnetLimit) int { return b.PrefixLength - a.PrefixLength })
+
+		s.ipv4Heaps = make([]*bucketHeap, len(s.IPv4SubnetLimits))
+		for i := range s.IPv4SubnetLimits {
+			s.ipv4Heaps[i] = &bucketHeap{
+				prefixBucket:  make([]prefixBucketWithExpiry, 0),
+				prefixToIndex: make(map[netip.Prefix]int),
+			}
+			heap.Init(s.ipv4Heaps[i])
+		}
+
+		s.ipv6Heaps = make([]*bucketHeap, len(s.IPv6SubnetLimits))
+		for i := range s.IPv6SubnetLimits {
+			s.ipv6Heaps[i] = &bucketHeap{
+				prefixBucket:  make([]prefixBucketWithExpiry, 0),
+				prefixToIndex: make(map[netip.Prefix]int),
+			}
+			heap.Init(s.ipv6Heaps[i])
+		}
+	})
+}
+
+// Allow returns true if requests for `ipAddr` are within specified rate limits
+func (s *SubnetLimiter) Allow(ipAddr netip.Addr, now time.Time) bool {
+	s.init()
+	s.mx.Lock()
+	defer s.mx.Unlock()
+
+	s.cleanUp(now)
+
+	var subNetLimits []SubnetLimit
+	var heaps []*bucketHeap
+	if ipAddr.Is4() {
+		subNetLimits = s.IPv4SubnetLimits
+		heaps = s.ipv4Heaps
+	} else {
+		subNetLimits = s.IPv6SubnetLimits
+		heaps = s.ipv6Heaps
+	}
+
+	for i, limit := range subNetLimits {
+		prefix, err := ipAddr.Prefix(limit.PrefixLength)
+		if err != nil {
+			return false // we have a ipaddr this shouldn't happen
+		}
+
+		bucket := heaps[i].Get(prefix)
+		if bucket == (prefixBucketWithExpiry{}) {
+			bucket = prefixBucketWithExpiry{
+				Prefix:      prefix,
+				tokenBucket: tokenBucket{rate.NewLimiter(rate.Limit(limit.RPS), limit.Burst)},
+				Expiry:      now,
+			}
+		}
+
+		if !bucket.Allow() {
+			// bucket is empty, its expiry would have been set correctly the last time
+			// it allowed a request.
+			return false
+		}
+		bucket.Expiry = bucket.FullAt(now).Add(s.GracePeriod)
+		heaps[i].Upsert(bucket)
+	}
+	return true
+}
+
+// cleanUp removes limiters that have expired by now.
+func (s *SubnetLimiter) cleanUp(now time.Time) {
+	for _, h := range s.ipv4Heaps {
+		h.Expire(now)
+	}
+	for _, h := range s.ipv6Heaps {
+		h.Expire(now)
+	}
+}
+
+// tokenBucket is a *rate.Limiter with a `FullAt` method.
+type tokenBucket struct {
+	*rate.Limiter
+}
+
+// FullAt returns the instant at which the bucket will be full.
+func (b *tokenBucket) FullAt(now time.Time) time.Time {
+	tokensNeeded := float64(b.Burst()) - b.TokensAt(now)
+	refillRate := float64(b.Limit())
+	eta := time.Duration((tokensNeeded / refillRate) * float64(time.Second))
+	return now.Add(eta)
+}
+
+// prefixBucketWithExpiry is a token bucket with a prefix and Expiry. The expiry is when the bucket
+// will be full with tokens.
+type prefixBucketWithExpiry struct {
+	tokenBucket
+	Prefix netip.Prefix
+	Expiry time.Time
+}
+
+// bucketHeap is a heap of buckets ordered by their Expiry. At expiry, the bucket
+// is removed from the heap as a full bucket is indistinguishable from a new bucket.
+type bucketHeap struct {
+	prefixBucket  []prefixBucketWithExpiry
+	prefixToIndex map[netip.Prefix]int
+}
+
+var _ heap.Interface = (*bucketHeap)(nil)
+
+// Upsert replaces the bucket with prefix `b.Prefix` with the provided bucket, `b`, or
+// inserts `b` if no bucket with prefix `b.Prefix` exists.
+func (h *bucketHeap) Upsert(b prefixBucketWithExpiry) {
+	if i, ok := h.prefixToIndex[b.Prefix]; ok {
+		h.prefixBucket[i] = b
+		heap.Fix(h, i)
+		return
+	}
+	heap.Push(h, b)
+}
+
+// Get returns the limiter for a prefix
+func (h *bucketHeap) Get(prefix netip.Prefix) prefixBucketWithExpiry {
+	if i, ok := h.prefixToIndex[prefix]; ok {
+		return h.prefixBucket[i]
+	}
+	return prefixBucketWithExpiry{}
+}
+
+// Expire removes elements with expiry before `expiry`
+func (h *bucketHeap) Expire(expiry time.Time) {
+	for h.Len() > 0 {
+		oldest := h.prefixBucket[0]
+		if oldest.Expiry.After(expiry) {
+			break
+		}
+		heap.Pop(h)
+	}
+}
+
+// Methods for the heap interface
+
+// Len returns the length of the heap
+func (h *bucketHeap) Len() int {
+	return len(h.prefixBucket)
+}
+
+// Less compares two elements in the heap
+func (h *bucketHeap) Less(i, j int) bool {
+	return h.prefixBucket[i].Expiry.Before(h.prefixBucket[j].Expiry)
+}
+
+// Swap swaps two elements in the heap
+func (h *bucketHeap) Swap(i, j int) {
+	h.prefixBucket[i], h.prefixBucket[j] = h.prefixBucket[j], h.prefixBucket[i]
+	h.prefixToIndex[h.prefixBucket[i].Prefix] = i
+	h.prefixToIndex[h.prefixBucket[j].Prefix] = j
+}
+
+// Push adds a new element to the heap
+func (h *bucketHeap) Push(x any) {
+	item := x.(prefixBucketWithExpiry)
+	h.prefixBucket = append(h.prefixBucket, item)
+	h.prefixToIndex[item.Prefix] = len(h.prefixBucket) - 1
+}
+
+// Pop removes and returns the top element from the heap
+func (h *bucketHeap) Pop() any {
+	n := len(h.prefixBucket)
+	item := h.prefixBucket[n-1]
+	h.prefixBucket = h.prefixBucket[0 : n-1]
+	delete(h.prefixToIndex, item.Prefix)
+	return item
+}
diff --git a/p2p/net/nat/nat.go b/p2p/net/nat/nat.go
index 6721b626..c45b067f 100644
--- a/p2p/net/nat/nat.go
+++ b/p2p/net/nat/nat.go
@@ -239,10 +239,11 @@ func (nat *NAT) establishMapping(ctx context.Context, protocol string, internalP
 	nat.natmu.Unlock()
 
 	if err != nil || externalPort == 0 {
+		// TODO: log.Event
 		if err != nil {
-			log.Warnf("NAT port mapping failed: protocol=%s internal_port=%d error=%q", protocol, internalPort, err)
+			log.Warnf("failed to establish port mapping: %s", err)
 		} else {
-			log.Warnf("NAT port mapping failed: protocol=%s internal_port=%d external_port=0", protocol, internalPort)
+			log.Warnf("failed to establish port mapping: newport = 0")
 		}
 		// we do not close if the mapping failed,
 		// because it may work again next time.
diff --git a/p2p/protocol/autonatv2/server.go b/p2p/protocol/autonatv2/server.go
index bb10e1e4..3c6d0d53 100644
--- a/p2p/protocol/autonatv2/server.go
+++ b/p2p/protocol/autonatv2/server.go
@@ -213,7 +213,7 @@ func (as *server) serveDialRequest(s network.Stream) EventDialRequestCompleted {
 	nonce := msg.GetDialRequest().Nonce
 
 	isDialDataRequired := as.dialDataRequestPolicy(s.Conn().RemoteMultiaddr(), dialAddr)
-	if isDialDataRequired && !as.limiter.AcceptDialDataRequest() {
+	if isDialDataRequired && !as.limiter.AcceptDialDataRequest(p) {
 		msg = pb.Message{
 			Msg: &pb.Message_DialResponse{
 				DialResponse: &pb.DialResponse{
@@ -442,7 +442,7 @@ func (r *rateLimiter) Accept(p peer.ID) bool {
 	return true
 }
 
-func (r *rateLimiter) AcceptDialDataRequest() bool {
+func (r *rateLimiter) AcceptDialDataRequest(p peer.ID) bool {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 	if r.closed {
diff --git a/p2p/protocol/identify/id.go b/p2p/protocol/identify/id.go
index 8350421c..b290abb5 100644
--- a/p2p/protocol/identify/id.go
+++ b/p2p/protocol/identify/id.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net/netip"
 	"slices"
 	"sync"
 	"time"
@@ -19,6 +20,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/protocol"
 	"github.com/libp2p/go-libp2p/core/record"
 	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
+	"github.com/libp2p/go-libp2p/p2p/internal/rate"
 	useragent "github.com/libp2p/go-libp2p/p2p/protocol/identify/internal/user-agent"
 	"github.com/libp2p/go-libp2p/p2p/protocol/identify/pb"
 
@@ -55,6 +57,21 @@ const (
 	connectedPeerMaxAddrs         = 500
 )
 
+var (
+	defaultNetworkPrefixRateLimits = []rate.PrefixLimit{
+		{Prefix: netip.MustParsePrefix("127.0.0.0/8"), Limit: rate.Limit{}}, // inf
+		{Prefix: netip.MustParsePrefix("::1/128"), Limit: rate.Limit{}},     // inf
+	}
+	defaultGlobalRateLimit      = rate.Limit{RPS: 2000, Burst: 3000}
+	defaultIPv4SubnetRateLimits = []rate.SubnetLimit{
+		{PrefixLength: 24, Limit: rate.Limit{RPS: 0.2, Burst: 10}}, // 1 every 5 seconds
+	}
+	defaultIPv6SubnetRateLimits = []rate.SubnetLimit{
+		{PrefixLength: 56, Limit: rate.Limit{RPS: 0.2, Burst: 10}}, // 1 every 5 seconds
+		{PrefixLength: 48, Limit: rate.Limit{RPS: 0.5, Burst: 20}}, // 1 every 2 seconds
+	}
+)
+
 type identifySnapshot struct {
 	seq       uint64
 	protocols []protocol.ID
@@ -174,6 +191,8 @@ type idService struct {
 	}
 
 	natEmitter *natEmitter
+
+	rateLimiter *rate.Limiter
 }
 
 type normalizer interface {
@@ -207,6 +226,15 @@ func NewIDService(h host.Host, opts ...Option) (*idService, error) {
 		setupCompleted:          make(chan struct{}),
 		metricsTracer:           cfg.metricsTracer,
 		timeout:                 cfg.timeout,
+		rateLimiter: &rate.Limiter{
+			GlobalLimit:         defaultGlobalRateLimit,
+			NetworkPrefixLimits: defaultNetworkPrefixRateLimits,
+			SubnetRateLimiter: rate.SubnetLimiter{
+				IPv4SubnetLimits: defaultIPv4SubnetRateLimits,
+				IPv6SubnetLimits: defaultIPv6SubnetRateLimits,
+				GracePeriod:      1 * time.Minute,
+			},
+		},
 	}
 
 	var normalize func(ma.Multiaddr) ma.Multiaddr
@@ -249,7 +277,7 @@ func NewIDService(h host.Host, opts ...Option) (*idService, error) {
 func (ids *idService) Start() {
 	ids.Host.Network().Notify((*netNotifiee)(ids))
 	ids.Host.SetStreamHandler(ID, ids.handleIdentifyRequest)
-	ids.Host.SetStreamHandler(IDPush, ids.handlePush)
+	ids.Host.SetStreamHandler(IDPush, ids.rateLimiter.Limit(ids.handlePush))
 	ids.updateSnapshot()
 	close(ids.setupCompleted)
 
@@ -869,7 +897,6 @@ func (ids *idService) consumeMessage(mes *pb.Identify, c network.Conn, isPush bo
 		ProtocolVersion:  pv,
 		AgentVersion:     av,
 	})
-
 }
 
 func (ids *idService) consumeSignedPeerRecord(p peer.ID, signedPeerRecord *record.Envelope) ([]ma.Multiaddr, error) {


Test patch:
diff --git a/p2p/host/resource-manager/conn_limiter_test.go b/p2p/host/resource-manager/conn_limiter_test.go
index 5c06b65a..89ce5d53 100644
--- a/p2p/host/resource-manager/conn_limiter_test.go
+++ b/p2p/host/resource-manager/conn_limiter_test.go
@@ -24,22 +24,6 @@ func TestItLimits(t *testing.T) {
 		require.NoError(t, err)
 		require.True(t, cl.addConn(otherIP))
 	})
-
-	t.Run("IPv4 removal", func(t *testing.T) {
-		ip, err := netip.ParseAddr("1.2.3.4")
-		require.NoError(t, err)
-		cl := newConnLimiter()
-		cl.connLimitPerSubnetV4[0].ConnCount = 1
-		require.True(t, cl.addConn(ip))
-
-		// should fail the second time
-		require.False(t, cl.addConn(ip))
-		// remove the connection
-		cl.rmConn(ip)
-		// should succeed now
-		require.True(t, cl.addConn(ip))
-	})
-
 	t.Run("IPv6", func(t *testing.T) {
 		ip, err := netip.ParseAddr("1:2:3:4::1")
 		require.NoError(t, err)
diff --git a/p2p/internal/rate/limiter_test.go b/p2p/internal/rate/limiter_test.go
new file mode 100644
index 00000000..c1126833
--- /dev/null
+++ b/p2p/internal/rate/limiter_test.go
@@ -0,0 +1,257 @@
+package rate
+
+import (
+	"fmt"
+	"net/netip"
+	"testing"
+	"time"
+
+	ma "github.com/multiformats/go-multiaddr"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/time/rate"
+)
+
+const rateLimitErrorTolerance = 0.05
+
+func getSleepDurationAndRequestCount(rps float64) (time.Duration, int) {
+	sleepDuration := 100 * time.Millisecond
+	requestCount := int(sleepDuration.Seconds() * float64(rps))
+	if requestCount < 1 {
+		// Adding 1ms to ensure we do get 1 request. If the rate is low enough that
+		// 100ms won't have a single request adding 1ms won't error here.
+		sleepDuration = time.Duration((1/rps)*float64(time.Second)) + 1*time.Millisecond
+		requestCount = 1
+	}
+	return sleepDuration, requestCount
+}
+
+func assertLimiter(t *testing.T, rl *Limiter, addr ma.Multiaddr, allowed, errorMargin int) {
+	t.Helper()
+	for i := 0; i < allowed; i++ {
+		require.True(t, rl.allow(addr))
+	}
+	for i := 0; i < errorMargin; i++ {
+		rl.allow(addr)
+	}
+	require.False(t, rl.allow(addr))
+}
+
+func TestLimiterGlobal(t *testing.T) {
+	addr := ma.StringCast("/ip4/127.0.0.1/udp/123/quic-v1")
+	limits := []Limit{
+		{RPS: 0.0, Burst: 1},
+		{RPS: 0.8, Burst: 1},
+		{RPS: 10, Burst: 20},
+		{RPS: 100, Burst: 200},
+		{RPS: 1000, Burst: 2000},
+	}
+	for _, limit := range limits {
+		t.Run(fmt.Sprintf("limit %0.1f", limit.RPS), func(t *testing.T) {
+			rl := &Limiter{
+				GlobalLimit: limit,
+			}
+			if limit.RPS == 0 {
+				// 0 implies no rate limiting, any large number would do
+				for i := 0; i < 1000; i++ {
+					require.True(t, rl.allow(addr))
+				}
+				return
+			}
+			assertLimiter(t, rl, addr, limit.Burst, int(limit.RPS*rateLimitErrorTolerance))
+			sleepDuration, requestCount := getSleepDurationAndRequestCount(limit.RPS)
+			time.Sleep(sleepDuration)
+			assertLimiter(t, rl, addr, requestCount, int(float64(requestCount)*rateLimitErrorTolerance))
+		})
+	}
+}
+
+func TestLimiterNetworkPrefix(t *testing.T) {
+	local := ma.StringCast("/ip4/127.0.0.1/udp/123/quic-v1")
+	public := ma.StringCast("/ip4/1.1.1.1/udp/123/quic-v1")
+	rl := &Limiter{
+		NetworkPrefixLimits: []PrefixLimit{
+			{Prefix: netip.MustParsePrefix("127.0.0.0/24"), Limit: Limit{}},
+		},
+		GlobalLimit: Limit{RPS: 10, Burst: 10},
+	}
+	// element within prefix is allowed even over the limit
+	for range rl.GlobalLimit.Burst + 100 {
+		require.True(t, rl.allow(local))
+	}
+	// rate limit public ips
+	assertLimiter(t, rl, public, rl.GlobalLimit.Burst, int(rl.GlobalLimit.RPS*rateLimitErrorTolerance))
+
+	// public ip rejected
+	require.False(t, rl.allow(public))
+	// local ip accepted
+	for range 100 {
+		require.True(t, rl.allow(local))
+	}
+}
+
+func TestLimiterNetworkPrefixWidth(t *testing.T) {
+	a1 := ma.StringCast("/ip4/1.1.1.1/udp/123/quic-v1")
+	a2 := ma.StringCast("/ip4/1.1.0.1/udp/123/quic-v1")
+
+	wideLimit := 20
+	narrowLimit := 10
+	rl := &Limiter{
+		NetworkPrefixLimits: []PrefixLimit{
+			{Prefix: netip.MustParsePrefix("1.1.0.0/16"), Limit: Limit{RPS: 0.01, Burst: wideLimit}},
+			{Prefix: netip.MustParsePrefix("1.1.1.0/24"), Limit: Limit{RPS: 0.01, Burst: narrowLimit}},
+		},
+	}
+	for range 2 * wideLimit {
+		rl.allow(a1)
+	}
+	// a1 rejected
+	require.False(t, rl.allow(a1))
+	// a2 accepted
+	for range wideLimit - narrowLimit {
+		require.True(t, rl.allow(a2))
+	}
+}
+
+func subnetAddrs(prefix netip.Prefix) func() netip.Addr {
+	next := prefix.Addr()
+	return func() netip.Addr {
+		addr := next
+		next = addr.Next()
+		if !prefix.Contains(addr) {
+			next = prefix.Addr()
+			addr = next
+		}
+		return addr
+	}
+}
+
+func TestSubnetLimiter(t *testing.T) {
+	assertOutput := func(outcome bool, rl *SubnetLimiter, subnetAddrs func() netip.Addr, n int) {
+		t.Helper()
+		for range n {
+			require.Equal(t, outcome, rl.Allow(subnetAddrs(), time.Now()), "%d", n)
+		}
+	}
+
+	t.Run("Simple", func(*testing.T) {
+		// Keep the refil rate low
+		v4Small := SubnetLimit{PrefixLength: 24, Limit: Limit{RPS: 0.0001, Burst: 10}}
+		v4Large := SubnetLimit{PrefixLength: 16, Limit: Limit{RPS: 0.0001, Burst: 19}}
+
+		v6Small := SubnetLimit{PrefixLength: 64, Limit: Limit{RPS: 0.0001, Burst: 10}}
+		v6Large := SubnetLimit{PrefixLength: 48, Limit: Limit{RPS: 0.0001, Burst: 17}}
+		rl := &SubnetLimiter{
+			IPv4SubnetLimits: []SubnetLimit{v4Large, v4Small},
+			IPv6SubnetLimits: []SubnetLimit{v6Large, v6Small},
+		}
+
+		v4SubnetAddr1 := subnetAddrs(netip.MustParsePrefix("192.168.1.1/24"))
+		v4SubnetAddr2 := subnetAddrs(netip.MustParsePrefix("192.168.2.1/24"))
+		v6SubnetAddr1 := subnetAddrs(netip.MustParsePrefix("2001:0:0:1::/64"))
+		v6SubnetAddr2 := subnetAddrs(netip.MustParsePrefix("2001:0:0:2::/64"))
+
+		assertOutput(true, rl, v4SubnetAddr1, v4Small.Burst)
+		assertOutput(false, rl, v4SubnetAddr1, v4Large.Burst)
+
+		assertOutput(true, rl, v4SubnetAddr2, v4Large.Burst-v4Small.Burst)
+		assertOutput(false, rl, v4SubnetAddr2, v4Large.Burst)
+
+		assertOutput(true, rl, v6SubnetAddr1, v6Small.Burst)
+		assertOutput(false, rl, v6SubnetAddr1, v6Large.Burst)
+
+		assertOutput(true, rl, v6SubnetAddr2, v6Large.Burst-v6Small.Burst)
+		assertOutput(false, rl, v6SubnetAddr2, v6Large.Burst)
+	})
+
+	t.Run("Complex", func(*testing.T) {
+		limits := []SubnetLimit{
+			{PrefixLength: 32, Limit: Limit{RPS: 0.01, Burst: 10}},
+			{PrefixLength: 24, Limit: Limit{RPS: 0.01, Burst: 20}},
+			{PrefixLength: 16, Limit: Limit{RPS: 0.01, Burst: 30}},
+			{PrefixLength: 8, Limit: Limit{RPS: 0.01, Burst: 40}},
+		}
+		rl := &SubnetLimiter{
+			IPv4SubnetLimits: limits,
+		}
+
+		snAddrs := []func() netip.Addr{
+			subnetAddrs(netip.MustParsePrefix("192.168.1.1/32")),
+			subnetAddrs(netip.MustParsePrefix("192.168.1.2/24")),
+			subnetAddrs(netip.MustParsePrefix("192.168.2.1/16")),
+			subnetAddrs(netip.MustParsePrefix("192.0.1.1/8")),
+		}
+		for i, addrsFunc := range snAddrs {
+			prev := 0
+			if i > 0 {
+				prev = limits[i-1].Burst
+			}
+			assertOutput(true, rl, addrsFunc, limits[i].Burst-prev)
+			assertOutput(false, rl, addrsFunc, limits[i].Burst)
+		}
+	})
+
+	t.Run("Zero", func(t *testing.T) {
+		sl := SubnetLimiter{}
+		for range 10000 {
+			require.True(t, sl.Allow(netip.IPv6Loopback(), time.Now()))
+		}
+	})
+}
+
+func TestSubnetLimiterCleanup(t *testing.T) {
+	tc := []struct {
+		Limit
+		TTL time.Duration
+	}{
+		{Limit: Limit{RPS: 1, Burst: 10}, TTL: 10 * time.Second},
+		{Limit: Limit{RPS: 0.1, Burst: 2}, TTL: 20 * time.Second},
+		{Limit: Limit{RPS: 1, Burst: 100}, TTL: 100 * time.Second},
+		{Limit: Limit{RPS: 3, Burst: 6}, TTL: 2 * time.Second},
+	}
+	for i, tt := range tc {
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			ip1, ip2 := netip.IPv6Loopback(), netip.MustParseAddr("2001::")
+			sl := SubnetLimiter{IPv6SubnetLimits: []SubnetLimit{{PrefixLength: 64, Limit: tt.Limit}}}
+			now := time.Now()
+			// Empty the ip1 bucket
+			for range tt.Burst {
+				require.True(t, sl.Allow(ip1, now))
+			}
+			for range tt.Burst / 2 {
+				require.True(t, sl.Allow(ip2, now))
+			}
+			epsilon := 100 * time.Millisecond
+			// just before ip1 expiry
+			now = now.Add(tt.TTL).Add(-epsilon)
+			sl.cleanUp(now) // ip2 will be removed
+			require.Equal(t, 1, sl.ipv6Heaps[0].Len())
+			// just after ip1 expiry
+			now = now.Add(2 * epsilon)
+			require.True(t, sl.Allow(ip2, now))        // remove the ip1 bucket
+			require.Equal(t, 1, sl.ipv6Heaps[0].Len()) // ip2 added in the previous call
+		})
+	}
+}
+
+func TestTokenBucketFullAfter(t *testing.T) {
+	tc := []struct {
+		*rate.Limiter
+		FullAfter time.Duration
+	}{
+		{Limiter: rate.NewLimiter(1, 10), FullAfter: 10 * time.Second},
+		{Limiter: rate.NewLimiter(0.01, 10), FullAfter: 1000 * time.Second},
+		{Limiter: rate.NewLimiter(0.01, 1), FullAfter: 100 * time.Second},
+	}
+	for i, tt := range tc {
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			b := tokenBucket{tt.Limiter}
+			now := time.Now()
+			for range b.Burst() {
+				tt.Allow()
+			}
+			epsilon := 10 * time.Millisecond
+			require.GreaterOrEqual(t, tt.FullAfter+epsilon, b.FullAt(now).Sub(now))
+			require.LessOrEqual(t, tt.FullAfter-epsilon, b.FullAt(now).Sub(now))
+		})
+	}
+}
diff --git a/p2p/protocol/autonatv2/server_test.go b/p2p/protocol/autonatv2/server_test.go
index c65aa5b8..65b1b131 100644
--- a/p2p/protocol/autonatv2/server_test.go
+++ b/p2p/protocol/autonatv2/server_test.go
@@ -445,7 +445,7 @@ func TestRateLimiterStress(t *testing.T) {
 							success.Add(1)
 							peerSuccesses[j].Add(1)
 						}
-						if r.AcceptDialDataRequest() {
+						if r.AcceptDialDataRequest(p) {
 							dialDataSuccesses.Add(1)
 						}
 						r.CompleteRequest(p)
diff --git a/p2p/security/tls/crypto_test.go b/p2p/security/tls/crypto_test.go
index 6171d178..efec9490 100644
--- a/p2p/security/tls/crypto_test.go
+++ b/p2p/security/tls/crypto_test.go
@@ -5,7 +5,6 @@ import (
 	"encoding/hex"
 	"testing"
 
-	"github.com/libp2p/go-libp2p/core/crypto/pb"
 	"github.com/libp2p/go-libp2p/core/peer"
 
 	"github.com/stretchr/testify/assert"
@@ -54,35 +53,31 @@ func TestNewIdentityCertificates(t *testing.T) {
 
 func TestVectors(t *testing.T) {
 	type testcase struct {
-		name    string
-		data    string
-		peerID  string
-		keyType pb.KeyType
-		error   string
+		name   string
+		data   string
+		peerID string
+		error  string
 	}
 
 	testcases := []testcase{
 		{
-			name:    "ED25519 Peer ID",
-			data:    "308201ae30820156a0030201020204499602d2300a06082a8648ce3d040302302031123010060355040a13096c69627032702e696f310a300806035504051301313020170d3735303130313133303030305a180f34303936303130313133303030305a302031123010060355040a13096c69627032702e696f310a300806035504051301313059301306072a8648ce3d020106082a8648ce3d030107034200040c901d423c831ca85e27c73c263ba132721bb9d7a84c4f0380b2a6756fd601331c8870234dec878504c174144fa4b14b66a651691606d8173e55bd37e381569ea37c307a3078060a2b0601040183a25a0101046a3068042408011220a77f1d92fedb59dddaea5a1c4abd1ac2fbde7d7b879ed364501809923d7c11b90440d90d2769db992d5e6195dbb08e706b6651e024fda6cfb8846694a435519941cac215a8207792e42849cccc6cd8136c6e4bde92a58c5e08cfd4206eb5fe0bf909300a06082a8648ce3d0403020346003043021f50f6b6c52711a881778718238f650c9fb48943ae6ee6d28427dc6071ae55e702203625f116a7a454db9c56986c82a25682f7248ea1cb764d322ea983ed36a31b77",
-			peerID:  "12D3KooWM6CgA9iBFZmcYAHA6A2qvbAxqfkmrYiRQuz3XEsk4Ksv",
-			keyType: pb.KeyType_Ed25519,
+			name:   "ED25519 Peer ID",
+			data:   "308201773082011ea003020102020900f5bd0debaa597f52300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200046bf9871220d71dcb3483ecdfcbfcc7c103f8509d0974b3c18ab1f1be1302d643103a08f7a7722c1b247ba3876fe2c59e26526f479d7718a85202ddbe47562358a37f307d307b060a2b0601040183a25a01010101ff046a30680424080112207fda21856709c5ae12fd6e8450623f15f11955d384212b89f56e7e136d2e17280440aaa6bffabe91b6f30c35e3aa4f94b1188fed96b0ffdd393f4c58c1c047854120e674ce64c788406d1c2c4b116581fd7411b309881c3c7f20b46e54c7e6fe7f0f300a06082a8648ce3d040302034700304402207d1a1dbd2bda235ff2ec87daf006f9b04ba076a5a5530180cd9c2e8f6399e09d0220458527178c7e77024601dbb1b256593e9b96d961b96349d1f560114f61a87595",
+			peerID: "12D3KooWJRSrypvnpHgc6ZAgyCni4KcSmbV7uGRaMw5LgMKT18fq",
 		},
 		{
-			name:    "ECDSA Peer ID",
-			data:    "308201f63082019da0030201020204499602d2300a06082a8648ce3d040302302031123010060355040a13096c69627032702e696f310a300806035504051301313020170d3735303130313133303030305a180f34303936303130313133303030305a302031123010060355040a13096c69627032702e696f310a300806035504051301313059301306072a8648ce3d020106082a8648ce3d030107034200040c901d423c831ca85e27c73c263ba132721bb9d7a84c4f0380b2a6756fd601331c8870234dec878504c174144fa4b14b66a651691606d8173e55bd37e381569ea381c23081bf3081bc060a2b0601040183a25a01010481ad3081aa045f0803125b3059301306072a8648ce3d020106082a8648ce3d03010703420004bf30511f909414ebdd3242178fd290f093a551cf75c973155de0bb5a96fedf6cb5d52da7563e794b512f66e60c7f55ba8a3acf3dd72a801980d205e8a1ad29f2044730450220064ea8124774caf8f50e57f436aa62350ce652418c019df5d98a3ac666c9386a022100aa59d704a931b5f72fb9222cb6cc51f954d04a4e2e5450f8805fe8918f71eaae300a06082a8648ce3d04030203470030440220799395b0b6c1e940a7e4484705f610ab51ed376f19ff9d7c16757cfbf61b8d4302206205c03fbb0f95205c779be86581d3e31c01871ad5d1f3435bcf375cb0e5088a",
-			peerID:  "QmfXbAwNjJLXfesgztEHe8HwgVDCMMpZ9Eax1HYq6hn9uE",
-			keyType: pb.KeyType_ECDSA,
+			name:   "ECDSA Peer ID",
+			data:   "308201c030820166a003020102020900eaf419a6e3edb4a6300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d030107034200048dbf1116c7c608d6d5292bd826c3feb53483a89fce434bf64538a359c8e07538ff71f6766239be6a146dcc1a5f3bb934bcd4ae2ae1d4da28ac68b4a20593f06ba381c63081c33081c0060a2b0601040183a25a01010101ff0481ae3081ab045f0803125b3059301306072a8648ce3d020106082a8648ce3d0301070342000484b93fa456a74bd0153919f036db7bc63c802f055bc7023395d0203de718ee0fc7b570b767cdd858aca6c7c4113ff002e78bd2138ac1a3b26dde3519e06979ad04483046022100bc84014cea5a41feabdf4c161096564b9ccf4b62fbef4fe1cd382c84e11101780221009204f086a84cb8ed8a9ddd7868dc90c792ee434adf62c66f99a08a5eba11615b300a06082a8648ce3d0403020348003045022054b437be9a2edf591312d68ff24bf91367ad4143f76cf80b5658f232ade820da022100e23b48de9df9c25d4c83ddddf75d2676f0b9318ee2a6c88a736d85eab94a912f",
+			peerID: "QmZcrvr3r4S3QvwFdae3c2EWTfo792Y14UpzCZurhmiWeX",
 		},
 		{
-			name:    "secp256k1 Peer ID",
-			data:    "308201ba3082015fa0030201020204499602d2300a06082a8648ce3d040302302031123010060355040a13096c69627032702e696f310a300806035504051301313020170d3735303130313133303030305a180f34303936303130313133303030305a302031123010060355040a13096c69627032702e696f310a300806035504051301313059301306072a8648ce3d020106082a8648ce3d030107034200040c901d423c831ca85e27c73c263ba132721bb9d7a84c4f0380b2a6756fd601331c8870234dec878504c174144fa4b14b66a651691606d8173e55bd37e381569ea38184308181307f060a2b0601040183a25a01010471306f0425080212210206dc6968726765b820f050263ececf7f71e4955892776c0970542efd689d2382044630440220145e15a991961f0d08cd15425bb95ec93f6ffa03c5a385eedc34ecf464c7a8ab022026b3109b8a3f40ef833169777eb2aa337cfb6282f188de0666d1bcec2a4690dd300a06082a8648ce3d0403020349003046022100e1a217eeef9ec9204b3f774a08b70849646b6a1e6b8b27f93dc00ed58545d9fe022100b00dafa549d0f03547878338c7b15e7502888f6d45db387e5ae6b5d46899cef0",
-			peerID:  "16Uiu2HAkutTMoTzDw1tCvSRtu6YoixJwS46S1ZFxW8hSx9fWHiPs",
-			keyType: pb.KeyType_Secp256k1,
+			name:   "secp256k1 Peer ID",
+			data:   "3082018230820128a003020102020900f3b305f55622cfdf300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d0301070342000458f7e9581748ff9bdd933b655cc0e5552a1248f840658cc221dec2186b5a2fe4641b86ab7590a3422cdbb1000cf97662f27e5910d7569f22feed8829c8b52e0fa38188308185308182060a2b0601040183a25a01010101ff0471306f042508021221026b053094d1112bce799dc8026040ae6d4eb574157929f1598172061f753d9b1b04463044022040712707e97794c478d93989aaa28ae1f71c03af524a8a4bd2d98424948a782302207b61b7f074b696a25fb9e0059141a811cccc4cc28042d9301b9b2a4015e87470300a06082a8648ce3d04030203480030450220143ae4d86fdc8675d2480bb6912eca5e39165df7f572d836aa2f2d6acfab13f8022100831d1979a98f0c4a6fb5069ca374de92f1a1205c962a6d90ad3d7554cb7d9df4",
+			peerID: "16Uiu2HAm2dSCBFxuge46aEt7U1oejtYuBUZXxASHqmcfVmk4gsbx",
 		},
 		{
 			name:  "Invalid certificate",
-			data:  "308201f73082019da0030201020204499602d2300a06082a8648ce3d040302302031123010060355040a13096c69627032702e696f310a300806035504051301313020170d3735303130313133303030305a180f34303936303130313133303030305a302031123010060355040a13096c69627032702e696f310a300806035504051301313059301306072a8648ce3d020106082a8648ce3d030107034200040c901d423c831ca85e27c73c263ba132721bb9d7a84c4f0380b2a6756fd601331c8870234dec878504c174144fa4b14b66a651691606d8173e55bd37e381569ea381c23081bf3081bc060a2b0601040183a25a01010481ad3081aa045f0803125b3059301306072a8648ce3d020106082a8648ce3d03010703420004bf30511f909414ebdd3242178fd290f093a551cf75c973155de0bb5a96fedf6cb5d52da7563e794b512f66e60c7f55ba8a3acf3dd72a801980d205e8a1ad29f204473045022100bb6e03577b7cc7a3cd1558df0da2b117dfdcc0399bc2504ebe7de6f65cade72802206de96e2a5be9b6202adba24ee0362e490641ac45c240db71fe955f2c5cf8df6e300a06082a8648ce3d0403020348003045022100e847f267f43717358f850355bdcabbefb2cfbf8a3c043b203a14788a092fe8db022027c1d04a2d41fd6b57a7e8b3989e470325de4406e52e084e34a3fd56eef0d0df",
+			data:  "308201773082011da003020102020830a73c5d896a1109300a06082a8648ce3d04030230003020170d3735303130313030303030305a180f34303936303130313030303030305a30003059301306072a8648ce3d020106082a8648ce3d03010703420004bbe62df9a7c1c46b7f1f21d556deec5382a36df146fb29c7f1240e60d7d5328570e3b71d99602b77a65c9b3655f62837f8d66b59f1763b8c9beba3be07778043a37f307d307b060a2b0601040183a25a01010101ff046a3068042408011220ec8094573afb9728088860864f7bcea2d4fd412fef09a8e2d24d482377c20db60440ecabae8354afa2f0af4b8d2ad871e865cb5a7c0c8d3dbdbf42de577f92461a0ebb0a28703e33581af7d2a4f2270fc37aec6261fcc95f8af08f3f4806581c730a300a06082a8648ce3d040302034800304502202dfb17a6fa0f94ee0e2e6a3b9fb6e986f311dee27392058016464bd130930a61022100ba4b937a11c8d3172b81e7cd04aedb79b978c4379c2b5b24d565dd5d67d3cb3c",
 			error: "signature invalid",
 		},
 	}
@@ -101,7 +96,6 @@ func TestVectors(t *testing.T) {
 				return
 			}
 			require.NoError(t, err)
-			require.Equal(t, tc.keyType, key.Type())
 			id, err := peer.IDFromPublicKey(key)
 			require.NoError(t, err)
 			expectedID, err := peer.Decode(tc.peerID)
diff --git a/scripts/test_analysis/cmd/gotest2sql/main.go b/scripts/test_analysis/cmd/gotest2sql/main.go
index f8c22590..05a247e2 100644
--- a/scripts/test_analysis/cmd/gotest2sql/main.go
+++ b/scripts/test_analysis/cmd/gotest2sql/main.go
@@ -11,7 +11,7 @@ import (
 	"os"
 	"time"
 
-	_ "modernc.org/sqlite"
+	_ "github.com/glebarez/go-sqlite"
 )
 
 type TestEvent struct {
diff --git a/scripts/test_analysis/go.mod b/scripts/test_analysis/go.mod
index 165036d0..89f60f02 100644
--- a/scripts/test_analysis/go.mod
+++ b/scripts/test_analysis/go.mod
@@ -2,17 +2,16 @@ module github.com/libp2p/go-libp2p/scripts/test_analysis
 
 go 1.23
 
-require modernc.org/sqlite v1.36.0
+require github.com/glebarez/go-sqlite v1.22.0
 
 require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
-	github.com/google/uuid v1.6.0 // indirect
+	github.com/google/uuid v1.5.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	modernc.org/libc v1.61.13 // indirect
-	modernc.org/mathutil v1.7.1 // indirect
-	modernc.org/memory v1.8.2 // indirect
+	golang.org/x/sys v0.15.0 // indirect
+	modernc.org/libc v1.37.6 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.7.2 // indirect
+	modernc.org/sqlite v1.28.0 // indirect
 )
diff --git a/scripts/test_analysis/go.sum b/scripts/test_analysis/go.sum
index e4f5e5a1..e635754c 100644
--- a/scripts/test_analysis/go.sum
+++ b/scripts/test_analysis/go.sum
@@ -1,47 +1,23 @@
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
-github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
+github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
-github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
-golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
-golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
-modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
-modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
-modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
-modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
-modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
-modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
-modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
-modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
-modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
-modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
-modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
-modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
-modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
-modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
-modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
-modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
-modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
-modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
-modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
-modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
-modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
-modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
+modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
+modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
diff --git a/scripts/test_analysis/main.go b/scripts/test_analysis/main.go
index a96e5598..e96a09cd 100644
--- a/scripts/test_analysis/main.go
+++ b/scripts/test_analysis/main.go
@@ -11,7 +11,7 @@ import (
 	"regexp"
 	"strings"
 
-	_ "modernc.org/sqlite"
+	_ "github.com/glebarez/go-sqlite"
 )
 
 const dbPath = "./test_results.db"
diff --git a/test-plans/go.mod b/test-plans/go.mod
index ef231457..c08cbfbb 100644
--- a/test-plans/go.mod
+++ b/test-plans/go.mod
@@ -96,6 +96,7 @@ require (
 	golang.org/x/sync v0.11.0 // indirect
 	golang.org/x/sys v0.30.0 // indirect
 	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.30.0 // indirect
 	google.golang.org/protobuf v1.36.5 // indirect
 	lukechampine.com/blake3 v1.4.0 // indirect
diff --git a/test-plans/go.sum b/test-plans/go.sum
index cfb9eba5..2cfac55b 100644
--- a/test-plans/go.sum
+++ b/test-plans/go.sum
@@ -423,8 +423,8 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=