github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/cache/gossipsub_spam_records_test.go (about) 1 package cache_test 2 3 import ( 4 "fmt" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/libp2p/go-libp2p/core/peer" 10 "github.com/stretchr/testify/require" 11 "go.uber.org/atomic" 12 13 "github.com/onflow/flow-go/module/metrics" 14 "github.com/onflow/flow-go/network/p2p" 15 netcache "github.com/onflow/flow-go/network/p2p/cache" 16 "github.com/onflow/flow-go/utils/unittest" 17 ) 18 19 // TestGossipSubSpamRecordCache_Add tests the Add method of the GossipSubSpamRecordCache. It tests 20 // adding a new record to the cache. 21 func TestGossipSubSpamRecordCache_Add(t *testing.T) { 22 // create a new instance of GossipSubSpamRecordCache. 23 cache := netcache.NewGossipSubSpamRecordCache(100, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 24 return p2p.GossipSubSpamRecord{ 25 Decay: 0, 26 Penalty: 0, 27 LastDecayAdjustment: time.Now(), 28 } 29 }) 30 31 adjustedEntity, err := cache.Adjust("peer0", func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 32 record.Decay = 0.1 33 record.Penalty = 0.5 34 35 return record 36 }) 37 require.NoError(t, err) 38 require.Equal(t, 0.1, adjustedEntity.Decay) 39 require.Equal(t, 0.5, adjustedEntity.Penalty) 40 41 // makes the cache full. 42 for i := 1; i <= 100; i++ { 43 adjustedEntity, err := cache.Adjust(peer.ID(fmt.Sprintf("peer%d", i)), func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 44 record.Decay = 0.1 45 record.Penalty = 0.5 46 47 return record 48 }) 49 50 require.NoError(t, err) 51 require.Equal(t, 0.1, adjustedEntity.Decay) 52 } 53 54 // retrieving an existing record should work. 55 for i := 1; i <= 100; i++ { 56 record, err, ok := cache.Get(peer.ID(fmt.Sprintf("peer%d", i))) 57 require.True(t, ok, fmt.Sprintf("record for peer%d should exist", i)) 58 require.NoError(t, err) 59 60 require.Equal(t, 0.1, record.Decay) 61 require.Equal(t, 0.5, record.Penalty) 62 } 63 64 // since cache is LRU, the first record should be evicted. 65 _, err, ok := cache.Get("peer0") 66 require.False(t, ok) 67 require.NoError(t, err) 68 } 69 70 // TestGossipSubSpamRecordCache_Concurrent_Adjust tests if the cache can be adjusted and retrieved concurrently. 71 // It adjusts the cache with a number of records concurrently and then checks if the cache can retrieve all records. 72 func TestGossipSubSpamRecordCache_Concurrent_Adjust(t *testing.T) { 73 cache := netcache.NewGossipSubSpamRecordCache(200, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 74 return p2p.GossipSubSpamRecord{ 75 Decay: 0, 76 Penalty: 0, 77 LastDecayAdjustment: time.Now(), 78 } 79 }) 80 81 // defines the number of records to be adjusted. 82 numRecords := 100 83 84 // uses a wait group to wait for all goroutines to finish. 85 var wg sync.WaitGroup 86 wg.Add(numRecords) 87 88 // adds the records concurrently. 89 for i := 0; i < numRecords; i++ { 90 go func(num int) { 91 defer wg.Done() 92 peerID := fmt.Sprintf("peer%d", num) 93 adjustedEntity, err := cache.Adjust(peer.ID(peerID), func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 94 record.Decay = 0.1 * float64(num) 95 record.Penalty = float64(num) 96 97 return record 98 }) 99 100 require.NoError(t, err) 101 require.Equal(t, 0.1*float64(num), adjustedEntity.Decay) 102 require.Equal(t, float64(num), adjustedEntity.Penalty) 103 }(i) 104 } 105 106 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "could not adjust all records concurrently on time") 107 108 // checks if the cache can retrieve all records. 109 for i := 0; i < numRecords; i++ { 110 peerID := fmt.Sprintf("peer%d", i) 111 record, err, found := cache.Get(peer.ID(peerID)) 112 require.True(t, found) 113 require.NoError(t, err) 114 115 expectedPenalty := float64(i) 116 require.Equal(t, expectedPenalty, record.Penalty, 117 "Get() returned incorrect penalty for record %s: expected %f, got %f", peerID, expectedPenalty, record.Penalty) 118 expectedDecay := 0.1 * float64(i) 119 require.Equal(t, expectedDecay, record.Decay, 120 "Get() returned incorrect decay for record %s: expected %f, got %f", peerID, expectedDecay, record.Decay) 121 } 122 } 123 124 // TestGossipSubSpamRecordCache_Adjust tests the Adjust method of the GossipSubSpamRecordCache. It tests if the cache can adjust 125 // the penalty of an existing record and add a new record. 126 func TestGossipSubSpamRecordCache_Adjust(t *testing.T) { 127 cache := netcache.NewGossipSubSpamRecordCache(200, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 128 return p2p.GossipSubSpamRecord{ 129 Decay: 0, 130 Penalty: 0, 131 LastDecayAdjustment: time.Now(), 132 } 133 }) 134 135 peerID := "peer1" 136 137 // test adjusting a non-existing record. 138 record, err := cache.Adjust(peer.ID(peerID), func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 139 record.Penalty = 0.7 140 return record 141 }) 142 require.NoError(t, err) 143 require.Equal(t, 0.7, record.Penalty) // checks if the penalty is adjusted correctly. 144 145 // test adjusting an existing record. 146 record, err = cache.Adjust(peer.ID(peerID), func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 147 record.Penalty = 0.8 148 return record 149 }) 150 require.NoError(t, err) 151 require.Equal(t, 0.8, record.Penalty) // checks if the penalty is adjusted correctly. 152 } 153 154 // TestGossipSubSpamRecordCache_Adjust_With_Preprocess tests Adjust method of the GossipSubSpamRecordCache when the cache 155 // has preprocessor functions. 156 // It tests when the cache has preprocessor functions, all preprocessor functions are called prior to the adjust function. 157 // Also, it tests if the pre-processor functions are called in the order they are added. 158 func TestGossipSubSpamRecordCache_Adjust_With_Preprocess(t *testing.T) { 159 cache := netcache.NewGossipSubSpamRecordCache(200, 160 unittest.Logger(), 161 metrics.NewNoopCollector(), 162 func() p2p.GossipSubSpamRecord { 163 return p2p.GossipSubSpamRecord{ 164 Decay: 0, 165 Penalty: 0, 166 LastDecayAdjustment: time.Now(), 167 } 168 }, 169 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 170 record.Penalty += 1.5 171 return record, nil 172 }, func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 173 record.Penalty *= 2 174 return record, nil 175 }) 176 177 peerID := "peer1" 178 179 // test adjusting a non-existing record. 180 record, err := cache.Adjust(peer.ID(peerID), func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 181 record.Penalty = 0.5 182 record.Decay = 0.1 183 return record 184 }) 185 require.NoError(t, err) 186 require.Equal(t, 0.5, record.Penalty) // checks if the penalty is adjusted correctly. 187 require.Equal(t, 0.1, record.Decay) // checks if the decay is adjusted correctly. 188 189 // tests updating the penalty of an existing record. 190 record, err = cache.Adjust(peer.ID(peerID), func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 191 record.Penalty += 0.7 192 return record 193 }) 194 require.NoError(t, err) 195 require.Equal(t, 4.7, record.Penalty) // (0.5+1.5) * 2 + 0.7 = 4.7 196 require.Equal(t, 0.1, record.Decay) // checks if the decay is not changed. 197 } 198 199 // TestGossipSubSpamRecordCache_Adjust_Preprocess_Error tests the Adjust method of the GossipSubSpamRecordCache. 200 // It tests if any of the preprocessor functions returns an error, the Adjust function effect 201 // is reverted, and the error is returned. 202 func TestGossipSubSpamRecordCache_Adjust_Preprocess_Error(t *testing.T) { 203 secondPreprocessorCalled := 0 204 cache := netcache.NewGossipSubSpamRecordCache(200, 205 unittest.Logger(), 206 metrics.NewNoopCollector(), 207 func() p2p.GossipSubSpamRecord { 208 return p2p.GossipSubSpamRecord{ 209 Decay: 0, 210 Penalty: 0, 211 LastDecayAdjustment: time.Now(), 212 } 213 }, 214 // the first preprocessor function does not return an error. 215 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 216 return record, nil 217 }, 218 // the second preprocessor function returns an error on the second call, and does not return an error on any other call. 219 // this means that adjustment should be successful on the first call, and should fail on the second call. 220 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 221 secondPreprocessorCalled++ 222 if secondPreprocessorCalled == 2 { 223 return record, fmt.Errorf("some error") 224 } 225 return record, nil 226 227 }) 228 229 peerID := unittest.PeerIdFixture(t) 230 231 // tests adjusting the penalty of a non-existing record; the record should be initiated and the penalty should be adjusted. 232 record, err := cache.Adjust(peerID, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 233 record.Penalty = 0.5 234 record.Decay = 0.1 235 return record 236 }) 237 require.NoError(t, err) 238 require.NotNil(t, record) 239 require.Equal(t, 0.5, record.Penalty) // checks if the penalty is not changed. 240 require.Equal(t, 0.1, record.Decay) // checks if the decay is not changed. 241 242 // tests adjusting the penalty of an existing record. 243 record, err = cache.Adjust(peerID, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 244 record.Penalty = 0.7 245 return record 246 }) 247 // since the second preprocessor function returns an error, the adjust function effect should be reverted. 248 // the error should be returned. 249 require.Error(t, err) 250 require.Nil(t, record) 251 252 // checks if the record is not changed. 253 record, err, found := cache.Get(peerID) 254 require.True(t, found) 255 require.NoError(t, err) 256 require.Equal(t, 0.5, record.Penalty) // checks if the penalty is not changed. 257 require.Equal(t, 0.1, record.Decay) // checks if the decay is not changed. 258 } 259 260 // TestGossipSubSpamRecordCache_ByValue tests if the cache stores the GossipSubSpamRecord by value. 261 // It adjusts the cache with a record and then modifies the record externally. 262 // It then checks if the record in the cache is still the original record. 263 // This is a desired behavior that is guaranteed by the underlying HeroCache library. 264 // In other words, we don't desire the records to be externally mutable after they are added to the cache (unless by a subsequent call to Adjust). 265 func TestGossipSubSpamRecordCache_ByValue(t *testing.T) { 266 cache := netcache.NewGossipSubSpamRecordCache(200, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 267 return p2p.GossipSubSpamRecord{ 268 Decay: 0, 269 Penalty: 0, 270 LastDecayAdjustment: time.Now(), 271 } 272 }) 273 274 peerID := unittest.PeerIdFixture(t) 275 // adjusts a non-existing record, which should initiate the record. 276 record, err := cache.Adjust(peerID, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 277 record.Penalty = 0.5 278 record.Decay = 0.1 279 return record 280 }) 281 require.NoError(t, err) 282 require.NotNil(t, record) 283 require.Equal(t, 0.5, record.Penalty) // checks if the penalty is not changed. 284 require.Equal(t, 0.1, record.Decay) // checks if the decay is not changed. 285 286 // get the record from the cache 287 record, err, found := cache.Get(peerID) 288 require.True(t, found) 289 require.NoError(t, err) 290 291 // modify the record 292 record.Decay = 0.2 293 record.Penalty = 0.8 294 295 // get the record from the cache again 296 record, err, found = cache.Get(peerID) 297 require.True(t, found) 298 require.NoError(t, err) 299 300 // check if the record is still the same 301 require.Equal(t, 0.1, record.Decay) 302 require.Equal(t, 0.5, record.Penalty) 303 } 304 305 // TestGossipSubSpamRecordCache_Get_With_Preprocessors tests if the cache applies the preprocessors to the records before returning them. 306 func TestGossipSubSpamRecordCache_Get_With_Preprocessors(t *testing.T) { 307 cache := netcache.NewGossipSubSpamRecordCache(10, unittest.Logger(), metrics.NewNoopCollector(), 308 func() p2p.GossipSubSpamRecord { 309 return p2p.GossipSubSpamRecord{ 310 Decay: 0, 311 Penalty: 0, 312 LastDecayAdjustment: time.Now(), 313 } 314 }, 315 // first preprocessor: adds 1 to the penalty. 316 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 317 record.Penalty++ 318 return record, nil 319 }, 320 // second preprocessor: multiplies the penalty by 2 321 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 322 record.Penalty *= 2 323 return record, nil 324 }, 325 ) 326 327 peerId := unittest.PeerIdFixture(t) 328 adjustedRecord, err := cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 329 record.Penalty = 1 330 record.Decay = 0.5 331 return record 332 }) 333 require.NoError(t, err) 334 require.Equal(t, 1.0, adjustedRecord.Penalty) 335 336 // verifies that the preprocessors were called and the record was adjusted accordingly. 337 cachedRecord, err, ok := cache.Get(peerId) 338 require.NoError(t, err) 339 require.True(t, ok) 340 341 // expected penalty is 4: the first preprocessor adds 1 to the penalty and the second preprocessor multiplies the penalty by 2. 342 // (1 + 1) * 2 = 4 343 require.Equal(t, 4.0, cachedRecord.Penalty) // penalty should be adjusted 344 require.Equal(t, 0.5, cachedRecord.Decay) // decay should not be modified 345 } 346 347 // TestGossipSubSpamRecordCache_Get_Preprocessor_Error tests if the cache returns an error if one of the preprocessors returns an error upon a Get. 348 // It adds a record to the cache and then checks if the cache returns an error upon a Get if one of the preprocessors returns an error. 349 // It also checks if a preprocessor is failed, the subsequent preprocessors are not called, and the original record is returned. 350 // In other words, the Get method acts atomically on the record for applying the preprocessors. If one of the preprocessors 351 // fails, the record is returned without applying the subsequent preprocessors. 352 func TestGossipSubSpamRecordCache_Get_Preprocessor_Error(t *testing.T) { 353 secondPreprocessorCalledCount := 0 354 thirdPreprocessorCalledCount := 0 355 356 cache := netcache.NewGossipSubSpamRecordCache(10, unittest.Logger(), metrics.NewNoopCollector(), 357 func() p2p.GossipSubSpamRecord { 358 return p2p.GossipSubSpamRecord{ 359 Decay: 0, 360 Penalty: 0, 361 LastDecayAdjustment: time.Now(), 362 } 363 }, 364 // first preprocessor: adds 1 to the penalty. 365 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 366 record.Penalty++ 367 return record, nil 368 }, 369 // second preprocessor: multiplies the penalty by 2 (this preprocessor returns an error on the third call and forward) 370 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 371 secondPreprocessorCalledCount++ 372 if secondPreprocessorCalledCount < 3 { 373 // on the first call, the preprocessor is successful 374 return record, nil 375 } else { 376 // on the second call, the preprocessor returns an error 377 return p2p.GossipSubSpamRecord{}, fmt.Errorf("error in preprocessor") 378 } 379 }, 380 // since second preprocessor returns an error on the second call, the third preprocessor should not be called more than once. 381 func(record p2p.GossipSubSpamRecord, lastUpdated time.Time) (p2p.GossipSubSpamRecord, error) { 382 thirdPreprocessorCalledCount++ 383 require.Less(t, secondPreprocessorCalledCount, 3) 384 return record, nil 385 }, 386 ) 387 388 peerId := unittest.PeerIdFixture(t) 389 adjustedRecord, err := cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 390 record.Penalty = 1 391 record.Decay = 0.5 392 return record 393 }) 394 require.NoError(t, err) 395 require.Equal(t, 1.0, adjustedRecord.Penalty) 396 require.Equal(t, 0.5, adjustedRecord.Decay) 397 398 // verifies that the preprocessors were called and the penalty was adjusted accordingly. 399 cachedRecord, err, ok := cache.Get(peerId) 400 require.NoError(t, err) 401 require.True(t, ok) 402 require.Equal(t, 2.0, cachedRecord.Penalty) // penalty should be adjusted by the first preprocessor (1 + 1 = 2) 403 require.Equal(t, 0.5, cachedRecord.Decay) 404 405 // query the cache again that should trigger the second preprocessor to return an error. 406 cachedRecord, err, ok = cache.Get(peerId) 407 require.Error(t, err) 408 require.False(t, ok) 409 require.Nil(t, cachedRecord) 410 411 // verifies that the third preprocessor was called only twice (two success calls). 412 require.Equal(t, 2, thirdPreprocessorCalledCount) 413 // verifies that the second preprocessor was called three times (two success calls and one failure call). 414 require.Equal(t, 3, secondPreprocessorCalledCount) 415 } 416 417 // TestGossipSubSpamRecordCache_Get_Without_Preprocessors tests when no preprocessors are provided to the cache constructor 418 // that the cache returns the original record without any modifications. 419 func TestGossipSubSpamRecordCache_Get_Without_Preprocessors(t *testing.T) { 420 cache := netcache.NewGossipSubSpamRecordCache(10, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 421 return p2p.GossipSubSpamRecord{ 422 Decay: 0, 423 Penalty: 0, 424 LastDecayAdjustment: time.Now(), 425 } 426 }) 427 428 peerId := unittest.PeerIdFixture(t) 429 adjustedRecord, err := cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 430 record.Penalty = 1 431 record.Decay = 0.5 432 return record 433 }) 434 require.NoError(t, err) 435 require.Equal(t, 1.0, adjustedRecord.Penalty) 436 require.Equal(t, 0.5, adjustedRecord.Decay) 437 438 // verifies that no preprocessors were called and the record was not adjusted. 439 cachedRecord, err, ok := cache.Get(peerId) 440 require.NoError(t, err) 441 require.True(t, ok) 442 require.Equal(t, 1.0, cachedRecord.Penalty) 443 require.Equal(t, 0.5, cachedRecord.Decay) 444 } 445 446 // TestGossipSubSpamRecordCache_Duplicate_Adjust_Sequential tests if the cache returns false when a duplicate record is added to the cache. 447 // This test evaluates that the cache de-duplicates the records based on their peer id and not content, and hence 448 // each peer id can only be added once to the cache. 449 func TestGossipSubSpamRecordCache_Duplicate_Adjust_Sequential(t *testing.T) { 450 cache := netcache.NewGossipSubSpamRecordCache(10, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 451 return p2p.GossipSubSpamRecord{ 452 Decay: 0, 453 Penalty: 0, 454 LastDecayAdjustment: time.Now(), 455 } 456 }) 457 458 peerId := unittest.PeerIdFixture(t) 459 adjustedRecord, err := cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 460 record.Penalty = 1 461 record.Decay = 0.5 462 return record 463 }) 464 require.NoError(t, err) 465 require.Equal(t, 1.0, adjustedRecord.Penalty) 466 require.Equal(t, 0.5, adjustedRecord.Decay) 467 468 // duplicate adjust should return the same record. 469 adjustedRecord, err = cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 470 record.Penalty = 1 471 record.Decay = 0.5 472 return record 473 }) 474 require.NoError(t, err) 475 require.Equal(t, 1.0, adjustedRecord.Penalty) 476 require.Equal(t, 0.5, adjustedRecord.Decay) 477 478 // verifies that the cache deduplicates the records based on their peer id and not content. 479 adjustedRecord, err = cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 480 record.Penalty = 3 481 record.Decay = 2 482 return record 483 }) 484 require.NoError(t, err) 485 require.Equal(t, 3.0, adjustedRecord.Penalty) 486 require.Equal(t, 2.0, adjustedRecord.Decay) 487 } 488 489 // TestGossipSubSpamRecordCache_Duplicate_Adjust_Concurrent tests if the cache returns false when a duplicate record is added to the cache. 490 // Test is the concurrent version of TestAppScoreCache_Duplicate_Adjust_Sequential. 491 func TestGossipSubSpamRecordCache_Duplicate_Adjust_Concurrent(t *testing.T) { 492 cache := netcache.NewGossipSubSpamRecordCache(10, unittest.Logger(), metrics.NewNoopCollector(), func() p2p.GossipSubSpamRecord { 493 return p2p.GossipSubSpamRecord{ 494 Decay: 0, 495 Penalty: 0, 496 LastDecayAdjustment: time.Now(), 497 } 498 }) 499 500 successAdd := atomic.Int32{} 501 successAdd.Store(0) 502 503 record1 := p2p.GossipSubSpamRecord{ 504 Decay: 1, 505 Penalty: 1, 506 } 507 508 record2 := p2p.GossipSubSpamRecord{ 509 Decay: 1, 510 Penalty: 2, 511 } 512 513 wg := sync.WaitGroup{} // wait group to wait for all goroutines to finish. 514 wg.Add(2) 515 peerId := unittest.PeerIdFixture(t) 516 // adds a record to the cache concurrently. 517 add := func(newRecord p2p.GossipSubSpamRecord) { 518 _, err := cache.Adjust(peerId, func(record p2p.GossipSubSpamRecord) p2p.GossipSubSpamRecord { 519 record.Penalty = newRecord.Penalty 520 record.Decay = newRecord.Decay 521 record.LastDecayAdjustment = newRecord.LastDecayAdjustment 522 return record 523 }) 524 require.NoError(t, err) 525 successAdd.Inc() 526 527 wg.Done() 528 } 529 530 go add(record1) 531 go add(record2) 532 533 unittest.RequireReturnsBefore(t, wg.Wait, 100*time.Millisecond, "could not add records to the cache") 534 535 // verifies that both of the records was added to the cache. 536 require.Equal(t, int32(2), successAdd.Load()) 537 538 // verifies that the record is adjusted to one of the records. 539 cachedRecord, err, ok := cache.Get(peerId) 540 require.NoError(t, err) 541 require.True(t, ok) 542 require.True(t, cachedRecord.Penalty == 1 && cachedRecord.Decay == 1 || cachedRecord.Penalty == 2 && cachedRecord.Decay == 1) 543 }