diff --git a/CHANGELOG.md b/CHANGELOG.md index 2882c93d1cb1..742c86102642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - GetBeaconStateV2: add Electra case. - Implement [consensus-specs/3875](https://github.com/ethereum/consensus-specs/pull/3875) - Tests to ensure sepolia config matches the official upstream yaml +- Added GetAggregatedAttestationV2 endpoint. ### Changed diff --git a/api/server/structs/endpoints_validator.go b/api/server/structs/endpoints_validator.go index dfb94daea20a..e87c8373347c 100644 --- a/api/server/structs/endpoints_validator.go +++ b/api/server/structs/endpoints_validator.go @@ -7,7 +7,8 @@ import ( ) type AggregateAttestationResponse struct { - Data *Attestation `json:"data"` + Version string `json:"version,omitempty"` + Data json.RawMessage `json:"data"` } type SubmitContributionAndProofsRequest struct { diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 3d99b2d291ee..cdf25998dd58 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -199,6 +199,15 @@ func (s *Service) validatorEndpoints( handler: server.GetAggregateAttestation, methods: []string{http.MethodGet}, }, + { + template: "/eth/v2/validator/aggregate_attestation", + name: namespace + ".GetAggregateAttestationV2", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + }, + handler: server.GetAggregateAttestationV2, + methods: []string{http.MethodGet}, + }, { template: "/eth/v1/validator/contribution_and_proofs", name: namespace + ".SubmitContributionAndProofs", diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index 6b7799303f31..0a574ae935ff 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -98,6 +98,7 @@ func Test_endpoints(t *testing.T) { "/eth/v1/validator/blinded_blocks/{slot}": {http.MethodGet}, "/eth/v1/validator/attestation_data": {http.MethodGet}, "/eth/v1/validator/aggregate_attestation": {http.MethodGet}, + "/eth/v2/validator/aggregate_attestation": {http.MethodGet}, "/eth/v1/validator/aggregate_and_proofs": {http.MethodPost}, "/eth/v1/validator/beacon_committee_subscriptions": {http.MethodPost}, "/eth/v1/validator/sync_committee_subscriptions": {http.MethodPost}, diff --git a/beacon-chain/rpc/eth/validator/BUILD.bazel b/beacon-chain/rpc/eth/validator/BUILD.bazel index dc941ae72578..fbeef46ceb49 100644 --- a/beacon-chain/rpc/eth/validator/BUILD.bazel +++ b/beacon-chain/rpc/eth/validator/BUILD.bazel @@ -40,6 +40,7 @@ go_library( "//monitoring/tracing/trace:go_default_library", "//network/httputil:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//proto/prysm/v1alpha1/attestation/aggregation/attestations:go_default_library", "//runtime/version:go_default_library", "//time/slots:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", @@ -92,6 +93,7 @@ go_test( "//time/slots:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_pkg_errors//:go_default_library", + "@com_github_prysmaticlabs_go_bitfield//:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], diff --git a/beacon-chain/rpc/eth/validator/handlers.go b/beacon-chain/rpc/eth/validator/handlers.go index 597af22476a6..c1dcb9504a0c 100644 --- a/beacon-chain/rpc/eth/validator/handlers.go +++ b/beacon-chain/rpc/eth/validator/handlers.go @@ -2,11 +2,13 @@ package validator import ( "bytes" + "cmp" "context" "encoding/json" "fmt" "io" "net/http" + "slices" "sort" "strconv" "time" @@ -31,6 +33,8 @@ import ( "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpbalpha "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1/attestation/aggregation/attestations" + "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/time/slots" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -46,71 +50,159 @@ func (s *Server) GetAggregateAttestation(w http.ResponseWriter, r *http.Request) if !ok { return } - _, slot, ok := shared.UintFromQuery(w, r, "slot", true) if !ok { return } - var match ethpbalpha.Att - var err error - - match, err = matchingAtt(s.AttestationsPool.AggregatedAttestations(), primitives.Slot(slot), attDataRoot) + agg := s.aggregatedAttestation(w, primitives.Slot(slot), attDataRoot, 0) + if agg == nil { + return + } + typedAgg, ok := agg.(*ethpbalpha.Attestation) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Attestation is not of type %T", ðpbalpha.Attestation{}), http.StatusInternalServerError) + return + } + data, err := json.Marshal(structs.AttFromConsensus(typedAgg)) if err != nil { - httputil.HandleError(w, "Could not get matching attestation: "+err.Error(), http.StatusInternalServerError) + httputil.HandleError(w, "Could not marshal attestation: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteJson(w, &structs.AggregateAttestationResponse{Data: data}) +} + +// GetAggregateAttestationV2 aggregates all attestations matching the given attestation data root and slot, returning the aggregated result. +func (s *Server) GetAggregateAttestationV2(w http.ResponseWriter, r *http.Request) { + _, span := trace.StartSpan(r.Context(), "validator.GetAggregateAttestationV2") + defer span.End() + + _, attDataRoot, ok := shared.HexFromQuery(w, r, "attestation_data_root", fieldparams.RootLength, true) + if !ok { + return + } + _, slot, ok := shared.UintFromQuery(w, r, "slot", true) + if !ok { + return + } + _, index, ok := shared.UintFromQuery(w, r, "committee_index", true) + if !ok { return } - if match == nil { - atts, err := s.AttestationsPool.UnaggregatedAttestations() + + agg := s.aggregatedAttestation(w, primitives.Slot(slot), attDataRoot, primitives.CommitteeIndex(index)) + if agg == nil { + return + } + resp := &structs.AggregateAttestationResponse{ + Version: version.String(agg.Version()), + } + if agg.Version() >= version.Electra { + typedAgg, ok := agg.(*ethpbalpha.AttestationElectra) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Attestation is not of type %T", ðpbalpha.AttestationElectra{}), http.StatusInternalServerError) + return + } + data, err := json.Marshal(structs.AttElectraFromConsensus(typedAgg)) if err != nil { - httputil.HandleError(w, "Could not get unaggregated attestations: "+err.Error(), http.StatusInternalServerError) + httputil.HandleError(w, "Could not marshal attestation: "+err.Error(), http.StatusInternalServerError) + return + } + resp.Data = data + } else { + typedAgg, ok := agg.(*ethpbalpha.Attestation) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Attestation is not of type %T", ðpbalpha.Attestation{}), http.StatusInternalServerError) return } - match, err = matchingAtt(atts, primitives.Slot(slot), attDataRoot) + data, err := json.Marshal(structs.AttFromConsensus(typedAgg)) if err != nil { - httputil.HandleError(w, "Could not get matching attestation: "+err.Error(), http.StatusInternalServerError) + httputil.HandleError(w, "Could not marshal attestation: "+err.Error(), http.StatusInternalServerError) return } + resp.Data = data } - if match == nil { - httputil.HandleError(w, "No matching attestation found", http.StatusNotFound) - return - } - - response := &structs.AggregateAttestationResponse{ - Data: &structs.Attestation{ - AggregationBits: hexutil.Encode(match.GetAggregationBits()), - Data: &structs.AttestationData{ - Slot: strconv.FormatUint(uint64(match.GetData().Slot), 10), - CommitteeIndex: strconv.FormatUint(uint64(match.GetData().CommitteeIndex), 10), - BeaconBlockRoot: hexutil.Encode(match.GetData().BeaconBlockRoot), - Source: &structs.Checkpoint{ - Epoch: strconv.FormatUint(uint64(match.GetData().Source.Epoch), 10), - Root: hexutil.Encode(match.GetData().Source.Root), - }, - Target: &structs.Checkpoint{ - Epoch: strconv.FormatUint(uint64(match.GetData().Target.Epoch), 10), - Root: hexutil.Encode(match.GetData().Target.Root), - }, - }, - Signature: hexutil.Encode(match.GetSignature()), - }} - httputil.WriteJson(w, response) + httputil.WriteJson(w, resp) +} + +func (s *Server) aggregatedAttestation(w http.ResponseWriter, slot primitives.Slot, attDataRoot []byte, index primitives.CommitteeIndex) ethpbalpha.Att { + var err error + + match, err := matchingAtts(s.AttestationsPool.AggregatedAttestations(), slot, attDataRoot, index) + if err != nil { + httputil.HandleError(w, "Could not get matching attestations: "+err.Error(), http.StatusInternalServerError) + return nil + } + if len(match) > 0 { + // If there are multiple matching aggregated attestations, + // then we return the one with the most aggregation bits. + slices.SortFunc(match, func(a, b ethpbalpha.Att) int { + return cmp.Compare(a.GetAggregationBits().Count(), b.GetAggregationBits().Count()) + }) + return match[0] + } + + atts, err := s.AttestationsPool.UnaggregatedAttestations() + if err != nil { + httputil.HandleError(w, "Could not get unaggregated attestations: "+err.Error(), http.StatusInternalServerError) + return nil + } + match, err = matchingAtts(atts, slot, attDataRoot, index) + if err != nil { + httputil.HandleError(w, "Could not get matching attestations: "+err.Error(), http.StatusInternalServerError) + return nil + } + if len(match) == 0 { + httputil.HandleError(w, "No matching attestations found", http.StatusNotFound) + return nil + } + agg, err := attestations.Aggregate(match) + if err != nil { + httputil.HandleError(w, "Could not aggregate unaggregated attestations: "+err.Error(), http.StatusInternalServerError) + return nil + } + + // Aggregating unaggregated attestations will in theory always return just one aggregate, + // so we can take the first one and be done with it. + return agg[0] } -func matchingAtt(atts []ethpbalpha.Att, slot primitives.Slot, attDataRoot []byte) (ethpbalpha.Att, error) { +func matchingAtts(atts []ethpbalpha.Att, slot primitives.Slot, attDataRoot []byte, index primitives.CommitteeIndex) ([]ethpbalpha.Att, error) { + if len(atts) == 0 { + return []ethpbalpha.Att{}, nil + } + + postElectra := atts[0].Version() >= version.Electra + + result := make([]ethpbalpha.Att, 0) for _, att := range atts { - if att.GetData().Slot == slot { - root, err := att.GetData().HashTreeRoot() + if att.GetData().Slot != slot { + continue + } + // We ignore the committee index from the request before Electra. + // This is because before Electra the committee index is part of the attestation data, + // meaning that comparing the data root is sufficient. + // Post-Electra the committee index in the data root is always 0, so we need to + // compare the committee index separately. + if postElectra { + ci, err := att.GetCommitteeIndex() if err != nil { - return nil, errors.Wrap(err, "could not get attestation data root") + return nil, err } - if bytes.Equal(root[:], attDataRoot) { - return att, nil + if ci != index { + continue } } + root, err := att.GetData().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "could not get attestation data root") + } + if bytes.Equal(root[:], attDataRoot) { + result = append(result, att) + } } - return nil, nil + + return result, nil } // SubmitContributionAndProofs publishes multiple signed sync committee contribution and proofs. diff --git a/beacon-chain/rpc/eth/validator/handlers_test.go b/beacon-chain/rpc/eth/validator/handlers_test.go index 0aa758fe48b8..cb268a831438 100644 --- a/beacon-chain/rpc/eth/validator/handlers_test.go +++ b/beacon-chain/rpc/eth/validator/handlers_test.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/v5/api/server/structs" mockChain "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" builderTest "github.com/prysmaticlabs/prysm/v5/beacon-chain/builder/testing" @@ -45,233 +46,527 @@ import ( ) func TestGetAggregateAttestation(t *testing.T) { - root1 := bytesutil.PadTo([]byte("root1"), 32) - sig1 := bytesutil.PadTo([]byte("sig1"), fieldparams.BLSSignatureLength) - attSlot1 := ðpbalpha.Attestation{ - AggregationBits: []byte{0, 1}, - Data: ðpbalpha.AttestationData{ - Slot: 1, - CommitteeIndex: 1, - BeaconBlockRoot: root1, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root1, - }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root1, - }, - }, - Signature: sig1, - } - root21 := bytesutil.PadTo([]byte("root2_1"), 32) - sig21 := bytesutil.PadTo([]byte("sig2_1"), fieldparams.BLSSignatureLength) - attslot21 := ðpbalpha.Attestation{ - AggregationBits: []byte{0, 1, 1}, - Data: ðpbalpha.AttestationData{ - Slot: 2, - CommitteeIndex: 1, - BeaconBlockRoot: root21, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root21, - }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root21, - }, - }, - Signature: sig21, - } - root22 := bytesutil.PadTo([]byte("root2_2"), 32) - sig22 := bytesutil.PadTo([]byte("sig2_2"), fieldparams.BLSSignatureLength) - attslot22 := ðpbalpha.Attestation{ - AggregationBits: []byte{0, 1, 1, 1}, - Data: ðpbalpha.AttestationData{ - Slot: 2, - CommitteeIndex: 1, - BeaconBlockRoot: root22, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root22, - }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root22, + t.Run("V1", func(t *testing.T) { + root1 := bytesutil.PadTo([]byte("root1"), 32) + sig1 := bytesutil.PadTo([]byte("sig1"), fieldparams.BLSSignatureLength) + attSlot1 := ðpbalpha.Attestation{ + AggregationBits: []byte{0, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 1, + CommitteeIndex: 1, + BeaconBlockRoot: root1, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root1, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root1, + }, }, - }, - Signature: sig22, - } - root31 := bytesutil.PadTo([]byte("root3_1"), 32) - sig31 := bls.NewAggregateSignature().Marshal() - attslot31 := ðpbalpha.Attestation{ - AggregationBits: []byte{1, 0}, - Data: ðpbalpha.AttestationData{ - Slot: 3, - CommitteeIndex: 1, - BeaconBlockRoot: root31, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root31, + Signature: sig1, + } + root21 := bytesutil.PadTo([]byte("root2_1"), 32) + sig21 := bytesutil.PadTo([]byte("sig2_1"), fieldparams.BLSSignatureLength) + attslot21 := ðpbalpha.Attestation{ + AggregationBits: []byte{0, 1, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 2, + CommitteeIndex: 1, + BeaconBlockRoot: root21, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root21, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root21, + }, }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root31, + Signature: sig21, + } + root22 := bytesutil.PadTo([]byte("root2_2"), 32) + sig22 := bytesutil.PadTo([]byte("sig2_2"), fieldparams.BLSSignatureLength) + attslot22 := ðpbalpha.Attestation{ + AggregationBits: []byte{0, 1, 1, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 2, + CommitteeIndex: 1, + BeaconBlockRoot: root22, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root22, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root22, + }, }, - }, - Signature: sig31, - } - root32 := bytesutil.PadTo([]byte("root3_2"), 32) - sig32 := bls.NewAggregateSignature().Marshal() - attslot32 := ðpbalpha.Attestation{ - AggregationBits: []byte{0, 1}, - Data: ðpbalpha.AttestationData{ - Slot: 3, - CommitteeIndex: 1, - BeaconBlockRoot: root32, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root32, + Signature: sig22, + } + root31 := bytesutil.PadTo([]byte("root3_1"), 32) + sig31 := bls.NewAggregateSignature().Marshal() + attslot31 := ðpbalpha.Attestation{ + AggregationBits: []byte{1, 0}, + Data: ðpbalpha.AttestationData{ + Slot: 3, + CommitteeIndex: 1, + BeaconBlockRoot: root31, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root31, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root31, + }, }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root32, + Signature: sig31, + } + root32 := bytesutil.PadTo([]byte("root3_2"), 32) + sig32 := bls.NewAggregateSignature().Marshal() + attslot32 := ðpbalpha.Attestation{ + AggregationBits: []byte{0, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 3, + CommitteeIndex: 1, + BeaconBlockRoot: root32, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root32, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root32, + }, }, - }, - Signature: sig32, - } - - pool := attestations.NewPool() - err := pool.SaveAggregatedAttestations([]ethpbalpha.Att{attSlot1, attslot21, attslot22}) - assert.NoError(t, err) - err = pool.SaveUnaggregatedAttestations([]ethpbalpha.Att{attslot31, attslot32}) - assert.NoError(t, err) - - s := &Server{ - AttestationsPool: pool, - } - - t.Run("matching aggregated att", func(t *testing.T) { - reqRoot, err := attslot22.Data.HashTreeRoot() - require.NoError(t, err) - attDataRoot := hexutil.Encode(reqRoot[:]) - url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + Signature: sig32, + } - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusOK, writer.Code) - resp := &structs.AggregateAttestationResponse{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - assert.DeepEqual(t, "0x00010101", resp.Data.AggregationBits) - assert.DeepEqual(t, hexutil.Encode(sig22), resp.Data.Signature) - assert.Equal(t, "2", resp.Data.Data.Slot) - assert.Equal(t, "1", resp.Data.Data.CommitteeIndex) - assert.DeepEqual(t, hexutil.Encode(root22), resp.Data.Data.BeaconBlockRoot) - require.NotNil(t, resp.Data.Data.Source) - assert.Equal(t, "1", resp.Data.Data.Source.Epoch) - assert.DeepEqual(t, hexutil.Encode(root22), resp.Data.Data.Source.Root) - require.NotNil(t, resp.Data.Data.Target) - assert.Equal(t, "1", resp.Data.Data.Target.Epoch) - assert.DeepEqual(t, hexutil.Encode(root22), resp.Data.Data.Target.Root) - }) - t.Run("matching unaggregated att", func(t *testing.T) { - reqRoot, err := attslot32.Data.HashTreeRoot() - require.NoError(t, err) - attDataRoot := hexutil.Encode(reqRoot[:]) - url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + pool := attestations.NewPool() + err := pool.SaveAggregatedAttestations([]ethpbalpha.Att{attSlot1, attslot21, attslot22}) + assert.NoError(t, err) + err = pool.SaveUnaggregatedAttestations([]ethpbalpha.Att{attslot31, attslot32}) + assert.NoError(t, err) - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusOK, writer.Code) - resp := &structs.AggregateAttestationResponse{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) - require.NotNil(t, resp) - require.NotNil(t, resp.Data) - assert.DeepEqual(t, "0x0001", resp.Data.AggregationBits) - assert.DeepEqual(t, hexutil.Encode(sig32), resp.Data.Signature) - assert.Equal(t, "3", resp.Data.Data.Slot) - assert.Equal(t, "1", resp.Data.Data.CommitteeIndex) - assert.DeepEqual(t, hexutil.Encode(root32), resp.Data.Data.BeaconBlockRoot) - require.NotNil(t, resp.Data.Data.Source) - assert.Equal(t, "1", resp.Data.Data.Source.Epoch) - assert.DeepEqual(t, hexutil.Encode(root32), resp.Data.Data.Source.Root) - require.NotNil(t, resp.Data.Data.Target) - assert.Equal(t, "1", resp.Data.Data.Target.Epoch) - assert.DeepEqual(t, hexutil.Encode(root32), resp.Data.Data.Target.Root) - }) - t.Run("no matching attestation", func(t *testing.T) { - attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) - url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + s := &Server{ + AttestationsPool: pool, + } + t.Run("matching aggregated att", func(t *testing.T) { + reqRoot, err := attslot22.Data.HashTreeRoot() + require.NoError(t, err) + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.AggregateAttestationResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation)) + + assert.Equal(t, "0x00010101", attestation.AggregationBits) + assert.Equal(t, hexutil.Encode(sig22), attestation.Signature) + assert.Equal(t, "2", attestation.Data.Slot) + assert.Equal(t, "1", attestation.Data.CommitteeIndex) + assert.Equal(t, hexutil.Encode(root22), attestation.Data.BeaconBlockRoot) + + // Source checkpoint checks + require.NotNil(t, attestation.Data.Source) + assert.Equal(t, "1", attestation.Data.Source.Epoch) + assert.Equal(t, hexutil.Encode(root22), attestation.Data.Source.Root) + + // Target checkpoint checks + require.NotNil(t, attestation.Data.Target) + assert.Equal(t, "1", attestation.Data.Target.Epoch) + assert.Equal(t, hexutil.Encode(root22), attestation.Data.Target.Root) + }) + t.Run("matching unaggregated att", func(t *testing.T) { + reqRoot, err := attslot32.Data.HashTreeRoot() + require.NoError(t, err) + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.AggregateAttestationResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation)) + + assert.Equal(t, "0x0001", attestation.AggregationBits) + assert.Equal(t, hexutil.Encode(sig32), attestation.Signature) + assert.Equal(t, "3", attestation.Data.Slot) + assert.Equal(t, "1", attestation.Data.CommitteeIndex) + assert.Equal(t, hexutil.Encode(root32), attestation.Data.BeaconBlockRoot) + + // Source checkpoint checks + require.NotNil(t, attestation.Data.Source) + assert.Equal(t, "1", attestation.Data.Source.Epoch) + assert.Equal(t, hexutil.Encode(root32), attestation.Data.Source.Root) + + // Target checkpoint checks + require.NotNil(t, attestation.Data.Target) + assert.Equal(t, "1", attestation.Data.Target.Epoch) + assert.Equal(t, hexutil.Encode(root32), attestation.Data.Target.Root) + }) + t.Run("no matching attestation", func(t *testing.T) { + attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusNotFound, writer.Code) - e := &httputil.DefaultJsonError{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) - assert.Equal(t, http.StatusNotFound, e.Code) - assert.Equal(t, true, strings.Contains(e.Message, "No matching attestation found")) - }) - t.Run("no attestation_data_root provided", func(t *testing.T) { - url := "http://example.com?slot=2" - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusNotFound, writer.Code) - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusBadRequest, writer.Code) - e := &httputil.DefaultJsonError{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) - assert.Equal(t, http.StatusBadRequest, e.Code) - assert.Equal(t, true, strings.Contains(e.Message, "attestation_data_root is required")) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusNotFound, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "No matching attestations found")) + }) + t.Run("no attestation_data_root provided", func(t *testing.T) { + url := "http://example.com?slot=2" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "attestation_data_root is required")) + }) + t.Run("invalid attestation_data_root provided", func(t *testing.T) { + url := "http://example.com?attestation_data_root=foo&slot=2" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "attestation_data_root is invalid")) + }) + t.Run("no slot provided", func(t *testing.T) { + attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + url := "http://example.com?attestation_data_root=" + attDataRoot + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "slot is required")) + }) + t.Run("invalid slot provided", func(t *testing.T) { + attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=foo" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "slot is invalid")) + }) }) - t.Run("invalid attestation_data_root provided", func(t *testing.T) { - url := "http://example.com?attestation_data_root=foo&slot=2" - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + t.Run("V2", func(t *testing.T) { + committeeBits := bitfield.NewBitvector64() + root1 := bytesutil.PadTo([]byte("root1"), 32) + sig1 := bytesutil.PadTo([]byte("sig1"), fieldparams.BLSSignatureLength) + committeeBits.SetBitAt(1, true) + attSlot1 := ðpbalpha.AttestationElectra{ + AggregationBits: []byte{0, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 1, + CommitteeIndex: 1, + BeaconBlockRoot: root1, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root1, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root1, + }, + }, + Signature: sig1, + CommitteeBits: committeeBits, + } + root21 := bytesutil.PadTo([]byte("root2_1"), 32) + sig21 := bytesutil.PadTo([]byte("sig2_1"), fieldparams.BLSSignatureLength) + attslot21 := ðpbalpha.AttestationElectra{ + AggregationBits: []byte{0, 1, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 2, + CommitteeIndex: 1, + BeaconBlockRoot: root21, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root21, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root21, + }, + }, + Signature: sig21, + CommitteeBits: committeeBits, + } + root22 := bytesutil.PadTo([]byte("root2_2"), 32) + sig22 := bytesutil.PadTo([]byte("sig2_2"), fieldparams.BLSSignatureLength) + attslot22 := ðpbalpha.AttestationElectra{ + AggregationBits: []byte{0, 1, 1, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 2, + CommitteeIndex: 1, + BeaconBlockRoot: root22, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root22, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root22, + }, + }, + Signature: sig22, + CommitteeBits: committeeBits, + } + root31 := bytesutil.PadTo([]byte("root3_1"), 32) + sig31 := bls.NewAggregateSignature().Marshal() + attslot31 := ðpbalpha.AttestationElectra{ + AggregationBits: []byte{1, 0}, + Data: ðpbalpha.AttestationData{ + Slot: 3, + CommitteeIndex: 1, + BeaconBlockRoot: root31, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root31, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root31, + }, + }, + Signature: sig31, + CommitteeBits: committeeBits, + } + root32 := bytesutil.PadTo([]byte("root3_2"), 32) + sig32 := bls.NewAggregateSignature().Marshal() + attslot32 := ðpbalpha.AttestationElectra{ + AggregationBits: []byte{0, 1}, + Data: ðpbalpha.AttestationData{ + Slot: 3, + CommitteeIndex: 1, + BeaconBlockRoot: root32, + Source: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root32, + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 1, + Root: root32, + }, + }, + Signature: sig32, + CommitteeBits: committeeBits, + } - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusBadRequest, writer.Code) - e := &httputil.DefaultJsonError{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) - assert.Equal(t, http.StatusBadRequest, e.Code) - assert.Equal(t, true, strings.Contains(e.Message, "attestation_data_root is invalid")) - }) - t.Run("no slot provided", func(t *testing.T) { - attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) - url := "http://example.com?attestation_data_root=" + attDataRoot - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + pool := attestations.NewPool() + err := pool.SaveAggregatedAttestations([]ethpbalpha.Att{attSlot1, attslot21, attslot22}) + assert.NoError(t, err) + err = pool.SaveUnaggregatedAttestations([]ethpbalpha.Att{attslot31, attslot32}) + assert.NoError(t, err) - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusBadRequest, writer.Code) - e := &httputil.DefaultJsonError{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) - assert.Equal(t, http.StatusBadRequest, e.Code) - assert.Equal(t, true, strings.Contains(e.Message, "slot is required")) - }) - t.Run("invalid slot provided", func(t *testing.T) { - attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) - url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=foo" - request := httptest.NewRequest(http.MethodGet, url, nil) - writer := httptest.NewRecorder() - writer.Body = &bytes.Buffer{} + s := &Server{ + AttestationsPool: pool, + } + t.Run("matching aggregated att", func(t *testing.T) { + reqRoot, err := attslot22.Data.HashTreeRoot() + require.NoError(t, err) + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.AggregateAttestationResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + var attestation structs.AttestationElectra + require.NoError(t, json.Unmarshal(resp.Data, &attestation)) + + assert.Equal(t, "0x00010101", attestation.AggregationBits) + assert.Equal(t, "0x0200000000000000", attestation.CommitteeBits) + assert.Equal(t, hexutil.Encode(sig22), attestation.Signature) + assert.Equal(t, "2", attestation.Data.Slot) + assert.Equal(t, "1", attestation.Data.CommitteeIndex) + assert.Equal(t, hexutil.Encode(root22), attestation.Data.BeaconBlockRoot) + + // Source checkpoint checks + require.NotNil(t, attestation.Data.Source) + assert.Equal(t, "1", attestation.Data.Source.Epoch) + assert.Equal(t, hexutil.Encode(root22), attestation.Data.Source.Root) + + // Target checkpoint checks + require.NotNil(t, attestation.Data.Target) + assert.Equal(t, "1", attestation.Data.Target.Epoch) + assert.Equal(t, hexutil.Encode(root22), attestation.Data.Target.Root) + }) + t.Run("matching unaggregated att", func(t *testing.T) { + reqRoot, err := attslot32.Data.HashTreeRoot() + require.NoError(t, err) + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.AggregateAttestationResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + var attestation structs.AttestationElectra + require.NoError(t, json.Unmarshal(resp.Data, &attestation)) + + assert.Equal(t, "0x0001", attestation.AggregationBits) + assert.Equal(t, "0x0200000000000000", attestation.CommitteeBits) + assert.Equal(t, hexutil.Encode(sig32), attestation.Signature) + assert.Equal(t, "3", attestation.Data.Slot) + assert.Equal(t, "1", attestation.Data.CommitteeIndex) + assert.Equal(t, hexutil.Encode(root32), attestation.Data.BeaconBlockRoot) + + // Source checkpoint checks + require.NotNil(t, attestation.Data.Source) + assert.Equal(t, "1", attestation.Data.Source.Epoch) + assert.Equal(t, hexutil.Encode(root32), attestation.Data.Source.Root) + + // Target checkpoint checks + require.NotNil(t, attestation.Data.Target) + assert.Equal(t, "1", attestation.Data.Target.Epoch) + assert.Equal(t, hexutil.Encode(root32), attestation.Data.Target.Root) + }) + t.Run("no matching attestation", func(t *testing.T) { + //attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + reqRoot, err := attslot32.Data.HashTreeRoot() + require.NoError(t, err) + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusNotFound, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusNotFound, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "No matching attestations found")) + }) + t.Run("no attestation_data_root provided", func(t *testing.T) { + url := "http://example.com?slot=2" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "attestation_data_root is required")) + }) - s.GetAggregateAttestation(writer, request) - assert.Equal(t, http.StatusBadRequest, writer.Code) - e := &httputil.DefaultJsonError{} - require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) - assert.Equal(t, http.StatusBadRequest, e.Code) - assert.Equal(t, true, strings.Contains(e.Message, "slot is invalid")) + t.Run("invalid attestation_data_root provided", func(t *testing.T) { + url := "http://example.com?attestation_data_root=foo&slot=2&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "attestation_data_root is invalid")) + }) + t.Run("no slot provided", func(t *testing.T) { + attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + url := "http://example.com?attestation_data_root=" + attDataRoot + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "slot is required")) + }) + t.Run("invalid slot provided", func(t *testing.T) { + attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=foo" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "slot is invalid")) + }) + t.Run("invalid committee_index provided", func(t *testing.T) { + attDataRoot := hexutil.Encode(bytesutil.PadTo([]byte("foo"), 32)) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3&committee_index=foo" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + e := &httputil.DefaultJsonError{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e)) + assert.Equal(t, http.StatusBadRequest, e.Code) + assert.Equal(t, true, strings.Contains(e.Message, "committee_index is invalid")) + }) }) } @@ -331,7 +626,10 @@ func TestGetAggregateAttestation_SameSlotAndRoot_ReturnMostAggregationBits(t *te resp := &structs.AggregateAttestationResponse{} require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) require.NotNil(t, resp) - assert.DeepEqual(t, "0x03000001", resp.Data.AggregationBits) + + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation)) + assert.DeepEqual(t, "0x03000001", attestation.AggregationBits) } func TestSubmitContributionAndProofs(t *testing.T) { diff --git a/validator/client/beacon-api/submit_aggregate_selection_proof.go b/validator/client/beacon-api/submit_aggregate_selection_proof.go index 3aeda87136d6..1d7269f0277f 100644 --- a/validator/client/beacon-api/submit_aggregate_selection_proof.go +++ b/validator/client/beacon-api/submit_aggregate_selection_proof.go @@ -2,6 +2,7 @@ package beacon_api import ( "context" + "encoding/json" "net/url" "strconv" @@ -52,7 +53,12 @@ func (c *beaconApiValidatorClient) submitAggregateSelectionProof( return nil, err } - aggregatedAttestation, err := convertAttestationToProto(aggregateAttestationResponse.Data) + var attData *structs.Attestation + if err := json.Unmarshal(aggregateAttestationResponse.Data, &attData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal aggregate attestation data") + } + + aggregatedAttestation, err := convertAttestationToProto(attData) if err != nil { return nil, errors.Wrap(err, "failed to convert aggregate attestation json to proto") } diff --git a/validator/client/beacon-api/submit_aggregate_selection_proof_test.go b/validator/client/beacon-api/submit_aggregate_selection_proof_test.go index f8cdcb1111d9..fed7b7558620 100644 --- a/validator/client/beacon-api/submit_aggregate_selection_proof_test.go +++ b/validator/client/beacon-api/submit_aggregate_selection_proof_test.go @@ -2,6 +2,7 @@ package beacon_api import ( "context" + "encoding/json" "errors" "fmt" "testing" @@ -124,6 +125,9 @@ func TestSubmitAggregateSelectionProof(t *testing.T) { test.attestationDataErr, ).Times(test.attestationDataCalled) + attestationJSON, err := json.Marshal(aggregateAttestation) + require.NoError(t, err) + // Call attestation data to get attestation data root to query aggregate attestation. jsonRestHandler.EXPECT().Get( gomock.Any(), @@ -132,7 +136,7 @@ func TestSubmitAggregateSelectionProof(t *testing.T) { ).SetArg( 2, structs.AggregateAttestationResponse{ - Data: jsonifyAttestation(aggregateAttestation), + Data: attestationJSON, }, ).Return( test.aggregateAttestationErr,