Files
prysm/beacon-chain/slasher/detect_attestations_test.go
2024-04-01 11:12:20 +00:00

1660 lines
56 KiB
Go

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 := &ethpb.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 := &ethpb.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)
// Initialize 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 (
// Approximately 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 attesting validators are evenly split 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 := &ethpb.AttestationData{
BeaconBlockRoot: bytesutil.PadTo(beaconBlockRoot, 32),
Source: &ethpb.Checkpoint{
Epoch: source,
Root: params.BeaconConfig().ZeroHash[:],
},
Target: &ethpb.Checkpoint{
Epoch: target,
Root: params.BeaconConfig().ZeroHash[:],
},
}
dataRoot, err := data.HashTreeRoot()
require.NoError(t, err)
return &slashertypes.IndexedAttestationWrapper{
IndexedAttestation: &ethpb.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 := &ethpb.AttestationData{
BeaconBlockRoot: bytesutil.PadTo(beaconBlockRoot, 32),
Source: &ethpb.Checkpoint{
Epoch: source,
Root: params.BeaconConfig().ZeroHash[:],
},
Target: &ethpb.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: &ethpb.IndexedAttestation{
AttestingIndices: indices,
Data: attestationData,
Signature: signature,
},
DataRoot: attestationDataRoot,
}
}