Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change span representation to a struct in Slasher #4981

Merged
merged 8 commits into from
Mar 3, 2020
Merged
2 changes: 0 additions & 2 deletions slasher/detection/attestations/BUILD.bazel
Expand Up @@ -14,10 +14,8 @@ go_library(
"//shared/params:go_default_library",
"//slasher/detection/attestations/iface:go_default_library",
"//slasher/detection/attestations/types:go_default_library",
"//slasher/detection/filter:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_prysmaticlabs_go_ssz//:go_default_library",
"@io_opencensus_go//trace:go_default_library",
],
)
Expand Down
4 changes: 2 additions & 2 deletions slasher/detection/attestations/iface/iface.go
Expand Up @@ -15,8 +15,8 @@ type SpanDetector interface {
validatorIdx uint64,
attData *ethpb.AttestationData,
) (*types.DetectionResult, error)
SpanForEpochByValidator(ctx context.Context, valIdx uint64, epoch uint64) ([3]uint16, error)
ValidatorSpansByEpoch(ctx context.Context, epoch uint64) map[uint64][3]uint16
SpanForEpochByValidator(ctx context.Context, valIdx uint64, epoch uint64) (types.Span, error)
ValidatorSpansByEpoch(ctx context.Context, epoch uint64) map[uint64]types.Span

// Write functions.
UpdateSpans(ctx context.Context, att *ethpb.IndexedAttestation) error
Expand Down
10 changes: 5 additions & 5 deletions slasher/detection/attestations/mock_spanner.go
Expand Up @@ -16,7 +16,7 @@ var _ = iface.SpanDetector(&MockSpanDetector{})
// spans from validators.
type MockSpanDetector struct {
// Slice of epochs for valindex => min-max span.
spans []map[uint64][2]uint16
spans []map[uint64]types.Span
lock sync.RWMutex
}

