diff --git a/CHANGELOG.md b/CHANGELOG.md index bbbb1e7f303..1f36fa51b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Updating to this release is recommended at your convenience. - Add Electra support and tests for light client functions. - fastssz version bump (better error messages). - SSE implementation that sheds stuck clients. [pr](https://github.com/prysmaticlabs/prysm/pull/14413) +- Added GetPoolAttesterSlashingsV2 endpoint. ### Changed diff --git a/api/server/structs/endpoints_beacon.go b/api/server/structs/endpoints_beacon.go index 9073dbc6456..aff21aad55a 100644 --- a/api/server/structs/endpoints_beacon.go +++ b/api/server/structs/endpoints_beacon.go @@ -176,7 +176,8 @@ type BLSToExecutionChangesPoolResponse struct { } type GetAttesterSlashingsResponse struct { - Data []*AttesterSlashing `json:"data"` + Version string `json:"version,omitempty"` + Data json.RawMessage `json:"data"` // Accepts both `[]*AttesterSlashing` and `[]*AttesterSlashingElectra` types } type GetProposerSlashingsResponse struct { diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index ac51e8b2586..f6b47009582 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -688,6 +688,15 @@ func (s *Service) beaconEndpoints( handler: server.GetAttesterSlashings, methods: []string{http.MethodGet}, }, + { + template: "/eth/v2/beacon/pool/attester_slashings", + name: namespace + ".GetAttesterSlashingsV2", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + }, + handler: server.GetAttesterSlashingsV2, + methods: []string{http.MethodGet}, + }, { template: "/eth/v1/beacon/pool/attester_slashings", name: namespace + ".SubmitAttesterSlashings", diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index 185182ff49c..d77dc4257b5 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -42,7 +42,7 @@ func Test_endpoints(t *testing.T) { "/eth/v1/beacon/blinded_blocks/{block_id}": {http.MethodGet}, "/eth/v1/beacon/pool/attestations": {http.MethodGet, http.MethodPost}, "/eth/v1/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost}, - "/eth/v2/beacon/pool/attester_slashings": {http.MethodPost}, + "/eth/v2/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost}, "/eth/v1/beacon/pool/proposer_slashings": {http.MethodGet, http.MethodPost}, "/eth/v1/beacon/pool/sync_committees": {http.MethodPost}, "/eth/v1/beacon/pool/voluntary_exits": {http.MethodGet, http.MethodPost}, diff --git a/beacon-chain/rpc/eth/beacon/handlers_pool.go b/beacon-chain/rpc/eth/beacon/handlers_pool.go index 0d92b94b0bb..0b5612bae65 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_pool.go +++ b/beacon-chain/rpc/eth/beacon/handlers_pool.go @@ -468,19 +468,65 @@ func (s *Server) GetAttesterSlashings(w http.ResponseWriter, r *http.Request) { return } sourceSlashings := s.SlashingsPool.PendingAttesterSlashings(ctx, headState, true /* return unlimited slashings */) - ss := make([]*eth.AttesterSlashing, 0, len(sourceSlashings)) - for _, slashing := range sourceSlashings { - s, ok := slashing.(*eth.AttesterSlashing) - if ok { - ss = append(ss, s) - } else { - httputil.HandleError(w, fmt.Sprintf("unable to convert slashing of type %T", slashing), http.StatusInternalServerError) + slashings := make([]*structs.AttesterSlashing, len(sourceSlashings)) + for i, slashing := range sourceSlashings { + as, ok := slashing.(*eth.AttesterSlashing) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Unable to convert slashing of type %T", slashing), http.StatusInternalServerError) return } + slashings[i] = structs.AttesterSlashingFromConsensus(as) } - slashings := structs.AttesterSlashingsFromConsensus(ss) + attBytes, err := json.Marshal(slashings) + if err != nil { + httputil.HandleError(w, fmt.Sprintf("Failed to marshal slashings: %v", err), http.StatusInternalServerError) + return + } + httputil.WriteJson(w, &structs.GetAttesterSlashingsResponse{Data: attBytes}) +} - httputil.WriteJson(w, &structs.GetAttesterSlashingsResponse{Data: slashings}) +// GetAttesterSlashingsV2 retrieves attester slashings known by the node but +// not necessarily incorporated into any block, supporting both AttesterSlashing and AttesterSlashingElectra. +func (s *Server) GetAttesterSlashingsV2(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "beacon.GetAttesterSlashingsV2") + defer span.End() + + headState, err := s.ChainInfoFetcher.HeadStateReadOnly(ctx) + if err != nil { + httputil.HandleError(w, "Could not get head state: "+err.Error(), http.StatusInternalServerError) + return + } + var attStructs []interface{} + sourceSlashings := s.SlashingsPool.PendingAttesterSlashings(ctx, headState, true /* return unlimited slashings */) + for _, slashing := range sourceSlashings { + if slashing.Version() >= version.Electra { + a, ok := slashing.(*eth.AttesterSlashingElectra) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Unable to convert electra slashing of type %T to an Electra slashing", slashing), http.StatusInternalServerError) + return + } + attStruct := structs.AttesterSlashingElectraFromConsensus(a) + attStructs = append(attStructs, attStruct) + } else { + a, ok := slashing.(*eth.AttesterSlashing) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Unable to convert slashing of type %T to a Phase0 slashing", slashing), http.StatusInternalServerError) + return + } + attStruct := structs.AttesterSlashingFromConsensus(a) + attStructs = append(attStructs, attStruct) + } + } + attBytes, err := json.Marshal(attStructs) + if err != nil { + httputil.HandleError(w, fmt.Sprintf("Failed to marshal slashing: %v", err), http.StatusInternalServerError) + return + } + resp := &structs.GetAttesterSlashingsResponse{ + Version: version.String(sourceSlashings[0].Version()), + Data: attBytes, + } + httputil.WriteJson(w, resp) } // SubmitAttesterSlashings submits an attester slashing object to node's pool and diff --git a/beacon-chain/rpc/eth/beacon/handlers_pool_test.go b/beacon-chain/rpc/eth/beacon/handlers_pool_test.go index 593eb8a3d85..9b549c1e45f 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_pool_test.go +++ b/beacon-chain/rpc/eth/beacon/handlers_pool_test.go @@ -985,9 +985,7 @@ func TestSubmitSignedBLSToExecutionChanges_Failures(t *testing.T) { } func TestGetAttesterSlashings(t *testing.T) { - bs, err := util.NewBeaconState() - require.NoError(t, err) - slashing1 := ðpbv1alpha1.AttesterSlashing{ + slashing1PreElectra := ðpbv1alpha1.AttesterSlashing{ Attestation_1: ðpbv1alpha1.IndexedAttestation{ AttestingIndices: []uint64{1, 10}, Data: ðpbv1alpha1.AttestationData{ @@ -1023,7 +1021,7 @@ func TestGetAttesterSlashings(t *testing.T) { Signature: bytesutil.PadTo([]byte("signature2"), 96), }, } - slashing2 := ðpbv1alpha1.AttesterSlashing{ + slashing2PreElectra := ðpbv1alpha1.AttesterSlashing{ Attestation_1: ðpbv1alpha1.IndexedAttestation{ AttestingIndices: []uint64{3, 30}, Data: ðpbv1alpha1.AttestationData{ @@ -1059,23 +1057,168 @@ func TestGetAttesterSlashings(t *testing.T) { Signature: bytesutil.PadTo([]byte("signature4"), 96), }, } - - s := &Server{ - ChainInfoFetcher: &blockchainmock.ChainService{State: bs}, - SlashingsPool: &slashingsmock.PoolMock{PendingAttSlashings: []ethpbv1alpha1.AttSlashing{slashing1, slashing2}}, + slashing1PostElectra := ðpbv1alpha1.AttesterSlashingElectra{ + Attestation_1: ðpbv1alpha1.IndexedAttestationElectra{ + AttestingIndices: []uint64{1, 10}, + Data: ðpbv1alpha1.AttestationData{ + Slot: 1, + CommitteeIndex: 1, + BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot1"), 32), + Source: ðpbv1alpha1.Checkpoint{ + Epoch: 1, + Root: bytesutil.PadTo([]byte("sourceroot1"), 32), + }, + Target: ðpbv1alpha1.Checkpoint{ + Epoch: 10, + Root: bytesutil.PadTo([]byte("targetroot1"), 32), + }, + }, + Signature: bytesutil.PadTo([]byte("signature1"), 96), + }, + Attestation_2: ðpbv1alpha1.IndexedAttestationElectra{ + AttestingIndices: []uint64{2, 20}, + Data: ðpbv1alpha1.AttestationData{ + Slot: 2, + CommitteeIndex: 2, + BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot2"), 32), + Source: ðpbv1alpha1.Checkpoint{ + Epoch: 2, + Root: bytesutil.PadTo([]byte("sourceroot2"), 32), + }, + Target: ðpbv1alpha1.Checkpoint{ + Epoch: 20, + Root: bytesutil.PadTo([]byte("targetroot2"), 32), + }, + }, + Signature: bytesutil.PadTo([]byte("signature2"), 96), + }, + } + slashing2PostElectra := ðpbv1alpha1.AttesterSlashingElectra{ + Attestation_1: ðpbv1alpha1.IndexedAttestationElectra{ + AttestingIndices: []uint64{3, 30}, + Data: ðpbv1alpha1.AttestationData{ + Slot: 3, + CommitteeIndex: 3, + BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot3"), 32), + Source: ðpbv1alpha1.Checkpoint{ + Epoch: 3, + Root: bytesutil.PadTo([]byte("sourceroot3"), 32), + }, + Target: ðpbv1alpha1.Checkpoint{ + Epoch: 30, + Root: bytesutil.PadTo([]byte("targetroot3"), 32), + }, + }, + Signature: bytesutil.PadTo([]byte("signature3"), 96), + }, + Attestation_2: ðpbv1alpha1.IndexedAttestationElectra{ + AttestingIndices: []uint64{4, 40}, + Data: ðpbv1alpha1.AttestationData{ + Slot: 4, + CommitteeIndex: 4, + BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot4"), 32), + Source: ðpbv1alpha1.Checkpoint{ + Epoch: 4, + Root: bytesutil.PadTo([]byte("sourceroot4"), 32), + }, + Target: ðpbv1alpha1.Checkpoint{ + Epoch: 40, + Root: bytesutil.PadTo([]byte("targetroot4"), 32), + }, + }, + Signature: bytesutil.PadTo([]byte("signature4"), 96), + }, } - request := httptest.NewRequest(http.MethodGet, "http://example.com/beacon/pool/attester_slashings", nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + t.Run("V1", func(t *testing.T) { + bs, err := util.NewBeaconState() + require.NoError(t, err) - s.GetAttesterSlashings(writer, request) - require.Equal(t, http.StatusOK, writer.Code) - resp := &structs.GetAttesterSlashingsResponse{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - assert.Equal(t, 2, len(resp.Data)) + s := &Server{ + ChainInfoFetcher: &blockchainmock.ChainService{State: bs}, + SlashingsPool: &slashingsmock.PoolMock{PendingAttSlashings: []ethpbv1alpha1.AttSlashing{slashing1PreElectra, slashing2PreElectra}}, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/attester_slashings", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAttesterSlashings(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + resp := &structs.GetAttesterSlashingsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + var slashings []*structs.AttesterSlashing + require.NoError(t, json.Unmarshal(resp.Data, &slashings)) + + ss, err := structs.AttesterSlashingsToConsensus(slashings) + require.NoError(t, err) + + require.DeepEqual(t, slashing1PreElectra, ss[0]) + require.DeepEqual(t, slashing2PreElectra, ss[1]) + }) + t.Run("V2-post-electra", func(t *testing.T) { + bs, err := util.NewBeaconStateElectra() + require.NoError(t, err) + + s := &Server{ + ChainInfoFetcher: &blockchainmock.ChainService{State: bs}, + SlashingsPool: &slashingsmock.PoolMock{PendingAttSlashings: []ethpbv1alpha1.AttSlashing{slashing1PostElectra, slashing2PostElectra}}, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v2/beacon/pool/attester_slashings", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAttesterSlashingsV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + resp := &structs.GetAttesterSlashingsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + assert.Equal(t, "electra", resp.Version) + + // Unmarshal resp.Data into a slice of slashings + var slashings []*structs.AttesterSlashingElectra + require.NoError(t, json.Unmarshal(resp.Data, &slashings)) + + ss, err := structs.AttesterSlashingsElectraToConsensus(slashings) + require.NoError(t, err) + + require.DeepEqual(t, slashing1PostElectra, ss[0]) + require.DeepEqual(t, slashing2PostElectra, ss[1]) + }) + t.Run("V2-pre-electra", func(t *testing.T) { + bs, err := util.NewBeaconState() + require.NoError(t, err) + + s := &Server{ + ChainInfoFetcher: &blockchainmock.ChainService{State: bs}, + SlashingsPool: &slashingsmock.PoolMock{PendingAttSlashings: []ethpbv1alpha1.AttSlashing{slashing1PreElectra, slashing2PreElectra}}, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/attester_slashings", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAttesterSlashingsV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code) + resp := &structs.GetAttesterSlashingsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + var slashings []*structs.AttesterSlashing + require.NoError(t, json.Unmarshal(resp.Data, &slashings)) + + ss, err := structs.AttesterSlashingsToConsensus(slashings) + require.NoError(t, err) + + require.DeepEqual(t, slashing1PreElectra, ss[0]) + require.DeepEqual(t, slashing2PreElectra, ss[1]) + }) } func TestGetProposerSlashings(t *testing.T) {