github.com/onflow/flow-go@v0.33.17/network/p2p/scoring/registry_test.go (about)

     1  package scoring_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"os"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/libp2p/go-libp2p/core/peer"
    13  	"github.com/rs/zerolog"
    14  	"github.com/stretchr/testify/assert"
    15  	testifymock "github.com/stretchr/testify/mock"
    16  	"github.com/stretchr/testify/require"
    17  	"go.uber.org/atomic"
    18  
    19  	"github.com/onflow/flow-go/config"
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/module/irrecoverable"
    22  	"github.com/onflow/flow-go/module/metrics"
    23  	"github.com/onflow/flow-go/module/mock"
    24  	"github.com/onflow/flow-go/network"
    25  	"github.com/onflow/flow-go/network/p2p"
    26  	netcache "github.com/onflow/flow-go/network/p2p/cache"
    27  	p2pconfig "github.com/onflow/flow-go/network/p2p/config"
    28  	p2pmsg "github.com/onflow/flow-go/network/p2p/message"
    29  	mockp2p "github.com/onflow/flow-go/network/p2p/mock"
    30  	"github.com/onflow/flow-go/network/p2p/scoring"
    31  	"github.com/onflow/flow-go/network/p2p/scoring/internal"
    32  	"github.com/onflow/flow-go/utils/unittest"
    33  )
    34  
    35  // TestScoreRegistry_FreshStart tests the app specific score computation of the node when there is no spam record for the peer id upon fresh start of the registry.
    36  // It tests the state that a staked peer with a valid role and valid subscriptions has no spam records; hence it should "eventually" be rewarded with the default reward
    37  // for its GossipSub app specific score. The "eventually" comes from the fact that the app specific score is updated asynchronously in the cache, and the cache is
    38  // updated when the app specific score function is called by GossipSub.
    39  func TestScoreRegistry_FreshStart(t *testing.T) {
    40  	peerID := peer.ID("peer-1")
    41  
    42  	cfg, err := config.DefaultConfig()
    43  	require.NoError(t, err)
    44  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
    45  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
    46  
    47  	reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t,
    48  		cfg.NetworkConfig.GossipSub.ScoringParameters,
    49  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
    50  		withStakedIdentities(peerID),
    51  		withValidSubscriptions(peerID))
    52  	ctx, cancel := context.WithCancel(context.Background())
    53  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
    54  	reg.Start(signalerCtx)
    55  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
    56  
    57  	defer stopRegistry(t, cancel, reg)
    58  
    59  	// initially, the spamRecords should not have the peer id, and there should be no app-specific score in the cache.
    60  	require.False(t, spamRecords.Has(peerID))
    61  	score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache.
    62  	require.False(t, exists)
    63  	require.Equal(t, time.Time{}, updated)
    64  	require.Equal(t, float64(0), score)
    65  
    66  	maxAppSpecificReward := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.MaxAppSpecificReward
    67  
    68  	queryTime := time.Now()
    69  	require.Eventually(t, func() bool {
    70  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
    71  		score := reg.AppSpecificScoreFunc()(peerID)
    72  		// since the peer id does not have a spam record, the app specific score should be the max app specific reward, which
    73  		// is the default reward for a staked peer that has valid subscriptions.
    74  		return score == maxAppSpecificReward
    75  	}, 5*time.Second, 100*time.Millisecond)
    76  
    77  	// still the spamRecords should not have the peer id (as there is no spam record for the peer id).
    78  	require.False(t, spamRecords.Has(peerID))
    79  
    80  	// however, the app specific score should be updated in the cache.
    81  	score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache.
    82  	require.True(t, exists)
    83  	require.True(t, updated.After(queryTime))
    84  	require.Equal(t, maxAppSpecificReward, score)
    85  
    86  	// stop the registry.
    87  	cancel()
    88  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
    89  }
    90  
    91  // TestScoreRegistry_PeerWithSpamRecord is a test suite designed to assess the app-specific penalty computation
    92  // in a scenario where a peer with a staked identity and valid subscriptions has a spam record. The suite runs multiple
    93  // sub-tests, each targeting a specific type of control message (graft, prune, ihave, iwant, RpcPublishMessage). The focus
    94  // is on the impact of spam records on the app-specific score, specifically how such records negate the default reward
    95  // a staked peer would otherwise receive, leaving only the penalty as the app-specific score. This testing reflects the
    96  // asynchronous nature of app-specific score updates in GossipSub's cache.
    97  func TestScoreRegistry_PeerWithSpamRecord(t *testing.T) {
    98  	t.Run("graft", func(t *testing.T) {
    99  		testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour)
   100  	})
   101  	t.Run("prune", func(t *testing.T) {
   102  		testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour)
   103  	})
   104  	t.Run("ihave", func(t *testing.T) {
   105  		testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour)
   106  	})
   107  	t.Run("iwant", func(t *testing.T) {
   108  		testScoreRegistryPeerWithSpamRecord(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour)
   109  	})
   110  	t.Run("RpcPublishMessage", func(t *testing.T) {
   111  		testScoreRegistryPeerWithSpamRecord(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour)
   112  	})
   113  }
   114  
   115  // testScoreRegistryPeerWithSpamRecord conducts an individual test within the TestScoreRegistry_PeerWithSpamRecord suite.
   116  // It evaluates the ScoreRegistry's handling of a staked peer with valid subscriptions when a spam record is present for
   117  // the peer ID. The function simulates the process of starting the registry, recording a misbehavior, and then verifying the
   118  // updates to the spam records and app-specific score cache based on the type of control message received.
   119  // Parameters:
   120  // - t *testing.T: The test context.
   121  // - messageType p2pmsg.ControlMessageType: The type of control message being tested.
   122  // - expectedPenalty float64: The expected penalty value for the given control message type.
   123  // This function specifically tests how the ScoreRegistry updates a peer's app-specific score in response to spam records,
   124  // emphasizing the removal of the default reward for staked peers with valid roles and focusing on the asynchronous update
   125  // mechanism of the app-specific score in the cache.
   126  func testScoreRegistryPeerWithSpamRecord(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) {
   127  	peerID := peer.ID("peer-1")
   128  
   129  	cfg, err := config.DefaultConfig()
   130  	require.NoError(t, err)
   131  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   132  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond
   133  
   134  	reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t,
   135  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   136  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   137  		withStakedIdentities(peerID),
   138  		withValidSubscriptions(peerID))
   139  	ctx, cancel := context.WithCancel(context.Background())
   140  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   141  	reg.Start(signalerCtx)
   142  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   143  
   144  	defer stopRegistry(t, cancel, reg)
   145  
   146  	// initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache.
   147  	require.False(t, spamRecords.Has(peerID))
   148  	score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache.
   149  	require.False(t, exists)
   150  	require.Equal(t, time.Time{}, updated)
   151  	require.Equal(t, float64(0), score)
   152  
   153  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   154  
   155  	// eventually, the app specific score should be updated in the cache.
   156  	require.Eventually(t, func() bool {
   157  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   158  		score := reg.AppSpecificScoreFunc()(peerID)
   159  		// since the peer id does not have a spam record, the app specific score should be the max app specific reward, which
   160  		// is the default reward for a staked peer that has valid subscriptions.
   161  		return scoreOptParameters.MaxAppSpecificReward == score
   162  	}, 5*time.Second, 100*time.Millisecond)
   163  
   164  	// report a misbehavior for the peer id.
   165  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   166  		PeerID:  peerID,
   167  		MsgType: messageType,
   168  	})
   169  
   170  	// the penalty should now be updated in the spamRecords
   171  	record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   172  	assert.True(t, ok)
   173  	assert.NoError(t, err)
   174  	assert.Less(t, math.Abs(expectedPenalty-record.Penalty), 10e-3)                                                                                                                                         // penalty should be updated to -10.
   175  	assert.Equal(t, scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state.
   176  
   177  	queryTime := time.Now()
   178  	// eventually, the app specific score should be updated in the cache.
   179  	require.Eventually(t, func() bool {
   180  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   181  		score := reg.AppSpecificScoreFunc()(peerID)
   182  		// this peer has a spam record, with no subscription penalty. Hence, the app specific score should only be the spam penalty,
   183  		// and the peer should be deprived of the default reward for its valid staked role.
   184  		// As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 0.1% error.
   185  		return math.Abs(expectedPenalty-score)/math.Max(expectedPenalty, score) < 0.001
   186  	}, 5*time.Second, 100*time.Millisecond)
   187  
   188  	// the app specific score should now be updated in the cache.
   189  	score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache.
   190  	require.True(t, exists)
   191  	require.True(t, updated.After(queryTime))
   192  	require.True(t, math.Abs(expectedPenalty-score)/math.Max(expectedPenalty, score) < 0.001)
   193  
   194  	// stop the registry.
   195  	cancel()
   196  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   197  }
   198  
   199  // TestScoreRegistry_SpamRecordWithUnknownIdentity is a test suite for verifying the behavior of the ScoreRegistry
   200  // when handling spam records associated with unknown identities. It tests various scenarios based on different control
   201  // message types, including graft, prune, ihave, iwant, and RpcPublishMessage. Each sub-test validates the app-specific
   202  // penalty computation and updates to the score registry when a peer with an unknown identity sends these control messages.
   203  func TestScoreRegistry_SpamRecordWithUnknownIdentity(t *testing.T) {
   204  	t.Run("graft", func(t *testing.T) {
   205  		testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour)
   206  	})
   207  	t.Run("prune", func(t *testing.T) {
   208  		testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour)
   209  	})
   210  	t.Run("ihave", func(t *testing.T) {
   211  		testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour)
   212  	})
   213  	t.Run("iwant", func(t *testing.T) {
   214  		testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour)
   215  	})
   216  	t.Run("RpcPublishMessage", func(t *testing.T) {
   217  		testScoreRegistrySpamRecordWithUnknownIdentity(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour)
   218  	})
   219  }
   220  
   221  // testScoreRegistrySpamRecordWithUnknownIdentity tests the app-specific penalty computation of the node when there
   222  // is a spam record for a peer ID with an unknown identity. It examines the functionality of the GossipSubAppSpecificScoreRegistry
   223  // under various conditions, including the initialization state, spam record creation, and the impact of different control message types.
   224  // Parameters:
   225  // - t *testing.T: The testing context.
   226  // - messageType p2pmsg.ControlMessageType: The type of control message being tested.
   227  // - expectedPenalty float64: The expected penalty value for the given control message type.
   228  // The function simulates the process of starting the registry, reporting a misbehavior for the peer ID, and verifying the
   229  // updates to the spam records and app-specific score cache. It ensures that the penalties are correctly computed and applied
   230  // based on the given control message type and the state of the peer ID (unknown identity and spam record presence).
   231  func testScoreRegistrySpamRecordWithUnknownIdentity(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) {
   232  	peerID := peer.ID("peer-1")
   233  	cfg, err := config.DefaultConfig()
   234  	require.NoError(t, err)
   235  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   236  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   237  
   238  	reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t,
   239  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   240  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   241  		withUnknownIdentity(peerID),
   242  		withValidSubscriptions(peerID))
   243  	ctx, cancel := context.WithCancel(context.Background())
   244  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   245  	reg.Start(signalerCtx)
   246  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   247  
   248  	defer stopRegistry(t, cancel, reg)
   249  
   250  	// initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache.
   251  	require.False(t, spamRecords.Has(peerID))
   252  	score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache.
   253  	require.False(t, exists)
   254  	require.Equal(t, time.Time{}, updated)
   255  	require.Equal(t, float64(0), score)
   256  
   257  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   258  
   259  	// eventually the app specific score should be updated in the cache to the penalty value for unknown identity.
   260  	require.Eventually(t, func() bool {
   261  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   262  		score := reg.AppSpecificScoreFunc()(peerID)
   263  		// peer does not have spam record, but has an unknown identity. Hence, the app specific score should be the staking penalty.
   264  		return scoreOptParameters.UnknownIdentityPenalty == score
   265  	}, 5*time.Second, 100*time.Millisecond)
   266  
   267  	// queryTime := time.Now()
   268  	// report a misbehavior for the peer id.
   269  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   270  		PeerID:  peerID,
   271  		MsgType: messageType,
   272  	})
   273  
   274  	// the penalty should now be updated.
   275  	record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   276  	require.True(t, ok)
   277  	require.NoError(t, err)
   278  	require.Less(t, math.Abs(expectedPenalty-record.Penalty), 10e-3)                                                                                                                                         // penalty should be updated to -10, we account for decay.
   279  	require.Equal(t, scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state.
   280  
   281  	queryTime := time.Now()
   282  	// eventually, the app specific score should be updated in the cache.
   283  	require.Eventually(t, func() bool {
   284  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   285  		score := reg.AppSpecificScoreFunc()(peerID)
   286  		// the peer has spam record as well as an unknown identity. Hence, the app specific score should be the spam penalty
   287  		// and the staking penalty.
   288  		// As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 0.1% error.
   289  		return unittest.AreNumericallyClose(expectedPenalty+scoreOptParameters.UnknownIdentityPenalty, score, 0.01)
   290  	}, 5*time.Second, 10*time.Millisecond)
   291  
   292  	// the app specific score should now be updated in the cache.
   293  	score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache.
   294  	require.True(t, exists)
   295  	require.True(t, updated.After(queryTime))
   296  
   297  	unittest.RequireNumericallyClose(t, expectedPenalty+scoreOptParameters.UnknownIdentityPenalty, score, 0.01)
   298  	assert.Equal(t, scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state.
   299  
   300  	// stop the registry.
   301  	cancel()
   302  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   303  }
   304  
   305  // TestScoreRegistry_SpamRecordWithSubscriptionPenalty is a test suite for verifying the behavior of the ScoreRegistry
   306  // in handling spam records associated with invalid subscriptions. It encompasses a series of sub-tests, each focusing on
   307  // a different control message type: graft, prune, ihave, iwant, and RpcPublishMessage. These sub-tests are designed to
   308  // validate the appropriate application of penalties in the ScoreRegistry when a peer with an invalid subscription is involved
   309  // in spam activities, as indicated by these control messages.
   310  func TestScoreRegistry_SpamRecordWithSubscriptionPenalty(t *testing.T) {
   311  	t.Run("graft", func(t *testing.T) {
   312  		testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgGraft, penaltyValueFixtures().GraftMisbehaviour)
   313  	})
   314  	t.Run("prune", func(t *testing.T) {
   315  		testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgPrune, penaltyValueFixtures().PruneMisbehaviour)
   316  	})
   317  	t.Run("ihave", func(t *testing.T) {
   318  		testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgIHave, penaltyValueFixtures().IHaveMisbehaviour)
   319  	})
   320  	t.Run("iwant", func(t *testing.T) {
   321  		testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.CtrlMsgIWant, penaltyValueFixtures().IWantMisbehaviour)
   322  	})
   323  	t.Run("RpcPublishMessage", func(t *testing.T) {
   324  		testScoreRegistrySpamRecordWithSubscriptionPenalty(t, p2pmsg.RpcPublishMessage, penaltyValueFixtures().PublishMisbehaviour)
   325  	})
   326  }
   327  
   328  // testScoreRegistrySpamRecordWithSubscriptionPenalty tests the application-specific penalty computation in the ScoreRegistry
   329  // when a spam record exists for a peer ID that also has an invalid subscription. The function simulates the process of
   330  // initializing the registry, handling spam records, and updating penalties based on various control message types.
   331  // Parameters:
   332  // - t *testing.T: The testing context.
   333  // - messageType p2pmsg.ControlMessageType: The type of control message being tested.
   334  // - expectedPenalty float64: The expected penalty value for the given control message type.
   335  // The function focuses on evaluating the registry's response to spam activities (as represented by control messages) from a
   336  // peer with invalid subscriptions. It verifies that penalties are accurately computed and applied, taking into account both
   337  // the spam record and the invalid subscription status of the peer.
   338  func testScoreRegistrySpamRecordWithSubscriptionPenalty(t *testing.T, messageType p2pmsg.ControlMessageType, expectedPenalty float64) {
   339  	peerID := peer.ID("peer-1")
   340  	cfg, err := config.DefaultConfig()
   341  	require.NoError(t, err)
   342  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   343  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   344  
   345  	reg, spamRecords, appScoreCache := newGossipSubAppSpecificScoreRegistry(t,
   346  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   347  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   348  		withStakedIdentities(peerID),
   349  		withInvalidSubscriptions(peerID))
   350  	ctx, cancel := context.WithCancel(context.Background())
   351  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   352  	reg.Start(signalerCtx)
   353  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   354  
   355  	defer stopRegistry(t, cancel, reg)
   356  
   357  	// initially, the spamRecords should not have the peer id; also the app specific score record should not be in the cache.
   358  	require.False(t, spamRecords.Has(peerID))
   359  	score, updated, exists := appScoreCache.Get(peerID) // get the score from the cache.
   360  	require.False(t, exists)
   361  	require.Equal(t, time.Time{}, updated)
   362  	require.Equal(t, float64(0), score)
   363  
   364  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   365  
   366  	// peer does not have spam record, but has invalid subscription. Hence, the app specific score should be subscription penalty.
   367  	// eventually the app specific score should be updated in the cache to the penalty value for subscription penalty.
   368  	require.Eventually(t, func() bool {
   369  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   370  		score := reg.AppSpecificScoreFunc()(peerID)
   371  		// peer does not have spam record, but has an invalid subscription penalty.
   372  		return scoreOptParameters.InvalidSubscriptionPenalty == score
   373  	}, 5*time.Second, 100*time.Millisecond)
   374  
   375  	// report a misbehavior for the peer id.
   376  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   377  		PeerID:  peerID,
   378  		MsgType: messageType,
   379  	})
   380  
   381  	// the penalty should now be updated.
   382  	record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   383  	assert.True(t, ok)
   384  	assert.NoError(t, err)
   385  	assert.Less(t, math.Abs(expectedPenalty-record.Penalty), 10e-3)
   386  	assert.Equal(t, scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state.
   387  
   388  	queryTime := time.Now()
   389  	// eventually, the app specific score should be updated in the cache.
   390  	require.Eventually(t, func() bool {
   391  		// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   392  		score := reg.AppSpecificScoreFunc()(peerID)
   393  		// the peer has spam record as well as an unknown identity. Hence, the app specific score should be the spam penalty
   394  		// and the staking penalty.
   395  		// As the app specific score in the cache and spam penalty in the spamRecords are updated at different times, we account for 0.1% error.
   396  		return unittest.AreNumericallyClose(expectedPenalty+scoreOptParameters.InvalidSubscriptionPenalty, score, 0.01)
   397  	}, 5*time.Second, 10*time.Millisecond)
   398  
   399  	// the app specific score should now be updated in the cache.
   400  	score, updated, exists = appScoreCache.Get(peerID) // get the score from the cache.
   401  	require.True(t, exists)
   402  	require.True(t, updated.After(queryTime))
   403  	unittest.RequireNumericallyClose(t, expectedPenalty+scoreOptParameters.InvalidSubscriptionPenalty, score, 0.01)
   404  
   405  	// stop the registry.
   406  	cancel()
   407  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   408  }
   409  
   410  // TestSpamPenaltyDecaysInCache tests that the spam penalty records decay over time in the cache.
   411  func TestScoreRegistry_SpamPenaltyDecaysInCache(t *testing.T) {
   412  	peerID := peer.ID("peer-1")
   413  	cfg, err := config.DefaultConfig()
   414  	require.NoError(t, err)
   415  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   416  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   417  
   418  	reg, _, _ := newGossipSubAppSpecificScoreRegistry(t,
   419  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   420  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   421  		withStakedIdentities(peerID),
   422  		withValidSubscriptions(peerID))
   423  	ctx, cancel := context.WithCancel(context.Background())
   424  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   425  	reg.Start(signalerCtx)
   426  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   427  
   428  	defer stopRegistry(t, cancel, reg)
   429  
   430  	// report a misbehavior for the peer id.
   431  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   432  		PeerID:  peerID,
   433  		MsgType: p2pmsg.CtrlMsgPrune,
   434  	})
   435  
   436  	time.Sleep(1 * time.Second) // wait for the penalty to decay.
   437  
   438  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   439  		PeerID:  peerID,
   440  		MsgType: p2pmsg.CtrlMsgGraft,
   441  	})
   442  
   443  	time.Sleep(1 * time.Second) // wait for the penalty to decay.
   444  
   445  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   446  		PeerID:  peerID,
   447  		MsgType: p2pmsg.CtrlMsgIHave,
   448  	})
   449  
   450  	time.Sleep(1 * time.Second) // wait for the penalty to decay.
   451  
   452  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   453  		PeerID:  peerID,
   454  		MsgType: p2pmsg.CtrlMsgIWant,
   455  	})
   456  
   457  	time.Sleep(1 * time.Second) // wait for the penalty to decay.
   458  
   459  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   460  		PeerID:  peerID,
   461  		MsgType: p2pmsg.RpcPublishMessage,
   462  	})
   463  
   464  	time.Sleep(1 * time.Second) // wait for the penalty to decay.
   465  
   466  	// the upper bound is the sum of the penalties without decay.
   467  	scoreUpperBound := penaltyValueFixtures().PruneMisbehaviour +
   468  		penaltyValueFixtures().GraftMisbehaviour +
   469  		penaltyValueFixtures().IHaveMisbehaviour +
   470  		penaltyValueFixtures().IWantMisbehaviour +
   471  		penaltyValueFixtures().PublishMisbehaviour
   472  	// the lower bound is the sum of the penalties with decay assuming the decay is applied 4 times to the sum of the penalties.
   473  	// in reality, the decay is applied 4 times to the first penalty, then 3 times to the second penalty, and so on.
   474  	r := scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor)()
   475  	scoreLowerBound := scoreUpperBound * math.Pow(r.Decay, 4)
   476  
   477  	// eventually, the app specific score should be updated in the cache.
   478  	require.Eventually(t, func() bool {
   479  		// when the app specific penalty function is called for the first time, the decay functionality should be kicked in
   480  		// the cache, and the penalty should be updated. Note that since the penalty values are negative, the default staked identity
   481  		// reward is not applied. Hence, the penalty is only comprised of the penalties.
   482  		score := reg.AppSpecificScoreFunc()(peerID)
   483  		// with decay, the penalty should be between the upper and lower bounds.
   484  		return score > scoreUpperBound && score < scoreLowerBound
   485  	}, 5*time.Second, 100*time.Millisecond)
   486  
   487  	// stop the registry.
   488  	cancel()
   489  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   490  }
   491  
   492  // TestSpamPenaltyDecayToZero tests that the spam penalty decays to zero over time, and when the spam penalty of
   493  // a peer is set back to zero, its app specific penalty is also reset to the initial state.
   494  func TestScoreRegistry_SpamPenaltyDecayToZero(t *testing.T) {
   495  	peerID := peer.ID("peer-1")
   496  	cfg, err := config.DefaultConfig()
   497  	require.NoError(t, err)
   498  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   499  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   500  
   501  	reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t,
   502  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   503  		func() p2p.GossipSubSpamRecord {
   504  			return p2p.GossipSubSpamRecord{
   505  				Decay:   0.02, // we choose a small decay value to speed up the test.
   506  				Penalty: 0,
   507  			}
   508  		},
   509  		withStakedIdentities(peerID),
   510  		withValidSubscriptions(peerID))
   511  
   512  	// starts the registry.
   513  	ctx, cancel := context.WithCancel(context.Background())
   514  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   515  	reg.Start(signalerCtx)
   516  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   517  
   518  	defer stopRegistry(t, cancel, reg)
   519  
   520  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   521  
   522  	// report a misbehavior for the peer id.
   523  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   524  		PeerID:  peerID,
   525  		MsgType: p2pmsg.CtrlMsgGraft,
   526  	})
   527  
   528  	// decays happen every second, so we wait for 1 second to make sure the penalty is updated.
   529  	time.Sleep(1 * time.Second)
   530  	// the penalty should now be updated, it should be still negative but greater than the penalty value (due to decay).
   531  	require.Eventually(t, func() bool {
   532  		score := reg.AppSpecificScoreFunc()(peerID)
   533  		// the penalty should be less than zero and greater than the penalty value (due to decay).
   534  		return score < 0 && score > penaltyValueFixtures().GraftMisbehaviour
   535  	}, 5*time.Second, 100*time.Millisecond)
   536  
   537  	require.Eventually(t, func() bool {
   538  		// the spam penalty should eventually decay to zero.
   539  		r, err, ok := spamRecords.Get(peerID)
   540  		return ok && err == nil && r.Penalty == 0.0
   541  	}, 5*time.Second, 100*time.Millisecond)
   542  
   543  	require.Eventually(t, func() bool {
   544  		// when the spam penalty is decayed to zero, the app specific penalty of the node should reset back to default staking reward.
   545  		return reg.AppSpecificScoreFunc()(peerID) == scoreOptParameters.StakedIdentityReward
   546  	}, 5*time.Second, 100*time.Millisecond)
   547  
   548  	// the penalty should now be zero.
   549  	record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   550  	assert.True(t, ok)
   551  	assert.NoError(t, err)
   552  	assert.Equal(t, 0.0, record.Penalty) // penalty should be zero.
   553  
   554  	// stop the registry.
   555  	cancel()
   556  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   557  }
   558  
   559  // TestPersistingUnknownIdentityPenalty tests that even though the spam penalty is decayed to zero, the unknown identity penalty
   560  // is persisted. This is because the unknown identity penalty is not decayed.
   561  func TestScoreRegistry_PersistingUnknownIdentityPenalty(t *testing.T) {
   562  	peerID := peer.ID("peer-1")
   563  
   564  	cfg, err := config.DefaultConfig()
   565  	require.NoError(t, err)
   566  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   567  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   568  
   569  	reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t,
   570  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   571  		func() p2p.GossipSubSpamRecord {
   572  			return p2p.GossipSubSpamRecord{
   573  				Decay:   0.02, // we choose a small decay value to speed up the test.
   574  				Penalty: 0,
   575  			}
   576  		},
   577  		withUnknownIdentity(peerID), // the peer id has an unknown identity.
   578  		withValidSubscriptions(peerID))
   579  
   580  	// starts the registry.
   581  	ctx, cancel := context.WithCancel(context.Background())
   582  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   583  	reg.Start(signalerCtx)
   584  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   585  
   586  	defer stopRegistry(t, cancel, reg)
   587  
   588  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   589  
   590  	// initially, the app specific score should be the default unknown identity penalty.
   591  	require.Eventually(t, func() bool {
   592  		score := reg.AppSpecificScoreFunc()(peerID)
   593  		return score == scoreOptParameters.UnknownIdentityPenalty
   594  	}, 5*time.Second, 100*time.Millisecond)
   595  
   596  	// report a misbehavior for the peer id.
   597  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   598  		PeerID:  peerID,
   599  		MsgType: p2pmsg.CtrlMsgGraft,
   600  	})
   601  
   602  	// decays happen every second, so we wait for 1 second to make sure the penalty is updated.
   603  	time.Sleep(1 * time.Second)
   604  
   605  	// the penalty should now be updated, it should be still negative but greater than the penalty value (due to decay).
   606  	require.Eventually(t, func() bool {
   607  		score := reg.AppSpecificScoreFunc()(peerID)
   608  		// Ideally, the score should be the sum of the default invalid subscription penalty and the graft penalty, however,
   609  		// due to exponential decay of the spam penalty and asynchronous update the app specific score; score should be in the range of [scoring.
   610  		// (scoring.DefaultUnknownIdentityPenalty+penaltyValueFixtures().GraftMisbehaviour, scoring.DefaultUnknownIdentityPenalty).
   611  		return score < scoreOptParameters.UnknownIdentityPenalty && score > scoreOptParameters.UnknownIdentityPenalty+penaltyValueFixtures().GraftMisbehaviour
   612  	}, 5*time.Second, 100*time.Millisecond)
   613  
   614  	require.Eventually(t, func() bool {
   615  		// the spam penalty should eventually decay to zero.
   616  		r, err, ok := spamRecords.Get(peerID)
   617  		return ok && err == nil && r.Penalty == 0.0
   618  	}, 5*time.Second, 100*time.Millisecond)
   619  
   620  	require.Eventually(t, func() bool {
   621  		// when the spam penalty is decayed to zero, the app specific penalty of the node should only contain the unknown identity penalty.
   622  		return reg.AppSpecificScoreFunc()(peerID) == scoreOptParameters.UnknownIdentityPenalty
   623  	}, 5*time.Second, 100*time.Millisecond)
   624  
   625  	// the spam penalty should now be zero in spamRecords.
   626  	record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   627  	assert.True(t, ok)
   628  	assert.NoError(t, err)
   629  	assert.Equal(t, 0.0, record.Penalty) // penalty should be zero.
   630  
   631  	// stop the registry.
   632  	cancel()
   633  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   634  }
   635  
   636  // TestPersistingInvalidSubscriptionPenalty tests that even though the spam penalty is decayed to zero, the invalid subscription penalty
   637  // is persisted. This is because the invalid subscription penalty is not decayed.
   638  func TestScoreRegistry_PersistingInvalidSubscriptionPenalty(t *testing.T) {
   639  	peerID := peer.ID("peer-1")
   640  
   641  	cfg, err := config.DefaultConfig()
   642  	require.NoError(t, err)
   643  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   644  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   645  
   646  	reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t,
   647  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   648  		func() p2p.GossipSubSpamRecord {
   649  			return p2p.GossipSubSpamRecord{
   650  				Decay:   0.02, // we choose a small decay value to speed up the test.
   651  				Penalty: 0,
   652  			}
   653  		},
   654  		withStakedIdentities(peerID),
   655  		withInvalidSubscriptions(peerID)) // the peer id has an invalid subscription
   656  
   657  	// starts the registry.
   658  	ctx, cancel := context.WithCancel(context.Background())
   659  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   660  	reg.Start(signalerCtx)
   661  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry")
   662  
   663  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   664  
   665  	// initially, the app specific score should be the default invalid subscription penalty.
   666  	require.Eventually(t, func() bool {
   667  		score := reg.AppSpecificScoreFunc()(peerID)
   668  		return score == scoreOptParameters.InvalidSubscriptionPenalty
   669  	}, 5*time.Second, 100*time.Millisecond)
   670  
   671  	// report a misbehavior for the peer id.
   672  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   673  		PeerID:  peerID,
   674  		MsgType: p2pmsg.CtrlMsgGraft,
   675  	})
   676  
   677  	// with reported spam, the app specific score should be the default invalid subscription penalty + the spam penalty.
   678  	require.Eventually(t, func() bool {
   679  		score := reg.AppSpecificScoreFunc()(peerID)
   680  		// Ideally, the score should be the sum of the default invalid subscription penalty and the graft penalty, however,
   681  		// due to exponential decay of the spam penalty and asynchronous update the app specific score; score should be in the range of [scoring.
   682  		// (DefaultInvalidSubscriptionPenalty+penaltyValueFixtures().GraftMisbehaviour, scoring.DefaultInvalidSubscriptionPenalty).
   683  		return score < scoreOptParameters.InvalidSubscriptionPenalty && score > scoreOptParameters.InvalidSubscriptionPenalty+penaltyValueFixtures().GraftMisbehaviour
   684  	}, 5*time.Second, 100*time.Millisecond)
   685  
   686  	require.Eventually(t, func() bool {
   687  		// the spam penalty should eventually decay to zero.
   688  		r, err, ok := spamRecords.Get(peerID)
   689  		return ok && err == nil && r.Penalty == 0.0
   690  	}, 5*time.Second, 100*time.Millisecond)
   691  
   692  	require.Eventually(t, func() bool {
   693  		// when the spam penalty is decayed to zero, the app specific penalty of the node should only contain the default invalid subscription penalty.
   694  		return reg.AppSpecificScoreFunc()(peerID) == scoreOptParameters.UnknownIdentityPenalty
   695  	}, 5*time.Second, 100*time.Millisecond)
   696  
   697  	// the spam penalty should now be zero in spamRecords.
   698  	record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   699  	assert.True(t, ok)
   700  	assert.NoError(t, err)
   701  	assert.Equal(t, 0.0, record.Penalty) // penalty should be zero.
   702  
   703  	// stop the registry.
   704  	cancel()
   705  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   706  }
   707  
   708  // TestScoreRegistry_TestSpamRecordDecayAdjustment ensures that spam record decay is increased each time a peers score reaches the scoring.IncreaseDecayThreshold eventually
   709  // sustained misbehavior will result in the spam record decay reaching the minimum decay speed .99, and the decay speed is reset to the max decay speed .8.
   710  func TestScoreRegistry_TestSpamRecordDecayAdjustment(t *testing.T) {
   711  	cfg, err := config.DefaultConfig()
   712  	require.NoError(t, err)
   713  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   714  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   715  	// increase configured DecayRateReductionFactor so that the decay time is increased faster
   716  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.DecayRateReductionFactor = .1
   717  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.PenaltyDecayEvaluationPeriod = time.Second
   718  
   719  	peer1 := unittest.PeerIdFixture(t)
   720  	peer2 := unittest.PeerIdFixture(t)
   721  	reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t,
   722  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   723  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   724  		withStakedIdentities(peer1, peer2),
   725  		withValidSubscriptions(peer1, peer2))
   726  
   727  	ctx, cancel := context.WithCancel(context.Background())
   728  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   729  	reg.Start(signalerCtx)
   730  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry")
   731  
   732  	// initially, the spamRecords should not have the peer ids.
   733  	assert.False(t, spamRecords.Has(peer1))
   734  	assert.False(t, spamRecords.Has(peer2))
   735  
   736  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   737  	scoringRegistryParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters
   738  	// since the both peers do not have a spam record, their app specific score should be the max app specific reward, which
   739  	// is the default reward for a staked peer that has valid subscriptions.
   740  	require.Eventually(t, func() bool {
   741  		// when the spam penalty is decayed to zero, the app specific penalty of the node should only contain the unknown identity penalty.
   742  		return scoreOptParameters.MaxAppSpecificReward == reg.AppSpecificScoreFunc()(peer1) && scoreOptParameters.MaxAppSpecificReward == reg.AppSpecificScoreFunc()(peer2)
   743  	}, 5*time.Second, 100*time.Millisecond)
   744  
   745  	// simulate sustained malicious activity from peer1, eventually the decay speed
   746  	// for a spam record should be reduced to the MinimumSpamPenaltyDecayFactor
   747  	prevDecay := scoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor
   748  	tolerance := 0.1
   749  	require.Eventually(t, func() bool {
   750  		reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   751  			PeerID:  peer1,
   752  			MsgType: p2pmsg.CtrlMsgPrune,
   753  		})
   754  		record, err, ok := spamRecords.Get(peer1)
   755  		require.NoError(t, err)
   756  		require.True(t, ok)
   757  		assert.Less(t, math.Abs(prevDecay-record.Decay), tolerance)
   758  		prevDecay = record.Decay
   759  		return record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor
   760  	}, 5*time.Second, 500*time.Millisecond)
   761  
   762  	// initialize a spam record for peer2
   763  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   764  		PeerID:  peer2,
   765  		MsgType: p2pmsg.CtrlMsgPrune,
   766  	})
   767  	// reduce penalty and increase Decay to scoring.MinimumSpamPenaltyDecayFactor
   768  	record, err := spamRecords.Adjust(peer2, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord {
   769  		record.Penalty = -.1
   770  		record.Decay = scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor
   771  		return record
   772  	})
   773  	require.NoError(t, err)
   774  	require.True(t, record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor)
   775  	require.True(t, record.Penalty == -.1)
   776  	// simulate sustained good behavior from peer 2, each time the spam record is read from the cache
   777  	// using Get method the record penalty will be decayed until it is eventually reset to
   778  	// 0 at this point the decay speed for the record should be reset to MaximumSpamPenaltyDecayFactor
   779  	// eventually after penalty reaches the skipDecaThreshold the record decay will be reset to scoringRegistryParameters.MaximumSpamPenaltyDecayFactor
   780  	require.Eventually(t, func() bool {
   781  		record, err, ok := spamRecords.Get(peer2)
   782  		require.NoError(t, err)
   783  		require.True(t, ok)
   784  		return record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor &&
   785  			record.Penalty == 0 &&
   786  			record.LastDecayAdjustment.IsZero()
   787  	}, 5*time.Second, time.Second)
   788  
   789  	// ensure decay can be reduced again after recovery for peerID 2
   790  	require.Eventually(t, func() bool {
   791  		reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   792  			PeerID:  peer2,
   793  			MsgType: p2pmsg.CtrlMsgPrune,
   794  		})
   795  		record, err, ok := spamRecords.Get(peer1)
   796  		require.NoError(t, err)
   797  		require.True(t, ok)
   798  		return record.Decay == scoringRegistryParameters.SpamRecordCache.Decay.MinimumSpamPenaltyDecayFactor
   799  	}, 5*time.Second, 500*time.Millisecond)
   800  
   801  	// stop the registry.
   802  	cancel()
   803  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   804  }
   805  
   806  // TestPeerSpamPenaltyClusterPrefixed evaluates the application-specific penalty calculation for a node when a spam record is present
   807  // for cluster-prefixed topics. In the case of an invalid control message notification marked as cluster-prefixed,
   808  // the application-specific penalty should be reduced by the default reduction factor. This test verifies the accurate computation
   809  // of the application-specific score under these conditions.
   810  func TestPeerSpamPenaltyClusterPrefixed(t *testing.T) {
   811  	ctlMsgTypes := p2pmsg.ControlMessageTypes()
   812  	peerIds := unittest.PeerIdFixtures(t, len(ctlMsgTypes))
   813  
   814  	cfg, err := config.DefaultConfig()
   815  	require.NoError(t, err)
   816  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   817  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   818  
   819  	reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t,
   820  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   821  		scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor),
   822  		withStakedIdentities(peerIds...),
   823  		withValidSubscriptions(peerIds...))
   824  
   825  	// starts the registry.
   826  	ctx, cancel := context.WithCancel(context.Background())
   827  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   828  	reg.Start(signalerCtx)
   829  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "failed to start GossipSubAppSpecificScoreRegistry")
   830  
   831  	scoreOptParameters := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore
   832  
   833  	for _, peerID := range peerIds {
   834  		// initially, the spamRecords should not have the peer id.
   835  		assert.False(t, spamRecords.Has(peerID))
   836  		// since the peer id does not have a spam record, the app specific score should (eventually, due to caching) be the max app specific reward, which
   837  		// is the default reward for a staked peer that has valid subscriptions.
   838  		require.Eventually(t, func() bool {
   839  			// calling the app specific score function when there is no app specific score in the cache should eventually update the cache.
   840  			score := reg.AppSpecificScoreFunc()(peerID)
   841  			// since the peer id does not have a spam record, the app specific score should be the max app specific reward, which
   842  			// is the default reward for a staked peer that has valid subscriptions.
   843  			return score == scoreOptParameters.MaxAppSpecificReward
   844  		}, 5*time.Second, 100*time.Millisecond)
   845  
   846  	}
   847  
   848  	// Report consecutive misbehavior's for the specified peer ID. Two misbehavior's are reported concurrently:
   849  	// 1. With IsClusterPrefixed set to false, ensuring the penalty applied to the application-specific score is not reduced.
   850  	// 2. With IsClusterPrefixed set to true, reducing the penalty added to the overall app-specific score by the default reduction factor.
   851  	for i, ctlMsgType := range ctlMsgTypes {
   852  		peerID := peerIds[i]
   853  		var wg sync.WaitGroup
   854  		wg.Add(2)
   855  		go func() {
   856  			defer wg.Done()
   857  			reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   858  				PeerID:    peerID,
   859  				MsgType:   ctlMsgType,
   860  				TopicType: p2p.CtrlMsgNonClusterTopicType,
   861  			})
   862  		}()
   863  		go func() {
   864  			defer wg.Done()
   865  			reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   866  				PeerID:    peerID,
   867  				MsgType:   ctlMsgType,
   868  				TopicType: p2p.CtrlMsgTopicTypeClusterPrefixed,
   869  			})
   870  		}()
   871  		unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "timed out waiting for goroutines to finish")
   872  
   873  		// expected penalty should be penaltyValueFixtures().GraftMisbehaviour * (1  + clusterReductionFactor)
   874  		expectedPenalty := penaltyValueFixture(ctlMsgType) * (1 + penaltyValueFixtures().ClusterPrefixedReductionFactor)
   875  
   876  		// the penalty should now be updated in the spamRecords
   877  		record, err, ok := spamRecords.Get(peerID) // get the record from the spamRecords.
   878  		assert.True(t, ok)
   879  		assert.NoError(t, err)
   880  		assert.Less(t, math.Abs(expectedPenalty-record.Penalty), 10e-3)
   881  		assert.Equal(t, scoring.InitAppScoreRecordStateFunc(cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor)().Decay, record.Decay)
   882  		// this peer has a spam record, with no subscription penalty. Hence, the app specific score should only be the spam penalty,
   883  		// and the peer should be deprived of the default reward for its valid staked role.
   884  		score := reg.AppSpecificScoreFunc()(peerID)
   885  		tolerance := 10e-3 // 0.1%
   886  		if expectedPenalty == 0 {
   887  			assert.Less(t, math.Abs(expectedPenalty), tolerance)
   888  		} else {
   889  			assert.Less(t, math.Abs(expectedPenalty-score)/expectedPenalty, tolerance)
   890  		}
   891  	}
   892  
   893  	// stop the registry.
   894  	cancel()
   895  	unittest.RequireCloseBefore(t, reg.Done(), 1*time.Second, "failed to stop GossipSubAppSpecificScoreRegistry")
   896  }
   897  
   898  // TestScoringRegistrySilencePeriod ensures that the scoring registry does not penalize nodes during the silence period, and
   899  // starts to penalize nodes only after the silence period is over.
   900  func TestScoringRegistrySilencePeriod(t *testing.T) {
   901  	peerID := unittest.PeerIdFixture(t)
   902  	silenceDuration := 5 * time.Second
   903  	silencedNotificationLogs := atomic.NewInt32(0)
   904  	hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) {
   905  		if level == zerolog.TraceLevel {
   906  			if message == scoring.NotificationSilencedMsg {
   907  				silencedNotificationLogs.Inc()
   908  			}
   909  		}
   910  	})
   911  	logger := zerolog.New(os.Stdout).Level(zerolog.TraceLevel).Hook(hook)
   912  
   913  	cfg, err := config.DefaultConfig()
   914  	require.NoError(t, err)
   915  	// refresh cached app-specific score every 100 milliseconds to speed up the test.
   916  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 100 * time.Millisecond
   917  	maximumSpamPenaltyDecayFactor := cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.SpamRecordCache.Decay.MaximumSpamPenaltyDecayFactor
   918  	reg, spamRecords, _ := newGossipSubAppSpecificScoreRegistry(t,
   919  		cfg.NetworkConfig.GossipSub.ScoringParameters,
   920  		scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor),
   921  		withUnknownIdentity(peerID),
   922  		withInvalidSubscriptions(peerID),
   923  		func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
   924  			// we set the scoring registry silence duration 10 seconds
   925  			// the peer is not expected to be penalized for the first 5 seconds of the test
   926  			// after that an invalid control message notification is processed and the peer
   927  			// should be penalized
   928  			cfg.ScoringRegistryStartupSilenceDuration = silenceDuration
   929  			// hooked logger will capture the number of logs related to ignored notifications
   930  			cfg.Logger = logger
   931  		})
   932  
   933  	ctx, cancel := context.WithCancel(context.Background())
   934  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   935  	defer stopRegistry(t, cancel, reg)
   936  	// capture approximate registry start time
   937  	reg.Start(signalerCtx)
   938  	unittest.RequireCloseBefore(t, reg.Ready(), 1*time.Second, "registry did not start in time")
   939  
   940  	registryStartTime := time.Now()
   941  	expectedNumOfSilencedNotif := 0
   942  	// while we are in the silence period all notifications should be ignored and the
   943  	// invalid subscription penalty should not be applied to the app specific score
   944  	// we ensure we stay within the silence duration by iterating only up until 1 second
   945  	// before silence period is over
   946  	for time.Since(registryStartTime) < (silenceDuration - time.Second) {
   947  		// report a misbehavior for the peer id.
   948  		reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   949  			PeerID:  peerID,
   950  			MsgType: p2pmsg.CtrlMsgGraft,
   951  		})
   952  		expectedNumOfSilencedNotif++
   953  		// spam records should not be created during the silence period
   954  		_, err, ok := spamRecords.Get(peerID)
   955  		assert.False(t, ok)
   956  		assert.NoError(t, err)
   957  		// initially, the app specific score should be the default invalid subscription penalty.
   958  		require.Equal(t, float64(0), reg.AppSpecificScoreFunc()(peerID))
   959  	}
   960  
   961  	invalidSubscriptionPenalty := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.InvalidSubscriptionPenalty
   962  
   963  	require.Eventually(t, func() bool {
   964  		// we expect to have logged a debug message for all notifications ignored.
   965  		require.Equal(t, int32(expectedNumOfSilencedNotif), silencedNotificationLogs.Load())
   966  		// after silence period the invalid subscription penalty should be applied to the app specific score
   967  		return invalidSubscriptionPenalty == reg.AppSpecificScoreFunc()(peerID)
   968  	}, 2*time.Second, 200*time.Millisecond)
   969  
   970  	// after silence period the peer has spam record as well as an unknown identity. Hence, the app specific score should be the spam penalty
   971  	// and the staking penalty.
   972  	reg.OnInvalidControlMessageNotification(&p2p.InvCtrlMsgNotif{
   973  		PeerID:  peerID,
   974  		MsgType: p2pmsg.CtrlMsgGraft,
   975  	})
   976  	// the penalty should now be applied and spam records created.
   977  	record, err, ok := spamRecords.Get(peerID)
   978  	assert.True(t, ok)
   979  	assert.NoError(t, err)
   980  	expectedPenalty := penaltyValueFixtures().GraftMisbehaviour
   981  	assert.Less(t, math.Abs(expectedPenalty-record.Penalty), 10e-3)
   982  	assert.Equal(t, scoring.InitAppScoreRecordStateFunc(maximumSpamPenaltyDecayFactor)().Decay, record.Decay) // decay should be initialized to the initial state.
   983  
   984  	require.Eventually(t, func() bool {
   985  		// we expect to have logged a debug message for all notifications ignored.
   986  		require.Equal(t, int32(expectedNumOfSilencedNotif), silencedNotificationLogs.Load())
   987  		// after silence period the invalid subscription penalty should be applied to the app specific score
   988  		return invalidSubscriptionPenalty+expectedPenalty-reg.AppSpecificScoreFunc()(peerID) < 0.1
   989  	}, 2*time.Second, 200*time.Millisecond)
   990  }
   991  
   992  // withStakedIdentities returns a function that sets the identity provider to return staked identities for the given peer ids.
   993  // It is used for testing purposes, and causes the given peer id to benefit from the staked identity reward in GossipSub.
   994  func withStakedIdentities(peerIds ...peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
   995  	return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
   996  		cfg.IdProvider.(*mock.IdentityProvider).On("ByPeerID", testifymock.AnythingOfType("peer.ID")).
   997  			Return(func(pid peer.ID) *flow.Identity {
   998  				for _, peerID := range peerIds {
   999  					if peerID == pid {
  1000  						return unittest.IdentityFixture()
  1001  					}
  1002  				}
  1003  				return nil
  1004  			}, func(pid peer.ID) bool {
  1005  				for _, peerID := range peerIds {
  1006  					if peerID == pid {
  1007  						return true
  1008  					}
  1009  				}
  1010  				return false
  1011  			}).Maybe()
  1012  	}
  1013  }
  1014  
  1015  // withValidSubscriptions returns a function that sets the subscription validator to return nil for the given peer ids.
  1016  // It is used for testing purposes and causes the given peer id to never be penalized for subscribing to invalid topics.
  1017  func withValidSubscriptions(peerIds ...peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
  1018  	return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
  1019  		cfg.Validator.(*mockp2p.SubscriptionValidator).
  1020  			On("CheckSubscribedToAllowedTopics", testifymock.AnythingOfType("peer.ID"), testifymock.Anything).
  1021  			Return(func(pid peer.ID, _ flow.Role) error {
  1022  				for _, peerID := range peerIds {
  1023  					if peerID == pid {
  1024  						return nil
  1025  					}
  1026  				}
  1027  				return fmt.Errorf("invalid subscriptions")
  1028  			}).Maybe()
  1029  	}
  1030  }
  1031  
  1032  // withUnknownIdentity returns a function that sets the identity provider to return an error for the given peer id.
  1033  // It is used for testing purposes, and causes the given peer id to be penalized for not having a staked identity.
  1034  func withUnknownIdentity(peer peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
  1035  	return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
  1036  		cfg.IdProvider.(*mock.IdentityProvider).On("ByPeerID", peer).Return(nil, false).Maybe()
  1037  	}
  1038  }
  1039  
  1040  // withInvalidSubscriptions returns a function that sets the subscription validator to return an error for the given peer id.
  1041  // It is used for testing purposes and causes the given peer id to be penalized for subscribing to invalid topics.
  1042  func withInvalidSubscriptions(peer peer.ID) func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
  1043  	return func(cfg *scoring.GossipSubAppSpecificScoreRegistryConfig) {
  1044  		cfg.Validator.(*mockp2p.SubscriptionValidator).On("CheckSubscribedToAllowedTopics",
  1045  			peer,
  1046  			testifymock.Anything).Return(fmt.Errorf("invalid subscriptions")).Maybe()
  1047  	}
  1048  }
  1049  
  1050  // newGossipSubAppSpecificScoreRegistry creates a new instance of GossipSubAppSpecificScoreRegistry along with its associated
  1051  // GossipSubSpamRecordCache and AppSpecificScoreCache. This function is primarily used in testing scenarios to set up a controlled
  1052  // environment for evaluating the behavior of the GossipSub scoring mechanism.
  1053  //
  1054  // The function accepts a variable number of options to configure the GossipSubAppSpecificScoreRegistryConfig, allowing for
  1055  // customization of the registry's behavior in tests. These options can modify various aspects of the configuration, such as
  1056  // penalty values, identity providers, validators, and caching mechanisms.
  1057  //
  1058  // Parameters:
  1059  // - t *testing.T: The test context, used for asserting the absence of errors during the setup.
  1060  // - params p2pconfig.ScoringParameters: The scoring parameters used to configure the registry.
  1061  // - initFunction scoring.SpamRecordInitFunc: The function used to initialize the spam records.
  1062  // - opts ...func(*scoring.GossipSubAppSpecificScoreRegistryConfig): A variadic set of functions that modify the registry's configuration.
  1063  //
  1064  // Returns:
  1065  // - *scoring.GossipSubAppSpecificScoreRegistry: The configured GossipSub application-specific score registry.
  1066  // - *netcache.GossipSubSpamRecordCache: The cache used for storing spam records.
  1067  // - *internal.AppSpecificScoreCache: The cache for storing application-specific scores.
  1068  //
  1069  // This function initializes and configures the scoring registry with default and test-specific settings. It sets up a spam record cache
  1070  // and an application-specific score cache with predefined sizes and functionalities. The function also configures the scoring parameters
  1071  // with test-specific values, particularly modifying the ScoreTTL value for the purpose of the tests. The creation and configuration of
  1072  // the GossipSubAppSpecificScoreRegistry are validated to ensure no errors occur during the process.
  1073  func newGossipSubAppSpecificScoreRegistry(t *testing.T,
  1074  	params p2pconfig.ScoringParameters,
  1075  	initFunction scoring.SpamRecordInitFunc,
  1076  	opts ...func(*scoring.GossipSubAppSpecificScoreRegistryConfig)) (*scoring.GossipSubAppSpecificScoreRegistry,
  1077  	*netcache.GossipSubSpamRecordCache,
  1078  	*internal.AppSpecificScoreCache) {
  1079  	cache := netcache.NewGossipSubSpamRecordCache(100,
  1080  		unittest.Logger(),
  1081  		metrics.NewNoopCollector(),
  1082  		initFunction,
  1083  		scoring.DefaultDecayFunction(params.ScoringRegistryParameters.SpamRecordCache.Decay))
  1084  	appSpecificScoreCache := internal.NewAppSpecificScoreCache(100, unittest.Logger(), metrics.NewNoopCollector())
  1085  
  1086  	validator := mockp2p.NewSubscriptionValidator(t)
  1087  	validator.On("Start", testifymock.Anything).Return().Maybe()
  1088  	done := make(chan struct{})
  1089  	close(done)
  1090  	f := func() <-chan struct{} {
  1091  		return done
  1092  	}
  1093  	validator.On("Ready").Return(f()).Maybe()
  1094  	validator.On("Done").Return(f()).Maybe()
  1095  	cfg := &scoring.GossipSubAppSpecificScoreRegistryConfig{
  1096  		Logger:     unittest.Logger(),
  1097  		Penalty:    penaltyValueFixtures(),
  1098  		IdProvider: mock.NewIdentityProvider(t),
  1099  		Validator:  validator,
  1100  		AppScoreCacheFactory: func() p2p.GossipSubApplicationSpecificScoreCache {
  1101  			return appSpecificScoreCache
  1102  		},
  1103  		SpamRecordCacheFactory: func() p2p.GossipSubSpamRecordCache {
  1104  			return cache
  1105  		},
  1106  		Parameters:                            params.ScoringRegistryParameters.AppSpecificScore,
  1107  		HeroCacheMetricsFactory:               metrics.NewNoopHeroCacheMetricsFactory(),
  1108  		NetworkingType:                        network.PrivateNetwork,
  1109  		AppSpecificScoreParams:                params.PeerScoring.Protocol.AppSpecificScore,
  1110  		ScoringRegistryStartupSilenceDuration: 0, // turn off silence period by default
  1111  	}
  1112  	for _, opt := range opts {
  1113  		opt(cfg)
  1114  	}
  1115  
  1116  	reg, err := scoring.NewGossipSubAppSpecificScoreRegistry(cfg)
  1117  	require.NoError(t, err, "failed to create GossipSubAppSpecificScoreRegistry")
  1118  
  1119  	return reg, cache, appSpecificScoreCache
  1120  }
  1121  
  1122  // penaltyValueFixtures returns a set of penalty values for testing purposes.
  1123  // The values are not realistic. The important thing is that they are different from each other. This is to make sure
  1124  // that the tests are not passing because of the default values.
  1125  func penaltyValueFixtures() p2pconfig.MisbehaviourPenalties {
  1126  	return p2pconfig.MisbehaviourPenalties{
  1127  		GraftMisbehaviour:              -100,
  1128  		PruneMisbehaviour:              -50,
  1129  		IHaveMisbehaviour:              -20,
  1130  		IWantMisbehaviour:              -10,
  1131  		ClusterPrefixedReductionFactor: .5,
  1132  		PublishMisbehaviour:            -10,
  1133  	}
  1134  }
  1135  
  1136  // penaltyValueFixture returns the set penalty of the provided control message type returned from the fixture func penaltyValueFixtures.
  1137  func penaltyValueFixture(msgType p2pmsg.ControlMessageType) float64 {
  1138  	penaltyValues := penaltyValueFixtures()
  1139  	switch msgType {
  1140  	case p2pmsg.CtrlMsgGraft:
  1141  		return penaltyValues.GraftMisbehaviour
  1142  	case p2pmsg.CtrlMsgPrune:
  1143  		return penaltyValues.PruneMisbehaviour
  1144  	case p2pmsg.CtrlMsgIHave:
  1145  		return penaltyValues.IHaveMisbehaviour
  1146  	case p2pmsg.CtrlMsgIWant:
  1147  		return penaltyValues.IWantMisbehaviour
  1148  	case p2pmsg.RpcPublishMessage:
  1149  		return penaltyValues.PublishMisbehaviour
  1150  	default:
  1151  		return penaltyValues.ClusterPrefixedReductionFactor
  1152  	}
  1153  }
  1154  
  1155  func stopRegistry(t *testing.T, cancel context.CancelFunc, registry *scoring.GossipSubAppSpecificScoreRegistry) {
  1156  	cancel()
  1157  	unittest.RequireCloseBefore(t, registry.Done(), 5*time.Second, "registry did not stop")
  1158  }