github.com/prysmaticlabs/prysm@v1.4.4/validator/db/kv/attester_protection_test.go (about) 1 package kv 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sync" 9 "testing" 10 11 types "github.com/prysmaticlabs/eth2-types" 12 ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" 13 "github.com/prysmaticlabs/prysm/shared/bytesutil" 14 "github.com/prysmaticlabs/prysm/shared/testutil/assert" 15 "github.com/prysmaticlabs/prysm/shared/testutil/require" 16 logTest "github.com/sirupsen/logrus/hooks/test" 17 bolt "go.etcd.io/bbolt" 18 ) 19 20 func TestPendingAttestationRecords_Flush(t *testing.T) { 21 queue := NewQueuedAttestationRecords() 22 23 // Add 5 atts 24 num := 5 25 for i := 0; i < num; i++ { 26 queue.Append(&AttestationRecord{ 27 Target: types.Epoch(i), 28 }) 29 } 30 31 res := queue.Flush() 32 assert.Equal(t, len(res), num, "Wrong number of flushed attestations") 33 assert.Equal(t, len(queue.records), 0, "Records were not cleared/flushed") 34 } 35 36 func TestPendingAttestationRecords_Len(t *testing.T) { 37 queue := NewQueuedAttestationRecords() 38 assert.Equal(t, queue.Len(), 0) 39 queue.Append(&AttestationRecord{}) 40 assert.Equal(t, queue.Len(), 1) 41 queue.Flush() 42 assert.Equal(t, queue.Len(), 0) 43 } 44 45 func TestStore_CheckSlashableAttestation_DoubleVote(t *testing.T) { 46 ctx := context.Background() 47 numValidators := 1 48 pubKeys := make([][48]byte, numValidators) 49 validatorDB := setupDB(t, pubKeys) 50 tests := []struct { 51 name string 52 existingAttestation *ethpb.IndexedAttestation 53 existingSigningRoot [32]byte 54 incomingAttestation *ethpb.IndexedAttestation 55 incomingSigningRoot [32]byte 56 want bool 57 }{ 58 { 59 name: "different signing root at same target equals a double vote", 60 existingAttestation: createAttestation(0, 1 /* Target */), 61 existingSigningRoot: [32]byte{1}, 62 incomingAttestation: createAttestation(0, 1 /* Target */), 63 incomingSigningRoot: [32]byte{2}, 64 want: true, 65 }, 66 { 67 name: "same signing root at same target is safe", 68 existingAttestation: createAttestation(0, 1 /* Target */), 69 existingSigningRoot: [32]byte{1}, 70 incomingAttestation: createAttestation(0, 1 /* Target */), 71 incomingSigningRoot: [32]byte{1}, 72 want: false, 73 }, 74 { 75 name: "different signing root at different target is safe", 76 existingAttestation: createAttestation(0, 1 /* Target */), 77 existingSigningRoot: [32]byte{1}, 78 incomingAttestation: createAttestation(0, 2 /* Target */), 79 incomingSigningRoot: [32]byte{2}, 80 want: false, 81 }, 82 { 83 name: "no data stored at target should not be considered a double vote", 84 existingAttestation: createAttestation(0, 1 /* Target */), 85 existingSigningRoot: [32]byte{1}, 86 incomingAttestation: createAttestation(0, 2 /* Target */), 87 incomingSigningRoot: [32]byte{1}, 88 want: false, 89 }, 90 } 91 for _, tt := range tests { 92 t.Run(tt.name, func(t *testing.T) { 93 err := validatorDB.SaveAttestationForPubKey( 94 ctx, 95 pubKeys[0], 96 tt.existingSigningRoot, 97 tt.existingAttestation, 98 ) 99 require.NoError(t, err) 100 slashingKind, err := validatorDB.CheckSlashableAttestation( 101 ctx, 102 pubKeys[0], 103 tt.incomingSigningRoot, 104 tt.incomingAttestation, 105 ) 106 if tt.want { 107 require.NotNil(t, err) 108 assert.Equal(t, DoubleVote, slashingKind) 109 } else { 110 require.NoError(t, err) 111 } 112 }) 113 } 114 } 115 116 func TestStore_CheckSlashableAttestation_SurroundVote_MultipleTargetsPerSource(t *testing.T) { 117 ctx := context.Background() 118 numValidators := 1 119 pubKeys := make([][48]byte, numValidators) 120 validatorDB := setupDB(t, pubKeys) 121 122 // Create an attestation with source 1 and target 50, save it. 123 firstAtt := createAttestation(1, 50) 124 err := validatorDB.SaveAttestationForPubKey(ctx, pubKeys[0], [32]byte{0}, firstAtt) 125 require.NoError(t, err) 126 127 // Create an attestation with source 1 and target 100, save it. 128 secondAtt := createAttestation(1, 100) 129 err = validatorDB.SaveAttestationForPubKey(ctx, pubKeys[0], [32]byte{1}, secondAtt) 130 require.NoError(t, err) 131 132 // Create an attestation with source 0 and target 51, which should surround 133 // our first attestation. Given there can be multiple attested target epochs per 134 // source epoch, we expect our logic to be able to catch this slashable offense. 135 evilAtt := createAttestation(firstAtt.Data.Source.Epoch-1, firstAtt.Data.Target.Epoch+1) 136 slashable, err := validatorDB.CheckSlashableAttestation(ctx, pubKeys[0], [32]byte{2}, evilAtt) 137 require.NotNil(t, err) 138 assert.Equal(t, SurroundingVote, slashable) 139 } 140 141 func TestStore_CheckSlashableAttestation_SurroundVote_54kEpochs(t *testing.T) { 142 ctx := context.Background() 143 numValidators := 1 144 numEpochs := types.Epoch(54000) 145 pubKeys := make([][48]byte, numValidators) 146 validatorDB := setupDB(t, pubKeys) 147 148 // Attest to every (source = epoch, target = epoch + 1) sequential pair 149 // since genesis up to and including the weak subjectivity period epoch (54,000). 150 err := validatorDB.update(func(tx *bolt.Tx) error { 151 bucket := tx.Bucket(pubKeysBucket) 152 pkBucket, err := bucket.CreateBucketIfNotExists(pubKeys[0][:]) 153 if err != nil { 154 return err 155 } 156 sourceEpochsBucket, err := pkBucket.CreateBucketIfNotExists(attestationSourceEpochsBucket) 157 if err != nil { 158 return err 159 } 160 for epoch := types.Epoch(1); epoch < numEpochs; epoch++ { 161 att := createAttestation(epoch-1, epoch) 162 sourceEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Source.Epoch) 163 targetEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Target.Epoch) 164 if err := sourceEpochsBucket.Put(sourceEpoch, targetEpoch); err != nil { 165 return err 166 } 167 } 168 return nil 169 }) 170 require.NoError(t, err) 171 172 tests := []struct { 173 name string 174 signingRoot [32]byte 175 attestation *ethpb.IndexedAttestation 176 want SlashingKind 177 }{ 178 { 179 name: "surround vote at half of the weak subjectivity period", 180 signingRoot: [32]byte{}, 181 attestation: createAttestation(numEpochs/2, numEpochs), 182 want: SurroundingVote, 183 }, 184 { 185 name: "spanning genesis to weak subjectivity period surround vote", 186 signingRoot: [32]byte{}, 187 attestation: createAttestation(0, numEpochs), 188 want: SurroundingVote, 189 }, 190 { 191 name: "simple surround vote at end of weak subjectivity period", 192 signingRoot: [32]byte{}, 193 attestation: createAttestation(numEpochs-3, numEpochs), 194 want: SurroundingVote, 195 }, 196 { 197 name: "non-slashable vote", 198 signingRoot: [32]byte{}, 199 attestation: createAttestation(numEpochs, numEpochs+1), 200 want: NotSlashable, 201 }, 202 } 203 for _, tt := range tests { 204 t.Run(tt.name, func(t *testing.T) { 205 slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, pubKeys[0], tt.signingRoot, tt.attestation) 206 if tt.want != NotSlashable { 207 require.NotNil(t, err) 208 } 209 assert.Equal(t, tt.want, slashingKind) 210 }) 211 } 212 } 213 214 func TestLowestSignedSourceEpoch_SaveRetrieve(t *testing.T) { 215 ctx := context.Background() 216 validatorDB, err := NewKVStore(ctx, t.TempDir(), &Config{}) 217 require.NoError(t, err, "Failed to instantiate DB") 218 t.Cleanup(func() { 219 require.NoError(t, validatorDB.Close(), "Failed to close database") 220 require.NoError(t, validatorDB.ClearDB(), "Failed to clear database") 221 }) 222 p0 := [48]byte{0} 223 p1 := [48]byte{1} 224 // Can save. 225 require.NoError( 226 t, 227 validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(100, 101)), 228 ) 229 require.NoError( 230 t, 231 validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(200, 201)), 232 ) 233 got, _, err := validatorDB.LowestSignedSourceEpoch(ctx, p0) 234 require.NoError(t, err) 235 require.Equal(t, types.Epoch(100), got) 236 got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p1) 237 require.NoError(t, err) 238 require.Equal(t, types.Epoch(200), got) 239 240 // Can replace. 241 require.NoError( 242 t, 243 validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(99, 100)), 244 ) 245 require.NoError( 246 t, 247 validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(199, 200)), 248 ) 249 got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p0) 250 require.NoError(t, err) 251 require.Equal(t, types.Epoch(99), got) 252 got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p1) 253 require.NoError(t, err) 254 require.Equal(t, types.Epoch(199), got) 255 256 // Can not replace. 257 require.NoError( 258 t, 259 validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(100, 101)), 260 ) 261 require.NoError( 262 t, 263 validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(200, 201)), 264 ) 265 got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p0) 266 require.NoError(t, err) 267 require.Equal(t, types.Epoch(99), got) 268 got, _, err = validatorDB.LowestSignedSourceEpoch(ctx, p1) 269 require.NoError(t, err) 270 require.Equal(t, types.Epoch(199), got) 271 } 272 273 func TestLowestSignedTargetEpoch_SaveRetrieveReplace(t *testing.T) { 274 ctx := context.Background() 275 validatorDB, err := NewKVStore(ctx, t.TempDir(), &Config{}) 276 require.NoError(t, err, "Failed to instantiate DB") 277 t.Cleanup(func() { 278 require.NoError(t, validatorDB.Close(), "Failed to close database") 279 require.NoError(t, validatorDB.ClearDB(), "Failed to clear database") 280 }) 281 p0 := [48]byte{0} 282 p1 := [48]byte{1} 283 // Can save. 284 require.NoError( 285 t, 286 validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(99, 100)), 287 ) 288 require.NoError( 289 t, 290 validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(199, 200)), 291 ) 292 got, _, err := validatorDB.LowestSignedTargetEpoch(ctx, p0) 293 require.NoError(t, err) 294 require.Equal(t, types.Epoch(100), got) 295 got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p1) 296 require.NoError(t, err) 297 require.Equal(t, types.Epoch(200), got) 298 299 // Can replace. 300 require.NoError( 301 t, 302 validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(98, 99)), 303 ) 304 require.NoError( 305 t, 306 validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(198, 199)), 307 ) 308 got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p0) 309 require.NoError(t, err) 310 require.Equal(t, types.Epoch(99), got) 311 got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p1) 312 require.NoError(t, err) 313 require.Equal(t, types.Epoch(199), got) 314 315 // Can not replace. 316 require.NoError( 317 t, 318 validatorDB.SaveAttestationForPubKey(ctx, p0, [32]byte{}, createAttestation(99, 100)), 319 ) 320 require.NoError( 321 t, 322 validatorDB.SaveAttestationForPubKey(ctx, p1, [32]byte{}, createAttestation(199, 200)), 323 ) 324 got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p0) 325 require.NoError(t, err) 326 require.Equal(t, types.Epoch(99), got) 327 got, _, err = validatorDB.LowestSignedTargetEpoch(ctx, p1) 328 require.NoError(t, err) 329 require.Equal(t, types.Epoch(199), got) 330 } 331 332 func TestStore_SaveAttestationsForPubKey(t *testing.T) { 333 ctx := context.Background() 334 numValidators := 1 335 pubKeys := make([][48]byte, numValidators) 336 validatorDB := setupDB(t, pubKeys) 337 atts := make([]*ethpb.IndexedAttestation, 0) 338 signingRoots := make([][32]byte, 0) 339 for i := types.Epoch(1); i < 10; i++ { 340 atts = append(atts, createAttestation(i-1, i)) 341 var sr [32]byte 342 copy(sr[:], fmt.Sprintf("%d", i)) 343 signingRoots = append(signingRoots, sr) 344 } 345 err := validatorDB.SaveAttestationsForPubKey( 346 ctx, 347 pubKeys[0], 348 signingRoots[:1], 349 atts, 350 ) 351 require.ErrorContains(t, "does not match number of attestations", err) 352 err = validatorDB.SaveAttestationsForPubKey( 353 ctx, 354 pubKeys[0], 355 signingRoots, 356 atts, 357 ) 358 require.NoError(t, err) 359 for _, att := range atts { 360 // Ensure the same attestations but different signing root lead to double votes. 361 slashingKind, err := validatorDB.CheckSlashableAttestation( 362 ctx, 363 pubKeys[0], 364 [32]byte{}, 365 att, 366 ) 367 require.NotNil(t, err) 368 require.Equal(t, DoubleVote, slashingKind) 369 } 370 } 371 372 func TestSaveAttestationForPubKey_BatchWrites_FullCapacity(t *testing.T) { 373 hook := logTest.NewGlobal() 374 ctx, cancel := context.WithCancel(context.Background()) 375 defer cancel() 376 numValidators := attestationBatchCapacity 377 pubKeys := make([][48]byte, numValidators) 378 validatorDB := setupDB(t, pubKeys) 379 380 // For each public key, we attempt to save an attestation with signing root. 381 var wg sync.WaitGroup 382 for i, pubKey := range pubKeys { 383 wg.Add(1) 384 go func(j types.Epoch, pk [48]byte, w *sync.WaitGroup) { 385 defer w.Done() 386 var signingRoot [32]byte 387 copy(signingRoot[:], fmt.Sprintf("%d", j)) 388 att := createAttestation(j, j+1) 389 err := validatorDB.SaveAttestationForPubKey(ctx, pk, signingRoot, att) 390 require.NoError(t, err) 391 }(types.Epoch(i), pubKey, &wg) 392 } 393 wg.Wait() 394 395 // We verify that we reached the max capacity of batched attestations 396 // before we are required to force flush them to the DB. 397 require.LogsContain(t, hook, "Reached max capacity of batched attestation records") 398 require.LogsDoNotContain(t, hook, "Batched attestation records write interval reached") 399 require.LogsContain(t, hook, "Successfully flushed batched attestations to DB") 400 require.Equal(t, 0, validatorDB.batchedAttestations.Len()) 401 402 // We then verify all the data we wanted to save is indeed saved to disk. 403 err := validatorDB.view(func(tx *bolt.Tx) error { 404 bucket := tx.Bucket(pubKeysBucket) 405 for i, pubKey := range pubKeys { 406 var signingRoot [32]byte 407 copy(signingRoot[:], fmt.Sprintf("%d", i)) 408 pkBucket := bucket.Bucket(pubKey[:]) 409 signingRootsBucket := pkBucket.Bucket(attestationSigningRootsBucket) 410 sourceEpochsBucket := pkBucket.Bucket(attestationSourceEpochsBucket) 411 412 source := bytesutil.Uint64ToBytesBigEndian(uint64(i)) 413 target := bytesutil.Uint64ToBytesBigEndian(uint64(i) + 1) 414 savedSigningRoot := signingRootsBucket.Get(target) 415 require.DeepEqual(t, signingRoot[:], savedSigningRoot) 416 savedTarget := sourceEpochsBucket.Get(source) 417 require.DeepEqual(t, signingRoot[:], savedSigningRoot) 418 require.DeepEqual(t, target, savedTarget) 419 } 420 return nil 421 }) 422 require.NoError(t, err) 423 } 424 425 func TestSaveAttestationForPubKey_BatchWrites_LowCapacity_TimerReached(t *testing.T) { 426 hook := logTest.NewGlobal() 427 ctx, cancel := context.WithCancel(context.Background()) 428 defer cancel() 429 // Number of validators equal to half the total capacity 430 // of batch attestation processing. This will allow us to 431 // test force flushing to the DB based on a timer instead 432 // of the max capacity being reached. 433 numValidators := attestationBatchCapacity / 2 434 pubKeys := make([][48]byte, numValidators) 435 validatorDB := setupDB(t, pubKeys) 436 437 // For each public key, we attempt to save an attestation with signing root. 438 var wg sync.WaitGroup 439 for i, pubKey := range pubKeys { 440 wg.Add(1) 441 go func(j types.Epoch, pk [48]byte, w *sync.WaitGroup) { 442 defer w.Done() 443 var signingRoot [32]byte 444 copy(signingRoot[:], fmt.Sprintf("%d", j)) 445 att := createAttestation(j, j+1) 446 err := validatorDB.SaveAttestationForPubKey(ctx, pk, signingRoot, att) 447 require.NoError(t, err) 448 }(types.Epoch(i), pubKey, &wg) 449 } 450 wg.Wait() 451 452 // We verify that we reached a timer interval for force flushing records 453 // before we are required to force flush them to the DB. 454 require.LogsDoNotContain(t, hook, "Reached max capacity of batched attestation records") 455 require.LogsContain(t, hook, "Batched attestation records write interval reached") 456 require.LogsContain(t, hook, "Successfully flushed batched attestations to DB") 457 require.Equal(t, 0, validatorDB.batchedAttestations.Len()) 458 459 // We then verify all the data we wanted to save is indeed saved to disk. 460 err := validatorDB.view(func(tx *bolt.Tx) error { 461 bucket := tx.Bucket(pubKeysBucket) 462 for i, pubKey := range pubKeys { 463 var signingRoot [32]byte 464 copy(signingRoot[:], fmt.Sprintf("%d", i)) 465 pkBucket := bucket.Bucket(pubKey[:]) 466 signingRootsBucket := pkBucket.Bucket(attestationSigningRootsBucket) 467 sourceEpochsBucket := pkBucket.Bucket(attestationSourceEpochsBucket) 468 469 source := bytesutil.Uint64ToBytesBigEndian(uint64(i)) 470 target := bytesutil.Uint64ToBytesBigEndian(uint64(i) + 1) 471 savedSigningRoot := signingRootsBucket.Get(target) 472 require.DeepEqual(t, signingRoot[:], savedSigningRoot) 473 savedTarget := sourceEpochsBucket.Get(source) 474 require.DeepEqual(t, signingRoot[:], savedSigningRoot) 475 require.DeepEqual(t, target, savedTarget) 476 } 477 return nil 478 }) 479 require.NoError(t, err) 480 } 481 482 func BenchmarkStore_CheckSlashableAttestation_Surround_SafeAttestation_54kEpochs(b *testing.B) { 483 numValidators := 1 484 numEpochs := types.Epoch(54000) 485 pubKeys := make([][48]byte, numValidators) 486 benchCheckSurroundVote(b, pubKeys, numEpochs, false /* surround */) 487 } 488 489 func BenchmarkStore_CheckSurroundVote_Surround_Slashable_54kEpochs(b *testing.B) { 490 numValidators := 1 491 numEpochs := types.Epoch(54000) 492 pubKeys := make([][48]byte, numValidators) 493 benchCheckSurroundVote(b, pubKeys, numEpochs, true /* surround */) 494 } 495 496 func benchCheckSurroundVote( 497 b *testing.B, 498 pubKeys [][48]byte, 499 numEpochs types.Epoch, 500 shouldSurround bool, 501 ) { 502 ctx := context.Background() 503 validatorDB, err := NewKVStore(ctx, filepath.Join(os.TempDir(), "benchsurroundvote"), &Config{ 504 PubKeys: pubKeys, 505 }) 506 require.NoError(b, err, "Failed to instantiate DB") 507 defer func() { 508 require.NoError(b, validatorDB.Close(), "Failed to close database") 509 require.NoError(b, validatorDB.ClearDB(), "Failed to clear database") 510 }() 511 // Every validator will have attested every (source, target) sequential pair 512 // since genesis up to and including the weak subjectivity period epoch (54,000). 513 err = validatorDB.update(func(tx *bolt.Tx) error { 514 for _, pubKey := range pubKeys { 515 bucket := tx.Bucket(pubKeysBucket) 516 pkBucket, err := bucket.CreateBucketIfNotExists(pubKey[:]) 517 if err != nil { 518 return err 519 } 520 sourceEpochsBucket, err := pkBucket.CreateBucketIfNotExists(attestationSourceEpochsBucket) 521 if err != nil { 522 return err 523 } 524 for epoch := types.Epoch(1); epoch < numEpochs; epoch++ { 525 att := createAttestation(epoch-1, epoch) 526 sourceEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Source.Epoch) 527 targetEpoch := bytesutil.EpochToBytesBigEndian(att.Data.Target.Epoch) 528 if err := sourceEpochsBucket.Put(sourceEpoch, targetEpoch); err != nil { 529 return err 530 } 531 } 532 } 533 return nil 534 }) 535 require.NoError(b, err) 536 537 // Will surround many attestations. 538 var surroundingVote *ethpb.IndexedAttestation 539 if shouldSurround { 540 surroundingVote = createAttestation(numEpochs/2, numEpochs) 541 } else { 542 surroundingVote = createAttestation(numEpochs+1, numEpochs+2) 543 } 544 b.ResetTimer() 545 for i := 0; i < b.N; i++ { 546 for _, pubKey := range pubKeys { 547 slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, pubKey, [32]byte{}, surroundingVote) 548 if shouldSurround { 549 require.NotNil(b, err) 550 assert.Equal(b, SurroundingVote, slashingKind) 551 } else { 552 require.NoError(b, err) 553 } 554 } 555 } 556 } 557 558 func createAttestation(source, target types.Epoch) *ethpb.IndexedAttestation { 559 return ðpb.IndexedAttestation{ 560 Data: ðpb.AttestationData{ 561 Source: ðpb.Checkpoint{ 562 Epoch: source, 563 }, 564 Target: ðpb.Checkpoint{ 565 Epoch: target, 566 }, 567 }, 568 } 569 } 570 571 func TestStore_flushAttestationRecords_InProgress(t *testing.T) { 572 s := &Store{} 573 s.batchedAttestationsFlushInProgress.Set() 574 575 hook := logTest.NewGlobal() 576 s.flushAttestationRecords(context.Background(), nil) 577 assert.LogsContain(t, hook, "Attempted to flush attestation records when already in progress") 578 }