Expand Down Expand Up @@ -57,13 +57,13 @@ func (s *MockSpanDetector) DetectSlashingForValidator(

// SpanForEpochByValidator returns the specific min-max span for a
// validator index in a given epoch.
func (s *MockSpanDetector) SpanForEpochByValidator(ctx context.Context, valIdx uint64, epoch uint64) ([3]uint16, error) {
return [3]uint16{0, 0, 0}, nil
func (s *MockSpanDetector) SpanForEpochByValidator(ctx context.Context, valIdx uint64, epoch uint64) (types.Span, error) {
return types.Span{MinSpan: 0, MaxSpan: 0, SigBytes: [2]byte{}, HasAttested: false}, nil
}

// ValidatorSpansByEpoch returns a list of all validator spans in a given epoch.
func (s *MockSpanDetector) ValidatorSpansByEpoch(ctx context.Context, epoch uint64) map[uint64][3]uint16 {
return make(map[uint64][3]uint16, 0)
func (s *MockSpanDetector) ValidatorSpansByEpoch(ctx context.Context, epoch uint64) map[uint64]types.Span {
return make(map[uint64]types.Span, 0)
}

// DeleteValidatorSpansByEpoch mocks the delete spans by epoch function.
Expand Down
139 changes: 63 additions & 76 deletions slasher/detection/attestations/spanner.go
Expand Up @@ -2,17 +2,13 @@ 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"
)

Expand All @@ -23,7 +19,7 @@ var _ = iface.SpanDetector(&SpanDetector{})
// 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
spans []map[uint64]types.Span
Copy link
Contributor Author

@0xKiwi 0xKiwi Mar 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not using a reference since the actual struct nearly the same size as the pointer would be, just an extra bool. There are also no nested/dynamic types. Size is static.
If this should be changed let me know.

lock sync.RWMutex
}

Expand All @@ -32,7 +28,7 @@ type SpanDetector struct {
// the beacon state.
func NewSpanDetector() *SpanDetector {
return &SpanDetector{
spans: make([]map[uint64][3]uint16, 256),
spans: make([]map[uint64]types.Span, 256),
}
}

Expand All @@ -44,8 +40,8 @@ func (s *SpanDetector) DetectSlashingForValidator(
validatorIdx uint64,
attData *ethpb.AttestationData,
) (*types.DetectionResult, error) {
ctx, span := trace.StartSpan(ctx, "detection.DetectSlashingForValidator")
defer span.End()
ctx, traceSpan := trace.StartSpan(ctx, "detection.DetectSlashingForValidator")
defer traceSpan.End()
sourceEpoch := attData.Source.Epoch
targetEpoch := attData.Target.Epoch
if (targetEpoch - sourceEpoch) > params.BeaconConfig().WeakSubjectivityPeriod {
Expand All @@ -61,54 +57,48 @@ func (s *SpanDetector) DetectSlashingForValidator(
numSpans := uint64(len(s.spans))

if sp := s.spans[sourceEpoch%numSpans]; sp != nil {
minSpan := sp[validatorIdx][0]
minSpan := sp[validatorIdx].MinSpan
if minSpan > 0 && minSpan < distance {
slashableEpoch := sourceEpoch + uint64(minSpan)
span := s.spans[slashableEpoch%numSpans][validatorIdx]
return &types.DetectionResult{
Kind: types.SurroundVote,
SlashableEpoch: sourceEpoch + uint64(minSpan),
SigBytes: span.SigBytes,
}, nil
}

maxSpan := sp[validatorIdx][1]
maxSpan := sp[validatorIdx].MaxSpan
if maxSpan > distance {
slashableEpoch := sourceEpoch + uint64(maxSpan)
span := s.spans[slashableEpoch%numSpans][validatorIdx]
return &types.DetectionResult{
Kind: types.SurroundVote,
SlashableEpoch: sourceEpoch + uint64(maxSpan),
SlashableEpoch: slashableEpoch,
SigBytes: span.SigBytes,
}, nil
}
}

if sp := s.spans[targetEpoch%numSpans]; sp != nil {
filterNum := sp[validatorIdx][2]
if filterNum == 0 {
// Check if the validator has attested for this epoch or not.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to check if it's the same dataroot in order to decide if its double. Doing it in another PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that will be handled by detection. This just makes sure the attestation triggers a hard DB check.

if !sp[validatorIdx].HasAttested {
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 &types.DetectionResult{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems strange to return double vote when it is not verified

Copy link
Contributor Author

@0xKiwi 0xKiwi Mar 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the detection, it lets the detection service to do a hard check, which we always need to do anyways for checking double votes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 2 bytes of the signature isn't safe enough to just compare the receiving attestation with it IMO, db checks are really quick with this strategy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats true

Kind: types.DoubleVote,
SlashableEpoch: targetEpoch,
SigBytes: sp[validatorIdx].SigBytes,
}, 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) {
func (s *SpanDetector) SpanForEpochByValidator(ctx context.Context, valIdx uint64, epoch uint64) (types.Span, error) {
ctx, span := trace.StartSpan(ctx, "detection.SpanForEpochByValidator")
defer span.End()
s.lock.RLock()
Expand All @@ -118,13 +108,13 @@ func (s *SpanDetector) SpanForEpochByValidator(ctx context.Context, valIdx uint6
if minMaxSpan, ok := span[valIdx]; ok {
return minMaxSpan, nil
}
return [3]uint16{}, fmt.Errorf("validator index %d not found in span map", valIdx)
return types.Span{}, fmt.Errorf("validator index %d not found in span map", valIdx)
}
return [3]uint16{}, fmt.Errorf("no data found for epoch %d", epoch)
return types.Span{}, 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 {
func (s *SpanDetector) ValidatorSpansByEpoch(ctx context.Context, epoch uint64) map[uint64]types.Span {
ctx, span := trace.StartSpan(ctx, "detection.ValidatorSpansByEpoch")
defer span.End()
s.lock.RLock()
Expand Down Expand Up @@ -160,50 +150,42 @@ func (s *SpanDetector) UpdateSpans(ctx context.Context, att *ethpb.IndexedAttest
// 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")
}
// Save the signature for the received attestation so we can have more detail to find it in the DB.
s.saveSigBytes(att, valIdx)
// 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 {
// saveSigBytes saves the first 2 bytes of the signature for the att we're updating the spans to.
// Later used to help us find the violating attestation in the DB.
func (s *SpanDetector) saveSigBytes(att *ethpb.IndexedAttestation, valIdx uint64) {
numSpans := uint64(len(s.spans))
target := attData.Target.Epoch
target := att.Data.Target.Epoch

// Check if there is an existing bloom filter, if so, don't modify it.
// Check if there is already info saved in span[2].
if sp := s.spans[target%numSpans]; sp == nil {
s.spans[target%numSpans] = make(map[uint64][3]uint16)
s.spans[target%numSpans] = make(map[uint64]types.Span)
}
filterNum := s.spans[target%numSpans][valIdx][2]
if filterNum != 0 {
return nil
// If the validator has already attested for this target epoch,
if s.spans[target%numSpans][valIdx].HasAttested {
return
}

// 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
sigBytes := [2]byte{0, 0}
if len(att.Signature) > 1 {
sigBytes = [2]byte{att.Signature[0], att.Signature[1]}
}
attFilter, err := filter.NewBloomFilter(attDataRoot[:])
if err != nil {
return err
// Save the signature bytes into the span for this epoch.
span := s.spans[target%numSpans][valIdx]
s.spans[target%numSpans][valIdx] = types.Span{
MinSpan: span.MinSpan,
MaxSpan: span.MaxSpan,
SigBytes: sigBytes,
HasAttested: true,
}
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
Expand All @@ -216,15 +198,17 @@ func (s *SpanDetector) updateMinSpan(source uint64, target uint64, valIdx uint64
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)
s.spans[epoch%numSpans] = make(map[uint64]types.Span)
}
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}
span := s.spans[epoch%numSpans][valIdx]
if span.MinSpan == 0 || span.MinSpan > newMinSpan {
s.spans[epoch%numSpans][valIdx] = types.Span{
MinSpan: newMinSpan,
MaxSpan: span.MaxSpan,
SigBytes: span.SigBytes,
HasAttested: span.HasAttested,
}
} else {
break
}
Expand All @@ -237,14 +221,17 @@ 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)
s.spans[epoch%numSpans] = make(map[uint64]types.Span)
}
minSpan := s.spans[epoch%numSpans][valIdx][0]
maxSpan := s.spans[epoch%numSpans][valIdx][1]
attFilter := s.spans[epoch%numSpans][valIdx][2]
span := s.spans[epoch%numSpans][valIdx]
newMaxSpan := uint16(target - epoch)
if newMaxSpan > maxSpan {
s.spans[epoch%numSpans][valIdx] = [3]uint16{minSpan, newMaxSpan, attFilter}
if newMaxSpan > span.MaxSpan {
s.spans[epoch%numSpans][valIdx] = types.Span{
MinSpan: span.MinSpan,
MaxSpan: newMaxSpan,
SigBytes: span.SigBytes,
HasAttested: span.HasAttested,
}
} else {
break
}
Expand Down