diff --git a/slasher/detection/attestations/spanner.go b/slasher/detection/attestations/spanner.go index e66adef636cb..d7d2ed37bd4c 100644 --- a/slasher/detection/attestations/spanner.go +++ b/slasher/detection/attestations/spanner.go @@ -28,6 +28,10 @@ var ( Name: "latest_max_span_distance_observed", Help: "The latest distance between target - source observed for max spans", }) + sourceLargerThenTargetObserved = promauto.NewCounter(prometheus.CounterOpts{ + Name: "attestation_source_larger_then_target", + Help: "The number of attestation data source epoch that aren larger then target epoch.", + }) ) // We look back 128 epochs when updating min/max spans @@ -64,11 +68,21 @@ func (s *SpanDetector) DetectSlashingsForAttestation( defer traceSpan.End() sourceEpoch := att.Data.Source.Epoch targetEpoch := att.Data.Target.Epoch - if (targetEpoch - sourceEpoch) > params.BeaconConfig().WeakSubjectivityPeriod { + dis := targetEpoch - sourceEpoch + + if sourceEpoch > targetEpoch { //Prevent underflow and handle source > target slashable cases. + dis = sourceEpoch - targetEpoch + tmp := sourceEpoch + sourceEpoch = targetEpoch + targetEpoch = tmp + sourceLargerThenTargetObserved.Inc() + } + + if dis > params.BeaconConfig().WeakSubjectivityPeriod { return nil, fmt.Errorf( "attestation span was greater than weak subjectivity period %d, received: %d", params.BeaconConfig().WeakSubjectivityPeriod, - targetEpoch-sourceEpoch, + dis, ) } @@ -82,7 +96,7 @@ func (s *SpanDetector) DetectSlashingsForAttestation( } var detections []*types.DetectionResult - distance := uint16(targetEpoch - sourceEpoch) + distance := uint16(dis) for _, idx := range att.AttestingIndices { if ctx.Err() != nil { return nil, errors.Wrap(ctx.Err(), "could not detect slashings") @@ -171,6 +185,11 @@ func (s *SpanDetector) saveSigBytes(ctx context.Context, att *ethpb.IndexedAttes ctx, traceSpan := trace.StartSpan(ctx, "spanner.saveSigBytes") defer traceSpan.End() target := att.Data.Target.Epoch + source := att.Data.Source.Epoch + // handle source > target well + if source > target { + target = source + } spanMap, err := s.slasherDB.EpochSpans(ctx, target, dbTypes.UseCache) if err != nil { return err @@ -220,6 +239,12 @@ func (s *SpanDetector) updateMinSpan(ctx context.Context, att *ethpb.IndexedAtte if source < 1 { return nil } + // handle source > target well + if source > target { + tmp := source + source = target + target = tmp + } valIndices := make([]uint64, len(att.AttestingIndices)) copy(valIndices, att.AttestingIndices) latestMinSpanDistanceObserved.Set(float64(att.Data.Target.Epoch - att.Data.Source.Epoch)) @@ -292,6 +317,12 @@ func (s *SpanDetector) updateMaxSpan(ctx context.Context, att *ethpb.IndexedAtte defer traceSpan.End() source := att.Data.Source.Epoch target := att.Data.Target.Epoch + // handle source > target well + if source > target { + tmp := source + source = target + target = tmp + } latestMaxSpanDistanceObserved.Set(float64(target - source)) valIndices := make([]uint64, len(att.AttestingIndices)) copy(valIndices, att.AttestingIndices) diff --git a/slasher/detection/attestations/spanner_test.go b/slasher/detection/attestations/spanner_test.go index 096028e76e39..c6e788945cad 100644 --- a/slasher/detection/attestations/spanner_test.go +++ b/slasher/detection/attestations/spanner_test.go @@ -456,6 +456,29 @@ func TestSpanDetector_DetectSlashingsForAttestation_Surround(t *testing.T) { 17: {0, 0}, }, }, + { + name: "Should slash if max span > distance && source > target", + sourceEpoch: 6, + targetEpoch: 3, + 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][3]uint16{ + 3: {0, 4}, + }, + }, + { + name: "Should not slash if max span = distance && source > target", + sourceEpoch: 6, + targetEpoch: 3, + shouldSlash: false, + // 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. + spansByEpochForValidator: map[uint64][3]uint16{ + 3: {0, 1}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -623,6 +646,87 @@ func TestSpanDetector_DetectSlashingsForAttestation_MultipleValidators(t *testin indexedAttestation(1, 5, []uint64{3}), }, }, + { + name: "3 of 4 validators slashed, differing surrounds source > target", + incomingAtt: ðpb.IndexedAttestation{ + AttestingIndices: []uint64{0, 1, 2, 3}, + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{ + Epoch: 7, + Root: []byte("good source"), + }, + Target: ðpb.Checkpoint{ + Epoch: 5, + Root: []byte("good target"), + }, + }, + Signature: []byte{1, 2}, + }, + slashableEpochs: []uint64{8, 9, 10, 0}, + // Detections - surround, surround, surround, none. + shouldSlash: []bool{true, true, true, false}, + // Atts in map: (src, epoch) - 0: (1, 8), 1: (3, 9), 2: (2, 10), 3: (4, 6) + atts: []*ethpb.IndexedAttestation{ + indexedAttestation(1, 8, []uint64{0}), + indexedAttestation(3, 9, []uint64{1}), + indexedAttestation(2, 10, []uint64{2}), + indexedAttestation(4, 6, []uint64{3}), + }, + }, + { + name: "3 of 4 validators slashed, differing surrounded source > target", + incomingAtt: ðpb.IndexedAttestation{ + AttestingIndices: []uint64{0, 1, 2, 3}, + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{ + Epoch: 9, + Root: []byte("good source"), + }, + Target: ðpb.Checkpoint{ + Epoch: 2, + Root: []byte("good target"), + }, + }, + Signature: []byte{1, 2}, + }, + slashableEpochs: []uint64{8, 8, 7, 0}, + // Detections - surround, surround, surround, none. + shouldSlash: []bool{true, true, true, false}, + // Atts in map: (src, epoch) - 0: (5, 8), 1: (3, 8), 2: (4, 7), 3: (1, 5) + atts: []*ethpb.IndexedAttestation{ + indexedAttestation(5, 8, []uint64{0}), + indexedAttestation(3, 8, []uint64{1}), + indexedAttestation(4, 7, []uint64{2}), + indexedAttestation(1, 5, []uint64{3}), + }, + }, + { + name: "3 of 4 validators slashed, differing surrounded source > target in update", + incomingAtt: ðpb.IndexedAttestation{ + AttestingIndices: []uint64{0, 1, 2, 3}, + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{ + Epoch: 2, + Root: []byte("good source"), + }, + Target: ðpb.Checkpoint{ + Epoch: 9, + Root: []byte("good target"), + }, + }, + Signature: []byte{1, 2}, + }, + slashableEpochs: []uint64{8, 8, 7, 0}, + // Detections - surround, surround, surround, none. + shouldSlash: []bool{true, true, true, false}, + // Atts in map: (src, epoch) - 0: (5, 8), 1: (3, 8), 2: (4, 7), 3: (1, 5) + atts: []*ethpb.IndexedAttestation{ + indexedAttestation(8, 5, []uint64{0}), + indexedAttestation(8, 3, []uint64{1}), + indexedAttestation(7, 4, []uint64{2}), + indexedAttestation(5, 1, []uint64{3}), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {