github.com/MetalBlockchain/metalgo@v1.11.9/network/ip_tracker.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package network 5 6 import ( 7 "crypto/rand" 8 "errors" 9 "sync" 10 11 "github.com/prometheus/client_golang/prometheus" 12 "go.uber.org/zap" 13 14 "github.com/MetalBlockchain/metalgo/ids" 15 "github.com/MetalBlockchain/metalgo/snow/validators" 16 "github.com/MetalBlockchain/metalgo/utils/bloom" 17 "github.com/MetalBlockchain/metalgo/utils/crypto/bls" 18 "github.com/MetalBlockchain/metalgo/utils/ips" 19 "github.com/MetalBlockchain/metalgo/utils/logging" 20 "github.com/MetalBlockchain/metalgo/utils/sampler" 21 "github.com/MetalBlockchain/metalgo/utils/set" 22 ) 23 24 const ( 25 saltSize = 32 26 minCountEstimate = 128 27 targetFalsePositiveProbability = .001 28 maxFalsePositiveProbability = .01 29 // By setting maxIPEntriesPerNode > 1, we allow nodes to update their IP at 30 // least once per bloom filter reset. 31 maxIPEntriesPerNode = 2 32 33 untrackedTimestamp = -2 34 olderTimestamp = -1 35 sameTimestamp = 0 36 newerTimestamp = 1 37 newTimestamp = 2 38 ) 39 40 var _ validators.SetCallbackListener = (*ipTracker)(nil) 41 42 func newIPTracker( 43 log logging.Logger, 44 registerer prometheus.Registerer, 45 ) (*ipTracker, error) { 46 bloomMetrics, err := bloom.NewMetrics("ip_bloom", registerer) 47 if err != nil { 48 return nil, err 49 } 50 tracker := &ipTracker{ 51 log: log, 52 numTrackedIPs: prometheus.NewGauge(prometheus.GaugeOpts{ 53 Name: "tracked_ips", 54 Help: "Number of IPs this node is willing to dial", 55 }), 56 numGossipableIPs: prometheus.NewGauge(prometheus.GaugeOpts{ 57 Name: "gossipable_ips", 58 Help: "Number of IPs this node is willing to gossip", 59 }), 60 bloomMetrics: bloomMetrics, 61 mostRecentTrackedIPs: make(map[ids.NodeID]*ips.ClaimedIPPort), 62 bloomAdditions: make(map[ids.NodeID]int), 63 connected: make(map[ids.NodeID]*ips.ClaimedIPPort), 64 gossipableIndices: make(map[ids.NodeID]int), 65 } 66 err = errors.Join( 67 registerer.Register(tracker.numTrackedIPs), 68 registerer.Register(tracker.numGossipableIPs), 69 ) 70 if err != nil { 71 return nil, err 72 } 73 return tracker, tracker.resetBloom() 74 } 75 76 type ipTracker struct { 77 log logging.Logger 78 numTrackedIPs prometheus.Gauge 79 numGossipableIPs prometheus.Gauge 80 bloomMetrics *bloom.Metrics 81 82 lock sync.RWMutex 83 // manuallyTracked contains the nodeIDs of all nodes whose connection was 84 // manually requested. 85 manuallyTracked set.Set[ids.NodeID] 86 // manuallyGossipable contains the nodeIDs of all nodes whose IP was 87 // manually configured to be gossiped. 88 manuallyGossipable set.Set[ids.NodeID] 89 90 // mostRecentTrackedIPs tracks the most recent IP of each node whose 91 // connection is desired. 92 // 93 // An IP is tracked if one of the following conditions are met: 94 // - The node was manually tracked 95 // - The node was manually requested to be gossiped 96 // - The node is a validator 97 mostRecentTrackedIPs map[ids.NodeID]*ips.ClaimedIPPort 98 // trackedIDs contains the nodeIDs of all nodes whose connection is desired. 99 trackedIDs set.Set[ids.NodeID] 100 101 // The bloom filter contains the most recent tracked IPs to avoid 102 // unnecessary IP gossip. 103 bloom *bloom.Filter 104 // To prevent validators from causing the bloom filter to have too many 105 // false positives, we limit each validator to maxIPEntriesPerValidator in 106 // the bloom filter. 107 bloomAdditions map[ids.NodeID]int // Number of IPs added to the bloom 108 bloomSalt []byte 109 maxBloomCount int 110 111 // Connected tracks the IP of currently connected peers, including tracked 112 // and untracked nodes. The IP is not necessarily the same IP as in 113 // mostRecentTrackedIPs. 114 connected map[ids.NodeID]*ips.ClaimedIPPort 115 116 // An IP is marked as gossipable if all of the following conditions are met: 117 // - The node is a validator or was manually requested to be gossiped 118 // - The node is connected 119 // - The IP the node connected with is its latest IP 120 gossipableIndices map[ids.NodeID]int 121 // gossipableIPs is guaranteed to be a subset of [mostRecentTrackedIPs]. 122 gossipableIPs []*ips.ClaimedIPPort 123 gossipableIDs set.Set[ids.NodeID] 124 } 125 126 // ManuallyTrack marks the provided nodeID as being desirable to connect to. 127 // 128 // In order for a node to learn about these nodeIDs, other nodes in the network 129 // must have marked them as gossipable. 130 // 131 // Even if nodes disagree on the set of manually tracked nodeIDs, they will not 132 // introduce persistent network gossip. 133 func (i *ipTracker) ManuallyTrack(nodeID ids.NodeID) { 134 i.lock.Lock() 135 defer i.lock.Unlock() 136 137 i.addTrackableID(nodeID) 138 i.manuallyTracked.Add(nodeID) 139 } 140 141 // ManuallyGossip marks the provided nodeID as being desirable to connect to and 142 // marks the IPs that this node provides as being valid to gossip. 143 // 144 // In order to avoid persistent network gossip, it's important for nodes in the 145 // network to agree upon manually gossiped nodeIDs. 146 func (i *ipTracker) ManuallyGossip(nodeID ids.NodeID) { 147 i.lock.Lock() 148 defer i.lock.Unlock() 149 150 i.addTrackableID(nodeID) 151 i.manuallyTracked.Add(nodeID) 152 153 i.addGossipableID(nodeID) 154 i.manuallyGossipable.Add(nodeID) 155 } 156 157 // WantsConnection returns true if any of the following conditions are met: 158 // 1. The node has been manually tracked. 159 // 2. The node has been manually gossiped. 160 // 3. The node is currently a validator. 161 func (i *ipTracker) WantsConnection(nodeID ids.NodeID) bool { 162 i.lock.RLock() 163 defer i.lock.RUnlock() 164 165 return i.trackedIDs.Contains(nodeID) 166 } 167 168 // ShouldVerifyIP is used as an optimization to avoid unnecessary IP 169 // verification. It returns true if all of the following conditions are met: 170 // 1. The provided IP is from a node whose connection is desired. 171 // 2. This IP is newer than the most recent IP we know of for the node. 172 func (i *ipTracker) ShouldVerifyIP(ip *ips.ClaimedIPPort) bool { 173 i.lock.RLock() 174 defer i.lock.RUnlock() 175 176 if !i.trackedIDs.Contains(ip.NodeID) { 177 return false 178 } 179 180 prevIP, ok := i.mostRecentTrackedIPs[ip.NodeID] 181 return !ok || // This would be the first IP 182 prevIP.Timestamp < ip.Timestamp // This would be a newer IP 183 } 184 185 // AddIP attempts to update the node's IP to the provided IP. This function 186 // assumes the provided IP has been verified. Returns true if all of the 187 // following conditions are met: 188 // 1. The provided IP is from a node whose connection is desired. 189 // 2. This IP is newer than the most recent IP we know of for the node. 190 // 191 // If the previous IP was marked as gossipable, calling this function will 192 // remove the IP from the gossipable set. 193 func (i *ipTracker) AddIP(ip *ips.ClaimedIPPort) bool { 194 i.lock.Lock() 195 defer i.lock.Unlock() 196 197 return i.addIP(ip) > sameTimestamp 198 } 199 200 // GetIP returns the most recent IP of the provided nodeID. If a connection to 201 // this nodeID is not desired, this function will return false. 202 func (i *ipTracker) GetIP(nodeID ids.NodeID) (*ips.ClaimedIPPort, bool) { 203 i.lock.RLock() 204 defer i.lock.RUnlock() 205 206 ip, ok := i.mostRecentTrackedIPs[nodeID] 207 return ip, ok 208 } 209 210 // Connected is called when a connection is established. The peer should have 211 // provided [ip] during the handshake. 212 func (i *ipTracker) Connected(ip *ips.ClaimedIPPort) { 213 i.lock.Lock() 214 defer i.lock.Unlock() 215 216 i.connected[ip.NodeID] = ip 217 if i.addIP(ip) >= sameTimestamp && i.gossipableIDs.Contains(ip.NodeID) { 218 i.addGossipableIP(ip) 219 } 220 } 221 222 func (i *ipTracker) addIP(ip *ips.ClaimedIPPort) int { 223 if !i.trackedIDs.Contains(ip.NodeID) { 224 return untrackedTimestamp 225 } 226 227 prevIP, ok := i.mostRecentTrackedIPs[ip.NodeID] 228 if !ok { 229 // This is the first IP we've heard from the validator, so it is the 230 // most recent. 231 i.updateMostRecentTrackedIP(ip) 232 // Because we didn't previously have an IP, we know we aren't currently 233 // connected to them. 234 return newTimestamp 235 } 236 237 if prevIP.Timestamp > ip.Timestamp { 238 return olderTimestamp // This IP is old than the previously known IP. 239 } 240 if prevIP.Timestamp == ip.Timestamp { 241 return sameTimestamp // This IP is equal to the previously known IP. 242 } 243 244 i.updateMostRecentTrackedIP(ip) 245 i.removeGossipableIP(ip.NodeID) 246 return newerTimestamp 247 } 248 249 // Disconnected is called when a connection to the peer is closed. 250 func (i *ipTracker) Disconnected(nodeID ids.NodeID) { 251 i.lock.Lock() 252 defer i.lock.Unlock() 253 254 delete(i.connected, nodeID) 255 i.removeGossipableIP(nodeID) 256 } 257 258 func (i *ipTracker) OnValidatorAdded(nodeID ids.NodeID, _ *bls.PublicKey, _ ids.ID, _ uint64) { 259 i.lock.Lock() 260 defer i.lock.Unlock() 261 262 i.addTrackableID(nodeID) 263 i.addGossipableID(nodeID) 264 } 265 266 func (i *ipTracker) addTrackableID(nodeID ids.NodeID) { 267 if i.trackedIDs.Contains(nodeID) { 268 return 269 } 270 271 i.trackedIDs.Add(nodeID) 272 ip, connected := i.connected[nodeID] 273 if !connected { 274 return 275 } 276 277 // Because we previously weren't tracking this nodeID, the IP from the 278 // connection is guaranteed to be the most up-to-date IP that we know. 279 i.updateMostRecentTrackedIP(ip) 280 } 281 282 func (i *ipTracker) addGossipableID(nodeID ids.NodeID) { 283 if i.gossipableIDs.Contains(nodeID) { 284 return 285 } 286 287 i.gossipableIDs.Add(nodeID) 288 connectedIP, connected := i.connected[nodeID] 289 if !connected { 290 return 291 } 292 293 if updatedIP, ok := i.mostRecentTrackedIPs[nodeID]; !ok || connectedIP.Timestamp != updatedIP.Timestamp { 294 return 295 } 296 297 i.addGossipableIP(connectedIP) 298 } 299 300 func (*ipTracker) OnValidatorWeightChanged(ids.NodeID, uint64, uint64) {} 301 302 func (i *ipTracker) OnValidatorRemoved(nodeID ids.NodeID, _ uint64) { 303 i.lock.Lock() 304 defer i.lock.Unlock() 305 306 if i.manuallyGossipable.Contains(nodeID) { 307 return 308 } 309 310 i.gossipableIDs.Remove(nodeID) 311 i.removeGossipableIP(nodeID) 312 313 if i.manuallyTracked.Contains(nodeID) { 314 return 315 } 316 317 i.trackedIDs.Remove(nodeID) 318 delete(i.mostRecentTrackedIPs, nodeID) 319 i.numTrackedIPs.Set(float64(len(i.mostRecentTrackedIPs))) 320 } 321 322 func (i *ipTracker) updateMostRecentTrackedIP(ip *ips.ClaimedIPPort) { 323 i.mostRecentTrackedIPs[ip.NodeID] = ip 324 i.numTrackedIPs.Set(float64(len(i.mostRecentTrackedIPs))) 325 326 oldCount := i.bloomAdditions[ip.NodeID] 327 if oldCount >= maxIPEntriesPerNode { 328 return 329 } 330 331 // If the validator set is growing rapidly, we should increase the size of 332 // the bloom filter. 333 if count := i.bloom.Count(); count >= i.maxBloomCount { 334 if err := i.resetBloom(); err != nil { 335 i.log.Error("failed to reset validator tracker bloom filter", 336 zap.Int("maxCount", i.maxBloomCount), 337 zap.Int("currentCount", count), 338 zap.Error(err), 339 ) 340 } else { 341 i.log.Info("reset validator tracker bloom filter", 342 zap.Int("currentCount", count), 343 ) 344 } 345 return 346 } 347 348 i.bloomAdditions[ip.NodeID] = oldCount + 1 349 bloom.Add(i.bloom, ip.GossipID[:], i.bloomSalt) 350 i.bloomMetrics.Count.Inc() 351 } 352 353 func (i *ipTracker) addGossipableIP(ip *ips.ClaimedIPPort) { 354 i.gossipableIndices[ip.NodeID] = len(i.gossipableIPs) 355 i.gossipableIPs = append(i.gossipableIPs, ip) 356 i.numGossipableIPs.Inc() 357 } 358 359 func (i *ipTracker) removeGossipableIP(nodeID ids.NodeID) { 360 indexToRemove, wasGossipable := i.gossipableIndices[nodeID] 361 if !wasGossipable { 362 return 363 } 364 365 newNumGossipable := len(i.gossipableIPs) - 1 366 if newNumGossipable != indexToRemove { 367 replacementIP := i.gossipableIPs[newNumGossipable] 368 i.gossipableIndices[replacementIP.NodeID] = indexToRemove 369 i.gossipableIPs[indexToRemove] = replacementIP 370 } 371 372 delete(i.gossipableIndices, nodeID) 373 i.gossipableIPs[newNumGossipable] = nil 374 i.gossipableIPs = i.gossipableIPs[:newNumGossipable] 375 i.numGossipableIPs.Dec() 376 } 377 378 // GetGossipableIPs returns the latest IPs of connected validators. The returned 379 // IPs will not contain [exceptNodeID] or any IPs contained in [exceptIPs]. If 380 // the number of eligible IPs to return low, it's possible that every IP will be 381 // iterated over while handling this call. 382 func (i *ipTracker) GetGossipableIPs( 383 exceptNodeID ids.NodeID, 384 exceptIPs *bloom.ReadFilter, 385 salt []byte, 386 maxNumIPs int, 387 ) []*ips.ClaimedIPPort { 388 var ( 389 uniform = sampler.NewUniform() 390 ips = make([]*ips.ClaimedIPPort, 0, maxNumIPs) 391 ) 392 393 i.lock.RLock() 394 defer i.lock.RUnlock() 395 396 uniform.Initialize(uint64(len(i.gossipableIPs))) 397 for len(ips) < maxNumIPs { 398 index, hasNext := uniform.Next() 399 if !hasNext { 400 return ips 401 } 402 403 ip := i.gossipableIPs[index] 404 if ip.NodeID == exceptNodeID { 405 continue 406 } 407 408 if !bloom.Contains(exceptIPs, ip.GossipID[:], salt) { 409 ips = append(ips, ip) 410 } 411 } 412 return ips 413 } 414 415 // ResetBloom prunes the current bloom filter. This must be called periodically 416 // to ensure that validators that change their IPs are updated correctly and 417 // that validators that left the validator set are removed. 418 func (i *ipTracker) ResetBloom() error { 419 i.lock.Lock() 420 defer i.lock.Unlock() 421 422 return i.resetBloom() 423 } 424 425 // Bloom returns the binary representation of the bloom filter along with the 426 // random salt. 427 func (i *ipTracker) Bloom() ([]byte, []byte) { 428 i.lock.RLock() 429 defer i.lock.RUnlock() 430 431 return i.bloom.Marshal(), i.bloomSalt 432 } 433 434 // resetBloom creates a new bloom filter with a reasonable size for the current 435 // validator set size. This function additionally populates the new bloom filter 436 // with the current most recently known IPs of validators. 437 func (i *ipTracker) resetBloom() error { 438 newSalt := make([]byte, saltSize) 439 _, err := rand.Reader.Read(newSalt) 440 if err != nil { 441 return err 442 } 443 444 count := max(maxIPEntriesPerNode*i.trackedIDs.Len(), minCountEstimate) 445 numHashes, numEntries := bloom.OptimalParameters( 446 count, 447 targetFalsePositiveProbability, 448 ) 449 newFilter, err := bloom.New(numHashes, numEntries) 450 if err != nil { 451 return err 452 } 453 454 i.bloom = newFilter 455 clear(i.bloomAdditions) 456 i.bloomSalt = newSalt 457 i.maxBloomCount = bloom.EstimateCount(numHashes, numEntries, maxFalsePositiveProbability) 458 459 for nodeID, ip := range i.mostRecentTrackedIPs { 460 bloom.Add(newFilter, ip.GossipID[:], newSalt) 461 i.bloomAdditions[nodeID] = 1 462 } 463 i.bloomMetrics.Reset(newFilter, i.maxBloomCount) 464 return nil 465 }