From 6fe86a3b30829d4d013a3b94d01a2c80d1ead324 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Sat, 22 Feb 2020 08:57:24 -0600 Subject: [PATCH] Define an Efficient Spanner Struct Implementation for Slasher (#4920) * more spanner additions * implement iface * begin implement * wrapped up spanner functions * rem interface * added in necessary comments * comments on enums * begin adding tests * test for detection * add all detection tests * moar tests * tests for deleting pass * dd test for update spans * tests for updating * include tracing utils * gaz * add mutexes * ivan feedback --- slasher/detection/attestations/BUILD.bazel | 7 +- slasher/detection/attestations/spanner.go | 197 ++++++++++++ .../detection/attestations/spanner_test.go | 289 ++++++++++++++++++ 3 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 slasher/detection/attestations/spanner.go create mode 100644 slasher/detection/attestations/spanner_test.go diff --git a/slasher/detection/attestations/BUILD.bazel b/slasher/detection/attestations/BUILD.bazel index 90bf15e79..9fa59ab3a 100644 --- a/slasher/detection/attestations/BUILD.bazel +++ b/slasher/detection/attestations/BUILD.bazel @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["attestations.go"], + srcs = [ + "attestations.go", + "spanner.go", + ], importpath = "github.com/prysmaticlabs/prysm/slasher/detection/attestations", visibility = ["//slasher:__subpackages__"], deps = [ @@ -19,6 +22,7 @@ go_test( srcs = [ "attestations_bench_test.go", "attestations_test.go", + "spanner_test.go", ], embed = [":go_default_library"], deps = [ @@ -27,6 +31,7 @@ go_test( "//slasher/db/testing:go_default_library", "//slasher/flags:go_default_library", "@com_github_gogo_protobuf//proto:go_default_library", + "@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library", "@com_github_urfave_cli//:go_default_library", ], ) diff --git a/slasher/detection/attestations/spanner.go b/slasher/detection/attestations/spanner.go new file mode 100644 index 000000000..4dfc1495c --- /dev/null +++ b/slasher/detection/attestations/spanner.go @@ -0,0 +1,197 @@ +package attestations + +import ( + "context" + "fmt" + "sync" + + ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + "github.com/prysmaticlabs/prysm/shared/params" + "go.opencensus.io/trace" +) + +// DetectionKind defines an enum type that +// gives us information on the type of slashable offense +// found when analyzing validator min-max spans. +type DetectionKind int + +const ( + // DoubleVote denotes a slashable offense in which + // a validator cast two conflicting attestations within + // the same target epoch. + DoubleVote DetectionKind = iota + // SurroundVote denotes a slashable offense in which + // a validator surrounded or was surrounded by a previous + // attestation created by the same validator. + SurroundVote +) + +// DetectionResult tells us the kind of slashable +// offense found from detecting on min-max spans + +// the slashable epoch for the offense. +type DetectionResult struct { + Kind DetectionKind + SlashableEpoch uint64 +} + +// SpanDetector defines a struct which can detect slashable +// attestation offenses by tracking validator min-max +// spans from validators. +type SpanDetector struct { + // Slice of epochs for valindex => min-max span. + spans []map[uint64][2]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][2]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, + sourceEpoch uint64, + targetEpoch uint64, +) (*DetectionResult, error) { + ctx, span := trace.StartSpan(ctx, "detection.DetectSlashingForValidator") + defer span.End() + 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 &DetectionResult{ + Kind: SurroundVote, + SlashableEpoch: uint64(minSpan) + sourceEpoch, + }, nil + } + + maxSpan := sp[validatorIdx][1] + if maxSpan > distance { + return &DetectionResult{ + Kind: SurroundVote, + SlashableEpoch: uint64(maxSpan) + sourceEpoch, + }, 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) ([2]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 [2]uint16{}, fmt.Errorf("validator index %d not found in span map", valIdx) + } + return [2]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][2]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] + // Update min and max spans. + s.updateMinSpan(source, target, valIdx) + s.updateMaxSpan(source, target, valIdx) + } + return nil +} + +// Updates a min span for a validator index given a source and target epoch +// for an attestation produced by the validator. +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 + } + } + } +} + +// Updates a max span for a validator index given a source and target epoch +// for an attestation produced by the validator. +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) + } + 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} + } else { + break + } + } +} diff --git a/slasher/detection/attestations/spanner_test.go b/slasher/detection/attestations/spanner_test.go new file mode 100644 index 000000000..035aa842e --- /dev/null +++ b/slasher/detection/attestations/spanner_test.go @@ -0,0 +1,289 @@ +package attestations + +import ( + "context" + "reflect" + "strings" + "testing" + + ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" +) + +func TestSpanDetector_DetectSlashingForValidator(t *testing.T) { + type testStruct struct { + name string + sourceEpoch uint64 + targetEpoch uint64 + slashableEpoch uint64 + shouldSlash bool + spansByEpochForValidator map[uint64][2]uint16 + } + tests := []testStruct{ + { + name: "Should slash if max span > distance", + sourceEpoch: 3, + targetEpoch: 6, + slashableEpoch: 7, + shouldSlash: true, + // Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to have + // committed a slashable offense by having a max span of 4 > distance. + spansByEpochForValidator: map[uint64][2]uint16{ + 3: {0, 4}, + }, + }, + { + name: "Should NOT slash if max span < distance", + sourceEpoch: 3, + targetEpoch: 6, + // Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to NOT + // have committed slashable offense by having a max span of 1 < distance. + shouldSlash: false, + spansByEpochForValidator: map[uint64][2]uint16{ + 3: {0, 1}, + }, + }, + { + name: "Should NOT slash if max span == distance", + sourceEpoch: 3, + targetEpoch: 6, + // Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to NOT + // have committed slashable offense by having a max span of 3 == distance. + shouldSlash: false, + spansByEpochForValidator: map[uint64][2]uint16{ + 3: {0, 3}, + }, + }, + { + name: "Should NOT slash if min span == 0", + sourceEpoch: 3, + targetEpoch: 6, + // Given a min span of 0 and no max span slashing, we want validator to NOT + // have committed a slashable offense if min span == 0. + shouldSlash: false, + spansByEpochForValidator: map[uint64][2]uint16{ + 3: {0, 1}, + }, + }, + { + name: "Should slash if min span > 0 and min span < distance", + sourceEpoch: 3, + targetEpoch: 6, + // Given a distance of (6 - 3) = 3, we want the validator at epoch 3 to have + // committed a slashable offense by having a min span of 1 < distance. + shouldSlash: true, + slashableEpoch: 4, + spansByEpochForValidator: map[uint64][2]uint16{ + 3: {1, 0}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + numEpochsToTrack := 100 + sd := &SpanDetector{ + spans: make([]map[uint64][2]uint16, numEpochsToTrack), + } + // We only care about validator index 0 for these tests for simplicity. + validatorIndex := uint64(0) + for k, v := range tt.spansByEpochForValidator { + sd.spans[k] = map[uint64][2]uint16{ + validatorIndex: v, + } + } + ctx := context.Background() + res, err := sd.DetectSlashingForValidator(ctx, validatorIndex, tt.sourceEpoch, tt.targetEpoch) + if err != nil { + t.Fatal(err) + } + if !tt.shouldSlash && res != nil { + t.Fatalf("Did not want validator to be slashed but found slashable offense: %v", res) + } + if tt.shouldSlash { + want := &DetectionResult{ + Kind: SurroundVote, + SlashableEpoch: tt.slashableEpoch, + } + if !reflect.DeepEqual(res, want) { + t.Errorf("Wanted: %v, received %v", want, res) + } + } + }) + } +} + +func TestSpanDetector_SpanForEpochByValidator(t *testing.T) { + numEpochsToTrack := 2 + sd := &SpanDetector{ + spans: make([]map[uint64][2]uint16, numEpochsToTrack), + } + epoch := uint64(1) + validatorIndex := uint64(40) + sd.spans[epoch] = map[uint64][2]uint16{ + validatorIndex: {3, 7}, + } + want := [2]uint16{3, 7} + ctx := context.Background() + res, err := sd.SpanForEpochByValidator(ctx, validatorIndex, epoch) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(want, res) { + t.Errorf("Wanted %v, received %v", want, res) + } + validatorIndex = uint64(0) + if _, err = sd.SpanForEpochByValidator( + ctx, + validatorIndex, + epoch, + ); err != nil && !strings.Contains(err.Error(), "validator index 0 not found") { + t.Errorf("Wanted validator index not found error, received %v", err) + } + validatorIndex = uint64(40) + epoch = uint64(3) + if _, err = sd.SpanForEpochByValidator( + ctx, + validatorIndex, + epoch, + ); err != nil && !strings.Contains(err.Error(), "no data found for epoch") { + t.Errorf("Wanted no data found for epoch error, received %v", err) + } +} + +func TestSpanDetector_ValidatorSpansByEpoch(t *testing.T) { + numEpochsToTrack := 2 + sd := &SpanDetector{ + spans: make([]map[uint64][2]uint16, numEpochsToTrack), + } + epoch := uint64(1) + validatorIndex := uint64(40) + want := map[uint64][2]uint16{ + validatorIndex: {3, 7}, + } + sd.spans[epoch] = want + res := sd.ValidatorSpansByEpoch(context.Background(), epoch) + if !reflect.DeepEqual(res, want) { + t.Errorf("Wanted %v, received %v", want, res) + } +} + +func TestSpanDetector_DeleteValidatorSpansByEpoch(t *testing.T) { + numEpochsToTrack := 2 + sd := &SpanDetector{ + spans: make([]map[uint64][2]uint16, numEpochsToTrack), + } + epoch := uint64(1) + validatorIndex := uint64(40) + sd.spans[epoch] = map[uint64][2]uint16{ + validatorIndex: {3, 7}, + } + ctx := context.Background() + if err := sd.DeleteValidatorSpansByEpoch( + ctx, + validatorIndex, + 0, /* epoch */ + ); err != nil && !strings.Contains(err.Error(), "no span map found at epoch 0") { + t.Errorf("Wanted error when deleting epoch 0, received: %v", err) + } + if err := sd.DeleteValidatorSpansByEpoch(ctx, validatorIndex, epoch); err != nil { + t.Fatal(err) + } + want := make(map[uint64][2]uint16) + if res := sd.ValidatorSpansByEpoch(ctx, epoch); !reflect.DeepEqual(res, want) { + t.Errorf("Wanted %v for epoch after deleting, received %v", want, res) + } +} + +func TestNewSpanDetector_UpdateSpans(t *testing.T) { + type testStruct struct { + name string + att *ethpb.IndexedAttestation + numEpochs uint64 + want []map[uint64][2]uint16 + } + tests := []testStruct{ + { + name: "Distance of 2 should update max spans accordingly", + att: ðpb.IndexedAttestation{ + AttestingIndices: []uint64{0, 1, 2}, + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{ + Epoch: 1, + }, + Target: ðpb.Checkpoint{ + Epoch: 3, + }, + }, + }, + numEpochs: 3, + want: []map[uint64][2]uint16{ + // Epoch 0. + nil, + // Epoch 1. + nil, + // Epoch 2. + { + 0: {0, 1}, + 1: {0, 1}, + 2: {0, 1}, + }, + }, + }, + { + name: "Distance of 4 should update max spans accordingly", + att: ðpb.IndexedAttestation{ + AttestingIndices: []uint64{0, 1, 2}, + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{ + Epoch: 0, + }, + Target: ðpb.Checkpoint{ + Epoch: 5, + }, + }, + }, + numEpochs: 5, + want: []map[uint64][2]uint16{ + // Epoch 0. + nil, + // Epoch 1. + { + 0: {0, 4}, + 1: {0, 4}, + 2: {0, 4}, + }, + // Epoch 2. + { + 0: {0, 3}, + 1: {0, 3}, + 2: {0, 3}, + }, + // Epoch 3. + { + 0: {0, 2}, + 1: {0, 2}, + 2: {0, 2}, + }, + // Epoch 4. + { + 0: {0, 1}, + 1: {0, 1}, + 2: {0, 1}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sd := &SpanDetector{ + spans: make([]map[uint64][2]uint16, tt.numEpochs), + } + ctx := context.Background() + if err := sd.UpdateSpans(ctx, tt.att); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(sd.spans, tt.want) { + t.Errorf("Wanted spans %v, received %v", tt.want, sd.spans) + } + }) + } +}