Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add /eth/v2/validator/aggregate_attestation #14481

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion api/server/structs/endpoints_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions beacon-chain/rpc/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions beacon-chain/rpc/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
2 changes: 2 additions & 0 deletions beacon-chain/rpc/eth/validator/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
],
Expand Down
149 changes: 117 additions & 32 deletions beacon-chain/rpc/eth/validator/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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"
Expand All @@ -52,61 +54,144 @@ func (s *Server) GetAggregateAttestation(w http.ResponseWriter, r *http.Request)
return
}

match := s.aggregateAttestation(w, primitives.Slot(slot), "", attDataRoot)
if match == nil {
return
}
att := &structs.Attestation{
saolyn marked this conversation as resolved.
Show resolved Hide resolved
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()),
}

data, err := json.Marshal(att)
if err != nil {
httputil.HandleError(w, "Could not get marshal attestation data: "+err.Error(), http.StatusInternalServerError)
saolyn marked this conversation as resolved.
Show resolved Hide resolved
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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should return a 400 / bad request since they are required fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They already do, within shared.UintFromQuery we have ValidateUint which handles the http error, same thing applies to shared.HexFromQuery

i := strconv.FormatUint(index, 10)
match := s.aggregateAttestation(w, primitives.Slot(slot), i, attDataRoot)
if match == nil {
return
}
resp := &structs.AggregateAttestationResponse{
Version: version.String(match.Version()),
}
if match.Version() >= version.Electra {
attPostElectra, ok := match.(*ethpbalpha.AttestationElectra)
if !ok {
httputil.HandleError(w, "Match is not of type AttestationElectra", http.StatusInternalServerError)
return
}
att := structs.AttElectraFromConsensus(attPostElectra)
data, err := json.Marshal(att)
if err != nil {
httputil.HandleError(w, "Could not get marshal attestation data: "+err.Error(), http.StatusInternalServerError)
saolyn marked this conversation as resolved.
Show resolved Hide resolved
return
}
resp.Data = data
} else {
attPreElectra, ok := match.(*ethpbalpha.Attestation)
if !ok {
httputil.HandleError(w, "Match is not of type Attestation", http.StatusInternalServerError)
return
}
att := structs.AttFromConsensus(attPreElectra)
data, err := json.Marshal(att)
if err != nil {
httputil.HandleError(w, "Could not get marshal attestation data: "+err.Error(), http.StatusInternalServerError)
saolyn marked this conversation as resolved.
Show resolved Hide resolved
return
}
resp.Data = data
}
httputil.WriteJson(w, resp)
}

func (s *Server) aggregateAttestation(w http.ResponseWriter, slot primitives.Slot, index string, attDataRoot []byte) ethpbalpha.Att {
var match ethpbalpha.Att
var err error

match, err = matchingAtt(s.AttestationsPool.AggregatedAttestations(), primitives.Slot(slot), attDataRoot)
match, err = matchingAtt(s.AttestationsPool.AggregatedAttestations(), slot, attDataRoot, index)
if err != nil {
httputil.HandleError(w, "Could not get matching attestation: "+err.Error(), http.StatusInternalServerError)
return
return nil
}
if match == nil {
atts, err := s.AttestationsPool.UnaggregatedAttestations()
if err != nil {
httputil.HandleError(w, "Could not get unaggregated attestations: "+err.Error(), http.StatusInternalServerError)
return
return nil
}
match, err = matchingAtt(atts, primitives.Slot(slot), attDataRoot)
match, err = matchingAtt(atts, slot, attDataRoot, index)
if err != nil {
httputil.HandleError(w, "Could not get matching attestation: "+err.Error(), http.StatusInternalServerError)
return
return nil
}
if match == nil {
httputil.HandleError(w, "No matching attestation found", http.StatusNotFound)
return nil
}
_, err = attestations.Aggregate([]ethpbalpha.Att{match})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_, err = attestations.Aggregate([]ethpbalpha.Att{match})
agg, err = attestations.Aggregate([]ethpbalpha.Att{matches})
...
return agg[0]

If they cannot be aggregate to just one (len(agg) == 1), then return best by committee bits count.

if err != nil {
httputil.HandleError(w, "Could not aggregate the matched unaggregated attestation: "+err.Error(), http.StatusInternalServerError)
return nil
}
}
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)
return match
}

func matchingAtt(atts []ethpbalpha.Att, slot primitives.Slot, attDataRoot []byte) (ethpbalpha.Att, error) {
func matchingAtt(atts []ethpbalpha.Att, slot primitives.Slot, attDataRoot []byte, index string) (ethpbalpha.Att, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are multiple that match, return the best one by most CommitteeBitsVal().Count(), assuming they cannot be further aggregated.

Or consider returning all matching atts.

func matchingAtts(atts []ethpbalpha.Att, slot primitives.Slot, attDataRoot []byte, index string) ([]ethpbalpha.Att, error)

for _, att := range atts {
if att.GetData().Slot == slot {
root, err := att.GetData().HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "could not get attestation data root")
}
if bytes.Equal(root[:], attDataRoot) {
return att, nil
if index == "" {
saolyn marked this conversation as resolved.
Show resolved Hide resolved
if bytes.Equal(root[:], attDataRoot) {
return att, nil
}
} else {
i, err := strconv.ParseUint(index, 10, 64)
if err != nil {
return att, err
}
bits := att.CommitteeBitsVal().BitAt(i)
saolyn marked this conversation as resolved.
Show resolved Hide resolved
if bytes.Equal(root[:], attDataRoot) && bits {
return att, nil
}
}
}
}
Expand Down
Loading