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

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