mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2025-01-03 16:37:39 +00:00
31b1e6a7a8
* Add double vote detection to spanner * Add documentation * Update slasher/detection/attestations/spanner.go * Gazelle * Fix filter output
253 lines
8.4 KiB
Go
253 lines
8.4 KiB
Go
package attestations
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
|
|
"github.com/prysmaticlabs/go-ssz"
|
|
"github.com/prysmaticlabs/prysm/shared/params"
|
|
"github.com/prysmaticlabs/prysm/slasher/detection/attestations/iface"
|
|
"github.com/prysmaticlabs/prysm/slasher/detection/attestations/types"
|
|
"github.com/prysmaticlabs/prysm/slasher/detection/filter"
|
|
"go.opencensus.io/trace"
|
|
)
|
|
|
|
var _ = iface.SpanDetector(&SpanDetector{})
|
|
|
|
// SpanDetector defines a struct which can detect slashable
|
|
// attestation offenses by tracking validator min-max
|
|
// spans from validators and attestation data roots.
|
|
type SpanDetector struct {
|
|
// Slice of epochs for valindex => min-max span + double vote filter
|
|
spans []map[uint64][3]uint16
|
|
lock sync.RWMutex
|
|
}
|
|
|
|
// NewSpanDetector creates a new instance of a struct tracking
|
|
// several epochs of min-max spans for each validator in
|
|
// the beacon state.
|
|
func NewSpanDetector() *SpanDetector {
|
|
return &SpanDetector{
|
|
spans: make([]map[uint64][3]uint16, 256),
|
|
}
|
|
}
|
|
|
|
// DetectSlashingForValidator uses a validator index and its corresponding
|
|
// min-max spans during an epoch to detect an epoch in which the validator
|
|
// committed a slashable attestation.
|
|
func (s *SpanDetector) DetectSlashingForValidator(
|
|
ctx context.Context,
|
|
validatorIdx uint64,
|
|
attData *ethpb.AttestationData,
|
|
) (*types.DetectionResult, error) {
|
|
ctx, span := trace.StartSpan(ctx, "detection.DetectSlashingForValidator")
|
|
defer span.End()
|
|
sourceEpoch := attData.Source.Epoch
|
|
targetEpoch := attData.Target.Epoch
|
|
if (targetEpoch - sourceEpoch) > params.BeaconConfig().WeakSubjectivityPeriod {
|
|
return nil, fmt.Errorf(
|
|
"attestation span was greater than weak subjectivity period %d, received: %d",
|
|
params.BeaconConfig().WeakSubjectivityPeriod,
|
|
targetEpoch-sourceEpoch,
|
|
)
|
|
}
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
distance := uint16(targetEpoch - sourceEpoch)
|
|
numSpans := uint64(len(s.spans))
|
|
|
|
if sp := s.spans[sourceEpoch%numSpans]; sp != nil {
|
|
minSpan := sp[validatorIdx][0]
|
|
if minSpan > 0 && minSpan < distance {
|
|
return &types.DetectionResult{
|
|
Kind: types.SurroundVote,
|
|
SlashableEpoch: sourceEpoch + uint64(minSpan),
|
|
}, nil
|
|
}
|
|
|
|
maxSpan := sp[validatorIdx][1]
|
|
if maxSpan > distance {
|
|
return &types.DetectionResult{
|
|
Kind: types.SurroundVote,
|
|
SlashableEpoch: sourceEpoch + uint64(maxSpan),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
if sp := s.spans[targetEpoch%numSpans]; sp != nil {
|
|
filterNum := sp[validatorIdx][2]
|
|
if filterNum == 0 {
|
|
return nil, nil
|
|
}
|
|
filterBytes := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(filterBytes, filterNum)
|
|
attFilter := filter.BloomFilter(filterBytes)
|
|
|
|
attDataRoot, err := ssz.HashTreeRoot(attData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
found, err := attFilter.Contains(attDataRoot[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return &types.DetectionResult{
|
|
Kind: types.DoubleVote,
|
|
SlashableEpoch: targetEpoch,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// SpanForEpochByValidator returns the specific min-max span for a
|
|
// validator index in a given epoch.
|
|
func (s *SpanDetector) SpanForEpochByValidator(ctx context.Context, valIdx uint64, epoch uint64) ([3]uint16, error) {
|
|
ctx, span := trace.StartSpan(ctx, "detection.SpanForEpochByValidator")
|
|
defer span.End()
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
numSpans := uint64(len(s.spans))
|
|
if span := s.spans[epoch%numSpans]; span != nil {
|
|
if minMaxSpan, ok := span[valIdx]; ok {
|
|
return minMaxSpan, nil
|
|
}
|
|
return [3]uint16{}, fmt.Errorf("validator index %d not found in span map", valIdx)
|
|
}
|
|
return [3]uint16{}, fmt.Errorf("no data found for epoch %d", epoch)
|
|
}
|
|
|
|
// ValidatorSpansByEpoch returns a list of all validator spans in a given epoch.
|
|
func (s *SpanDetector) ValidatorSpansByEpoch(ctx context.Context, epoch uint64) map[uint64][3]uint16 {
|
|
ctx, span := trace.StartSpan(ctx, "detection.ValidatorSpansByEpoch")
|
|
defer span.End()
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
numSpans := uint64(len(s.spans))
|
|
return s.spans[epoch%numSpans]
|
|
}
|
|
|
|
// DeleteValidatorSpansByEpoch deletes a min-max span for a validator
|
|
// index from a min-max span in a given epoch.
|
|
func (s *SpanDetector) DeleteValidatorSpansByEpoch(ctx context.Context, validatorIdx uint64, epoch uint64) error {
|
|
ctx, span := trace.StartSpan(ctx, "detection.DeleteValidatorSpansByEpoch")
|
|
defer span.End()
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
numSpans := uint64(len(s.spans))
|
|
if val := s.spans[epoch%numSpans]; val != nil {
|
|
delete(val, validatorIdx)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("no span map found at epoch %d", epoch)
|
|
}
|
|
|
|
// UpdateSpans given an indexed attestation for all of its attesting indices.
|
|
func (s *SpanDetector) UpdateSpans(ctx context.Context, att *ethpb.IndexedAttestation) error {
|
|
ctx, span := trace.StartSpan(ctx, "detection.UpdateSpans")
|
|
defer span.End()
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
source := att.Data.Source.Epoch
|
|
target := att.Data.Target.Epoch
|
|
// Update spansForEpoch[valIdx] using the source/target data for
|
|
// each validator in attesting indices.
|
|
for i := 0; i < len(att.AttestingIndices); i++ {
|
|
valIdx := att.AttestingIndices[i]
|
|
// Mark the double vote filter.
|
|
if err := s.markAttFilter(att.Data, valIdx); err != nil {
|
|
return errors.Wrap(err, "failed to update attestation filter")
|
|
}
|
|
// Update min and max spans.
|
|
s.updateMinSpan(source, target, valIdx)
|
|
s.updateMaxSpan(source, target, valIdx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// markAttFilter sets the third uint16 in the target epochs span to a bloom filter
|
|
// with the attestation data root as the key in set. After creating the []byte for the bloom filter,
|
|
// it encoded into a uint16 to keep the data structure for the spanner simple and clean.
|
|
// A bloom filter is used to prevent collision when using such a small data size.
|
|
func (s *SpanDetector) markAttFilter(attData *ethpb.AttestationData, valIdx uint64) error {
|
|
numSpans := uint64(len(s.spans))
|
|
target := attData.Target.Epoch
|
|
|
|
// Check if there is an existing bloom filter, if so, don't modify it.
|
|
if sp := s.spans[target%numSpans]; sp == nil {
|
|
s.spans[target%numSpans] = make(map[uint64][3]uint16)
|
|
}
|
|
filterNum := s.spans[target%numSpans][valIdx][2]
|
|
if filterNum != 0 {
|
|
return nil
|
|
}
|
|
|
|
// Generate the attestation data root and use it as the only key in the bloom filter.
|
|
attDataRoot, err := ssz.HashTreeRoot(attData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
attFilter, err := filter.NewBloomFilter(attDataRoot[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filterNum = binary.LittleEndian.Uint16(attFilter)
|
|
|
|
// Set the bloom filter back into the span for the epoch.
|
|
minSpan := s.spans[target%numSpans][valIdx][0]
|
|
maxSpan := s.spans[target%numSpans][valIdx][1]
|
|
s.spans[target%numSpans][valIdx] = [3]uint16{minSpan, maxSpan, filterNum}
|
|
return nil
|
|
}
|
|
|
|
// Updates a min span for a validator index given a source and target epoch
|
|
// for an attestation produced by the validator. Used for catching surrounding votes.
|
|
func (s *SpanDetector) updateMinSpan(source uint64, target uint64, valIdx uint64) {
|
|
numSpans := uint64(len(s.spans))
|
|
if source < 1 {
|
|
return
|
|
}
|
|
for epochInt := int64(source - 1); epochInt >= 0; epochInt-- {
|
|
epoch := uint64(epochInt)
|
|
if sp := s.spans[epoch%numSpans]; sp == nil {
|
|
s.spans[epoch%numSpans] = make(map[uint64][3]uint16)
|
|
}
|
|
newMinSpan := uint16(target - epoch)
|
|
minSpan := s.spans[epoch%numSpans][valIdx][0]
|
|
maxSpan := s.spans[epoch%numSpans][valIdx][1]
|
|
attFilter := s.spans[epoch%numSpans][valIdx][2]
|
|
if minSpan == 0 || minSpan > newMinSpan {
|
|
fmt.Printf("epoch %d, valIdx %d: %v\n", epoch%numSpans, valIdx, [3]uint16{newMinSpan, maxSpan, attFilter})
|
|
s.spans[epoch%numSpans][valIdx] = [3]uint16{newMinSpan, maxSpan, attFilter}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Updates a max span for a validator index given a source and target epoch
|
|
// for an attestation produced by the validator. Used for catching surrounded votes.
|
|
func (s *SpanDetector) updateMaxSpan(source uint64, target uint64, valIdx uint64) {
|
|
numSpans := uint64(len(s.spans))
|
|
for epoch := source + 1; epoch < target; epoch++ {
|
|
if sp := s.spans[epoch%numSpans]; sp == nil {
|
|
s.spans[epoch%numSpans] = make(map[uint64][3]uint16)
|
|
}
|
|
minSpan := s.spans[epoch%numSpans][valIdx][0]
|
|
maxSpan := s.spans[epoch%numSpans][valIdx][1]
|
|
attFilter := s.spans[epoch%numSpans][valIdx][2]
|
|
newMaxSpan := uint16(target - epoch)
|
|
if newMaxSpan > maxSpan {
|
|
s.spans[epoch%numSpans][valIdx] = [3]uint16{minSpan, newMaxSpan, attFilter}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|