Files
prysm/beacon-chain/slasher/helpers.go
Bastin 92bd211e4d upgrade v6 to v7 (#15989)
* upgrade v6 to v7

* changelog

* update-go-ssz
2025-11-06 16:16:23 +00:00

274 lines
10 KiB
Go

package slasher
import (
"bytes"
"context"
"fmt"
"strconv"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/slasherkv"
slashertypes "github.com/OffchainLabs/prysm/v7/beacon-chain/slasher/types"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/container/slice"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/sirupsen/logrus"
)
// Group a list of attestations into batches by validator chunk index.
// This way, we can detect on the batch of attestations for each validator chunk index
// concurrently, and also allowing us to effectively use a single 2D chunk
// for slashing detection through this logical grouping.
func (s *Service) groupByValidatorChunkIndex(
attestations []*slashertypes.IndexedAttestationWrapper,
) map[uint64][]*slashertypes.IndexedAttestationWrapper {
groupedAttestations := make(map[uint64][]*slashertypes.IndexedAttestationWrapper)
for _, attestation := range attestations {
validatorChunkIndexes := make(map[uint64]bool)
for _, validatorIndex := range attestation.IndexedAttestation.GetAttestingIndices() {
validatorChunkIndex := s.params.validatorChunkIndex(primitives.ValidatorIndex(validatorIndex))
validatorChunkIndexes[validatorChunkIndex] = true
}
for validatorChunkIndex := range validatorChunkIndexes {
groupedAttestations[validatorChunkIndex] = append(
groupedAttestations[validatorChunkIndex],
attestation,
)
}
}
return groupedAttestations
}
// Group attestations by the chunk index their source epoch corresponds to.
func (s *Service) groupByChunkIndex(
attestations []*slashertypes.IndexedAttestationWrapper,
) map[uint64][]*slashertypes.IndexedAttestationWrapper {
attestationsByChunkIndex := make(map[uint64][]*slashertypes.IndexedAttestationWrapper)
for _, attestation := range attestations {
chunkIndex := s.params.chunkIndex(attestation.IndexedAttestation.GetData().Source.Epoch)
attestationsByChunkIndex[chunkIndex] = append(attestationsByChunkIndex[chunkIndex], attestation)
}
return attestationsByChunkIndex
}
// This function returns a list of valid attestations, a list of attestations that are
// valid in the future, and the number of attestations dropped.
func (s *Service) filterAttestations(
attWrappers []*slashertypes.IndexedAttestationWrapper, currentEpoch primitives.Epoch,
) (valid, validInFuture []*slashertypes.IndexedAttestationWrapper, numDropped int) {
valid = make([]*slashertypes.IndexedAttestationWrapper, 0, len(attWrappers))
validInFuture = make([]*slashertypes.IndexedAttestationWrapper, 0, len(attWrappers))
for _, attWrapper := range attWrappers {
if attWrapper == nil || !validateAttestationIntegrity(attWrapper.IndexedAttestation) {
numDropped++
continue
}
// If an attestation's source is epoch is older than the max history length
// we keep track of for slashing detection, we drop it.
if attWrapper.IndexedAttestation.GetData().Source.Epoch+s.params.historyLength <= currentEpoch {
numDropped++
continue
}
// If an attestation's target epoch is in the future, we defer processing for later.
if attWrapper.IndexedAttestation.GetData().Target.Epoch > currentEpoch {
validInFuture = append(validInFuture, attWrapper)
continue
}
// The attestation is valid.
valid = append(valid, attWrapper)
}
return
}
// Validates the attestation data integrity, ensuring we have no nil values for
// source and target epochs, and that the source epoch of the attestation must
// be less than the target epoch, which is a precondition for performing slashing
// detection (except for the genesis epoch).
func validateAttestationIntegrity(att ethpb.IndexedAtt) bool {
// If an attestation is malformed, we drop it.
if att == nil || att.IsNil() || att.GetData().Source == nil || att.GetData().Target == nil {
return false
}
sourceEpoch := att.GetData().Source.Epoch
targetEpoch := att.GetData().Target.Epoch
// The genesis epoch is a special case, since all attestations formed in it
// will have source and target 0, and they should be considered valid.
if sourceEpoch == 0 && targetEpoch == 0 {
return true
}
// All valid attestations must have source epoch < target epoch.
return sourceEpoch < targetEpoch
}
// Validates the signed beacon block header integrity, ensuring we have no nil values.
func validateBlockHeaderIntegrity(header *ethpb.SignedBeaconBlockHeader) bool {
// If a signed block header is malformed, we drop it.
if header == nil ||
header.Header == nil ||
len(header.Signature) != fieldparams.BLSSignatureLength ||
bytes.Equal(header.Signature, make([]byte, fieldparams.BLSSignatureLength)) {
return false
}
return true
}
func logAttesterSlashing(slashing ethpb.AttSlashing) {
indices := slice.IntersectionUint64(slashing.FirstAttestation().GetAttestingIndices(), slashing.SecondAttestation().GetAttestingIndices())
log.WithFields(logrus.Fields{
"validatorIndex": indices,
"prevSourceEpoch": slashing.FirstAttestation().GetData().Source.Epoch,
"prevTargetEpoch": slashing.FirstAttestation().GetData().Target.Epoch,
"sourceEpoch": slashing.SecondAttestation().GetData().Source.Epoch,
"targetEpoch": slashing.SecondAttestation().GetData().Target.Epoch,
}).Info("Attester slashing detected")
}
func logProposerSlashing(slashing *ethpb.ProposerSlashing) {
log.WithFields(logrus.Fields{
"validatorIndex": slashing.Header_1.Header.ProposerIndex,
"slot": slashing.Header_1.Header.Slot,
}).Info("Proposer slashing detected")
}
// Turns a uint64 value to a string representation.
func uintToString(val uint64) string {
return strconv.FormatUint(val, 10)
}
// If an existing signing root does not match an incoming proposal signing root,
// we then have a double block proposer slashing event.
func isDoubleProposal(incomingSigningRoot, existingSigningRoot [32]byte) bool {
// If the existing signing root is the zero hash, we do not consider
// this a double proposal.
if existingSigningRoot == params.BeaconConfig().ZeroHash {
return false
}
return incomingSigningRoot != existingSigningRoot
}
type GetChunkFromDatabaseFilters struct {
ChunkKind slashertypes.ChunkKind
ValidatorIndex primitives.ValidatorIndex
SourceEpoch primitives.Epoch
IsDisplayAllValidatorsInChunk bool
IsDisplayAllEpochsInChunk bool
}
// GetChunkFromDatabase Utility function aiming at retrieving a chunk from the
// database.
func GetChunkFromDatabase(
ctx context.Context,
dbPath string,
filters GetChunkFromDatabaseFilters,
params *Parameters,
) (lastEpochForValidatorIndex primitives.Epoch, chunkIndex, validatorChunkIndex uint64, chunk Chunker, err error) {
// init store
d, err := slasherkv.NewKVStore(ctx, dbPath)
if err != nil {
return lastEpochForValidatorIndex, chunkIndex, validatorChunkIndex, chunk, fmt.Errorf("could not open database at path %s: %w", dbPath, err)
}
defer closeDB(d)
// init service
s := Service{
params: params,
serviceCfg: &ServiceConfig{
Database: d,
},
}
// variables
validatorIndex := filters.ValidatorIndex
sourceEpoch := filters.SourceEpoch
chunkKind := filters.ChunkKind
validatorChunkIndex = s.params.validatorChunkIndex(validatorIndex)
chunkIndex = s.params.chunkIndex(sourceEpoch)
// before getting the chunk, we need to verify if the requested epoch is in database
lastEpochForValidator, err := s.serviceCfg.Database.LastEpochWrittenForValidators(ctx, []primitives.ValidatorIndex{validatorIndex})
if err != nil {
return lastEpochForValidatorIndex,
chunkIndex,
validatorChunkIndex,
chunk,
fmt.Errorf("could not get last epoch written for validator %d: %w", validatorIndex, err)
}
if len(lastEpochForValidator) == 0 {
return lastEpochForValidatorIndex,
chunkIndex,
validatorChunkIndex,
chunk,
fmt.Errorf("could not get information at epoch %d for validator %d: there's no record found in slasher database",
sourceEpoch, validatorIndex,
)
}
lastEpochForValidatorIndex = lastEpochForValidator[0].Epoch
// if the epoch requested is within the range, we can proceed to get the chunk, otherwise return error
atBestSmallestEpoch := lastEpochForValidatorIndex.Sub(uint64(params.historyLength))
if sourceEpoch < atBestSmallestEpoch || sourceEpoch > lastEpochForValidatorIndex {
return lastEpochForValidatorIndex,
chunkIndex,
validatorChunkIndex,
chunk,
fmt.Errorf("requested epoch %d is outside the slasher history length %d, data can be provided within the epoch range [%d:%d] for validator %d",
sourceEpoch, params.historyLength, atBestSmallestEpoch, lastEpochForValidatorIndex, validatorIndex,
)
}
// fetch chunk from DB
chunk, err = s.getChunkFromDatabase(ctx, chunkKind, validatorChunkIndex, chunkIndex)
if err != nil {
return lastEpochForValidatorIndex,
chunkIndex,
validatorChunkIndex,
chunk,
fmt.Errorf("could not get chunk at index %d: %w", chunkIndex, err)
}
return lastEpochForValidatorIndex, chunkIndex, validatorChunkIndex, chunk, nil
}
func closeDB(d *slasherkv.Store) {
if err := d.Close(); err != nil {
log.WithError(err).Error("Could not close database")
}
}
// unifyAttWrapperVersion ensures that the two wrappers wrap indexed attestations of the same version.
// If versions differ, the wrapped attestation with the lower version will be converted to the higher version.
func unifyAttWrapperVersion(w1 *slashertypes.IndexedAttestationWrapper, w2 *slashertypes.IndexedAttestationWrapper) {
if w1.IndexedAttestation.Version() == w2.IndexedAttestation.Version() {
return
}
if w1.IndexedAttestation.Version() != version.Electra {
w1.IndexedAttestation = &ethpb.IndexedAttestationElectra{
AttestingIndices: w1.IndexedAttestation.GetAttestingIndices(),
Data: w1.IndexedAttestation.GetData(),
Signature: w1.IndexedAttestation.GetSignature(),
}
return
}
w2.IndexedAttestation = &ethpb.IndexedAttestationElectra{
AttestingIndices: w2.IndexedAttestation.GetAttestingIndices(),
Data: w2.IndexedAttestation.GetData(),
Signature: w2.IndexedAttestation.GetSignature(),
}
}