diff --git a/proto/beacon/rpc/v1/services.proto b/proto/beacon/rpc/v1/services.proto index 09ac75c1e..9afa2f9a6 100644 --- a/proto/beacon/rpc/v1/services.proto +++ b/proto/beacon/rpc/v1/services.proto @@ -5,7 +5,6 @@ package ethereum.beacon.rpc.v1; import "google/protobuf/empty.proto"; import "proto/eth/v1alpha1/beacon_block.proto"; import "proto/eth/v1alpha1/attestation.proto"; -import "google/api/annotations.proto"; service AttesterService { rpc RequestAttestation(AttestationRequest) returns (ethereum.eth.v1alpha1.AttestationData); diff --git a/slasher/rpc/detect_update_min_max_span.go b/slasher/rpc/detect_update_min_max_span.go index ecedb6f15..e6857b0c0 100644 --- a/slasher/rpc/detect_update_min_max_span.go +++ b/slasher/rpc/detect_update_min_max_span.go @@ -32,7 +32,8 @@ func detectMin(attestationEpochSpan uint64, attestationSourceEpoch uint64) uint64 { minSpan := uint64(recorderEpochSpan.MinEpochSpan) - if minSpan < attestationEpochSpan { + + if minSpan > 0 && minSpan < attestationEpochSpan { return minSpan + attestationSourceEpoch } return 0 @@ -45,6 +46,13 @@ func detectMin(attestationEpochSpan uint64, // Logic for this detection method was designed by https://github.com/protolambda // Detailed here: https://github.com/protolambda/eth2-surround/blob/master/README.md#min-max-surround func (ss *Server) DetectAndUpdateMaxEpochSpan(ctx context.Context, source uint64, target uint64, validatorIdx uint64) (uint64, error) { + if target < source { + return 0, fmt.Errorf( + "target: %d < source: %d ", + target, + source, + ) + } targetEpoch, span, spanMap, err := ss.detectSlashingByEpochSpan(source, target, validatorIdx, detectMax) if err != nil { return 0, err @@ -53,7 +61,7 @@ func (ss *Server) DetectAndUpdateMaxEpochSpan(ctx context.Context, source uint64 return targetEpoch, nil } for i := uint64(1); i < target-source; i++ { - val := uint32(span - i - 1) + val := uint32(span - i) if _, ok := spanMap.EpochSpanMap[source+i]; !ok { spanMap.EpochSpanMap[source+i] = ðpb.MinMaxEpochSpan{} } @@ -77,6 +85,13 @@ func (ss *Server) DetectAndUpdateMaxEpochSpan(ctx context.Context, source uint64 // Logic is following the detection method designed by https://github.com/protolambda // Detailed here: https://github.com/protolambda/eth2-surround/blob/master/README.md#min-max-surround func (ss *Server) DetectAndUpdateMinEpochSpan(ctx context.Context, source uint64, target uint64, validatorIdx uint64) (uint64, error) { + if target < source { + return 0, fmt.Errorf( + "target: %d < source: %d ", + target, + source, + ) + } targetEpoch, _, spanMap, err := ss.detectSlashingByEpochSpan(source, target, validatorIdx, detectMin) if err != nil { return 0, err @@ -109,7 +124,7 @@ func (ss *Server) DetectAndUpdateMinEpochSpan(ctx context.Context, source uint64 // attestation target. This method receives a detector function in order to be used // for both surrounding and surrounded vote cases. func (ss *Server) detectSlashingByEpochSpan(source, target, validatorIdx uint64, detector detectFn) (uint64, uint64, *ethpb.EpochSpanMap, error) { - span := target - source + 1 + span := target - source if span > params.BeaconConfig().WeakSubjectivityPeriod { return 0, span, nil, fmt.Errorf("target: %d - source: %d > weakSubjectivityPeriod", params.BeaconConfig().WeakSubjectivityPeriod, diff --git a/slasher/rpc/server.go b/slasher/rpc/server.go index b9c5c1102..86019ceab 100644 --- a/slasher/rpc/server.go +++ b/slasher/rpc/server.go @@ -45,7 +45,18 @@ func (ss *Server) IsSlashableAttestation(ctx context.Context, req *ethpb.Indexed } } - //TODO(#3133): add surround detection + for _, idx := range indices { + atts, err := ss.DetectSurroundVotes(ctx, req.Data.Source.Epoch, req.Data.Target.Epoch, idx) + if err != nil { + return nil, err + } + for _, ia := range atts { + atsSlashinngRes.AttesterSlashing = append(atsSlashinngRes.AttesterSlashing, ðpb.AttesterSlashing{ + Attestation_1: req, + Attestation_2: ia, + }) + } + } return atsSlashinngRes, nil } @@ -87,3 +98,40 @@ func (ss *Server) SlashableAttestations(req *types.Empty, server ethpb.Slasher_S //TODO(3133): implement stream provider for newly discovered listening to slashable attestation. return status.Error(codes.Unimplemented, "not implemented") } + +// DetectSurroundVotes is a method used to return the attestation that were detected +// by min max surround detection method. +func (ss *Server) DetectSurroundVotes(ctx context.Context, source uint64, target uint64, validatorIdx uint64) ([]*ethpb.IndexedAttestation, error) { + minTargetEpoch, err := ss.DetectAndUpdateMinEpochSpan(ctx, source, target, validatorIdx) + if err != nil { + return nil, err + } + maxTargetEpoch, err := ss.DetectAndUpdateMaxEpochSpan(ctx, source, target, validatorIdx) + if err != nil { + return nil, err + } + var idxAtts []*ethpb.IndexedAttestation + if minTargetEpoch > 0 { + attestations, err := ss.SlasherDB.IndexedAttestation(minTargetEpoch, validatorIdx) + if err != nil { + return nil, err + } + for _, ia := range attestations { + if ia.Data.Source.Epoch > source && ia.Data.Target.Epoch < target { + idxAtts = append(idxAtts, ia) + } + } + } + if maxTargetEpoch > 0 { + attestations, err := ss.SlasherDB.IndexedAttestation(maxTargetEpoch, validatorIdx) + if err != nil { + return nil, err + } + for _, ia := range attestations { + if ia.Data.Source.Epoch < source && ia.Data.Target.Epoch > target { + idxAtts = append(idxAtts, ia) + } + } + } + return idxAtts, nil +} diff --git a/slasher/rpc/server_test.go b/slasher/rpc/server_test.go index 8d78d414e..6065fb952 100644 --- a/slasher/rpc/server_test.go +++ b/slasher/rpc/server_test.go @@ -418,3 +418,154 @@ func TestServer_DontSlashSameAttestationData(t *testing.T) { t.Errorf("Should not return slashaing proof for same data: %v", sr) } } + +func TestServer_SlashSurroundedAttestation(t *testing.T) { + dbs := db.SetupSlasherDB(t) + defer db.TeardownSlasherDB(t, dbs) + ctx := context.Background() + slasherServer := &Server{ + ctx: ctx, + SlasherDB: dbs, + } + ia1 := ðpb.IndexedAttestation{ + CustodyBit_0Indices: []uint64{0}, + CustodyBit_1Indices: []uint64{}, + Signature: []byte("sig2"), + Data: ðpb.AttestationData{ + Slot: 4*params.BeaconConfig().SlotsPerEpoch + 1, + Index: 0, + BeaconBlockRoot: []byte("block1"), + Source: ðpb.Checkpoint{Epoch: 1}, + Target: ðpb.Checkpoint{Epoch: 4}, + }, + } + ia2 := ðpb.IndexedAttestation{ + CustodyBit_0Indices: []uint64{0}, + CustodyBit_1Indices: []uint64{}, + Signature: []byte("sig1"), + Data: ðpb.AttestationData{ + Slot: 4*params.BeaconConfig().SlotsPerEpoch + 1, + Index: 0, + BeaconBlockRoot: []byte("block2"), + Source: ðpb.Checkpoint{Epoch: 2}, + Target: ðpb.Checkpoint{Epoch: 3}, + }, + } + want := ðpb.AttesterSlashing{ + Attestation_1: ia2, + Attestation_2: ia1, + } + + if _, err := slasherServer.IsSlashableAttestation(ctx, ia1); err != nil { + t.Errorf("Could not call RPC method: %v", err) + } + sr, err := slasherServer.IsSlashableAttestation(ctx, ia2) + if err != nil { + t.Errorf("Could not call RPC method: %v", err) + } + if len(sr.AttesterSlashing) != 1 { + t.Fatalf("Should return 1 slashaing proof: %v", sr.AttesterSlashing) + } + if !proto.Equal(sr.AttesterSlashing[0], want) { + t.Errorf("Wanted slashing proof: %v got: %v", want, sr.AttesterSlashing[0]) + + } +} + +func TestServer_SlashSurroundAttestation(t *testing.T) { + dbs := db.SetupSlasherDB(t) + defer db.TeardownSlasherDB(t, dbs) + ctx := context.Background() + slasherServer := &Server{ + ctx: ctx, + SlasherDB: dbs, + } + ia1 := ðpb.IndexedAttestation{ + CustodyBit_0Indices: []uint64{0}, + CustodyBit_1Indices: []uint64{}, + Signature: []byte("sig2"), + Data: ðpb.AttestationData{ + Slot: 4*params.BeaconConfig().SlotsPerEpoch + 1, + Index: 0, + BeaconBlockRoot: []byte("block1"), + Source: ðpb.Checkpoint{Epoch: 2}, + Target: ðpb.Checkpoint{Epoch: 3}, + }, + } + ia2 := ðpb.IndexedAttestation{ + CustodyBit_0Indices: []uint64{0}, + CustodyBit_1Indices: []uint64{}, + Signature: []byte("sig1"), + Data: ðpb.AttestationData{ + Slot: 4*params.BeaconConfig().SlotsPerEpoch + 1, + Index: 0, + BeaconBlockRoot: []byte("block2"), + Source: ðpb.Checkpoint{Epoch: 1}, + Target: ðpb.Checkpoint{Epoch: 4}, + }, + } + want := ðpb.AttesterSlashing{ + Attestation_1: ia2, + Attestation_2: ia1, + } + + if _, err := slasherServer.IsSlashableAttestation(ctx, ia1); err != nil { + t.Errorf("Could not call RPC method: %v", err) + } + sr, err := slasherServer.IsSlashableAttestation(ctx, ia2) + if err != nil { + t.Errorf("Could not call RPC method: %v", err) + } + if len(sr.AttesterSlashing) != 1 { + t.Fatalf("Should return 1 slashaing proof: %v", sr.AttesterSlashing) + } + if !proto.Equal(sr.AttesterSlashing[0], want) { + t.Errorf("Wanted slashing proof: %v got: %v", want, sr.AttesterSlashing[0]) + + } +} + +func TestServer_DontSlashValidAttestations(t *testing.T) { + dbs := db.SetupSlasherDB(t) + defer db.TeardownSlasherDB(t, dbs) + ctx := context.Background() + slasherServer := &Server{ + ctx: ctx, + SlasherDB: dbs, + } + ia1 := ðpb.IndexedAttestation{ + CustodyBit_0Indices: []uint64{0}, + CustodyBit_1Indices: []uint64{}, + Signature: []byte("sig2"), + Data: ðpb.AttestationData{ + Slot: 5*params.BeaconConfig().SlotsPerEpoch + 1, + Index: 0, + BeaconBlockRoot: []byte("block1"), + Source: ðpb.Checkpoint{Epoch: 2}, + Target: ðpb.Checkpoint{Epoch: 4}, + }, + } + ia2 := ðpb.IndexedAttestation{ + CustodyBit_0Indices: []uint64{0}, + CustodyBit_1Indices: []uint64{}, + Signature: []byte("sig1"), + Data: ðpb.AttestationData{ + Slot: 5*params.BeaconConfig().SlotsPerEpoch + 1, + Index: 0, + BeaconBlockRoot: []byte("block2"), + Source: ðpb.Checkpoint{Epoch: 3}, + Target: ðpb.Checkpoint{Epoch: 5}, + }, + } + + if _, err := slasherServer.IsSlashableAttestation(ctx, ia1); err != nil { + t.Errorf("Could not call RPC method: %v", err) + } + sr, err := slasherServer.IsSlashableAttestation(ctx, ia2) + if err != nil { + t.Errorf("Could not call RPC method: %v", err) + } + if len(sr.AttesterSlashing) != 0 { + t.Errorf("Should not return slashaing proof for same data: %v", sr) + } +}