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

Prepare spanner for double vote detection and fix a few bugs #4940

Merged
merged 10 commits into from Feb 26, 2020
57 changes: 29 additions & 28 deletions slasher/detection/attestations/spanner.go
Expand Up @@ -58,11 +58,12 @@ func NewSpanDetector() *SpanDetector {
func (s *SpanDetector) DetectSlashingForValidator(
ctx context.Context,
validatorIdx uint64,
sourceEpoch uint64,
targetEpoch uint64,
attData *ethpb.AttestationData,
) (*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",
Expand All @@ -79,15 +80,15 @@ func (s *SpanDetector) DetectSlashingForValidator(
if minSpan > 0 && minSpan < distance {
return &DetectionResult{
Kind: SurroundVote,
SlashableEpoch: uint64(minSpan) + sourceEpoch,
SlashableEpoch: sourceEpoch + uint64(minSpan),
}, nil
}

maxSpan := sp[validatorIdx][1]
if maxSpan > distance {
return &DetectionResult{
Kind: SurroundVote,
SlashableEpoch: uint64(maxSpan) + sourceEpoch,
SlashableEpoch: sourceEpoch + uint64(maxSpan),
}, nil
}
}
Expand Down Expand Up @@ -156,40 +157,40 @@ func (s *SpanDetector) UpdateSpans(ctx context.Context, att *ethpb.IndexedAttest
}

// Updates a min span for a validator index given a source and target epoch
// for an attestation produced by the validator.
// 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 > 0 {
for epoch := source - 1; epoch > 0; epoch-- {
val := uint16(target - (epoch))
if sp := s.spans[epoch%numSpans]; sp == nil {
s.spans[epoch%numSpans] = make(map[uint64][2]uint16)
}
minSpan := s.spans[epoch%numSpans][valIdx][0]
maxSpan := s.spans[epoch%numSpans][valIdx][1]
if minSpan == 0 || minSpan > val {
s.spans[epoch%numSpans][valIdx] = [2]uint16{val, maxSpan}
} else {
break
}
if source < 1 {
return
}
for epoch := source - 1; epoch >= 0; epoch-- {
newMinSpan := uint16(target - epoch)
if sp := s.spans[epoch%numSpans]; sp == nil {
s.spans[epoch%numSpans] = make(map[uint64][2]uint16)
}
minSpan := s.spans[epoch%numSpans][valIdx][0]
maxSpan := s.spans[epoch%numSpans][valIdx][1]
if minSpan == 0 || minSpan > newMinSpan {
s.spans[epoch%numSpans][valIdx] = [2]uint16{newMinSpan, maxSpan}
} else {
break
}
}
}

// Updates a max span for a validator index given a source and target epoch
// for an attestation produced by the validator.
// 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))
distance := target - source
for epoch := uint64(1); epoch < distance; epoch++ {
val := uint16(distance - epoch)
if sp := s.spans[source+epoch%numSpans]; sp == nil {
s.spans[source+epoch%numSpans] = make(map[uint64][2]uint16)
for epoch := source + 1; epoch < target; epoch++ {
if sp := s.spans[epoch%numSpans]; sp == nil {
s.spans[epoch%numSpans] = make(map[uint64][2]uint16)
}
minSpan := s.spans[source+epoch%numSpans][valIdx][0]
maxSpan := s.spans[source+epoch%numSpans][valIdx][1]
if maxSpan < val {
s.spans[source+epoch%numSpans][valIdx] = [2]uint16{minSpan, val}
minSpan := s.spans[epoch%numSpans][valIdx][0]
maxSpan := s.spans[epoch%numSpans][valIdx][1]
newMaxSpan := uint16(target - epoch)
if newMaxSpan > maxSpan {
s.spans[epoch%numSpans][valIdx] = [2]uint16{minSpan, newMaxSpan}
} else {
break
}
Expand Down
249 changes: 247 additions & 2 deletions slasher/detection/attestations/spanner_test.go
Expand Up @@ -76,6 +76,132 @@ func TestSpanDetector_DetectSlashingForValidator(t *testing.T) {
3: {1, 0},
},
},
// Proto Max Span Tests from the eth2-surround repo.
{
name: "Proto max span test #1",
sourceEpoch: 8,
targetEpoch: 18,
// Given a distance of (18 - 8) = 10, we want the validator to not have
// a slashable act.
shouldSlash: false,
spansByEpochForValidator: map[uint64][2]uint16{
0: {4, 0},
1: {2, 0},
2: {1, 0},
4: {0, 2},
5: {0, 1},
},
},
{
name: "Proto max span test #2",
sourceEpoch: 4,
targetEpoch: 12,
// Given a distance of (4 - 12) = 8, we want the validator to not commit a slashable offense.
shouldSlash: false,
slashableEpoch: 0,
spansByEpochForValidator: map[uint64][2]uint16{
4: {14, 2},
5: {13, 1},
6: {12, 0},
7: {11, 0},
9: {0, 9},
10: {0, 8},
11: {0, 7},
12: {0, 6},
13: {0, 5},
14: {0, 4},
15: {0, 3},
16: {0, 2},
17: {0, 1},
},
},
{
name: "Proto max span test #3",
sourceEpoch: 10,
targetEpoch: 15,
// Given a distance of (4 - 12) = 8, we want the validator to not commit a slashable offense.
shouldSlash: true,
slashableEpoch: 18,
spansByEpochForValidator: map[uint64][2]uint16{
4: {14, 2},
5: {13, 7},
6: {12, 6},
7: {11, 5},
8: {0, 4},
9: {0, 9},
10: {0, 8},
11: {0, 7},
12: {0, 6},
13: {0, 5},
14: {0, 4},
15: {0, 3},
16: {0, 2},
17: {0, 1},
},
},
// Proto Min Span Tests from the eth2-surround repo.
{
name: "Proto min span test #1",
sourceEpoch: 4,
targetEpoch: 6,
shouldSlash: false,
spansByEpochForValidator: map[uint64][2]uint16{
1: {5, 0},
2: {4, 0},
3: {3, 0},
},
},
{
name: "Proto min span test #2",
sourceEpoch: 11,
targetEpoch: 15,
shouldSlash: false,
spansByEpochForValidator: map[uint64][2]uint16{
1: {5, 0},
2: {4, 0},
3: {3, 0},
4: {14, 0},
5: {13, 1},
6: {12, 0},
7: {11, 0},
8: {10, 0},
9: {9, 0},
10: {8, 0},
11: {7, 0},
12: {6, 0},
14: {0, 4},
15: {0, 3},
16: {0, 2},
17: {0, 1},
},
},
{
name: "Proto min span test #3",
sourceEpoch: 9,
targetEpoch: 19,
shouldSlash: true,
slashableEpoch: 14,
spansByEpochForValidator: map[uint64][2]uint16{
0: {5, 0},
1: {4, 0},
2: {3, 0},
3: {11, 0},
4: {10, 1},
5: {9, 0},
6: {8, 0},
7: {7, 0},
8: {6, 0},
9: {5, 0},
10: {7, 0},
11: {6, 3},
12: {0, 2},
13: {0, 1},
14: {0, 3},
15: {0, 2},
16: {0, 1},
17: {0, 0},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -91,7 +217,15 @@ func TestSpanDetector_DetectSlashingForValidator(t *testing.T) {
}
}
ctx := context.Background()
res, err := sd.DetectSlashingForValidator(ctx, validatorIndex, tt.sourceEpoch, tt.targetEpoch)
attData := &ethpb.AttestationData{
Source: &ethpb.Checkpoint{
Epoch: tt.sourceEpoch,
},
Target: &ethpb.Checkpoint{
Epoch: tt.targetEpoch,
},
}
res, err := sd.DetectSlashingForValidator(ctx, validatorIndex, attData)
if err != nil {
t.Fatal(err)
}
Expand All @@ -111,6 +245,113 @@ func TestSpanDetector_DetectSlashingForValidator(t *testing.T) {
}
}

func TestSpanDetector_DetectSlashingForValidator_MultipleValidators(t *testing.T) {
shayzluf marked this conversation as resolved.
Show resolved Hide resolved
type testStruct struct {
name string
sourceEpochs []uint64
targetEpochs []uint64
slashableEpochs []uint64
shouldSlash []bool
spansByEpoch []map[uint64][2]uint16
}
tests := []testStruct{
{
name: "3 of 5 validators slashed",
sourceEpochs: []uint64{0, 2, 4, 5, 1},
targetEpochs: []uint64{10, 3, 5, 9, 8},
slashableEpochs: []uint64{6, 0, 7, 8, 0},
// Detections - surrounding, none, surrounded, surrounding, none.
shouldSlash: []bool{true, false, true, true, false},
// Atts in map: (src, epoch) - 0: (2, 6), 1: (1, 2), 2: (1, 7), 3: (6, 8), 4: (0, 3)
spansByEpoch: []map[uint64][2]uint16{
// Epoch 0.
{
0: {6, 0},
1: {2, 0},
2: {7, 0},
3: {8, 0},
},
// Epoch 1.
{
0: {5, 0},
3: {7, 0},
4: {0, 1},
},
// Epoch 2.
{
2: {0, 5},
3: {6, 0},
4: {0, 2},
},
// Epoch 3.
{
0: {0, 3},
2: {0, 4},
3: {5, 0},
},
// Epoch 4.
{
0: {0, 2},
2: {0, 3},
3: {4, 0},
},
// Epoch 5.
{
0: {0, 1},
2: {0, 2},
3: {3, 0},
},
// Epoch 6.
{
2: {0, 1},
},
// Epoch 7.
{
3: {0, 1},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
numEpochsToTrack := 100
sd := &SpanDetector{
spans: make([]map[uint64][2]uint16, numEpochsToTrack),
}
for i := 0; i < len(tt.spansByEpoch); i++ {
sd.spans[i] = tt.spansByEpoch[i]
}
ctx := context.Background()
for valIdx := uint64(0); valIdx < uint64(len(tt.shouldSlash)); valIdx++ {
attData := &ethpb.AttestationData{
Source: &ethpb.Checkpoint{
Epoch: tt.sourceEpochs[valIdx],
},
Target: &ethpb.Checkpoint{
Epoch: tt.targetEpochs[valIdx],
},
}
res, err := sd.DetectSlashingForValidator(ctx, valIdx, attData)
if err != nil {
t.Fatal(err)
}
if !tt.shouldSlash[valIdx] && res != nil {
t.Fatalf("Did not want validator to be slashed but found slashable offense: %v", res)
}
if tt.shouldSlash[valIdx] {
want := &DetectionResult{
Kind: SurroundVote,
SlashableEpoch: tt.slashableEpochs[valIdx],
}
if !reflect.DeepEqual(res, want) {
t.Errorf("Wanted: %v, received %v", want, res)
}
}
}
})
}
}

func TestSpanDetector_SpanForEpochByValidator(t *testing.T) {
numEpochsToTrack := 2
sd := &SpanDetector{
Expand Down Expand Up @@ -217,7 +458,11 @@ func TestNewSpanDetector_UpdateSpans(t *testing.T) {
numEpochs: 3,
want: []map[uint64][2]uint16{
// Epoch 0.
nil,
{
0: {3, 0},
1: {3, 0},
2: {3, 0},
},
// Epoch 1.
nil,
// Epoch 2.
Expand Down