Slasher: Fix surrounded votes false negative (#13612)

* `LastEpochWrittenForValidators`: Use golang if.

* `SaveLastEpochsWrittenForValidators`: Refactor.

* `SaveLastEpochsWrittenForValidators`: Fix when `epochByValIndex` > `batchSize`.

Before this commit, `TestStore_LastEpochWrittenForValidators` works if `validatorsCount <= 10000`
and stops working if `validatorsCount > 10000`.

* Slasher: Detect surrounded votes in multiple batches.

Fixes https://github.com/prysmaticlabs/prysm/issues/13591.
This commit is contained in:
Manu NALEPA
2024-02-13 16:50:33 +01:00
committed by GitHub
parent f2f10e7381
commit 3d13c69ef3
4 changed files with 73 additions and 59 deletions

View File

@@ -46,17 +46,18 @@ func (s *Store) LastEpochWrittenForValidators(
for i, encodedIndex := range encodedIndexes { for i, encodedIndex := range encodedIndexes {
var epoch primitives.Epoch var epoch primitives.Epoch
epochBytes := bkt.Get(encodedIndex) if epochBytes := bkt.Get(encodedIndex); epochBytes != nil {
if epochBytes != nil {
if err := epoch.UnmarshalSSZ(epochBytes); err != nil { if err := epoch.UnmarshalSSZ(epochBytes); err != nil {
return err return err
} }
} }
attestedEpochs = append(attestedEpochs, &slashertypes.AttestedEpochForValidator{ attestedEpoch := &slashertypes.AttestedEpochForValidator{
ValidatorIndex: validatorIndexes[i], ValidatorIndex: validatorIndexes[i],
Epoch: epoch, Epoch: epoch,
}) }
attestedEpochs = append(attestedEpochs, attestedEpoch)
} }
return nil return nil
@@ -83,18 +84,20 @@ func (s *Store) SaveLastEpochsWrittenForValidators(
return ctx.Err() return ctx.Err()
} }
encodedIndex := encodeValidatorIndex(valIndex)
encodedEpoch, err := epoch.MarshalSSZ() encodedEpoch, err := epoch.MarshalSSZ()
if err != nil { if err != nil {
return err return err
} }
encodedIndexes = append(encodedIndexes, encodeValidatorIndex(valIndex)) encodedIndexes = append(encodedIndexes, encodedIndex)
encodedEpochs = append(encodedEpochs, encodedEpoch) encodedEpochs = append(encodedEpochs, encodedEpoch)
} }
// The list of validators might be too massive for boltdb to handle in a single transaction, // The list of validators might be too massive for boltdb to handle in a single transaction,
// so instead we split it into batches and write each batch. // so instead we split it into batches and write each batch.
for i := 0; i < len(encodedIndexes); i += batchSize { for start := 0; start < len(encodedIndexes); start += batchSize {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
@@ -105,17 +108,14 @@ func (s *Store) SaveLastEpochsWrittenForValidators(
} }
bkt := tx.Bucket(attestedEpochsByValidator) bkt := tx.Bucket(attestedEpochsByValidator)
end := min(start+batchSize, len(encodedIndexes))
minimum := i + batchSize for j, encodedIndex := range encodedIndexes[start:end] {
if minimum > len(encodedIndexes) {
minimum = len(encodedIndexes)
}
for j, encodedIndex := range encodedIndexes[i:minimum] {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
if err := bkt.Put(encodedIndex, encodedEpochs[j]); err != nil {
if err := bkt.Put(encodedIndex, encodedEpochs[j+start]); err != nil {
return err return err
} }
} }

View File

@@ -46,21 +46,29 @@ func TestStore_AttestationRecordForValidator_SaveRetrieve(t *testing.T) {
func TestStore_LastEpochWrittenForValidators(t *testing.T) { func TestStore_LastEpochWrittenForValidators(t *testing.T) {
ctx := context.Background() ctx := context.Background()
beaconDB := setupDB(t) beaconDB := setupDB(t)
indices := []primitives.ValidatorIndex{1, 2, 3}
epoch := primitives.Epoch(5) validatorsCount := 11000
indices := make([]primitives.ValidatorIndex, validatorsCount)
epochs := make([]primitives.Epoch, validatorsCount)
for i := 0; i < validatorsCount; i++ {
indices[i] = primitives.ValidatorIndex(i)
epochs[i] = primitives.Epoch(i)
}
attestedEpochs, err := beaconDB.LastEpochWrittenForValidators(ctx, indices) attestedEpochs, err := beaconDB.LastEpochWrittenForValidators(ctx, indices)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, true, len(attestedEpochs) == len(indices)) require.Equal(t, true, len(attestedEpochs) == len(indices))
for _, item := range attestedEpochs { for _, item := range attestedEpochs {
require.Equal(t, primitives.Epoch(0), item.Epoch) require.Equal(t, primitives.Epoch(0), item.Epoch)
} }
epochsByValidator := map[primitives.ValidatorIndex]primitives.Epoch{ epochsByValidator := make(map[primitives.ValidatorIndex]primitives.Epoch, validatorsCount)
1: epoch, for i := 0; i < validatorsCount; i++ {
2: epoch, epochsByValidator[indices[i]] = epochs[i]
3: epoch,
} }
err = beaconDB.SaveLastEpochsWrittenForValidators(ctx, epochsByValidator) err = beaconDB.SaveLastEpochsWrittenForValidators(ctx, epochsByValidator)
require.NoError(t, err) require.NoError(t, err)
@@ -70,9 +78,10 @@ func TestStore_LastEpochWrittenForValidators(t *testing.T) {
for i, retrievedEpoch := range retrievedEpochs { for i, retrievedEpoch := range retrievedEpochs {
want := &slashertypes.AttestedEpochForValidator{ want := &slashertypes.AttestedEpochForValidator{
Epoch: epoch, Epoch: epochs[i],
ValidatorIndex: indices[i], ValidatorIndex: indices[i],
} }
require.DeepEqual(t, want, retrievedEpoch) require.DeepEqual(t, want, retrievedEpoch)
} }
} }

View File

@@ -54,9 +54,7 @@ func (s *Service) checkSlashableAttestations(
surroundStart := time.Now() surroundStart := time.Now()
for validatorChunkIndex, attestations := range groupedByValidatorChunkIndexAtts { for validatorChunkIndex, attestations := range groupedByValidatorChunkIndexAtts {
// The fact that we use always slashertypes.MinSpan is probably the root cause of surroundSlashings, err := s.checkSurrounds(ctx, attestations, currentEpoch, validatorChunkIndex)
// https://github.com/prysmaticlabs/prysm/issues/13591
surroundSlashings, err := s.checkSurrounds(ctx, attestations, slashertypes.MinSpan, currentEpoch, validatorChunkIndex)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -107,12 +105,12 @@ func (s *Service) checkSlashableAttestations(
func (s *Service) checkSurrounds( func (s *Service) checkSurrounds(
ctx context.Context, ctx context.Context,
attestations []*slashertypes.IndexedAttestationWrapper, attestations []*slashertypes.IndexedAttestationWrapper,
chunkKind slashertypes.ChunkKind,
currentEpoch primitives.Epoch, currentEpoch primitives.Epoch,
validatorChunkIndex uint64, validatorChunkIndex uint64,
) (map[[fieldparams.RootLength]byte]*ethpb.AttesterSlashing, error) { ) (map[[fieldparams.RootLength]byte]*ethpb.AttesterSlashing, error) {
// Map of updated chunks by chunk index, which will be saved at the end. // Map of updated chunks by chunk index, which will be saved at the end.
updatedChunks := make(map[uint64]Chunker) updatedMinChunks, updatedMaxChunks := map[uint64]Chunker{}, map[uint64]Chunker{}
groupedByChunkIndexAtts := s.groupByChunkIndex(attestations) groupedByChunkIndexAtts := s.groupByChunkIndex(attestations)
validatorIndexes := s.params.validatorIndexesInChunk(validatorChunkIndex) validatorIndexes := s.params.validatorIndexesInChunk(validatorChunkIndex)
@@ -120,14 +118,19 @@ func (s *Service) checkSurrounds(
// Update epoch for validators. // Update epoch for validators.
for _, validatorIndex := range validatorIndexes { for _, validatorIndex := range validatorIndexes {
// This function modifies `updatedChunks` in place. // This function modifies `updatedMinChunks` in place.
if err := s.epochUpdateForValidator(ctx, updatedChunks, validatorChunkIndex, chunkKind, currentEpoch, validatorIndex); err != nil { if err := s.epochUpdateForValidator(ctx, updatedMinChunks, validatorChunkIndex, slashertypes.MinSpan, currentEpoch, validatorIndex); err != nil {
return nil, errors.Wrapf(err, "could not update validator index chunks %d", validatorIndex) return nil, errors.Wrapf(err, "could not update validator index for min chunks %d", validatorIndex)
}
// This function modifies `updatedMaxChunks` in place.
if err := s.epochUpdateForValidator(ctx, updatedMaxChunks, validatorChunkIndex, slashertypes.MaxSpan, currentEpoch, validatorIndex); err != nil {
return nil, errors.Wrapf(err, "could not update validator index for max chunks %d", validatorIndex)
} }
} }
// Check for surrounding votes. // Check for surrounding votes.
surroundingSlashings, err := s.updateSpans(ctx, updatedChunks, groupedByChunkIndexAtts, slashertypes.MinSpan, validatorChunkIndex, currentEpoch) surroundingSlashings, err := s.updateSpans(ctx, updatedMinChunks, groupedByChunkIndexAtts, slashertypes.MinSpan, validatorChunkIndex, currentEpoch)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not update min attestation spans for validator chunk index %d", validatorChunkIndex) return nil, errors.Wrapf(err, "could not update min attestation spans for validator chunk index %d", validatorChunkIndex)
} }
@@ -137,7 +140,7 @@ func (s *Service) checkSurrounds(
} }
// Check for surrounded votes. // Check for surrounded votes.
surroundedSlashings, err := s.updateSpans(ctx, updatedChunks, groupedByChunkIndexAtts, slashertypes.MaxSpan, validatorChunkIndex, currentEpoch) surroundedSlashings, err := s.updateSpans(ctx, updatedMaxChunks, groupedByChunkIndexAtts, slashertypes.MaxSpan, validatorChunkIndex, currentEpoch)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "could not update max attestation spans for validator chunk index %d", validatorChunkIndex) return nil, errors.Wrapf(err, "could not update max attestation spans for validator chunk index %d", validatorChunkIndex)
} }
@@ -146,8 +149,13 @@ func (s *Service) checkSurrounds(
slashings[root] = slashing slashings[root] = slashing
} }
if err := s.saveUpdatedChunks(ctx, updatedChunks, chunkKind, validatorChunkIndex); err != nil { // Save updated chunks into the database.
return nil, err if err := s.saveUpdatedChunks(ctx, updatedMinChunks, slashertypes.MinSpan, validatorChunkIndex); err != nil {
return nil, errors.Wrap(err, "could not save chunks for min spans")
}
if err := s.saveUpdatedChunks(ctx, updatedMaxChunks, slashertypes.MaxSpan, validatorChunkIndex); err != nil {
return nil, errors.Wrap(err, "could not save chunks for max spans")
} }
return slashings, nil return slashings, nil

View File

@@ -171,7 +171,6 @@ func Test_processAttestations(t *testing.T) {
name: "Detects surrounding vote (source 50, target 51), (source 0, target 1000) - single step", name: "Detects surrounding vote (source 50, target 51), (source 0, target 1000) - single step",
steps: []*step{ steps: []*step{
{ {
currentEpoch: 1000, currentEpoch: 1000,
attestationsInfo: []*attestationInfo{ attestationsInfo: []*attestationInfo{
{source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil}, {source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil},
@@ -190,7 +189,6 @@ func Test_processAttestations(t *testing.T) {
name: "Detects surrounding vote (source 50, target 51), (source 0, target 1000) - two steps", name: "Detects surrounding vote (source 50, target 51), (source 0, target 1000) - two steps",
steps: []*step{ steps: []*step{
{ {
currentEpoch: 1000, currentEpoch: 1000,
attestationsInfo: []*attestationInfo{ attestationsInfo: []*attestationInfo{
{source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil}, {source: 50, target: 51, indices: []uint64{0}, beaconBlockRoot: nil},
@@ -229,31 +227,30 @@ func Test_processAttestations(t *testing.T) {
}, },
}, },
}, },
// Uncomment when https://github.com/prysmaticlabs/prysm/issues/13591 is fixed {
// { name: "Detects surrounded vote (source 0, target 3), (source 1, target 2) - two steps",
// name: "Detects surrounded vote (source 0, target 3), (source 1, target 2) - two steps", steps: []*step{
// steps: []*step{ {
// { currentEpoch: 4,
// currentEpoch: 4, attestationsInfo: []*attestationInfo{
// attestationsInfo: []*attestationInfo{ {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil},
// {source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, },
// }, expectedSlashingsInfo: nil,
// expectedSlashingsInfo: nil, },
// }, {
// { currentEpoch: 4,
// currentEpoch: 4, attestationsInfo: []*attestationInfo{
// attestationsInfo: []*attestationInfo{ {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil},
// {source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, },
// }, expectedSlashingsInfo: []*slashingInfo{
// expectedSlashingsInfo: []*slashingInfo{ {
// { attestationInfo_1: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil},
// attestationInfo_1: &attestationInfo{source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil}, attestationInfo_2: &attestationInfo{source: 0, target: 3, indices: []uint64{0, 1}, beaconBlockRoot: nil},
// attestationInfo_2: &attestationInfo{source: 1, target: 2, indices: []uint64{0, 1}, beaconBlockRoot: nil}, },
// }, },
// }, },
// }, },
// }, },
// },
{ {
name: "Detects double vote, (source 1, target 2), (source 0, target 2) - single step", name: "Detects double vote, (source 1, target 2), (source 0, target 2) - single step",
steps: []*step{ steps: []*step{