package slasher import ( "context" "fmt" "math/rand" "testing" "time" mock "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" dbtest "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/testing" slashingsmock "github.com/prysmaticlabs/prysm/v5/beacon-chain/operations/slashings/mock" slashertypes "github.com/prysmaticlabs/prysm/v5/beacon-chain/slasher/types" "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/crypto/bls" "github.com/prysmaticlabs/prysm/v5/crypto/bls/common" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" "github.com/prysmaticlabs/prysm/v5/testing/util" "github.com/prysmaticlabs/prysm/v5/time/slots" logTest "github.com/sirupsen/logrus/hooks/test" ) func Test_processAttestations(t *testing.T) { type ( attestationInfo struct { source primitives.Epoch target primitives.Epoch indices []uint64 beaconBlockRoot []byte } slashingInfo struct { attestationInfo_1 *attestationInfo attestationInfo_2 *attestationInfo } step struct { currentEpoch primitives.Epoch attestationsInfo []*attestationInfo expectedSlashingsInfo []*slashingInfo } ) tests := []struct { name string steps []*step }{ { name: "Same target with different signing roots - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{2}}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, attestationInfo_2: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{2}}, }, }, }, }, }, { name: "Same target with different signing roots - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{2}}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, attestationInfo_2: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{2}}, }, }, }, }, }, { name: "Same target with same signing roots - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Same target with same signing roots - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: []byte{1}}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Detects surrounding vote (source 1, target 2), (source 0, target 3) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects surrounding vote (source 1, target 2), (source 0, target 3) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects surrounding vote (source 50, target 51), (source 0, target 1000) - single step", steps: []*step{ { currentEpoch: 1000, attestationsInfo: []*attestationInfo{ {source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil}, {source: 0, target: 1000, indices: []uint64{0}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 0, target: 1000, indices: []uint64{0}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects surrounding vote (source 50, target 51), (source 0, target 1000) - two steps", steps: []*step{ { currentEpoch: 1000, attestationsInfo: []*attestationInfo{ {source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 1000, attestationsInfo: []*attestationInfo{ {source: 0, target: 1000, indices: []uint64{0}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 0, target: 1000, indices: []uint64{0}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects surrounded vote (source 0, target 3), (source 1, target 2) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects surrounded vote (source 0, target 3), (source 1, target 2) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects double vote, (source 1, target 2), (source 0, target 2) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Detects double vote, (source 1, target 2), (source 0, target 2) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: []*slashingInfo{ { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, }, }, }, }, { name: "Not slashable, surrounding but non-overlapping attesting indices within same validator chunk index - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0}, beaconBlockRoot: nil}, {source: 0, target: 3, indices: []uint64{1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounding but non-overlapping attesting indices within same validator chunk index - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounded but non-overlapping attesting indices within same validator chunk index - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 1, target: 2, indices: []uint64{2, 3}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounded but non-overlapping attesting indices within same validator chunk index - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{2, 3}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounding but non-overlapping attesting indices in different validator chunk index - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0}, beaconBlockRoot: nil}, {source: 1, target: 2, indices: []uint64{params.BeaconConfig().MinGenesisActiveValidatorCount - 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounding but non-overlapping attesting indices in different validator chunk index - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{params.BeaconConfig().MinGenesisActiveValidatorCount - 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounded but non-overlapping attesting indices in different validator chunk index - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0}, beaconBlockRoot: nil}, {source: 1, target: 2, indices: []uint64{params.BeaconConfig().MinGenesisActiveValidatorCount - 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, surrounded but non-overlapping attesting indices in different validator chunk index - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{params.BeaconConfig().MinGenesisActiveValidatorCount - 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 1, target 2), (source 2, target 3) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 2, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 1, target 2), (source 2, target 3) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 2, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 0, target 3), (source 2, target 4) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 2, target: 4, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 0, target 3), (source 2, target 4) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 2, target: 4, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 0, target 2), (source 0, target 3) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 0, target 2), (source 0, target 3) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 0, target 3), (source 0, target 2) - single step", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, {source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, { name: "Not slashable, (source 0, target 3), (source 0, target 2) - two steps", steps: []*step{ { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, { currentEpoch: 4, attestationsInfo: []*attestationInfo{ {source: 0, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, }, expectedSlashingsInfo: nil, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create context. ctx := context.Background() // Configure logging. hook := logTest.NewGlobal() defer hook.Reset() // Configure the slasher database. slasherDB := dbtest.SetupSlasherDB(t) // Configure the beacon state. beaconState, err := util.NewBeaconState() require.NoError(t, err) // Create the mock chain service. mockChain := &mock.ChainService{State: beaconState} // Create the mock slashing pool inserter. mockSlashingPoolInserter := &slashingsmock.PoolMock{} // Create the service configuration. serviceConfig := &ServiceConfig{ Database: slasherDB, HeadStateFetcher: mockChain, AttestationStateFetcher: mockChain, SlashingPoolInserter: mockSlashingPoolInserter, } // Create the slasher service. slasherService, err := New(context.Background(), serviceConfig) require.NoError(t, err) // Initialize validators in the state. numVals := params.BeaconConfig().MinGenesisActiveValidatorCount validators := make([]*ethpb.Validator, numVals) privateKeys := make([]bls.SecretKey, numVals) for i := uint64(0); i < numVals; i++ { // Create a random private key. privateKey, err := bls.RandKey() require.NoError(t, err) // Add the private key to the list. privateKeys[i] = privateKey // Derive the public key from the private key. publicKey := privateKey.PublicKey().Marshal() // Initialize the validator. validator := ðpb.Validator{PublicKey: publicKey} // Add the validator to the list. validators[i] = validator } // Set the validators into the state. err = beaconState.SetValidators(validators) require.NoError(t, err) // Compute the signing domain. domain, err := signing.Domain( beaconState.Fork(), 0, params.BeaconConfig().DomainBeaconAttester, beaconState.GenesisValidatorsRoot(), ) require.NoError(t, err) for _, step := range tt.steps { // Build attestation wrappers. attestationsCount := len(step.attestationsInfo) attestationWrappers := make([]*slashertypes.IndexedAttestationWrapper, 0, attestationsCount) for _, attestationInfo := range step.attestationsInfo { // Create a wrapped attestation. attestationWrapper := createAttestationWrapper( t, domain, privateKeys, attestationInfo.source, attestationInfo.target, attestationInfo.indices, attestationInfo.beaconBlockRoot, ) // Add the wrapped attestation to the list. attestationWrappers = append(attestationWrappers, attestationWrapper) } // Build expected attester slashings. expectedSlashings := make(map[[fieldparams.RootLength]byte]*ethpb.AttesterSlashing, len(step.expectedSlashingsInfo)) for _, slashingInfo := range step.expectedSlashingsInfo { // Create attestations. wrapper_1 := createAttestationWrapper( t, domain, privateKeys, slashingInfo.attestationInfo_1.source, slashingInfo.attestationInfo_1.target, slashingInfo.attestationInfo_1.indices, slashingInfo.attestationInfo_1.beaconBlockRoot, ) wrapper_2 := createAttestationWrapper( t, domain, privateKeys, slashingInfo.attestationInfo_2.source, slashingInfo.attestationInfo_2.target, slashingInfo.attestationInfo_2.indices, slashingInfo.attestationInfo_2.beaconBlockRoot, ) // Create the attester slashing. expectedSlashing := ðpb.AttesterSlashing{ Attestation_1: wrapper_1.IndexedAttestation, Attestation_2: wrapper_2.IndexedAttestation, } root, err := expectedSlashing.HashTreeRoot() require.NoError(t, err, "failed to hash tree root") // Add the attester slashing to the map. expectedSlashings[root] = expectedSlashing } // Get the currentSlot for the current epoch. currentSlot, err := slots.EpochStart(step.currentEpoch) require.NoError(t, err) // Process the attestations. processedSlashings := slasherService.processAttestations(ctx, attestationWrappers, currentSlot) // Check the processed slashings correspond to the expected slashings. require.Equal(t, len(expectedSlashings), len(processedSlashings), "processed slashings count not equal to expected") for root := range expectedSlashings { // Check the expected slashing is in the processed slashings. processedSlashing, ok := processedSlashings[root] require.Equal(t, true, ok, "processed slashing not found") // Check the root matches controlRoot, err := processedSlashing.HashTreeRoot() require.NoError(t, err, "failed to hash tree root") require.Equal(t, root, controlRoot, "root not equal") } } }) } } func Test_processQueuedAttestations_MultipleChunkIndices(t *testing.T) { hook := logTest.NewGlobal() defer hook.Reset() slasherDB := dbtest.SetupSlasherDB(t) ctx, cancel := context.WithCancel(context.Background()) slasherParams := DefaultParams() // We process submit attestations from chunk index 0 to chunk index 1. // What we want to test here is if we can proceed // with processing queued attestations once the chunk index changes. // For example, epochs 0 - 15 are chunk 0, epochs 16 - 31 are chunk 1, etc. startEpoch := primitives.Epoch(slasherParams.chunkSize) endEpoch := primitives.Epoch(slasherParams.chunkSize + 1) currentTime := time.Now() totalSlots := uint64(startEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch) secondsSinceGenesis := time.Duration(totalSlots * params.BeaconConfig().SecondsPerSlot) genesisTime := currentTime.Add(-secondsSinceGenesis * time.Second) beaconState, err := util.NewBeaconState() require.NoError(t, err) mockChain := &mock.ChainService{ State: beaconState, } s, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, HeadStateFetcher: mockChain, AttestationStateFetcher: mockChain, SlashingPoolInserter: &slashingsmock.PoolMock{}, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(t, err) s.genesisTime = genesisTime currentSlotChan := make(chan primitives.Slot) s.wg.Add(1) go func() { s.processQueuedAttestations(ctx, currentSlotChan) }() for i := startEpoch; i <= endEpoch; i++ { source := primitives.Epoch(0) target := primitives.Epoch(0) if i != 0 { source = i - 1 target = i } var sr [32]byte copy(sr[:], fmt.Sprintf("%d", i)) att := createAttestationWrapperEmptySig(t, source, target, []uint64{0}, sr[:]) s.attsQueue = newAttestationsQueue() s.attsQueue.push(att) slot, err := slots.EpochStart(i) require.NoError(t, err) require.NoError(t, mockChain.State.SetSlot(slot)) s.serviceCfg.HeadStateFetcher = mockChain currentSlotChan <- slot } time.Sleep(time.Millisecond * 200) cancel() s.wg.Wait() require.LogsDoNotContain(t, hook, "Slashable offenses found") require.LogsDoNotContain(t, hook, "Could not detect") } func Test_processQueuedAttestations_OverlappingChunkIndices(t *testing.T) { hook := logTest.NewGlobal() defer hook.Reset() slasherDB := dbtest.SetupSlasherDB(t) ctx, cancel := context.WithCancel(context.Background()) slasherParams := DefaultParams() startEpoch := primitives.Epoch(slasherParams.chunkSize) currentTime := time.Now() totalSlots := uint64(startEpoch) * uint64(params.BeaconConfig().SlotsPerEpoch) secondsSinceGenesis := time.Duration(totalSlots * params.BeaconConfig().SecondsPerSlot) genesisTime := currentTime.Add(-secondsSinceGenesis * time.Second) beaconState, err := util.NewBeaconState() require.NoError(t, err) mockChain := &mock.ChainService{ State: beaconState, } s, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, HeadStateFetcher: mockChain, AttestationStateFetcher: mockChain, SlashingPoolInserter: &slashingsmock.PoolMock{}, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(t, err) s.genesisTime = genesisTime currentSlotChan := make(chan primitives.Slot) s.wg.Add(1) go func() { s.processQueuedAttestations(ctx, currentSlotChan) }() // We create two attestations fully spanning chunk indices 0 and chunk 1 att1 := createAttestationWrapperEmptySig(t, primitives.Epoch(slasherParams.chunkSize-2), primitives.Epoch(slasherParams.chunkSize), []uint64{0, 1}, nil) att2 := createAttestationWrapperEmptySig(t, primitives.Epoch(slasherParams.chunkSize-1), primitives.Epoch(slasherParams.chunkSize+1), []uint64{0, 1}, nil) // We attempt to process the batch. s.attsQueue = newAttestationsQueue() s.attsQueue.push(att1) s.attsQueue.push(att2) slot, err := slots.EpochStart(att2.IndexedAttestation.Data.Target.Epoch) require.NoError(t, err) mockChain.Slot = &slot s.serviceCfg.HeadStateFetcher = mockChain currentSlotChan <- slot time.Sleep(time.Millisecond * 200) cancel() s.wg.Wait() require.LogsDoNotContain(t, hook, "Slashable offenses found") require.LogsDoNotContain(t, hook, "Could not detect") } func Test_updatedChunkByChunkIndex(t *testing.T) { neutralMin, neutralMax := uint16(65535), uint16(0) testCases := []struct { name string chunkSize uint64 validatorChunkSize uint64 historyLength primitives.Epoch currentEpoch primitives.Epoch validatorChunkIndex uint64 latestUpdatedEpochByValidatorIndex map[primitives.ValidatorIndex]primitives.Epoch initialMinChunkByChunkIndex map[uint64][]uint16 expectedMinChunkByChunkIndex map[uint64][]uint16 initialMaxChunkByChunkIndex map[uint64][]uint16 expectedMaxChunkByChunkIndex map[uint64][]uint16 }{ { name: "start with no data - first chunk", chunkSize: 4, validatorChunkSize: 2, historyLength: 8, currentEpoch: 2, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: nil, initialMinChunkByChunkIndex: nil, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, }, initialMaxChunkByChunkIndex: nil, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, }, }, { name: "start with no data - second chunk", chunkSize: 4, validatorChunkSize: 2, historyLength: 8, currentEpoch: 5, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: nil, initialMinChunkByChunkIndex: nil, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, 1: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, }, initialMaxChunkByChunkIndex: nil, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, 1: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, }, }, { name: "start with some data - first chunk", chunkSize: 4, validatorChunkSize: 2, historyLength: 8, currentEpoch: 2, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: map[primitives.ValidatorIndex]primitives.Epoch{42: 0, 43: 1}, initialMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {14, 9999, 9999, 9999, 15, 16, 9999, 9999}, }, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {14, neutralMin, neutralMin, 9999, 15, 16, neutralMin, 9999}, }, initialMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {70, 9999, 9999, 9999, 71, 72, 9999, 9999}, }, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {70, neutralMax, neutralMax, 9999, 71, 72, neutralMax, 9999}, }, }, { name: "start with some data - second chunk", chunkSize: 4, validatorChunkSize: 2, historyLength: 8, currentEpoch: 5, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: map[primitives.ValidatorIndex]primitives.Epoch{42: 1, 43: 2}, initialMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {14, 13, 9999, 9999, 15, 16, 17, 9999}, }, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {14, 13, neutralMin, neutralMin, 15, 16, 17, neutralMin}, // | validator 42 | validator 43 | 1: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, }, initialMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {70, 69, 9999, 9999, 71, 72, 73, 9999}, }, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {70, 69, neutralMax, neutralMax, 71, 72, 73, neutralMax}, // | validator 42 | validator 43 | 1: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, }, }, { name: "start with some data - third chunk", chunkSize: 4, validatorChunkSize: 2, historyLength: 12, currentEpoch: 9, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: map[primitives.ValidatorIndex]primitives.Epoch{42: 5, 43: 6}, initialMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 1: {14, 13, 9999, 9999, 15, 16, 17, 9999}, }, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 1: {14, 13, neutralMin, neutralMin, 15, 16, 17, neutralMin}, // | validator 42 | validator 43 | 2: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, }, initialMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 1: {70, 69, 9999, 9999, 71, 72, 73, 9999}, }, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 1: {70, 69, neutralMax, neutralMax, 71, 72, 73, neutralMax}, // | validator 42 | validator 43 | 2: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, }, }, { name: "start with some data - third chunk - wrap to first chunk", chunkSize: 4, validatorChunkSize: 2, historyLength: 12, currentEpoch: 14, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: map[primitives.ValidatorIndex]primitives.Epoch{42: 9, 43: 10}, initialMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {55, 55, 55, 55, 55, 55, 55, 55}, 1: {66, 66, 66, 66, 66, 66, 66, 66}, // | validator 42 | validator 43 | 2: {77, 77, 9999, 9999, 77, 77, 77, 9999}, }, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 2: {77, 77, neutralMin, neutralMin, 77, 77, 77, neutralMin}, // | validator 42 | validator 43 | 0: {neutralMin, neutralMin, neutralMin, 55, neutralMin, neutralMin, neutralMin, 55}, }, initialMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {55, 55, 55, 55, 55, 55, 55, 55}, 1: {66, 66, 66, 66, 66, 66, 66, 66}, // | validator 42 | validator 43 | 2: {77, 77, 9999, 9999, 77, 77, 77, 9999}, }, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 2: {77, 77, neutralMax, neutralMax, 77, 77, 77, neutralMax}, // | validator 42 | validator 43 | 0: {neutralMax, neutralMax, neutralMax, 55, neutralMax, neutralMax, neutralMax, 55}, }, }, { name: "start with some data - high latest updated epoch", chunkSize: 4, validatorChunkSize: 2, historyLength: 12, currentEpoch: 16, validatorChunkIndex: 21, latestUpdatedEpochByValidatorIndex: map[primitives.ValidatorIndex]primitives.Epoch{42: 2, 43: 3}, initialMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {55, 55, 55, 55, 55, 55, 55, 55}, 1: {66, 66, 66, 66, 66, 66, 66, 66}, 2: {77, 77, 77, 77, 77, 77, 77, 77}, }, expectedMinChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, 1: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, 2: {neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin, neutralMin}, }, initialMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {55, 55, 55, 55, 55, 55, 55, 55}, 1: {66, 66, 66, 66, 66, 66, 66, 66}, 2: {77, 77, 77, 77, 77, 77, 77, 77}, }, expectedMaxChunkByChunkIndex: map[uint64][]uint16{ // | validator 42 | validator 43 | 0: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, 1: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, 2: {neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax, neutralMax}, }, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { // Create context. ctx := context.Background() // Initialize the slasher database. slasherDB := dbtest.SetupSlasherDB(t) // Intialize the slasher service. service := &Service{ params: &Parameters{ chunkSize: tt.chunkSize, validatorChunkSize: tt.validatorChunkSize, historyLength: tt.historyLength, }, serviceCfg: &ServiceConfig{Database: slasherDB}, latestEpochUpdatedForValidator: tt.latestUpdatedEpochByValidatorIndex, } // Save min initial chunks if they exist. if tt.initialMinChunkByChunkIndex != nil { minChunkerByChunkerIndex := map[uint64]Chunker{} for chunkIndex, minChunk := range tt.initialMinChunkByChunkIndex { minChunkerByChunkerIndex[chunkIndex] = &MinSpanChunksSlice{data: minChunk} } minChunkerByChunkerIndexByValidatorChunkerIndex := map[uint64]map[uint64]Chunker{ tt.validatorChunkIndex: minChunkerByChunkerIndex, } err := service.saveChunksToDisk(ctx, slashertypes.MinSpan, minChunkerByChunkerIndexByValidatorChunkerIndex) require.NoError(t, err) } // Save max initial chunks if they exist. if tt.initialMaxChunkByChunkIndex != nil { maxChunkerByChunkerIndex := map[uint64]Chunker{} for chunkIndex, maxChunk := range tt.initialMaxChunkByChunkIndex { maxChunkerByChunkerIndex[chunkIndex] = &MaxSpanChunksSlice{data: maxChunk} } maxChunkerByChunkerIndexByValidatorChunkerIndex := map[uint64]map[uint64]Chunker{ tt.validatorChunkIndex: maxChunkerByChunkerIndex, } err := service.saveChunksToDisk(ctx, slashertypes.MaxSpan, maxChunkerByChunkerIndexByValidatorChunkerIndex) require.NoError(t, err) } // Get chunks. actualMinChunkByChunkIndex, err := service.updatedChunkByChunkIndex( ctx, slashertypes.MinSpan, tt.currentEpoch, tt.validatorChunkIndex, ) // Compare the actual and expected chunks. require.NoError(t, err) require.Equal(t, len(tt.expectedMinChunkByChunkIndex), len(actualMinChunkByChunkIndex)) for chunkIndex, expectedMinChunk := range tt.expectedMinChunkByChunkIndex { actualMinChunk, ok := actualMinChunkByChunkIndex[chunkIndex] require.Equal(t, true, ok) require.Equal(t, len(expectedMinChunk), len(actualMinChunk.Chunk())) require.DeepSSZEqual(t, expectedMinChunk, actualMinChunk.Chunk()) } actualMaxChunkByChunkIndex, err := service.updatedChunkByChunkIndex( ctx, slashertypes.MaxSpan, tt.currentEpoch, tt.validatorChunkIndex, ) require.NoError(t, err) require.Equal(t, len(tt.expectedMaxChunkByChunkIndex), len(actualMaxChunkByChunkIndex)) for chunkIndex, expectedMaxChunk := range tt.expectedMaxChunkByChunkIndex { actualMaxChunk, ok := actualMaxChunkByChunkIndex[chunkIndex] require.Equal(t, true, ok) require.Equal(t, len(expectedMaxChunk), len(actualMaxChunk.Chunk())) require.DeepSSZEqual(t, expectedMaxChunk, actualMaxChunk.Chunk()) } }) } } func Test_applyAttestationForValidator_MinSpanChunk(t *testing.T) { ctx := context.Background() slasherDB := dbtest.SetupSlasherDB(t) srv, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(t, err) // We initialize an empty chunks slice. currentEpoch := primitives.Epoch(3) validatorChunkIndex := uint64(0) validatorIdx := primitives.ValidatorIndex(0) chunksByChunkIdx := map[uint64]Chunker{} // We apply attestation with (source 1, target 2) for our validator. source := primitives.Epoch(1) target := primitives.Epoch(2) att := createAttestationWrapperEmptySig(t, source, target, nil, nil) slashing, err := srv.applyAttestationForValidator( ctx, chunksByChunkIdx, att, slashertypes.MinSpan, validatorChunkIndex, validatorIdx, currentEpoch, ) require.NoError(t, err) require.IsNil(t, slashing) att.IndexedAttestation.AttestingIndices = []uint64{uint64(validatorIdx)} err = slasherDB.SaveAttestationRecordsForValidators( ctx, []*slashertypes.IndexedAttestationWrapper{att}, ) require.NoError(t, err) // Next, we apply an attestation with (source 0, target 3) and // expect a slashable offense to be returned. source = primitives.Epoch(0) target = primitives.Epoch(3) slashableAtt := createAttestationWrapperEmptySig(t, source, target, nil, nil) slashing, err = srv.applyAttestationForValidator( ctx, chunksByChunkIdx, slashableAtt, slashertypes.MinSpan, validatorChunkIndex, validatorIdx, currentEpoch, ) require.NoError(t, err) require.NotNil(t, slashing) } func Test_applyAttestationForValidator_MaxSpanChunk(t *testing.T) { ctx := context.Background() slasherDB := dbtest.SetupSlasherDB(t) srv, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(t, err) // We initialize an empty chunks slice. currentEpoch := primitives.Epoch(3) validatorChunkIndex := uint64(0) validatorIdx := primitives.ValidatorIndex(0) chunksByChunkIdx := map[uint64]Chunker{} // We apply attestation with (source 0, target 3) for our validator. source := primitives.Epoch(0) target := primitives.Epoch(3) att := createAttestationWrapperEmptySig(t, source, target, nil, nil) slashing, err := srv.applyAttestationForValidator( ctx, chunksByChunkIdx, att, slashertypes.MaxSpan, validatorChunkIndex, validatorIdx, currentEpoch, ) require.NoError(t, err) require.Equal(t, true, slashing == nil) att.IndexedAttestation.AttestingIndices = []uint64{uint64(validatorIdx)} err = slasherDB.SaveAttestationRecordsForValidators( ctx, []*slashertypes.IndexedAttestationWrapper{att}, ) require.NoError(t, err) // Next, we apply an attestation with (source 1, target 2) and // expect a slashable offense to be returned. source = primitives.Epoch(1) target = primitives.Epoch(2) slashableAtt := createAttestationWrapperEmptySig(t, source, target, nil, nil) slashing, err = srv.applyAttestationForValidator( ctx, chunksByChunkIdx, slashableAtt, slashertypes.MaxSpan, validatorChunkIndex, validatorIdx, currentEpoch, ) require.NoError(t, err) require.NotNil(t, slashing) } func Test_loadChunks_MinSpans(t *testing.T) { testLoadChunks(t, slashertypes.MinSpan) } func Test_loadChunks_MaxSpans(t *testing.T) { testLoadChunks(t, slashertypes.MaxSpan) } func testLoadChunks(t *testing.T, kind slashertypes.ChunkKind) { slasherDB := dbtest.SetupSlasherDB(t) ctx := context.Background() // Check if the chunk at chunk index already exists in-memory. s, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(t, err) defaultParams := s.params // If a chunk at a chunk index does not exist, ensure it // is initialized as an empty chunk. var emptyChunk Chunker if kind == slashertypes.MinSpan { emptyChunk = EmptyMinSpanChunksSlice(defaultParams) } else { emptyChunk = EmptyMaxSpanChunksSlice(defaultParams) } chunkIdx := uint64(2) received, err := s.loadChunksFromDisk(ctx, 0, kind, []uint64{chunkIdx}) require.NoError(t, err) wanted := map[uint64]Chunker{ chunkIdx: emptyChunk, } require.DeepEqual(t, wanted, received) // Save chunks to disk, then load them properly from disk. var existingChunk Chunker if kind == slashertypes.MinSpan { existingChunk = EmptyMinSpanChunksSlice(defaultParams) } else { existingChunk = EmptyMaxSpanChunksSlice(defaultParams) } validatorIdx := primitives.ValidatorIndex(0) epochInChunk := primitives.Epoch(0) targetEpoch := primitives.Epoch(2) err = setChunkDataAtEpoch( defaultParams, existingChunk.Chunk(), validatorIdx, epochInChunk, targetEpoch, ) require.NoError(t, err) require.DeepNotEqual(t, existingChunk, emptyChunk) updatedChunks := map[uint64]Chunker{ 2: existingChunk, 4: existingChunk, 6: existingChunk, } chunkByChunkIndexByValidatorChunkIndex := map[uint64]map[uint64]Chunker{ 0: updatedChunks, } err = s.saveChunksToDisk(ctx, kind, chunkByChunkIndexByValidatorChunkIndex) require.NoError(t, err) // Check if the retrieved chunks match what we just saved to disk. received, err = s.loadChunksFromDisk(ctx, 0, kind, []uint64{2, 4, 6}) require.NoError(t, err) require.DeepEqual(t, updatedChunks, received) } func TestService_processQueuedAttestations(t *testing.T) { hook := logTest.NewGlobal() slasherDB := dbtest.SetupSlasherDB(t) beaconState, err := util.NewBeaconState() require.NoError(t, err) slot, err := slots.EpochStart(1) require.NoError(t, err) require.NoError(t, beaconState.SetSlot(slot)) mockChain := &mock.ChainService{ State: beaconState, Slot: &slot, } s, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, HeadStateFetcher: mockChain, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(t, err) s.attsQueue.extend([]*slashertypes.IndexedAttestationWrapper{ createAttestationWrapperEmptySig(t, 0, 1, []uint64{0, 1} /* indices */, nil /* signingRoot */), }) ctx, cancel := context.WithCancel(context.Background()) tickerChan := make(chan primitives.Slot) s.wg.Add(1) go func() { s.processQueuedAttestations(ctx, tickerChan) }() // Send a value over the ticker. tickerChan <- 1 cancel() s.wg.Wait() assert.LogsContain(t, hook, "Start processing queued attestations") assert.LogsContain(t, hook, "Done processing queued attestations") } func Benchmark_saveChunksToDisk(b *testing.B) { // Define the parameters. const ( chunkKind = slashertypes.MinSpan validatorsChunksCount = 6000 // Corresponds to 1_536_000 validators x 256 validators / chunk chunkIndex uint64 = 13 validatorChunkIndex uint64 = 42 ) params := DefaultParams() // Get a context. ctx := context.Background() chunkByChunkIndexByValidatorChunkIndex := make(map[uint64]map[uint64]Chunker, validatorsChunksCount) // Populate the chunkers. for i := 0; i < validatorsChunksCount; i++ { data := make([]uint16, params.chunkSize) for j := 0; j < int(params.chunkSize); j++ { data[j] = uint16(rand.Intn(1 << 16)) } chunker := map[uint64]Chunker{chunkIndex: &MinSpanChunksSlice{params: params, data: data}} chunkByChunkIndexByValidatorChunkIndex[uint64(i)] = chunker } // Initialize the slasher database. slasherDB := dbtest.SetupSlasherDB(b) // Initialize the slasher service. service, err := New(ctx, &ServiceConfig{Database: slasherDB}) require.NoError(b, err) // Reset the benchmark timer. b.ResetTimer() // Run the benchmark. for i := 0; i < b.N; i++ { b.StartTimer() err = service.saveChunksToDisk(ctx, slashertypes.MinSpan, chunkByChunkIndexByValidatorChunkIndex) b.StopTimer() require.NoError(b, err) } } func BenchmarkCheckSlashableAttestations(b *testing.B) { slasherDB := dbtest.SetupSlasherDB(b) beaconState, err := util.NewBeaconState() require.NoError(b, err) slot := primitives.Slot(0) mockChain := &mock.ChainService{ State: beaconState, Slot: &slot, } s, err := New(context.Background(), &ServiceConfig{ Database: slasherDB, StateNotifier: &mock.MockStateNotifier{}, HeadStateFetcher: mockChain, ClockWaiter: startup.NewClockSynchronizer(), }) require.NoError(b, err) b.Run("1 attestation 1 validator", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 1, 1 /* validator */) }) b.Run("1 attestation 100 validators", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 1, 100 /* validator */) }) b.Run("1 attestation 1000 validators", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 1, 1000 /* validator */) }) b.Run("100 attestations 1 validator", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 100, 1 /* validator */) }) b.Run("100 attestations 100 validators", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 100, 100 /* validator */) }) b.Run("100 attestations 1000 validators", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 100, 1000 /* validator */) }) b.Run("1000 attestations 1 validator", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 1000, 1 /* validator */) }) b.Run("1000 attestations 100 validators", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 1000, 100 /* validator */) }) b.Run("1000 attestations 1000 validators", func(b *testing.B) { b.ResetTimer() runAttestationsBenchmark(b, s, 1000, 1000 /* validator */) }) } func runAttestationsBenchmark(b *testing.B, s *Service, numAtts, numValidators uint64) { indices := make([]uint64, numValidators) for i := uint64(0); i < numValidators; i++ { indices[i] = i } atts := make([]*slashertypes.IndexedAttestationWrapper, numAtts) for i := uint64(0); i < numAtts; i++ { source := primitives.Epoch(i) target := primitives.Epoch(i + 1) var signingRoot [32]byte copy(signingRoot[:], fmt.Sprintf("%d", i)) atts[i] = createAttestationWrapperEmptySig( b, source, target, /* target */ indices, /* indices */ signingRoot[:], /* signingRoot */ ) } for i := 0; i < b.N; i++ { numEpochs := numAtts totalSeconds := numEpochs * uint64(params.BeaconConfig().SlotsPerEpoch) * params.BeaconConfig().SecondsPerSlot genesisTime := time.Now().Add(-time.Second * time.Duration(totalSeconds)) s.genesisTime = genesisTime epoch := slots.EpochsSinceGenesis(genesisTime) _, err := s.checkSlashableAttestations(context.Background(), epoch, atts) require.NoError(b, err) } } func Benchmark_checkSurroundVotes(b *testing.B) { const ( // Approximatively the number of Holesky active validators on 2024-02-16 // This number is both a multiple of 32 (the number of slots per epoch) and 256 (the number of validators per chunk) validatorsCount = 1_638_400 slotsPerEpoch = 32 targetEpoch = 42 sourceEpoch = 43 currentEpoch = 43 ) // Create a context. ctx := context.Background() // Initialize the slasher database. slasherDB := dbtest.SetupSlasherDB(b) // Initialize the slasher service. service, err := New(ctx, &ServiceConfig{Database: slasherDB}) require.NoError(b, err) // Create the attesting validators indexes. // The best case scenario would be to have all validators attesting for a slot with contiguous indexes. // So for 1_638_400 validators with 32 slots per epoch, we would have 48_000 attestation wrappers per slot. // With 256 validators per chunk, we would have only 188 modified chunks. // // In this benchmark, we use the worst case scenario where attestating validators are evenly splitted across all validators chunks. // We also suppose that only one chunk per validator chunk index is modified. // For one given validator index, multiple chunk indexes could be modified. // // With 1_638_400 validators we have 6400 chunks. If exactly 8 validators per chunks attest, we have: // 6_400 chunks * 8 = 51_200 validators attesting per slot. And 51_200 validators * 32 slots = 1_638_400 // attesting validators per epoch. // ==> Attesting validator indexes will be computed as follows: // validator chunk index 0 validator chunk index 1 validator chunk index 6_399 // [0, 32, 64, 96, 128, 160, 192, 224 | 256, 288, 320, 352, 384, 416, 448, 480 | ... | ..., 1_638_606, 1_638_368, 1_638_400] // attestingValidatorsCount := validatorsCount / slotsPerEpoch validatorIndexes := make([]uint64, attestingValidatorsCount) for i := 0; i < attestingValidatorsCount; i++ { validatorIndexes[i] = 32 * uint64(i) } // Create the attestation wrapper. // This benchmark assume that all validators produced the exact same head, source and target votes. attWrapper := createAttestationWrapperEmptySig(b, sourceEpoch, targetEpoch, validatorIndexes, nil) attWrappers := []*slashertypes.IndexedAttestationWrapper{attWrapper} // Run the benchmark. b.ResetTimer() for i := 0; i < b.N; i++ { b.StartTimer() _, err = service.checkSurroundVotes(ctx, attWrappers, currentEpoch) b.StopTimer() require.NoError(b, err) } } // createAttestationWrapperEmptySig creates an attestation wrapper with source and target, // for validators with indices, and a beacon block root (corresponding to the head vote). // For source and target epochs, the corresponding root is null. // The signature of the returned wrapped attestation is empty. func createAttestationWrapperEmptySig( t testing.TB, source, target primitives.Epoch, indices []uint64, beaconBlockRoot []byte, ) *slashertypes.IndexedAttestationWrapper { data := ðpb.AttestationData{ BeaconBlockRoot: bytesutil.PadTo(beaconBlockRoot, 32), Source: ðpb.Checkpoint{ Epoch: source, Root: params.BeaconConfig().ZeroHash[:], }, Target: ðpb.Checkpoint{ Epoch: target, Root: params.BeaconConfig().ZeroHash[:], }, } dataRoot, err := data.HashTreeRoot() require.NoError(t, err) return &slashertypes.IndexedAttestationWrapper{ IndexedAttestation: ðpb.IndexedAttestation{ AttestingIndices: indices, Data: data, Signature: params.BeaconConfig().EmptySignature[:], }, DataRoot: dataRoot, } } // createAttestationWrapper creates an attestation wrapper with source and target, // for validators with indices, and a beacon block root (corresponding to the head vote). // For source and target epochs, the corresponding root is null. // if validatorIndice = indices[i], then the corresponding private key is privateKeys[validatorIndice]. func createAttestationWrapper( t testing.TB, domain []byte, privateKeys []common.SecretKey, source, target primitives.Epoch, indices []uint64, beaconBlockRoot []byte, ) *slashertypes.IndexedAttestationWrapper { // Create attestation data. attestationData := ðpb.AttestationData{ BeaconBlockRoot: bytesutil.PadTo(beaconBlockRoot, 32), Source: ðpb.Checkpoint{ Epoch: source, Root: params.BeaconConfig().ZeroHash[:], }, Target: ðpb.Checkpoint{ Epoch: target, Root: params.BeaconConfig().ZeroHash[:], }, } // Compute attestation data root. attestationDataRoot, err := attestationData.HashTreeRoot() require.NoError(t, err) // Create valid signatures for all input attestations in the test. signingRoot, err := signing.ComputeSigningRoot(attestationData, domain) require.NoError(t, err) // For each attesting indice in the indexed attestation, create a signature. signatures := make([]bls.Signature, 0, len(indices)) for _, indice := range indices { // Check that the indice is within the range of private keys. require.Equal(t, true, indice < uint64(len(privateKeys))) // Retrieve the corresponding private key. privateKey := privateKeys[indice] // Sign the signing root. signature := privateKey.Sign(signingRoot[:]) // Append the signature to the signatures list. signatures = append(signatures, signature) } // Compute the aggregated signature. signature := bls.AggregateSignatures(signatures).Marshal() // Create the attestation wrapper. return &slashertypes.IndexedAttestationWrapper{ IndexedAttestation: ðpb.IndexedAttestation{ AttestingIndices: indices, Data: attestationData, Signature: signature, }, DataRoot: attestationDataRoot, } }