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  }