prysm-pulse/slasher/detection/attestations/spanner.go
Ivan Martinez 31b1e6a7a8
Add double vote detection to spanner (#4954)
* Add double vote detection to spanner

* Add documentation

* Update slasher/detection/attestations/spanner.go

* Gazelle

* Fix filter output
2020-02-27 17:21:05 -05:00

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
}
}
}