diff --git a/CHANGELOG.md b/CHANGELOG.md index d1cfc8ebe4c..4e90239f50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - Add ability to rollback node's internal state during processing. - Change how unsafe protobuf state is created to prevent unnecessary copies. - Added benchmarks for process slots for Capella, Deneb, Electra +- Added GetAggregatedAttestationV2 endpoint. ### Changed diff --git a/api/server/structs/endpoints_validator.go b/api/server/structs/endpoints_validator.go index 8fc4b7d83fa..da932c71692 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 cdd46f11cec..e63d152ef6b 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 3270327ff53..341f2e41ee8 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -101,6 +101,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/v2/validator/aggregate_and_proofs": {http.MethodPost}, "/eth/v1/validator/beacon_committee_subscriptions": {http.MethodPost}, diff --git a/beacon-chain/rpc/eth/validator/BUILD.bazel b/beacon-chain/rpc/eth/validator/BUILD.bazel index ed434774d47..146795faa50 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", @@ -82,6 +83,7 @@ go_test( "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", "//crypto/bls:go_default_library", + "//crypto/bls/common:go_default_library", "//encoding/bytesutil:go_default_library", "//network/httputil:go_default_library", "//proto/prysm/v1alpha1:go_default_library", @@ -93,6 +95,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 5fc5c68e3e7..375558a8e7c 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" @@ -32,6 +34,7 @@ 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" @@ -48,71 +51,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 } - match, err = matchingAtt(atts, primitives.Slot(slot), attDataRoot) + 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 + } + 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 + 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(b.GetAggregationBits().Count(), a.GetAggregationBits().Count()) + }) + return match[0] } - 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) + 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 5b5d2b10111..39d22e6f505 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" "github.com/prysmaticlabs/prysm/v5/api/server/structs" mockChain "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" @@ -35,6 +36,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/crypto/bls" + "github.com/prysmaticlabs/prysm/v5/crypto/bls/common" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpbalpha "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -48,292 +50,337 @@ 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, - }, - }, - 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, - }, - }, - 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, - } + root2 := bytesutil.PadTo([]byte("root2"), 32) + key, err := bls.RandKey() + require.NoError(t, err) + sig := key.Sign([]byte("sig")) - 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) + t.Run("V1", func(t *testing.T) { + createAttestation := func(slot primitives.Slot, aggregationBits bitfield.Bitlist, root []byte) *ethpbalpha.Attestation { + return ðpbalpha.Attestation{ + AggregationBits: aggregationBits, + Data: createAttestationData(slot, 1, 1, root), + Signature: sig.Marshal(), + } + } - s := &Server{ - AttestationsPool: pool, - } + aggSlot1_Root1_1 := createAttestation(1, bitfield.Bitlist{0b11100}, root1) + aggSlot1_Root1_2 := createAttestation(1, bitfield.Bitlist{0b10111}, root1) + aggSlot1_Root2 := createAttestation(1, bitfield.Bitlist{0b11100}, root2) + aggSlot2 := createAttestation(2, bitfield.Bitlist{0b11100}, root1) + unaggSlot3_Root1_1 := createAttestation(3, bitfield.Bitlist{0b11000}, root1) + unaggSlot3_Root1_2 := createAttestation(3, bitfield.Bitlist{0b10100}, root1) + unaggSlot3_Root2 := createAttestation(3, bitfield.Bitlist{0b11000}, root2) + unaggSlot4 := createAttestation(4, bitfield.Bitlist{0b11000}, root1) + + compareResult := func( + t *testing.T, + attestation structs.Attestation, + expectedSlot string, + expectedAggregationBits string, + expectedRoot []byte, + expectedSig []byte, + ) { + assert.Equal(t, expectedAggregationBits, attestation.AggregationBits, "Unexpected aggregation bits in attestation") + assert.Equal(t, hexutil.Encode(expectedSig), attestation.Signature, "Signature mismatch") + assert.Equal(t, expectedSlot, attestation.Data.Slot, "Slot mismatch in attestation data") + assert.Equal(t, "1", attestation.Data.CommitteeIndex, "Committee index mismatch") + assert.Equal(t, hexutil.Encode(expectedRoot), attestation.Data.BeaconBlockRoot, "Beacon block root mismatch") + + // Source checkpoint checks + require.NotNil(t, attestation.Data.Source, "Source checkpoint should not be nil") + assert.Equal(t, "1", attestation.Data.Source.Epoch, "Source epoch mismatch") + assert.Equal(t, hexutil.Encode(expectedRoot), attestation.Data.Source.Root, "Source root mismatch") + + // Target checkpoint checks + require.NotNil(t, attestation.Data.Target, "Target checkpoint should not be nil") + assert.Equal(t, "1", attestation.Data.Target.Epoch, "Target epoch mismatch") + assert.Equal(t, hexutil.Encode(expectedRoot), attestation.Data.Target.Root, "Target root mismatch") + } - t.Run("matching aggregated att", func(t *testing.T) { - reqRoot, err := attslot22.Data.HashTreeRoot() + pool := attestations.NewPool() + require.NoError(t, pool.SaveUnaggregatedAttestations([]ethpbalpha.Att{unaggSlot3_Root1_1, unaggSlot3_Root1_2, unaggSlot3_Root2, unaggSlot4}), "Failed to save unaggregated attestations") + unagg, err := pool.UnaggregatedAttestations() 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{} + require.Equal(t, 4, len(unagg), "Expected 4 unaggregated attestations") + require.NoError(t, pool.SaveAggregatedAttestations([]ethpbalpha.Att{aggSlot1_Root1_1, aggSlot1_Root1_2, aggSlot1_Root2, aggSlot2}), "Failed to save aggregated attestations") + agg := pool.AggregatedAttestations() + require.Equal(t, 4, len(agg), "Expected 4 aggregated attestations") + s := &Server{ + AttestationsPool: pool, + } - 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{} + t.Run("non-matching attestation request", func(t *testing.T) { + reqRoot, err := aggSlot2.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() - 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.GetAggregateAttestation(writer, request) + assert.Equal(t, http.StatusNotFound, writer.Code, "Expected HTTP status NotFound for non-matching request") + }) + t.Run("1 matching aggregated attestation", func(t *testing.T) { + reqRoot, err := aggSlot2.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() - 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) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") - 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{} + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") - 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{} + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") - 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")) + compareResult(t, attestation, "2", hexutil.Encode(aggSlot2.AggregationBits), root1, sig.Marshal()) + }) + t.Run("multiple matching aggregated attestations - return the one with most bits", func(t *testing.T) { + reqRoot, err := aggSlot1_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestation(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + + compareResult(t, attestation, "1", hexutil.Encode(aggSlot1_Root1_2.AggregationBits), root1, sig.Marshal()) + }) + t.Run("1 matching unaggregated attestation", func(t *testing.T) { + reqRoot, err := unaggSlot4.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=4" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestation(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + compareResult(t, attestation, "4", hexutil.Encode(unaggSlot4.AggregationBits), root1, sig.Marshal()) + }) + t.Run("multiple matching unaggregated attestations - their aggregate is returned", func(t *testing.T) { + reqRoot, err := unaggSlot3_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestation(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.Attestation + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + sig1, err := bls.SignatureFromBytes(unaggSlot3_Root1_1.Signature) + require.NoError(t, err) + sig2, err := bls.SignatureFromBytes(unaggSlot3_Root1_2.Signature) + require.NoError(t, err) + expectedSig := bls.AggregateSignatures([]common.Signature{sig1, sig2}) + compareResult(t, attestation, "3", hexutil.Encode(bitfield.Bitlist{0b11100}), root1, expectedSig.Marshal()) + }) }) - 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{} + t.Run("V2", func(t *testing.T) { + createAttestation := func(slot primitives.Slot, aggregationBits bitfield.Bitlist, root []byte, bits uint64) *ethpbalpha.AttestationElectra { + committeeBits := bitfield.NewBitvector64() + committeeBits.SetBitAt(bits, true) + + return ðpbalpha.AttestationElectra{ + CommitteeBits: committeeBits, + AggregationBits: aggregationBits, + Data: createAttestationData(slot, 0, 1, root), + Signature: sig.Marshal(), + } + } - 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")) + aggSlot1_Root1_1 := createAttestation(1, bitfield.Bitlist{0b11100}, root1, 1) + aggSlot1_Root1_2 := createAttestation(1, bitfield.Bitlist{0b10111}, root1, 1) + aggSlot1_Root2 := createAttestation(1, bitfield.Bitlist{0b11100}, root2, 1) + aggSlot2 := createAttestation(2, bitfield.Bitlist{0b11100}, root1, 1) + unaggSlot3_Root1_1 := createAttestation(3, bitfield.Bitlist{0b11000}, root1, 1) + unaggSlot3_Root1_2 := createAttestation(3, bitfield.Bitlist{0b10100}, root1, 1) + unaggSlot3_Root2 := createAttestation(3, bitfield.Bitlist{0b11000}, root2, 1) + unaggSlot4 := createAttestation(4, bitfield.Bitlist{0b11000}, root1, 1) + + compareResult := func( + t *testing.T, + attestation structs.AttestationElectra, + expectedSlot string, + expectedAggregationBits string, + expectedRoot []byte, + expectedSig []byte, + expectedBits string, + ) { + assert.Equal(t, expectedAggregationBits, attestation.AggregationBits, "Unexpected aggregation bits in attestation") + assert.Equal(t, expectedBits, attestation.CommitteeBits) + assert.Equal(t, hexutil.Encode(expectedSig), attestation.Signature, "Signature mismatch") + assert.Equal(t, expectedSlot, attestation.Data.Slot, "Slot mismatch in attestation data") + assert.Equal(t, "0", attestation.Data.CommitteeIndex, "Committee index mismatch") + assert.Equal(t, hexutil.Encode(expectedRoot), attestation.Data.BeaconBlockRoot, "Beacon block root mismatch") + + // Source checkpoint checks + require.NotNil(t, attestation.Data.Source, "Source checkpoint should not be nil") + assert.Equal(t, "1", attestation.Data.Source.Epoch, "Source epoch mismatch") + assert.Equal(t, hexutil.Encode(expectedRoot), attestation.Data.Source.Root, "Source root mismatch") + + // Target checkpoint checks + require.NotNil(t, attestation.Data.Target, "Target checkpoint should not be nil") + assert.Equal(t, "1", attestation.Data.Target.Epoch, "Target epoch mismatch") + assert.Equal(t, hexutil.Encode(expectedRoot), attestation.Data.Target.Root, "Target root mismatch") + } + + pool := attestations.NewPool() + require.NoError(t, pool.SaveUnaggregatedAttestations([]ethpbalpha.Att{unaggSlot3_Root1_1, unaggSlot3_Root1_2, unaggSlot3_Root2, unaggSlot4}), "Failed to save unaggregated attestations") + unagg, err := pool.UnaggregatedAttestations() + require.NoError(t, err) + require.Equal(t, 4, len(unagg), "Expected 4 unaggregated attestations") + require.NoError(t, pool.SaveAggregatedAttestations([]ethpbalpha.Att{aggSlot1_Root1_1, aggSlot1_Root1_2, aggSlot1_Root2, aggSlot2}), "Failed to save aggregated attestations") + agg := pool.AggregatedAttestations() + require.Equal(t, 4, len(agg), "Expected 4 aggregated attestations") + s := &Server{ + AttestationsPool: pool, + } + t.Run("non-matching attestation request", func(t *testing.T) { + reqRoot, err := aggSlot2.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" + "&committee_index=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + assert.Equal(t, http.StatusNotFound, writer.Code, "Expected HTTP status NotFound for non-matching request") + }) + t.Run("1 matching aggregated attestation", func(t *testing.T) { + reqRoot, err := aggSlot2.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + "&committee_index=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.AttestationElectra + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + + compareResult(t, attestation, "2", hexutil.Encode(aggSlot2.AggregationBits), root1, sig.Marshal(), "0x0200000000000000") + }) + t.Run("multiple matching aggregated attestations - return the one with most bits", func(t *testing.T) { + reqRoot, err := aggSlot1_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" + "&committee_index=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.AttestationElectra + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + + compareResult(t, attestation, "1", hexutil.Encode(aggSlot1_Root1_2.AggregationBits), root1, sig.Marshal(), "0x0200000000000000") + }) + t.Run("1 matching unaggregated attestation", func(t *testing.T) { + reqRoot, err := unaggSlot4.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=4" + "&committee_index=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.AttestationElectra + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + compareResult(t, attestation, "4", hexutil.Encode(unaggSlot4.AggregationBits), root1, sig.Marshal(), "0x0200000000000000") + }) + t.Run("multiple matching unaggregated attestations - their aggregate is returned", func(t *testing.T) { + reqRoot, err := unaggSlot3_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" + "&committee_index=1" + request := httptest.NewRequest(http.MethodGet, url, nil) + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp structs.AggregateAttestationResponse + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), &resp), "Failed to unmarshal response") + require.NotNil(t, resp.Data, "Response data should not be nil") + + var attestation structs.AttestationElectra + require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") + sig1, err := bls.SignatureFromBytes(unaggSlot3_Root1_1.Signature) + require.NoError(t, err) + sig2, err := bls.SignatureFromBytes(unaggSlot3_Root1_2.Signature) + require.NoError(t, err) + expectedSig := bls.AggregateSignatures([]common.Signature{sig1, sig2}) + compareResult(t, attestation, "3", hexutil.Encode(bitfield.Bitlist{0b11100}), root1, expectedSig.Marshal(), "0x0200000000000000") + }) }) } -func TestGetAggregateAttestation_SameSlotAndRoot_ReturnMostAggregationBits(t *testing.T) { - root := bytesutil.PadTo([]byte("root"), 32) - sig := bytesutil.PadTo([]byte("sig"), fieldparams.BLSSignatureLength) - att1 := ðpbalpha.Attestation{ - AggregationBits: []byte{3, 0, 0, 1}, - Data: ðpbalpha.AttestationData{ - Slot: 1, - CommitteeIndex: 1, - BeaconBlockRoot: root, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root, - }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root, - }, +func createAttestationData( + slot primitives.Slot, + committeeIndex primitives.CommitteeIndex, + epoch primitives.Epoch, + root []byte, +) *ethpbalpha.AttestationData { + return ðpbalpha.AttestationData{ + Slot: slot, + CommitteeIndex: committeeIndex, + BeaconBlockRoot: root, + Source: ðpbalpha.Checkpoint{ + Epoch: epoch, + Root: root, }, - Signature: sig, - } - att2 := ðpbalpha.Attestation{ - AggregationBits: []byte{0, 3, 0, 1}, - Data: ðpbalpha.AttestationData{ - Slot: 1, - CommitteeIndex: 1, - BeaconBlockRoot: root, - Source: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root, - }, - Target: ðpbalpha.Checkpoint{ - Epoch: 1, - Root: root, - }, + Target: ðpbalpha.Checkpoint{ + Epoch: epoch, + Root: root, }, - Signature: sig, - } - pool := attestations.NewPool() - err := pool.SaveAggregatedAttestations([]ethpbalpha.Att{att1, att2}) - assert.NoError(t, err) - s := &Server{ - AttestationsPool: pool, } - reqRoot, err := att1.Data.HashTreeRoot() - require.NoError(t, err) - attDataRoot := hexutil.Encode(reqRoot[:]) - url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" - 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) - assert.DeepEqual(t, "0x03000001", resp.Data.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 3aeda87136d..1d7269f0277 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 f8cdcb1111d..6fa1abebed4 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(jsonifyAttestation(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,