Files
node/network/ip_tracker_test.go

1394 lines
42 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package network
import (
"testing"
"github.com/luxfi/metric"
"github.com/stretchr/testify/require"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/node/utils/bloom"
"github.com/luxfi/net/endpoints"
)
func newTestIPTracker(t *testing.T) *ipTracker {
tracker, err := newIPTracker(
nil,
log.NewNoOpLogger(),
metric.NewRegistry(),
)
require.NoError(t, err)
return tracker
}
func newerTestIP(ip *endpoints.ClaimedIPPort) *endpoints.ClaimedIPPort {
return endpoints.NewClaimedIPPort(
ip.Cert,
ip.AddrPort,
ip.Timestamp+1,
ip.Signature,
)
}
func requireEqual(t *testing.T, expected, actual *ipTracker) {
require := require.New(t)
require.Equal(expected.tracked, actual.tracked)
require.Equal(expected.bloomAdditions, actual.bloomAdditions)
require.Equal(expected.maxBloomCount, actual.maxBloomCount)
require.Equal(expected.connected, actual.connected)
require.Equal(expected.net, actual.net)
}
func requireMetricsConsistent(t *testing.T, tracker *ipTracker) {
_ = require.New(t)
// Metric assertions commented out because metric wrapper types don't expose metric.Collector interface
// require.InDelta(float64(len(tracker.tracked)), testutil.ToFloat64(tracker.numTrackedPeers), 0)
// var numGossipableIPs int
// for _, net := range tracker.net {
// numGossipableIPs += len(net.gossipableIndices)
// }
// require.InDelta(float64(numGossipableIPs), testutil.ToFloat64(tracker.numGossipableIPs), 0)
// require.InDelta(float64(len(tracker.net)), testutil.ToFloat64(tracker.numTrackedNets), 0)
// require.InDelta(float64(tracker.bloom.Count()), testutil.ToFloat64(tracker.bloomMetrics.Count), 0)
// require.InDelta(float64(tracker.maxBloomCount), testutil.ToFloat64(tracker.bloomMetrics.MaxCount), 0)
}
func TestIPTracker_ManuallyTrack(t *testing.T) {
netID := ids.GenerateTestID()
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
expectedChange func(*ipTracker)
}{
{
name: "non-connected non-validator",
initialState: newTestIPTracker,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
manuallyTracked: true,
validatedNets: make(set.Set[ids.ID]),
trackedNets: make(set.Set[ids.ID]),
}
},
},
{
name: "connected non-validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
manuallyTracked: true,
validatedNets: make(set.Set[ids.ID]),
trackedNets: make(set.Set[ids.ID]),
ip: ip,
}
tracker.bloomAdditions[ip.NodeID] = 1
},
},
{
name: "non-connected tracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].manuallyTracked = true
},
},
{
name: "non-connected untracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].manuallyTracked = true
},
},
{
name: "connected tracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].manuallyTracked = true
},
},
{
name: "connected untracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].manuallyTracked = true
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
testState.ManuallyTrack(ip.NodeID)
test.expectedChange(expectedState)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_ManuallyGossip(t *testing.T) {
netID := ids.GenerateTestID()
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
netID ids.ID
expectedChange func(*ipTracker)
}{
{
name: "non-connected tracked non-validator",
initialState: newTestIPTracker,
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Inc()
tracker.numTrackedNets.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
manuallyTracked: true,
validatedNets: set.Of(constants.PrimaryNetworkID),
trackedNets: set.Of(constants.PrimaryNetworkID),
}
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: set.Of(ip.NodeID),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: make(map[ids.NodeID]int),
}
},
},
{
name: "non-connected untracked non-validator",
initialState: newTestIPTracker,
netID: netID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Inc()
tracker.numTrackedNets.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
validatedNets: set.Of(netID),
trackedNets: make(set.Set[ids.ID]),
}
tracker.net[netID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: set.Of(ip.NodeID),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: make(map[ids.NodeID]int),
}
},
},
{
name: "connected tracked non-validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Inc()
tracker.numGossipableIPs.Inc()
tracker.numTrackedNets.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
manuallyTracked: true,
validatedNets: set.Of(constants.PrimaryNetworkID),
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
tracker.bloomAdditions[ip.NodeID] = 1
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: set.Of(ip.NodeID),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: map[ids.NodeID]int{
ip.NodeID: 0,
},
gossipableIPs: []*endpoints.ClaimedIPPort{
ip,
},
}
},
},
{
name: "connected untracked non-validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: netID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Inc()
tracker.numTrackedNets.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
validatedNets: set.Of(netID),
trackedNets: make(set.Set[ids.ID]),
ip: ip,
}
tracker.bloomAdditions[ip.NodeID] = 1
tracker.net[netID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: set.Of(ip.NodeID),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: make(map[ids.NodeID]int),
}
},
},
{
name: "non-connected tracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].manuallyTracked = true
tracker.net[constants.PrimaryNetworkID].manuallyGossipable = set.Of(ip.NodeID)
},
},
{
name: "non-connected untracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
return tracker
},
netID: netID,
expectedChange: func(tracker *ipTracker) {
tracker.net[netID].manuallyGossipable = set.Of(ip.NodeID)
},
},
{
name: "connected tracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].manuallyTracked = true
tracker.net[constants.PrimaryNetworkID].manuallyGossipable = set.Of(ip.NodeID)
},
},
{
name: "connected untracked validator",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
return tracker
},
netID: netID,
expectedChange: func(tracker *ipTracker) {
tracker.net[netID].manuallyGossipable = set.Of(ip.NodeID)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
testState.ManuallyGossip(test.netID, ip.NodeID)
test.expectedChange(expectedState)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_ShouldVerifyIP(t *testing.T) {
newerIP := newerTestIP(ip)
tests := []struct {
name string
tracker func(t *testing.T) *ipTracker
ip *endpoints.ClaimedIPPort
expectedTrackAllNets bool
expectedTrackRequestedNets bool
}{
{
name: "node not tracked",
tracker: newTestIPTracker,
ip: ip,
expectedTrackAllNets: false,
expectedTrackRequestedNets: false,
},
{
name: "undesired connection",
tracker: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(ids.GenerateTestID(), ip.NodeID, 0)
return tracker
},
ip: ip,
expectedTrackAllNets: true,
expectedTrackRequestedNets: false,
},
{
name: "desired connection first IP",
tracker: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
ip: ip,
expectedTrackAllNets: true,
expectedTrackRequestedNets: true,
},
{
name: "desired connection older IP",
tracker: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(newerIP))
return tracker
},
ip: ip,
expectedTrackAllNets: false,
expectedTrackRequestedNets: false,
},
{
name: "desired connection same IP",
tracker: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
ip: ip,
expectedTrackAllNets: false,
expectedTrackRequestedNets: false,
},
{
name: "desired connection newer IP",
tracker: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
ip: newerIP,
expectedTrackAllNets: true,
expectedTrackRequestedNets: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
tracker := test.tracker(t)
require.Equal(test.expectedTrackAllNets, tracker.ShouldVerifyIP(test.ip, true))
require.Equal(test.expectedTrackRequestedNets, tracker.ShouldVerifyIP(test.ip, false))
})
}
}
func TestIPTracker_AddIP(t *testing.T) {
netID := ids.GenerateTestID()
newerIP := newerTestIP(ip)
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
ip *endpoints.ClaimedIPPort
expectedChange func(*ipTracker)
expectedUpdatedAndDesired bool
}{
{
name: "non-validator",
initialState: newTestIPTracker,
ip: ip,
expectedChange: func(*ipTracker) {},
expectedUpdatedAndDesired: false,
},
{
name: "first known IP of tracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].ip = ip
tracker.bloomAdditions[ip.NodeID] = 1
},
expectedUpdatedAndDesired: true,
},
{
name: "first known IP of untracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
return tracker
},
ip: ip,
expectedUpdatedAndDesired: false,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].ip = ip
tracker.bloomAdditions[ip.NodeID] = 1
},
},
{
name: "older IP of tracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(newerIP))
return tracker
},
ip: ip,
expectedUpdatedAndDesired: false,
expectedChange: func(*ipTracker) {},
},
{
name: "older IP of untracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
require.False(t, tracker.AddIP(newerIP))
return tracker
},
ip: ip,
expectedUpdatedAndDesired: false,
expectedChange: func(*ipTracker) {},
},
{
name: "same IP of tracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
ip: ip,
expectedUpdatedAndDesired: false,
expectedChange: func(*ipTracker) {},
},
{
name: "same IP of untracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
require.False(t, tracker.AddIP(ip))
return tracker
},
ip: ip,
expectedUpdatedAndDesired: false,
expectedChange: func(*ipTracker) {},
},
{
name: "disconnected newer IP of tracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
ip: newerIP,
expectedUpdatedAndDesired: true,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[newerIP.NodeID].ip = newerIP
tracker.bloomAdditions[newerIP.NodeID] = 2
},
},
{
name: "disconnected newer IP of untracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
require.False(t, tracker.AddIP(ip))
return tracker
},
ip: newerIP,
expectedUpdatedAndDesired: false,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[newerIP.NodeID].ip = newerIP
tracker.bloomAdditions[newerIP.NodeID] = 2
},
},
{
name: "connected newer IP of tracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
ip: newerIP,
expectedUpdatedAndDesired: true,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[newerIP.NodeID].ip = newerIP
tracker.bloomAdditions[newerIP.NodeID] = 2
tracker.net[constants.PrimaryNetworkID].gossipableIPs[0] = newerIP
},
},
{
name: "connected newer IP of untracked node",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
ip: newerIP,
expectedUpdatedAndDesired: false,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[newerIP.NodeID].ip = newerIP
tracker.bloomAdditions[newerIP.NodeID] = 2
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
updated := testState.AddIP(test.ip)
test.expectedChange(expectedState)
require.Equal(t, test.expectedUpdatedAndDesired, updated)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_Connected(t *testing.T) {
netID := ids.GenerateTestID()
newerIP := newerTestIP(ip)
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
ip *endpoints.ClaimedIPPort
expectedChange func(*ipTracker)
}{
{
name: "non-validator",
initialState: newTestIPTracker,
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
},
},
{
name: "first known IP of node tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.tracked[ip.NodeID].ip = ip
tracker.bloomAdditions[ip.NodeID] = 1
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
net := tracker.net[constants.PrimaryNetworkID]
net.gossipableIndices[ip.NodeID] = 0
net.gossipableIPs = []*endpoints.ClaimedIPPort{
ip,
}
},
},
{
name: "first known IP of node not tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].ip = ip
tracker.bloomAdditions[ip.NodeID] = 1
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
},
},
{
name: "connected with older IP of node tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(newerIP))
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
net := tracker.net[constants.PrimaryNetworkID]
net.gossipableIndices[newerIP.NodeID] = 0
net.gossipableIPs = []*endpoints.ClaimedIPPort{
newerIP,
}
},
},
{
name: "connected with older IP of node not tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
require.False(t, tracker.AddIP(newerIP))
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
},
},
{
name: "connected with newer IP of node tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
ip: newerIP,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.tracked[newerIP.NodeID].ip = newerIP
tracker.bloomAdditions[newerIP.NodeID] = 2
tracker.connected[newerIP.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: newerIP,
}
net := tracker.net[constants.PrimaryNetworkID]
net.gossipableIndices[newerIP.NodeID] = 0
net.gossipableIPs = []*endpoints.ClaimedIPPort{
newerIP,
}
},
},
{
name: "connected with newer IP of node not tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
require.False(t, tracker.AddIP(ip))
return tracker
},
ip: newerIP,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[newerIP.NodeID].ip = newerIP
tracker.bloomAdditions[newerIP.NodeID] = 2
tracker.connected[newerIP.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: newerIP,
}
},
},
{
name: "connected with same IP of node tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
net := tracker.net[constants.PrimaryNetworkID]
net.gossipableIndices[ip.NodeID] = 0
net.gossipableIPs = []*endpoints.ClaimedIPPort{
ip,
}
},
},
{
name: "connected with same IP of node not tracking net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
require.False(t, tracker.AddIP(ip))
return tracker
},
ip: ip,
expectedChange: func(tracker *ipTracker) {
tracker.connected[ip.NodeID] = &connectedNode{
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
testState.Connected(test.ip, set.Of(constants.PrimaryNetworkID))
test.expectedChange(expectedState)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_Disconnected(t *testing.T) {
netID := ids.GenerateTestID()
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
expectedChange func(*ipTracker)
}{
{
name: "not gossipable",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
expectedChange: func(*ipTracker) {},
},
{
name: "latest gossipable",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Dec()
delete(tracker.connected, ip.NodeID)
net := tracker.net[constants.PrimaryNetworkID]
delete(net.gossipableIndices, ip.NodeID)
net.gossipableIPs = net.gossipableIPs[:0]
},
},
{
name: "non-latest gossipable",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, otherIP.NodeID, 0)
tracker.Connected(otherIP, set.Of(constants.PrimaryNetworkID))
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Dec()
delete(tracker.connected, ip.NodeID)
net := tracker.net[constants.PrimaryNetworkID]
net.gossipableIndices = map[ids.NodeID]int{
otherIP.NodeID: 0,
}
net.gossipableIPs = []*endpoints.ClaimedIPPort{
otherIP,
}
},
},
{
name: "remove multiple gossipable IPs",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID, netID))
return tracker
},
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Add(-2)
delete(tracker.connected, ip.NodeID)
primaryNet := tracker.net[constants.PrimaryNetworkID]
delete(primaryNet.gossipableIndices, ip.NodeID)
primaryNet.gossipableIPs = primaryNet.gossipableIPs[:0]
net := tracker.net[netID]
delete(net.gossipableIndices, ip.NodeID)
net.gossipableIPs = net.gossipableIPs[:0]
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
testState.Disconnected(ip.NodeID)
expectedState.Disconnected(ip.NodeID)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_OnValidatorAdded(t *testing.T) {
newerIP := newerTestIP(ip)
netID := ids.GenerateTestID()
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
netID ids.ID
expectedChange func(*ipTracker)
}{
{
name: "manually tracked",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyTrack(ip.NodeID)
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID].validatedNets.Add(constants.PrimaryNetworkID)
tracker.tracked[ip.NodeID].trackedNets.Add(constants.PrimaryNetworkID)
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: make(set.Set[ids.NodeID]),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: make(map[ids.NodeID]int),
}
},
},
{
name: "manually tracked and connected",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyTrack(ip.NodeID)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.tracked[ip.NodeID].validatedNets.Add(constants.PrimaryNetworkID)
tracker.tracked[ip.NodeID].trackedNets.Add(constants.PrimaryNetworkID)
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: make(set.Set[ids.NodeID]),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: map[ids.NodeID]int{
ip.NodeID: 0,
},
gossipableIPs: []*endpoints.ClaimedIPPort{
ip,
},
}
},
},
{
name: "manually tracked and connected with older IP",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyTrack(ip.NodeID)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
require.True(t, tracker.AddIP(newerIP))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.tracked[ip.NodeID].validatedNets.Add(constants.PrimaryNetworkID)
tracker.tracked[ip.NodeID].trackedNets.Add(constants.PrimaryNetworkID)
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: make(set.Set[ids.NodeID]),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: map[ids.NodeID]int{
ip.NodeID: 0,
},
gossipableIPs: []*endpoints.ClaimedIPPort{
newerIP,
},
}
},
},
{
name: "manually gossiped",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyGossip(constants.PrimaryNetworkID, ip.NodeID)
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(*ipTracker) {},
},
{
name: "disconnected",
initialState: newTestIPTracker,
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.tracked[ip.NodeID] = &trackedNode{
validatedNets: set.Of(constants.PrimaryNetworkID),
trackedNets: set.Of(constants.PrimaryNetworkID),
}
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: make(set.Set[ids.NodeID]),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: make(map[ids.NodeID]int),
}
},
},
{
name: "connected",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
validatedNets: set.Of(constants.PrimaryNetworkID),
trackedNets: set.Of(constants.PrimaryNetworkID),
ip: ip,
}
tracker.bloomAdditions[ip.NodeID] = 1
tracker.net[constants.PrimaryNetworkID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: make(set.Set[ids.NodeID]),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: map[ids.NodeID]int{
ip.NodeID: 0,
},
gossipableIPs: []*endpoints.ClaimedIPPort{
ip,
},
}
},
},
{
name: "connected to other net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: netID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedNets.Inc()
tracker.tracked[ip.NodeID] = &trackedNode{
validatedNets: set.Of(netID),
trackedNets: make(set.Set[ids.ID]),
ip: ip,
}
tracker.bloomAdditions[ip.NodeID] = 1
tracker.net[netID] = &gossipableNet{
numGossipableIPs: tracker.numGossipableIPs,
manuallyGossipable: make(set.Set[ids.NodeID]),
gossipableIDs: set.Of(ip.NodeID),
gossipableIndices: make(map[ids.NodeID]int),
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
testState.OnValidatorAdded(test.netID, ip.NodeID, 0)
test.expectedChange(expectedState)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_OnValidatorRemoved(t *testing.T) {
netID := ids.GenerateTestID()
tests := []struct {
name string
initialState func(t *testing.T) *ipTracker
netID ids.ID
expectedChange func(*ipTracker)
}{
{
name: "remove last validator of net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Dec()
tracker.numTrackedNets.Dec()
delete(tracker.tracked, ip.NodeID)
delete(tracker.net, constants.PrimaryNetworkID)
},
},
{
name: "manually tracked not gossipable",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyTrack(ip.NodeID)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
require.True(t, tracker.AddIP(ip))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedNets.Dec()
node := tracker.tracked[ip.NodeID]
node.validatedNets.Remove(constants.PrimaryNetworkID)
node.trackedNets.Remove(constants.PrimaryNetworkID)
delete(tracker.net, constants.PrimaryNetworkID)
},
},
{
name: "manually tracked latest gossipable",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyTrack(ip.NodeID)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numGossipableIPs.Dec()
tracker.numTrackedNets.Dec()
node := tracker.tracked[ip.NodeID]
node.validatedNets.Remove(constants.PrimaryNetworkID)
node.trackedNets.Remove(constants.PrimaryNetworkID)
delete(tracker.net, constants.PrimaryNetworkID)
},
},
{
name: "manually gossiped",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyGossip(constants.PrimaryNetworkID, ip.NodeID)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(*ipTracker) {},
},
{
name: "manually gossiped on other net",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.ManuallyGossip(constants.PrimaryNetworkID, ip.NodeID)
tracker.OnValidatorAdded(netID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: netID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedNets.Dec()
tracker.tracked[ip.NodeID].validatedNets.Remove(netID)
delete(tracker.net, netID)
},
},
{
name: "non-latest gossipable",
initialState: func(t *testing.T) *ipTracker {
tracker := newTestIPTracker(t)
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, otherIP.NodeID, 0)
tracker.Connected(otherIP, set.Of(constants.PrimaryNetworkID))
return tracker
},
netID: constants.PrimaryNetworkID,
expectedChange: func(tracker *ipTracker) {
tracker.numTrackedPeers.Dec()
tracker.numGossipableIPs.Dec()
delete(tracker.tracked, ip.NodeID)
net := tracker.net[constants.PrimaryNetworkID]
net.gossipableIDs.Remove(ip.NodeID)
net.gossipableIndices = map[ids.NodeID]int{
otherIP.NodeID: 0,
}
net.gossipableIPs = []*endpoints.ClaimedIPPort{
otherIP,
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testState := test.initialState(t)
expectedState := test.initialState(t)
testState.OnValidatorRemoved(test.netID, ip.NodeID, 0)
test.expectedChange(expectedState)
requireEqual(t, expectedState, testState)
requireMetricsConsistent(t, testState)
})
}
}
func TestIPTracker_BloomGrows(t *testing.T) {
tests := []struct {
name string
add func(tracker *ipTracker)
}{
{
name: "Add Validator",
add: func(tracker *ipTracker) {
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ids.GenerateTestNodeID(), 0)
},
},
{
name: "Manually Track",
add: func(tracker *ipTracker) {
tracker.ManuallyTrack(ids.GenerateTestNodeID())
},
},
{
name: "Manually Gossip",
add: func(tracker *ipTracker) {
tracker.ManuallyGossip(ids.GenerateTestID(), ids.GenerateTestNodeID())
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
tracker := newTestIPTracker(t)
initialMaxBloomCount := tracker.maxBloomCount
for i := 0; i < 2048; i++ {
test.add(tracker)
}
requireMetricsConsistent(t, tracker)
require.NoError(tracker.ResetBloom())
require.Greater(tracker.maxBloomCount, initialMaxBloomCount)
requireMetricsConsistent(t, tracker)
})
}
}
func TestIPTracker_BloomResetsDynamically(t *testing.T) {
require := require.New(t)
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.OnValidatorRemoved(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.maxBloomCount = 1
tracker.Connected(otherIP, set.Of(constants.PrimaryNetworkID))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, otherIP.NodeID, 0)
requireMetricsConsistent(t, tracker)
bloomBytes, salt := tracker.Bloom()
readFilter, err := bloom.Parse(bloomBytes)
require.NoError(err)
require.False(bloom.Contains(readFilter, ip.GossipID[:], salt))
require.True(bloom.Contains(readFilter, otherIP.GossipID[:], salt))
}
func TestIPTracker_PreventBloomFilterAddition(t *testing.T) {
require := require.New(t)
newerIP := newerTestIP(ip)
newestIP := newerTestIP(newerIP)
tracker := newTestIPTracker(t)
tracker.ManuallyGossip(constants.PrimaryNetworkID, ip.NodeID)
require.True(tracker.AddIP(ip))
require.True(tracker.AddIP(newerIP))
require.True(tracker.AddIP(newestIP))
require.Equal(maxIPEntriesPerNode, tracker.bloomAdditions[ip.NodeID])
requireMetricsConsistent(t, tracker)
}
func TestIPTracker_GetGossipableIPs(t *testing.T) {
netIDA := ids.GenerateTestID()
netIDB := ids.GenerateTestID()
unknownChainID := ids.GenerateTestID()
tracker := newTestIPTracker(t)
tracker.Connected(ip, set.Of(constants.PrimaryNetworkID, netIDA))
tracker.Connected(otherIP, set.Of(constants.PrimaryNetworkID, netIDA, netIDB))
tracker.OnValidatorAdded(constants.PrimaryNetworkID, ip.NodeID, 0)
tracker.OnValidatorAdded(netIDA, otherIP.NodeID, 0)
tracker.OnValidatorAdded(netIDB, otherIP.NodeID, 0)
myFilterBytes, mySalt := tracker.Bloom()
myFilter, err := bloom.Parse(myFilterBytes)
require.NoError(t, err)
tests := []struct {
name string
toIterate set.Set[ids.ID]
allowed set.Set[ids.ID]
nodeID ids.NodeID
filter *bloom.ReadFilter
salt []byte
expected []*endpoints.ClaimedIPPort
}{
{
name: "fetch both nets IPs",
toIterate: set.Of(constants.PrimaryNetworkID, netIDA),
allowed: set.Of(constants.PrimaryNetworkID, netIDA),
nodeID: ids.EmptyNodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: []*endpoints.ClaimedIPPort{ip, otherIP},
},
{
name: "filter nodeID",
toIterate: set.Of(constants.PrimaryNetworkID, netIDA),
allowed: set.Of(constants.PrimaryNetworkID, netIDA),
nodeID: ip.NodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: []*endpoints.ClaimedIPPort{otherIP},
},
{
name: "filter duplicate nodeIDs",
toIterate: set.Of(netIDA, netIDB),
allowed: set.Of(netIDA, netIDB),
nodeID: ids.EmptyNodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: []*endpoints.ClaimedIPPort{otherIP},
},
{
name: "filter known IPs",
toIterate: set.Of(constants.PrimaryNetworkID, netIDA),
allowed: set.Of(constants.PrimaryNetworkID, netIDA),
nodeID: ids.EmptyNodeID,
filter: func() *bloom.ReadFilter {
filter, err := bloom.New(8, 1024)
require.NoError(t, err)
bloom.Add(filter, ip.GossipID[:], nil)
readFilter, err := bloom.Parse(filter.Marshal())
require.NoError(t, err)
return readFilter
}(),
salt: nil,
expected: []*endpoints.ClaimedIPPort{otherIP},
},
{
name: "filter everything",
toIterate: set.Of(constants.PrimaryNetworkID, netIDA, netIDB),
allowed: set.Of(constants.PrimaryNetworkID, netIDA, netIDB),
nodeID: ids.EmptyNodeID,
filter: myFilter,
salt: mySalt,
expected: nil,
},
{
name: "only fetch primary network IPs",
toIterate: set.Of(constants.PrimaryNetworkID),
allowed: set.Of(constants.PrimaryNetworkID),
nodeID: ids.EmptyNodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: []*endpoints.ClaimedIPPort{ip},
},
{
name: "only fetch net IPs",
toIterate: set.Of(netIDA),
allowed: set.Of(netIDA),
nodeID: ids.EmptyNodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: []*endpoints.ClaimedIPPort{otherIP},
},
{
name: "filter net",
toIterate: set.Of(constants.PrimaryNetworkID, netIDA),
allowed: set.Of(constants.PrimaryNetworkID),
nodeID: ids.EmptyNodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: []*endpoints.ClaimedIPPort{ip},
},
{
name: "skip unknown net",
toIterate: set.Of(unknownChainID),
allowed: set.Of(unknownChainID),
nodeID: ids.EmptyNodeID,
filter: bloom.EmptyFilter,
salt: nil,
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gossipableIPs := getGossipableIPs(
tracker,
test.toIterate,
test.allowed.Contains,
test.nodeID,
test.filter,
test.salt,
2,
)
require.ElementsMatch(t, test.expected, gossipableIPs)
})
}
}