github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/tracer/gossipSubScoreTracer_test.go (about)

     1  package tracer_test
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"testing"
     7  	"time"
     8  
     9  	pubsub "github.com/libp2p/go-libp2p-pubsub"
    10  	"github.com/libp2p/go-libp2p/core/peer"
    11  	"github.com/rs/zerolog"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/mock"
    14  	"github.com/stretchr/testify/require"
    15  	"go.uber.org/atomic"
    16  
    17  	"github.com/onflow/flow-go/config"
    18  	"github.com/onflow/flow-go/model/flow"
    19  	"github.com/onflow/flow-go/module"
    20  	"github.com/onflow/flow-go/module/irrecoverable"
    21  	"github.com/onflow/flow-go/module/metrics"
    22  	mockmodule "github.com/onflow/flow-go/module/mock"
    23  	"github.com/onflow/flow-go/network/channels"
    24  	"github.com/onflow/flow-go/network/p2p"
    25  	p2ptest "github.com/onflow/flow-go/network/p2p/test"
    26  	"github.com/onflow/flow-go/network/p2p/tracer"
    27  	validator "github.com/onflow/flow-go/network/validator/pubsub"
    28  	"github.com/onflow/flow-go/utils/unittest"
    29  )
    30  
    31  // TestGossipSubScoreTracer tests the functionality of the GossipSubScoreTracer, which logs the scores
    32  // of the libp2p nodes using the GossipSub protocol. The test sets up three nodes with the same role,
    33  // and subscribes them to a common topic. One of these three nodes is furnished with the score tracer, and the test
    34  // examines whether the tracer node is able to trace the local score of other two nodes properly.
    35  // The test also checks that the correct metrics are being called for each score update.
    36  //
    37  // The test performs the following steps:
    38  // 1. Creates a logger hook to count the number of times the score logs at the interval specified.
    39  // 2. Creates a mockPeerScoreMetrics object and sets it as a metrics collector for the tracer node.
    40  // 3. Creates three nodes with same roles and sets their roles as consensus, access, and tracer, respectively.
    41  // 4. Sets some fixed scores for the nodes for the sake of testing based on their roles.
    42  // 5. Starts the nodes and lets them discover each other.
    43  // 6. Subscribes the nodes to a common topic.
    44  // 7. Expects the tracer node to have the correct app scores, a non-zero score, an existing behaviour score, an existing
    45  // IP score, and an existing mesh score.
    46  // 8. Expects the score tracer to log the scores at least once.
    47  // 9. Checks that the correct metrics are being called for each score update.
    48  func TestGossipSubScoreTracer(t *testing.T) {
    49  	ctx, cancel := context.WithCancel(context.Background())
    50  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
    51  	sporkId := unittest.IdentifierFixture()
    52  	idProvider := mockmodule.NewIdentityProvider(t)
    53  	defer cancel()
    54  
    55  	loggerCycle := atomic.NewInt32(0)
    56  
    57  	// 1. Creates a logger hook to count the number of times the score logs at the interval specified.
    58  	hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) {
    59  		if level == zerolog.DebugLevel {
    60  			if message == tracer.PeerScoreLogMessage {
    61  				loggerCycle.Inc()
    62  			}
    63  		}
    64  	})
    65  	logger := zerolog.New(io.Discard).Level(zerolog.DebugLevel).Hook(hook)
    66  
    67  	// sets some fixed scores for the nodes for sake of testing based on their roles.
    68  	consensusScore := float64(87)
    69  	accessScore := float64(77)
    70  
    71  	// 2. Creates a mockPeerScoreMetrics object and sets it as a metrics collector for the tracer node.
    72  	scoreMetrics := mockmodule.NewGossipSubScoringMetrics(t)
    73  	topic1 := channels.TopicFromChannel(channels.PushBlocks, sporkId)
    74  
    75  	// 3. Creates three nodes with different roles and sets their roles as consensus, access, and tracer, respectively.
    76  	cfg, err := config.DefaultConfig()
    77  	require.NoError(t, err)
    78  	// tracer will update the score and local mesh every 1 second (for testing purposes)
    79  	cfg.NetworkConfig.GossipSub.RpcTracer.LocalMeshLogInterval = 1 * time.Second
    80  	cfg.NetworkConfig.GossipSub.RpcTracer.ScoreTracerInterval = 1 * time.Second
    81  	// the libp2p node updates the subscription list as well as the app-specific score every 10 milliseconds (for testing purposes)
    82  	cfg.NetworkConfig.GossipSub.SubscriptionProvider.UpdateInterval = 10 * time.Millisecond
    83  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond
    84  	tracerNode, tracerId := p2ptest.NodeFixture(
    85  		t,
    86  		sporkId,
    87  		t.Name(),
    88  		idProvider,
    89  		p2ptest.WithMetricsCollector(&mockPeerScoreMetrics{
    90  			NoopCollector: metrics.NoopCollector{},
    91  			c:             scoreMetrics,
    92  		}),
    93  		p2ptest.WithLogger(logger),
    94  		p2ptest.OverrideFlowConfig(cfg),
    95  		p2ptest.EnablePeerScoringWithOverride(&p2p.PeerScoringConfigOverride{
    96  			AppSpecificScoreParams: func(pid peer.ID) float64 {
    97  				id, ok := idProvider.ByPeerID(pid)
    98  				require.True(t, ok)
    99  
   100  				switch id.Role {
   101  				case flow.RoleConsensus:
   102  					return consensusScore
   103  				case flow.RoleAccess:
   104  					return accessScore
   105  				default:
   106  					t.Fatalf("unexpected role: %s", id.Role)
   107  				}
   108  				return 0
   109  			},
   110  			TopicScoreParams: map[channels.Topic]*pubsub.TopicScoreParams{
   111  				topic1: {
   112  					// set the topic score params to some fixed values for sake of testing.
   113  					// Note that these values are not realistic and should not be used in production.
   114  					TopicWeight:                     1,
   115  					TimeInMeshQuantum:               1 * time.Second,
   116  					TimeInMeshWeight:                1,
   117  					TimeInMeshCap:                   1000,
   118  					FirstMessageDeliveriesWeight:    1,
   119  					FirstMessageDeliveriesDecay:     0.999,
   120  					FirstMessageDeliveriesCap:       1000,
   121  					MeshMessageDeliveriesWeight:     -1,
   122  					MeshMessageDeliveriesDecay:      0.999,
   123  					MeshMessageDeliveriesThreshold:  100,
   124  					MeshMessageDeliveriesActivation: 1 * time.Second,
   125  					MeshMessageDeliveriesCap:        1000,
   126  					MeshFailurePenaltyWeight:        -1,
   127  					MeshFailurePenaltyDecay:         0.999,
   128  					InvalidMessageDeliveriesWeight:  -1,
   129  					InvalidMessageDeliveriesDecay:   0.999,
   130  				},
   131  			},
   132  		}),
   133  		p2ptest.WithRole(flow.RoleConsensus))
   134  
   135  	idProvider.On("ByPeerID", tracerNode.ID()).Return(&tracerId, true).Maybe()
   136  
   137  	consensusNode, consensusId := p2ptest.NodeFixture(
   138  		t,
   139  		sporkId,
   140  		t.Name(),
   141  		idProvider,
   142  		p2ptest.WithRole(flow.RoleConsensus))
   143  	idProvider.On("ByPeerID", consensusNode.ID()).Return(&consensusId, true).Maybe()
   144  
   145  	accessNode, accessId := p2ptest.NodeFixture(
   146  		t,
   147  		sporkId,
   148  		t.Name(),
   149  		idProvider,
   150  		p2ptest.WithRole(flow.RoleAccess))
   151  	idProvider.On("ByPeerID", accessNode.ID()).Return(&accessId, true).Maybe()
   152  
   153  	nodes := []p2p.LibP2PNode{tracerNode, consensusNode, accessNode}
   154  	ids := flow.IdentityList{&tracerId, &consensusId, &accessId}
   155  
   156  	// 5. Starts the nodes and lets them discover each other.
   157  	p2ptest.StartNodes(t, signalerCtx, nodes)
   158  	defer p2ptest.StopNodes(t, nodes, cancel)
   159  
   160  	p2ptest.LetNodesDiscoverEachOther(t, ctx, nodes, ids)
   161  
   162  	// 9. Checks that the correct metrics are being called for each score update.
   163  	scoreMetrics.On("OnOverallPeerScoreUpdated", mock.Anything).Return()
   164  	scoreMetrics.On("OnAppSpecificScoreUpdated", mock.Anything).Return()
   165  	scoreMetrics.On("OnIPColocationFactorUpdated", mock.Anything).Return()
   166  	scoreMetrics.On("OnBehaviourPenaltyUpdated", mock.Anything).Return()
   167  	scoreMetrics.On("OnTimeInMeshUpdated", topic1, mock.Anything).Return()
   168  	scoreMetrics.On("OnFirstMessageDeliveredUpdated", topic1, mock.Anything).Return()
   169  	scoreMetrics.On("OnMeshMessageDeliveredUpdated", topic1, mock.Anything).Return()
   170  	scoreMetrics.On("OnMeshMessageDeliveredUpdated", topic1, mock.Anything).Return()
   171  	scoreMetrics.On("OnInvalidMessageDeliveredUpdated", topic1, mock.Anything).Return()
   172  	scoreMetrics.On("SetWarningStateCount", uint(0)).Return()
   173  
   174  	// 6. Subscribes the nodes to a common topic.
   175  	_, err = tracerNode.Subscribe(
   176  		topic1,
   177  		validator.TopicValidator(
   178  			unittest.Logger(),
   179  			unittest.AllowAllPeerFilter()))
   180  	require.NoError(t, err)
   181  
   182  	_, err = consensusNode.Subscribe(
   183  		topic1,
   184  		validator.TopicValidator(
   185  			unittest.Logger(),
   186  			unittest.AllowAllPeerFilter()))
   187  	require.NoError(t, err)
   188  
   189  	_, err = accessNode.Subscribe(
   190  		topic1,
   191  		validator.TopicValidator(
   192  			unittest.Logger(),
   193  			unittest.AllowAllPeerFilter()))
   194  	require.NoError(t, err)
   195  
   196  	// 7. Expects the tracer node to have the correct app scores, a non-zero score, an existing behaviour score, an existing
   197  	// IP score, and an existing mesh score.
   198  	require.Eventually(t, func() bool {
   199  		// we expect the tracerNode to have the consensusNodes and accessNodes with the correct app scores.
   200  		exposer := tracerNode.PeerScoreExposer()
   201  		score, ok := exposer.GetAppScore(consensusNode.ID())
   202  		if !ok || score != consensusScore {
   203  			return false
   204  		}
   205  
   206  		score, ok = exposer.GetAppScore(accessNode.ID())
   207  		if !ok || score != accessScore {
   208  			return false
   209  		}
   210  
   211  		// we expect the tracerNode to have the consensusNodes and accessNodes with a non-zero score.
   212  		score, ok = exposer.GetScore(consensusNode.ID())
   213  		if !ok || score == 0 {
   214  			return false
   215  		}
   216  
   217  		score, ok = exposer.GetScore(accessNode.ID())
   218  		if !ok || score == 0 {
   219  			return false
   220  		}
   221  
   222  		// we expect the tracerNode to have the consensusNodes and accessNodes with an existing behaviour score and ip score.
   223  		_, ok = exposer.GetBehaviourPenalty(consensusNode.ID())
   224  		if !ok {
   225  			return false
   226  		}
   227  
   228  		_, ok = exposer.GetIPColocationFactor(consensusNode.ID())
   229  		if !ok {
   230  			return false
   231  		}
   232  
   233  		_, ok = exposer.GetBehaviourPenalty(accessNode.ID())
   234  		if !ok {
   235  			return false
   236  		}
   237  
   238  		_, ok = exposer.GetIPColocationFactor(accessNode.ID())
   239  		if !ok {
   240  			return false
   241  		}
   242  
   243  		// we expect the tracerNode to have the consensusNodes and accessNodes with an existing mesh score.
   244  		consensusMeshScores, ok := exposer.GetTopicScores(consensusNode.ID())
   245  		if !ok {
   246  			return false
   247  		}
   248  		_, ok = consensusMeshScores[topic1.String()]
   249  		if !ok {
   250  			return false
   251  		}
   252  
   253  		accessMeshScore, ok := exposer.GetTopicScores(accessNode.ID())
   254  		if !ok {
   255  			return false
   256  		}
   257  		_, ok = accessMeshScore[topic1.String()]
   258  		return ok
   259  	}, 2*time.Second, 10*time.Millisecond)
   260  
   261  	time.Sleep(2 * time.Second)
   262  
   263  	// 8. Expects the score tracer to log the scores at least once.
   264  	assert.Eventually(t, func() bool {
   265  		return loggerCycle.Load() > 0
   266  	}, 2*time.Second, 10*time.Millisecond)
   267  }
   268  
   269  type mockPeerScoreMetrics struct {
   270  	metrics.NoopCollector
   271  	c module.GossipSubScoringMetrics
   272  }
   273  
   274  func (m *mockPeerScoreMetrics) OnOverallPeerScoreUpdated(f float64) {
   275  	m.c.OnOverallPeerScoreUpdated(f)
   276  }
   277  
   278  func (m *mockPeerScoreMetrics) OnAppSpecificScoreUpdated(f float64) {
   279  	m.c.OnAppSpecificScoreUpdated(f)
   280  }
   281  
   282  func (m *mockPeerScoreMetrics) OnIPColocationFactorUpdated(f float64) {
   283  	m.c.OnIPColocationFactorUpdated(f)
   284  }
   285  
   286  func (m *mockPeerScoreMetrics) OnBehaviourPenaltyUpdated(f float64) {
   287  	m.c.OnBehaviourPenaltyUpdated(f)
   288  }
   289  
   290  func (m *mockPeerScoreMetrics) OnTimeInMeshUpdated(topic channels.Topic, duration time.Duration) {
   291  	m.c.OnTimeInMeshUpdated(topic, duration)
   292  }
   293  
   294  func (m *mockPeerScoreMetrics) OnFirstMessageDeliveredUpdated(topic channels.Topic, f float64) {
   295  	m.c.OnFirstMessageDeliveredUpdated(topic, f)
   296  }
   297  
   298  func (m *mockPeerScoreMetrics) OnMeshMessageDeliveredUpdated(topic channels.Topic, f float64) {
   299  	m.c.OnMeshMessageDeliveredUpdated(topic, f)
   300  }
   301  
   302  func (m *mockPeerScoreMetrics) OnInvalidMessageDeliveredUpdated(topic channels.Topic, f float64) {
   303  	m.c.OnInvalidMessageDeliveredUpdated(topic, f)
   304  }
   305  
   306  func (m *mockPeerScoreMetrics) SetWarningStateCount(u uint) {
   307  	m.c.SetWarningStateCount(u)
   308  }
   309  
   310  var _ module.GossipSubScoringMetrics = (*mockPeerScoreMetrics)(nil)