github.com/ethereum-optimism/optimism@v1.7.2/op-node/p2p/store/scorebook_test.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"strconv"
     6  	"testing"
     7  	"time"
     8  
     9  	//nolint:all
    10  	"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoreds"
    11  
    12  	"github.com/ethereum-optimism/optimism/op-service/clock"
    13  	"github.com/ethereum-optimism/optimism/op-service/testlog"
    14  	"github.com/ethereum/go-ethereum/log"
    15  	ds "github.com/ipfs/go-datastore"
    16  	"github.com/ipfs/go-datastore/sync"
    17  	"github.com/libp2p/go-libp2p/core/peer"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestGetEmptyScoreComponents(t *testing.T) {
    22  	id := peer.ID("aaaa")
    23  	store := createMemoryStore(t)
    24  	assertPeerScores(t, store, id, PeerScores{})
    25  }
    26  
    27  func TestRoundTripGossipScore(t *testing.T) {
    28  	id := peer.ID("aaaa")
    29  	store := createMemoryStore(t)
    30  	score := 123.45
    31  	res, err := store.SetScore(id, &GossipScores{Total: score})
    32  	require.NoError(t, err)
    33  
    34  	expected := PeerScores{Gossip: GossipScores{Total: score}}
    35  	require.Equal(t, expected, res)
    36  
    37  	assertPeerScores(t, store, id, expected)
    38  }
    39  
    40  func TestUpdateGossipScore(t *testing.T) {
    41  	id := peer.ID("aaaa")
    42  	store := createMemoryStore(t)
    43  	score := 123.45
    44  	setScoreRequired(t, store, id, &GossipScores{Total: 444.223})
    45  	setScoreRequired(t, store, id, &GossipScores{Total: score})
    46  
    47  	assertPeerScores(t, store, id, PeerScores{Gossip: GossipScores{Total: score}})
    48  }
    49  
    50  func TestIncrementValidResponses(t *testing.T) {
    51  	id := peer.ID("aaaa")
    52  	store := createMemoryStore(t)
    53  	inc := IncrementValidResponses{Cap: 2.1}
    54  	setScoreRequired(t, store, id, inc)
    55  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{ValidResponses: 1}})
    56  
    57  	setScoreRequired(t, store, id, inc)
    58  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{ValidResponses: 2}})
    59  
    60  	setScoreRequired(t, store, id, inc)
    61  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{ValidResponses: 2.1}})
    62  }
    63  
    64  func TestIncrementErrorResponses(t *testing.T) {
    65  	id := peer.ID("aaaa")
    66  	store := createMemoryStore(t)
    67  	inc := IncrementErrorResponses{Cap: 2.1}
    68  	setScoreRequired(t, store, id, inc)
    69  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{ErrorResponses: 1}})
    70  
    71  	setScoreRequired(t, store, id, inc)
    72  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{ErrorResponses: 2}})
    73  
    74  	setScoreRequired(t, store, id, inc)
    75  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{ErrorResponses: 2.1}})
    76  }
    77  
    78  func TestIncrementRejectedPayloads(t *testing.T) {
    79  	id := peer.ID("aaaa")
    80  	store := createMemoryStore(t)
    81  	inc := IncrementRejectedPayloads{Cap: 2.1}
    82  	setScoreRequired(t, store, id, inc)
    83  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{RejectedPayloads: 1}})
    84  
    85  	setScoreRequired(t, store, id, inc)
    86  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{RejectedPayloads: 2}})
    87  
    88  	setScoreRequired(t, store, id, inc)
    89  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{RejectedPayloads: 2.1}})
    90  }
    91  
    92  func TestDecayApplicationScores(t *testing.T) {
    93  	id := peer.ID("aaaa")
    94  	store := createMemoryStore(t)
    95  	for i := 0; i < 10; i++ {
    96  		setScoreRequired(t, store, id, IncrementValidResponses{Cap: 100})
    97  		setScoreRequired(t, store, id, IncrementErrorResponses{Cap: 100})
    98  		setScoreRequired(t, store, id, IncrementRejectedPayloads{Cap: 100})
    99  	}
   100  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{
   101  		ValidResponses:   10,
   102  		ErrorResponses:   10,
   103  		RejectedPayloads: 10,
   104  	}})
   105  
   106  	setScoreRequired(t, store, id, &DecayApplicationScores{
   107  		ValidResponseDecay:   0.8,
   108  		ErrorResponseDecay:   0.4,
   109  		RejectedPayloadDecay: 0.5,
   110  		DecayToZero:          0.1,
   111  	})
   112  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{
   113  		ValidResponses:   10 * 0.8,
   114  		ErrorResponses:   10 * 0.4,
   115  		RejectedPayloads: 10 * 0.5,
   116  	}})
   117  
   118  	// Should be set to exactly zero when below DecayToZero
   119  	setScoreRequired(t, store, id, &DecayApplicationScores{
   120  		ValidResponseDecay:   0.8,
   121  		ErrorResponseDecay:   0.4,
   122  		RejectedPayloadDecay: 0.5,
   123  		DecayToZero:          5,
   124  	})
   125  	assertPeerScores(t, store, id, PeerScores{ReqResp: ReqRespScores{
   126  		ValidResponses:   10 * 0.8 * 0.8, // Not yet below 5 so preserved
   127  		ErrorResponses:   0,
   128  		RejectedPayloads: 0,
   129  	}})
   130  }
   131  
   132  func TestStoreScoresForMultiplePeers(t *testing.T) {
   133  	id1 := peer.ID("aaaa")
   134  	id2 := peer.ID("bbbb")
   135  	store := createMemoryStore(t)
   136  	score1 := 123.45
   137  	score2 := 453.22
   138  	setScoreRequired(t, store, id1, &GossipScores{Total: score1})
   139  	setScoreRequired(t, store, id2, &GossipScores{Total: score2})
   140  
   141  	assertPeerScores(t, store, id1, PeerScores{Gossip: GossipScores{Total: score1}})
   142  	assertPeerScores(t, store, id2, PeerScores{Gossip: GossipScores{Total: score2}})
   143  }
   144  
   145  func TestPersistData(t *testing.T) {
   146  	id := peer.ID("aaaa")
   147  	score := 123.45
   148  	backingStore := sync.MutexWrap(ds.NewMapDatastore())
   149  	store := createPeerstoreWithBacking(t, backingStore)
   150  
   151  	setScoreRequired(t, store, id, &GossipScores{Total: score})
   152  
   153  	// Close and recreate a new store from the same backing
   154  	require.NoError(t, store.Close())
   155  	store = createPeerstoreWithBacking(t, backingStore)
   156  
   157  	assertPeerScores(t, store, id, PeerScores{Gossip: GossipScores{Total: score}})
   158  }
   159  
   160  func TestCloseCompletes(t *testing.T) {
   161  	store := createMemoryStore(t)
   162  	require.NoError(t, store.Close())
   163  }
   164  
   165  func TestPrune(t *testing.T) {
   166  	ctx, cancelFunc := context.WithCancel(context.Background())
   167  	defer cancelFunc()
   168  	logger := testlog.Logger(t, log.LevelInfo)
   169  	store := sync.MutexWrap(ds.NewMapDatastore())
   170  	clock := clock.NewDeterministicClock(time.UnixMilli(1000))
   171  	book, err := newScoreBook(ctx, logger, clock, store, 24*time.Hour)
   172  	require.NoError(t, err)
   173  
   174  	hasScoreRecorded := func(id peer.ID) bool {
   175  		scores, err := book.GetPeerScores(id)
   176  		require.NoError(t, err)
   177  		return scores != PeerScores{}
   178  	}
   179  
   180  	firstStore := clock.Now()
   181  	// Set some scores all 30 minutes apart so they have different expiry times
   182  	setScoreRequired(t, book, "aaaa", &GossipScores{Total: 123.45})
   183  	clock.AdvanceTime(30 * time.Minute)
   184  	setScoreRequired(t, book, "bbbb", &GossipScores{Total: 123.45})
   185  	clock.AdvanceTime(30 * time.Minute)
   186  	setScoreRequired(t, book, "cccc", &GossipScores{Total: 123.45})
   187  	clock.AdvanceTime(30 * time.Minute)
   188  	setScoreRequired(t, book, "dddd", &GossipScores{Total: 123.45})
   189  	clock.AdvanceTime(30 * time.Minute)
   190  
   191  	// Update bbbb again which should extend its expiry
   192  	setScoreRequired(t, book, "bbbb", &GossipScores{Total: 123.45})
   193  
   194  	require.True(t, hasScoreRecorded("aaaa"))
   195  	require.True(t, hasScoreRecorded("bbbb"))
   196  	require.True(t, hasScoreRecorded("cccc"))
   197  	require.True(t, hasScoreRecorded("dddd"))
   198  
   199  	elapsedTime := clock.Now().Sub(firstStore)
   200  	timeToFirstExpiry := book.book.recordExpiry - elapsedTime
   201  	// Advance time until the score for aaaa should be pruned.
   202  	clock.AdvanceTime(timeToFirstExpiry + 1)
   203  	require.NoError(t, book.book.prune())
   204  	// Clear the cache so reads have to come from the database
   205  	book.book.cache.Purge()
   206  	require.False(t, hasScoreRecorded("aaaa"), "should have pruned aaaa record")
   207  
   208  	// Advance time so cccc, dddd and the original bbbb entry should be pruned
   209  	clock.AdvanceTime(90 * time.Minute)
   210  	require.NoError(t, book.book.prune())
   211  	// Clear the cache so reads have to come from the database
   212  	book.book.cache.Purge()
   213  
   214  	require.False(t, hasScoreRecorded("cccc"), "should have pruned cccc record")
   215  	require.False(t, hasScoreRecorded("dddd"), "should have pruned cccc record")
   216  
   217  	require.True(t, hasScoreRecorded("bbbb"), "should not prune bbbb record")
   218  }
   219  
   220  func TestPruneMultipleBatches(t *testing.T) {
   221  	ctx, cancelFunc := context.WithCancel(context.Background())
   222  	defer cancelFunc()
   223  	logger := testlog.Logger(t, log.LevelInfo)
   224  	clock := clock.NewDeterministicClock(time.UnixMilli(1000))
   225  	book, err := newScoreBook(ctx, logger, clock, sync.MutexWrap(ds.NewMapDatastore()), 24*time.Hour)
   226  	require.NoError(t, err)
   227  
   228  	hasScoreRecorded := func(id peer.ID) bool {
   229  		scores, err := book.GetPeerScores(id)
   230  		require.NoError(t, err)
   231  		return scores != PeerScores{}
   232  	}
   233  
   234  	// Set scores for more peers than the max batch size
   235  	peerCount := maxPruneBatchSize*3 + 5
   236  	for i := 0; i < peerCount; i++ {
   237  		setScoreRequired(t, book, peer.ID(strconv.Itoa(i)), &GossipScores{Total: 123.45})
   238  	}
   239  	clock.AdvanceTime(book.book.recordExpiry + 1)
   240  	require.NoError(t, book.book.prune())
   241  	// Clear the cache so reads have to come from the database
   242  	book.book.cache.Purge()
   243  
   244  	for i := 0; i < peerCount; i++ {
   245  		require.Falsef(t, hasScoreRecorded(peer.ID(strconv.Itoa(i))), "Should prune record peer %v", i)
   246  	}
   247  }
   248  
   249  // Check that scores that are eligible for pruning are not returned, even if they haven't yet been removed
   250  func TestIgnoreOutdatedScores(t *testing.T) {
   251  	ctx, cancelFunc := context.WithCancel(context.Background())
   252  	defer cancelFunc()
   253  	logger := testlog.Logger(t, log.LevelInfo)
   254  	clock := clock.NewDeterministicClock(time.UnixMilli(1000))
   255  	retentionPeriod := 24 * time.Hour
   256  	book, err := newScoreBook(ctx, logger, clock, sync.MutexWrap(ds.NewMapDatastore()), retentionPeriod)
   257  	require.NoError(t, err)
   258  
   259  	setScoreRequired(t, book, "a", &GossipScores{Total: 123.45})
   260  	clock.AdvanceTime(retentionPeriod + 1)
   261  
   262  	// Not available from cache
   263  	scores, err := book.GetPeerScores("a")
   264  	require.NoError(t, err)
   265  	require.Equal(t, scores, PeerScores{})
   266  
   267  	book.book.cache.Purge()
   268  	// Not available from disk
   269  	scores, err = book.GetPeerScores("a")
   270  	require.NoError(t, err)
   271  	require.Equal(t, scores, PeerScores{})
   272  }
   273  
   274  func assertPeerScores(t *testing.T, store ExtendedPeerstore, id peer.ID, expected PeerScores) {
   275  	result, err := store.GetPeerScores(id)
   276  	require.NoError(t, err)
   277  	require.Equal(t, result, expected)
   278  
   279  	score, err := store.GetPeerScore(id)
   280  	require.NoError(t, err)
   281  	require.Equal(t, expected.Gossip.Total, score)
   282  }
   283  
   284  func createMemoryStore(t *testing.T) ExtendedPeerstore {
   285  	store := sync.MutexWrap(ds.NewMapDatastore())
   286  	return createPeerstoreWithBacking(t, store)
   287  }
   288  
   289  func createPeerstoreWithBacking(t *testing.T, store *sync.MutexDatastore) ExtendedPeerstore {
   290  	ps, err := pstoreds.NewPeerstore(context.Background(), store, pstoreds.DefaultOpts())
   291  	require.NoError(t, err, "Failed to create peerstore")
   292  	logger := testlog.Logger(t, log.LevelInfo)
   293  	c := clock.NewDeterministicClock(time.UnixMilli(100))
   294  	eps, err := NewExtendedPeerstore(context.Background(), logger, c, ps, store, 24*time.Hour)
   295  	require.NoError(t, err)
   296  	t.Cleanup(func() {
   297  		_ = eps.Close()
   298  	})
   299  	return eps
   300  }
   301  
   302  func setScoreRequired(t *testing.T, store ScoreDatastore, id peer.ID, diff ScoreDiff) {
   303  	_, err := store.SetScore(id, diff)
   304  	require.NoError(t, err)
   305  }