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 }