github.com/prysmaticlabs/prysm@v1.4.4/validator/slashing-protection/local/standard-protection-format/import_test.go (about) 1 package interchangeformat 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "reflect" 10 "testing" 11 12 types "github.com/prysmaticlabs/eth2-types" 13 ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" 14 "github.com/prysmaticlabs/prysm/shared/testutil/assert" 15 "github.com/prysmaticlabs/prysm/shared/testutil/require" 16 "github.com/prysmaticlabs/prysm/validator/db/kv" 17 dbtest "github.com/prysmaticlabs/prysm/validator/db/testing" 18 "github.com/prysmaticlabs/prysm/validator/slashing-protection/local/standard-protection-format/format" 19 valtest "github.com/prysmaticlabs/prysm/validator/testing" 20 logTest "github.com/sirupsen/logrus/hooks/test" 21 ) 22 23 func TestStore_ImportInterchangeData_BadJSON(t *testing.T) { 24 ctx := context.Background() 25 validatorDB := dbtest.SetupDB(t, nil) 26 27 buf := bytes.NewBuffer([]byte("helloworld")) 28 err := ImportStandardProtectionJSON(ctx, validatorDB, buf) 29 require.ErrorContains(t, "could not unmarshal slashing protection JSON file", err) 30 } 31 32 func TestStore_ImportInterchangeData_NilData_FailsSilently(t *testing.T) { 33 hook := logTest.NewGlobal() 34 ctx := context.Background() 35 validatorDB := dbtest.SetupDB(t, nil) 36 37 interchangeJSON := &format.EIPSlashingProtectionFormat{} 38 encoded, err := json.Marshal(interchangeJSON) 39 require.NoError(t, err) 40 41 buf := bytes.NewBuffer(encoded) 42 err = ImportStandardProtectionJSON(ctx, validatorDB, buf) 43 require.NoError(t, err) 44 require.LogsContain(t, hook, "No slashing protection data to import") 45 } 46 47 func TestStore_ImportInterchangeData_BadFormat_PreventsDBWrites(t *testing.T) { 48 ctx := context.Background() 49 numValidators := 10 50 publicKeys, err := valtest.CreateRandomPubKeys(numValidators) 51 require.NoError(t, err) 52 validatorDB := dbtest.SetupDB(t, publicKeys) 53 54 // First we setup some mock attesting and proposal histories and create a mock 55 // standard slashing protection format JSON struct. 56 attestingHistory, proposalHistory := valtest.MockAttestingAndProposalHistories(publicKeys) 57 standardProtectionFormat, err := valtest.MockSlashingProtectionJSON(publicKeys, attestingHistory, proposalHistory) 58 require.NoError(t, err) 59 60 // We replace a slot of one of the blocks with junk data. 61 standardProtectionFormat.Data[0].SignedBlocks[0].Slot = "BadSlot" 62 63 // We encode the standard slashing protection struct into a JSON format. 64 blob, err := json.Marshal(standardProtectionFormat) 65 require.NoError(t, err) 66 buf := bytes.NewBuffer(blob) 67 68 // Next, we attempt to import it into our validator database and check that 69 // we obtain an error during the import process. 70 err = ImportStandardProtectionJSON(ctx, validatorDB, buf) 71 assert.NotNil(t, err) 72 73 // Next, we attempt to retrieve the attesting and proposals histories from our database and 74 // verify nothing was saved to the DB. If there is an error in the import process, we need to make 75 // sure writing is an atomic operation: either the import succeeds and saves the slashing protection 76 // data to our DB, or it does not. 77 for i := 0; i < len(publicKeys); i++ { 78 for _, att := range attestingHistory[i] { 79 indexedAtt := ðpb.IndexedAttestation{ 80 Data: ðpb.AttestationData{ 81 Source: ðpb.Checkpoint{ 82 Epoch: att.Source, 83 }, 84 Target: ðpb.Checkpoint{ 85 Epoch: att.Target, 86 }, 87 }, 88 } 89 slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, publicKeys[i], [32]byte{}, indexedAtt) 90 // We expect we do not have an attesting history for each attestation 91 require.NoError(t, err) 92 require.Equal(t, kv.NotSlashable, slashingKind) 93 } 94 receivedHistory, err := validatorDB.ProposalHistoryForPubKey(ctx, publicKeys[i]) 95 require.NoError(t, err) 96 require.DeepEqual( 97 t, 98 make([]*kv.Proposal, 0), 99 receivedHistory, 100 "Imported proposal signing root is different than the empty default", 101 ) 102 } 103 } 104 105 func TestStore_ImportInterchangeData_OK(t *testing.T) { 106 ctx := context.Background() 107 numValidators := 10 108 publicKeys, err := valtest.CreateRandomPubKeys(numValidators) 109 require.NoError(t, err) 110 validatorDB := dbtest.SetupDB(t, publicKeys) 111 112 // First we setup some mock attesting and proposal histories and create a mock 113 // standard slashing protection format JSON struct. 114 attestingHistory, proposalHistory := valtest.MockAttestingAndProposalHistories(publicKeys) 115 standardProtectionFormat, err := valtest.MockSlashingProtectionJSON(publicKeys, attestingHistory, proposalHistory) 116 require.NoError(t, err) 117 118 // We encode the standard slashing protection struct into a JSON format. 119 blob, err := json.Marshal(standardProtectionFormat) 120 require.NoError(t, err) 121 buf := bytes.NewBuffer(blob) 122 123 // Next, we attempt to import it into our validator database. 124 err = ImportStandardProtectionJSON(ctx, validatorDB, buf) 125 require.NoError(t, err) 126 127 // Next, we attempt to retrieve the attesting and proposals histories from our database and 128 // verify those indeed match the originally generated mock histories. 129 for i := 0; i < len(publicKeys); i++ { 130 for _, att := range attestingHistory[i] { 131 indexedAtt := ðpb.IndexedAttestation{ 132 Data: ðpb.AttestationData{ 133 Source: ðpb.Checkpoint{ 134 Epoch: att.Source, 135 }, 136 Target: ðpb.Checkpoint{ 137 Epoch: att.Target, 138 }, 139 }, 140 } 141 slashingKind, err := validatorDB.CheckSlashableAttestation(ctx, publicKeys[i], [32]byte{}, indexedAtt) 142 // We expect we have an attesting history for the attestation and when 143 // attempting to verify the same att is slashable with a different signing root, 144 // we expect to receive a double vote slashing kind. 145 require.NotNil(t, err) 146 require.Equal(t, kv.DoubleVote, slashingKind) 147 } 148 149 proposals := proposalHistory[i].Proposals 150 151 receivedProposalHistory, err := validatorDB.ProposalHistoryForPubKey(ctx, publicKeys[i]) 152 require.NoError(t, err) 153 rootsBySlot := make(map[types.Slot][]byte) 154 for _, proposal := range receivedProposalHistory { 155 rootsBySlot[proposal.Slot] = proposal.SigningRoot 156 } 157 for _, proposal := range proposals { 158 receivedRoot, ok := rootsBySlot[proposal.Slot] 159 require.DeepEqual(t, true, ok) 160 require.DeepEqual( 161 t, 162 receivedRoot, 163 proposal.SigningRoot, 164 "Imported proposals are different then the generated ones", 165 ) 166 } 167 } 168 } 169 170 func Test_validateMetadata(t *testing.T) { 171 goodRoot := [32]byte{1} 172 goodStr := make([]byte, hex.EncodedLen(len(goodRoot))) 173 hex.Encode(goodStr, goodRoot[:]) 174 tests := []struct { 175 name string 176 interchangeJSON *format.EIPSlashingProtectionFormat 177 dbGenesisValidatorRoot []byte 178 wantErr bool 179 wantFatal string 180 }{ 181 { 182 name: "Incorrect version for EIP format should fail", 183 interchangeJSON: &format.EIPSlashingProtectionFormat{ 184 Metadata: struct { 185 InterchangeFormatVersion string `json:"interchange_format_version"` 186 GenesisValidatorsRoot string `json:"genesis_validators_root"` 187 }{ 188 InterchangeFormatVersion: "1", 189 GenesisValidatorsRoot: string(goodStr), 190 }, 191 }, 192 wantErr: true, 193 }, 194 { 195 name: "Junk data for version should fail", 196 interchangeJSON: &format.EIPSlashingProtectionFormat{ 197 Metadata: struct { 198 InterchangeFormatVersion string `json:"interchange_format_version"` 199 GenesisValidatorsRoot string `json:"genesis_validators_root"` 200 }{ 201 InterchangeFormatVersion: "asdljas$d", 202 GenesisValidatorsRoot: string(goodStr), 203 }, 204 }, 205 wantErr: true, 206 }, 207 { 208 name: "Proper version field should pass", 209 interchangeJSON: &format.EIPSlashingProtectionFormat{ 210 Metadata: struct { 211 InterchangeFormatVersion string `json:"interchange_format_version"` 212 GenesisValidatorsRoot string `json:"genesis_validators_root"` 213 }{ 214 InterchangeFormatVersion: format.InterchangeFormatVersion, 215 GenesisValidatorsRoot: string(goodStr), 216 }, 217 }, 218 wantErr: false, 219 }, 220 } 221 for _, tt := range tests { 222 t.Run(tt.name, func(t *testing.T) { 223 validatorDB := dbtest.SetupDB(t, nil) 224 ctx := context.Background() 225 if err := validateMetadata(ctx, validatorDB, tt.interchangeJSON); (err != nil) != tt.wantErr { 226 t.Errorf("validateMetadata() error = %v, wantErr %v", err, tt.wantErr) 227 } 228 229 }) 230 } 231 } 232 233 func Test_validateMetadataGenesisValidatorRoot(t *testing.T) { 234 goodRoot := [32]byte{1} 235 goodStr := make([]byte, hex.EncodedLen(len(goodRoot))) 236 hex.Encode(goodStr, goodRoot[:]) 237 secondRoot := [32]byte{2} 238 secondStr := make([]byte, hex.EncodedLen(len(secondRoot))) 239 hex.Encode(secondStr, secondRoot[:]) 240 241 tests := []struct { 242 name string 243 interchangeJSON *format.EIPSlashingProtectionFormat 244 dbGenesisValidatorRoot []byte 245 wantErr bool 246 }{ 247 { 248 name: "Same genesis roots should not fail", 249 interchangeJSON: &format.EIPSlashingProtectionFormat{ 250 Metadata: struct { 251 InterchangeFormatVersion string `json:"interchange_format_version"` 252 GenesisValidatorsRoot string `json:"genesis_validators_root"` 253 }{ 254 InterchangeFormatVersion: format.InterchangeFormatVersion, 255 GenesisValidatorsRoot: string(goodStr), 256 }, 257 }, 258 dbGenesisValidatorRoot: goodRoot[:], 259 wantErr: false, 260 }, 261 { 262 name: "Different genesis roots should not fail", 263 interchangeJSON: &format.EIPSlashingProtectionFormat{ 264 Metadata: struct { 265 InterchangeFormatVersion string `json:"interchange_format_version"` 266 GenesisValidatorsRoot string `json:"genesis_validators_root"` 267 }{ 268 InterchangeFormatVersion: format.InterchangeFormatVersion, 269 GenesisValidatorsRoot: string(secondStr), 270 }, 271 }, 272 dbGenesisValidatorRoot: goodRoot[:], 273 wantErr: true, 274 }, 275 } 276 for _, tt := range tests { 277 t.Run(tt.name, func(t *testing.T) { 278 validatorDB := dbtest.SetupDB(t, nil) 279 ctx := context.Background() 280 require.NoError(t, validatorDB.SaveGenesisValidatorsRoot(ctx, tt.dbGenesisValidatorRoot)) 281 err := validateMetadata(ctx, validatorDB, tt.interchangeJSON) 282 if tt.wantErr { 283 require.ErrorContains(t, "genesis validator root doesnt match the one that is stored", err) 284 } else { 285 require.NoError(t, err) 286 } 287 288 }) 289 } 290 } 291 292 func Test_parseUniqueSignedBlocksByPubKey(t *testing.T) { 293 numValidators := 4 294 publicKeys, err := valtest.CreateRandomPubKeys(numValidators) 295 require.NoError(t, err) 296 roots := valtest.CreateMockRoots(numValidators) 297 tests := []struct { 298 name string 299 data []*format.ProtectionData 300 want map[[48]byte][]*format.SignedBlock 301 wantErr bool 302 }{ 303 { 304 name: "nil values are skipped", 305 data: []*format.ProtectionData{ 306 { 307 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 308 SignedBlocks: []*format.SignedBlock{ 309 { 310 Slot: "1", 311 SigningRoot: fmt.Sprintf("%x", roots[0]), 312 }, 313 nil, 314 }, 315 }, 316 { 317 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 318 SignedBlocks: []*format.SignedBlock{ 319 { 320 Slot: "3", 321 SigningRoot: fmt.Sprintf("%x", roots[2]), 322 }, 323 }, 324 }, 325 }, 326 want: map[[48]byte][]*format.SignedBlock{ 327 publicKeys[0]: { 328 { 329 Slot: "1", 330 SigningRoot: fmt.Sprintf("%x", roots[0]), 331 }, 332 { 333 Slot: "3", 334 SigningRoot: fmt.Sprintf("%x", roots[2]), 335 }, 336 }, 337 }, 338 }, 339 { 340 name: "same blocks but different public keys are parsed correctly", 341 data: []*format.ProtectionData{ 342 { 343 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 344 SignedBlocks: []*format.SignedBlock{ 345 { 346 Slot: "1", 347 SigningRoot: fmt.Sprintf("%x", roots[0]), 348 }, 349 { 350 Slot: "2", 351 SigningRoot: fmt.Sprintf("%x", roots[1]), 352 }, 353 }, 354 }, 355 { 356 Pubkey: fmt.Sprintf("%x", publicKeys[1]), 357 SignedBlocks: []*format.SignedBlock{ 358 { 359 Slot: "1", 360 SigningRoot: fmt.Sprintf("%x", roots[0]), 361 }, 362 { 363 Slot: "2", 364 SigningRoot: fmt.Sprintf("%x", roots[1]), 365 }, 366 }, 367 }, 368 }, 369 want: map[[48]byte][]*format.SignedBlock{ 370 publicKeys[0]: { 371 { 372 Slot: "1", 373 SigningRoot: fmt.Sprintf("%x", roots[0]), 374 }, 375 { 376 Slot: "2", 377 SigningRoot: fmt.Sprintf("%x", roots[1]), 378 }, 379 }, 380 publicKeys[1]: { 381 { 382 Slot: "1", 383 SigningRoot: fmt.Sprintf("%x", roots[0]), 384 }, 385 { 386 Slot: "2", 387 SigningRoot: fmt.Sprintf("%x", roots[1]), 388 }, 389 }, 390 }, 391 }, 392 { 393 name: "disjoint sets of signed blocks by the same public key are parsed correctly", 394 data: []*format.ProtectionData{ 395 { 396 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 397 SignedBlocks: []*format.SignedBlock{ 398 { 399 Slot: "1", 400 SigningRoot: fmt.Sprintf("%x", roots[0]), 401 }, 402 { 403 Slot: "2", 404 SigningRoot: fmt.Sprintf("%x", roots[1]), 405 }, 406 }, 407 }, 408 { 409 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 410 SignedBlocks: []*format.SignedBlock{ 411 { 412 Slot: "3", 413 SigningRoot: fmt.Sprintf("%x", roots[2]), 414 }, 415 }, 416 }, 417 }, 418 want: map[[48]byte][]*format.SignedBlock{ 419 publicKeys[0]: { 420 { 421 Slot: "1", 422 SigningRoot: fmt.Sprintf("%x", roots[0]), 423 }, 424 { 425 Slot: "2", 426 SigningRoot: fmt.Sprintf("%x", roots[1]), 427 }, 428 { 429 Slot: "3", 430 SigningRoot: fmt.Sprintf("%x", roots[2]), 431 }, 432 }, 433 }, 434 }, 435 { 436 name: "full duplicate entries are uniquely parsed", 437 data: []*format.ProtectionData{ 438 { 439 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 440 SignedBlocks: []*format.SignedBlock{ 441 { 442 Slot: "1", 443 SigningRoot: fmt.Sprintf("%x", roots[0]), 444 }, 445 }, 446 }, 447 { 448 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 449 SignedBlocks: []*format.SignedBlock{ 450 { 451 Slot: "1", 452 SigningRoot: fmt.Sprintf("%x", roots[0]), 453 }, 454 }, 455 }, 456 }, 457 want: map[[48]byte][]*format.SignedBlock{ 458 publicKeys[0]: { 459 { 460 Slot: "1", 461 SigningRoot: fmt.Sprintf("%x", roots[0]), 462 }, 463 { 464 Slot: "1", 465 SigningRoot: fmt.Sprintf("%x", roots[0]), 466 }, 467 }, 468 }, 469 }, 470 { 471 name: "intersecting duplicate public key entries are handled properly", 472 data: []*format.ProtectionData{ 473 { 474 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 475 SignedBlocks: []*format.SignedBlock{ 476 { 477 Slot: "1", 478 SigningRoot: fmt.Sprintf("%x", roots[0]), 479 }, 480 { 481 Slot: "2", 482 SigningRoot: fmt.Sprintf("%x", roots[1]), 483 }, 484 }, 485 }, 486 { 487 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 488 SignedBlocks: []*format.SignedBlock{ 489 { 490 Slot: "2", 491 SigningRoot: fmt.Sprintf("%x", roots[1]), 492 }, 493 { 494 Slot: "3", 495 SigningRoot: fmt.Sprintf("%x", roots[2]), 496 }, 497 }, 498 }, 499 }, 500 want: map[[48]byte][]*format.SignedBlock{ 501 publicKeys[0]: { 502 { 503 Slot: "1", 504 SigningRoot: fmt.Sprintf("%x", roots[0]), 505 }, 506 { 507 Slot: "2", 508 SigningRoot: fmt.Sprintf("%x", roots[1]), 509 }, 510 { 511 Slot: "2", 512 SigningRoot: fmt.Sprintf("%x", roots[1]), 513 }, 514 { 515 Slot: "3", 516 SigningRoot: fmt.Sprintf("%x", roots[2]), 517 }, 518 }, 519 }, 520 }, 521 } 522 for _, tt := range tests { 523 t.Run(tt.name, func(t *testing.T) { 524 got, err := parseBlocksForUniquePublicKeys(tt.data) 525 if (err != nil) != tt.wantErr { 526 t.Errorf("parseBlocksForUniquePublicKeys() error = %v, wantErr %v", err, tt.wantErr) 527 return 528 } 529 if !reflect.DeepEqual(got, tt.want) { 530 t.Errorf("parseBlocksForUniquePublicKeys() got = %v, want %v", got, tt.want) 531 } 532 }) 533 } 534 } 535 536 func Test_parseUniqueSignedAttestationsByPubKey(t *testing.T) { 537 numValidators := 4 538 publicKeys, err := valtest.CreateRandomPubKeys(numValidators) 539 require.NoError(t, err) 540 roots := valtest.CreateMockRoots(numValidators) 541 tests := []struct { 542 name string 543 data []*format.ProtectionData 544 want map[[48]byte][]*format.SignedAttestation 545 wantErr bool 546 }{ 547 { 548 name: "nil values are skipped", 549 data: []*format.ProtectionData{ 550 { 551 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 552 SignedAttestations: []*format.SignedAttestation{ 553 { 554 SourceEpoch: "1", 555 TargetEpoch: "3", 556 SigningRoot: fmt.Sprintf("%x", roots[0]), 557 }, 558 nil, 559 }, 560 }, 561 { 562 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 563 SignedAttestations: []*format.SignedAttestation{ 564 { 565 SourceEpoch: "3", 566 TargetEpoch: "5", 567 SigningRoot: fmt.Sprintf("%x", roots[2]), 568 }, 569 }, 570 }, 571 }, 572 want: map[[48]byte][]*format.SignedAttestation{ 573 publicKeys[0]: { 574 { 575 SourceEpoch: "1", 576 TargetEpoch: "3", 577 SigningRoot: fmt.Sprintf("%x", roots[0]), 578 }, 579 { 580 SourceEpoch: "3", 581 TargetEpoch: "5", 582 SigningRoot: fmt.Sprintf("%x", roots[2]), 583 }, 584 }, 585 }, 586 }, 587 { 588 name: "same attestations but different public keys are parsed correctly", 589 data: []*format.ProtectionData{ 590 { 591 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 592 SignedAttestations: []*format.SignedAttestation{ 593 { 594 SourceEpoch: "1", 595 SigningRoot: fmt.Sprintf("%x", roots[0]), 596 }, 597 { 598 SourceEpoch: "2", 599 SigningRoot: fmt.Sprintf("%x", roots[1]), 600 }, 601 }, 602 }, 603 { 604 Pubkey: fmt.Sprintf("%x", publicKeys[1]), 605 SignedAttestations: []*format.SignedAttestation{ 606 { 607 SourceEpoch: "1", 608 SigningRoot: fmt.Sprintf("%x", roots[0]), 609 }, 610 { 611 SourceEpoch: "2", 612 SigningRoot: fmt.Sprintf("%x", roots[1]), 613 }, 614 }, 615 }, 616 }, 617 want: map[[48]byte][]*format.SignedAttestation{ 618 publicKeys[0]: { 619 { 620 SourceEpoch: "1", 621 SigningRoot: fmt.Sprintf("%x", roots[0]), 622 }, 623 { 624 SourceEpoch: "2", 625 SigningRoot: fmt.Sprintf("%x", roots[1]), 626 }, 627 }, 628 publicKeys[1]: { 629 { 630 SourceEpoch: "1", 631 SigningRoot: fmt.Sprintf("%x", roots[0]), 632 }, 633 { 634 SourceEpoch: "2", 635 SigningRoot: fmt.Sprintf("%x", roots[1]), 636 }, 637 }, 638 }, 639 }, 640 { 641 name: "disjoint sets of signed attestations by the same public key are parsed correctly", 642 data: []*format.ProtectionData{ 643 { 644 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 645 SignedAttestations: []*format.SignedAttestation{ 646 { 647 SourceEpoch: "1", 648 TargetEpoch: "3", 649 SigningRoot: fmt.Sprintf("%x", roots[0]), 650 }, 651 { 652 SourceEpoch: "2", 653 TargetEpoch: "4", 654 SigningRoot: fmt.Sprintf("%x", roots[1]), 655 }, 656 }, 657 }, 658 { 659 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 660 SignedAttestations: []*format.SignedAttestation{ 661 { 662 SourceEpoch: "3", 663 TargetEpoch: "5", 664 SigningRoot: fmt.Sprintf("%x", roots[2]), 665 }, 666 }, 667 }, 668 }, 669 want: map[[48]byte][]*format.SignedAttestation{ 670 publicKeys[0]: { 671 { 672 SourceEpoch: "1", 673 TargetEpoch: "3", 674 SigningRoot: fmt.Sprintf("%x", roots[0]), 675 }, 676 { 677 SourceEpoch: "2", 678 TargetEpoch: "4", 679 SigningRoot: fmt.Sprintf("%x", roots[1]), 680 }, 681 { 682 SourceEpoch: "3", 683 TargetEpoch: "5", 684 SigningRoot: fmt.Sprintf("%x", roots[2]), 685 }, 686 }, 687 }, 688 }, 689 { 690 name: "full duplicate entries are uniquely parsed", 691 data: []*format.ProtectionData{ 692 { 693 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 694 SignedAttestations: []*format.SignedAttestation{ 695 { 696 SourceEpoch: "1", 697 SigningRoot: fmt.Sprintf("%x", roots[0]), 698 }, 699 }, 700 }, 701 { 702 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 703 SignedAttestations: []*format.SignedAttestation{ 704 { 705 SourceEpoch: "1", 706 SigningRoot: fmt.Sprintf("%x", roots[0]), 707 }, 708 }, 709 }, 710 }, 711 want: map[[48]byte][]*format.SignedAttestation{ 712 publicKeys[0]: { 713 { 714 SourceEpoch: "1", 715 SigningRoot: fmt.Sprintf("%x", roots[0]), 716 }, 717 { 718 SourceEpoch: "1", 719 SigningRoot: fmt.Sprintf("%x", roots[0]), 720 }, 721 }, 722 }, 723 }, 724 { 725 name: "intersecting duplicate public key entries are handled properly", 726 data: []*format.ProtectionData{ 727 { 728 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 729 SignedAttestations: []*format.SignedAttestation{ 730 { 731 SourceEpoch: "1", 732 SigningRoot: fmt.Sprintf("%x", roots[0]), 733 }, 734 { 735 SourceEpoch: "2", 736 SigningRoot: fmt.Sprintf("%x", roots[1]), 737 }, 738 }, 739 }, 740 { 741 Pubkey: fmt.Sprintf("%x", publicKeys[0]), 742 SignedAttestations: []*format.SignedAttestation{ 743 { 744 SourceEpoch: "2", 745 SigningRoot: fmt.Sprintf("%x", roots[1]), 746 }, 747 { 748 SourceEpoch: "3", 749 SigningRoot: fmt.Sprintf("%x", roots[2]), 750 }, 751 }, 752 }, 753 }, 754 want: map[[48]byte][]*format.SignedAttestation{ 755 publicKeys[0]: { 756 { 757 SourceEpoch: "1", 758 SigningRoot: fmt.Sprintf("%x", roots[0]), 759 }, 760 { 761 SourceEpoch: "2", 762 SigningRoot: fmt.Sprintf("%x", roots[1]), 763 }, 764 { 765 SourceEpoch: "2", 766 SigningRoot: fmt.Sprintf("%x", roots[1]), 767 }, 768 { 769 SourceEpoch: "3", 770 SigningRoot: fmt.Sprintf("%x", roots[2]), 771 }, 772 }, 773 }, 774 }, 775 } 776 for _, tt := range tests { 777 t.Run(tt.name, func(t *testing.T) { 778 got, err := parseAttestationsForUniquePublicKeys(tt.data) 779 if (err != nil) != tt.wantErr { 780 t.Errorf("parseAttestationsForUniquePublicKeys() error = %v, wantErr %v", err, tt.wantErr) 781 return 782 } 783 if !reflect.DeepEqual(got, tt.want) { 784 t.Errorf("parseAttestationsForUniquePublicKeys() got = %v, want %v", got, tt.want) 785 } 786 }) 787 } 788 } 789 790 func Test_filterSlashablePubKeysFromBlocks(t *testing.T) { 791 var tests = []struct { 792 name string 793 expected [][48]byte 794 given map[[48]byte][]*format.SignedBlock 795 }{ 796 { 797 name: "No slashable keys returns empty", 798 expected: make([][48]byte, 0), 799 given: map[[48]byte][]*format.SignedBlock{ 800 {1}: { 801 { 802 Slot: "1", 803 }, 804 { 805 Slot: "2", 806 }, 807 }, 808 {2}: { 809 { 810 Slot: "2", 811 }, 812 { 813 Slot: "3", 814 }, 815 }, 816 }, 817 }, 818 { 819 name: "Empty data returns empty", 820 expected: make([][48]byte, 0), 821 given: make(map[[48]byte][]*format.SignedBlock), 822 }, 823 { 824 name: "Properly finds public keys with slashable data", 825 expected: [][48]byte{ 826 {1}, {3}, 827 }, 828 given: map[[48]byte][]*format.SignedBlock{ 829 {1}: { 830 { 831 Slot: "1", 832 }, 833 { 834 Slot: "1", 835 }, 836 { 837 Slot: "2", 838 }, 839 }, 840 {2}: { 841 { 842 Slot: "2", 843 }, 844 { 845 Slot: "3", 846 }, 847 }, 848 {3}: { 849 { 850 Slot: "3", 851 }, 852 { 853 Slot: "3", 854 }, 855 }, 856 }, 857 }, 858 { 859 name: "Considers nil signing roots and mismatched signing roots when determining slashable keys", 860 expected: [][48]byte{ 861 {2}, {3}, 862 }, 863 given: map[[48]byte][]*format.SignedBlock{ 864 // Different signing roots and same slot should not be slashable. 865 {1}: { 866 { 867 Slot: "1", 868 SigningRoot: fmt.Sprintf("%#x", [32]byte{1}), 869 }, 870 { 871 Slot: "1", 872 SigningRoot: fmt.Sprintf("%#x", [32]byte{1}), 873 }, 874 }, 875 // No signing root specified but same slot should be slashable. 876 {2}: { 877 { 878 Slot: "2", 879 }, 880 { 881 Slot: "2", 882 }, 883 }, 884 // No signing root in one slot, and same slot with signing root should be slashable. 885 {3}: { 886 { 887 Slot: "3", 888 }, 889 { 890 Slot: "3", 891 SigningRoot: fmt.Sprintf("%#x", [32]byte{3}), 892 }, 893 }, 894 }, 895 }, 896 } 897 for _, tt := range tests { 898 tt := tt 899 t.Run(tt.name, func(t *testing.T) { 900 ctx := context.Background() 901 historyByPubKey := make(map[[48]byte]kv.ProposalHistoryForPubkey) 902 for pubKey, signedBlocks := range tt.given { 903 proposalHistory, err := transformSignedBlocks(ctx, signedBlocks) 904 require.NoError(t, err) 905 historyByPubKey[pubKey] = *proposalHistory 906 } 907 slashablePubKeys := filterSlashablePubKeysFromBlocks(context.Background(), historyByPubKey) 908 wantedPubKeys := make(map[[48]byte]bool) 909 for _, pk := range tt.expected { 910 wantedPubKeys[pk] = true 911 wantedPubKeys[pk] = true 912 } 913 for _, pk := range slashablePubKeys { 914 ok := wantedPubKeys[pk] 915 require.Equal(t, true, ok) 916 } 917 }) 918 } 919 } 920 921 func Test_filterSlashablePubKeysFromAttestations(t *testing.T) { 922 ctx := context.Background() 923 tests := []struct { 924 name string 925 previousAttsByPubKey map[[48]byte][]*format.SignedAttestation 926 incomingAttsByPubKey map[[48]byte][]*format.SignedAttestation 927 want map[[48]byte]bool 928 wantErr bool 929 }{ 930 { 931 name: "Properly filters out double voting attester keys", 932 previousAttsByPubKey: map[[48]byte][]*format.SignedAttestation{ 933 {1}: { 934 { 935 SourceEpoch: "2", 936 TargetEpoch: "4", 937 }, 938 { 939 SourceEpoch: "2", 940 TargetEpoch: "4", 941 }, 942 }, 943 {2}: { 944 { 945 SourceEpoch: "2", 946 TargetEpoch: "4", 947 }, 948 { 949 SourceEpoch: "2", 950 TargetEpoch: "5", 951 }, 952 }, 953 {3}: { 954 { 955 SourceEpoch: "2", 956 TargetEpoch: "4", 957 }, 958 { 959 SourceEpoch: "2", 960 TargetEpoch: "4", 961 }, 962 }, 963 }, 964 want: map[[48]byte]bool{ 965 {1}: true, 966 {3}: true, 967 }, 968 }, 969 { 970 name: "Returns empty if no keys are slashable", 971 previousAttsByPubKey: map[[48]byte][]*format.SignedAttestation{ 972 {1}: { 973 { 974 SourceEpoch: "2", 975 TargetEpoch: "4", 976 }, 977 }, 978 {2}: { 979 { 980 SourceEpoch: "2", 981 TargetEpoch: "4", 982 }, 983 { 984 SourceEpoch: "2", 985 TargetEpoch: "5", 986 }, 987 }, 988 {3}: { 989 { 990 SourceEpoch: "2", 991 TargetEpoch: "4", 992 }, 993 { 994 SourceEpoch: "3", 995 TargetEpoch: "6", 996 }, 997 }, 998 }, 999 want: map[[48]byte]bool{}, 1000 }, 1001 { 1002 name: "Properly filters out surround voting attester keys", 1003 previousAttsByPubKey: map[[48]byte][]*format.SignedAttestation{ 1004 {1}: { 1005 { 1006 SourceEpoch: "2", 1007 TargetEpoch: "4", 1008 }, 1009 { 1010 SourceEpoch: "1", 1011 TargetEpoch: "5", 1012 }, 1013 }, 1014 {2}: { 1015 { 1016 SourceEpoch: "2", 1017 TargetEpoch: "4", 1018 }, 1019 { 1020 SourceEpoch: "2", 1021 TargetEpoch: "5", 1022 }, 1023 }, 1024 {3}: { 1025 { 1026 SourceEpoch: "2", 1027 TargetEpoch: "5", 1028 }, 1029 { 1030 SourceEpoch: "3", 1031 TargetEpoch: "4", 1032 }, 1033 }, 1034 }, 1035 want: map[[48]byte]bool{ 1036 {1}: true, 1037 {3}: true, 1038 }, 1039 }, 1040 } 1041 for _, tt := range tests { 1042 t.Run(tt.name, func(t *testing.T) { 1043 attestingHistoriesByPubKey := make(map[[48]byte][]*kv.AttestationRecord) 1044 pubKeys := make([][48]byte, 0) 1045 for pubKey := range tt.incomingAttsByPubKey { 1046 pubKeys = append(pubKeys, pubKey) 1047 } 1048 validatorDB := dbtest.SetupDB(t, pubKeys) 1049 for pubKey, signedAtts := range tt.incomingAttsByPubKey { 1050 attestingHistory, err := transformSignedAttestations(pubKey, signedAtts) 1051 require.NoError(t, err) 1052 for _, att := range attestingHistory { 1053 indexedAtt := createAttestation(att.Source, att.Target) 1054 err := validatorDB.SaveAttestationForPubKey(ctx, pubKey, att.SigningRoot, indexedAtt) 1055 require.NoError(t, err) 1056 } 1057 } 1058 got, err := filterSlashablePubKeysFromAttestations(ctx, validatorDB, attestingHistoriesByPubKey) 1059 if (err != nil) != tt.wantErr { 1060 t.Errorf("filterSlashablePubKeysFromAttestations() error = %v, wantErr %v", err, tt.wantErr) 1061 return 1062 } 1063 for _, pubKey := range got { 1064 ok := tt.want[pubKey] 1065 assert.Equal(t, true, ok) 1066 } 1067 }) 1068 } 1069 }