From 9382ae736d1854b2eafef1bb4600d015cfa65f19 Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:13:57 -0600 Subject: [PATCH] validator REST: attestation v2 (#14633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * fixing tests * adding unit tests * fixing tests * adding back v1 usage * changelog * rolling back test and adding placeholder * adding electra tests * adding attestation nil check based on review * reduce code duplication * linting * fixing tests * based on sammy review * radek feedback * adding fall back for pre electra and updated tests * fixing api calls and associated tests * gaz * Update validator/client/beacon-api/propose_attestation.go Co-authored-by: RadosÅ‚aw Kapka * review feedback * add missing fallback * fixing tests --------- Co-authored-by: RadosÅ‚aw Kapka --- CHANGELOG.md | 1 + validator/client/aggregate.go | 1 + validator/client/beacon-api/BUILD.bazel | 1 + .../beacon-api/beacon_api_validator_client.go | 21 +- .../beacon-api/beacon_block_json_helpers.go | 28 ++ .../beacon-api/beacon_block_proto_helpers.go | 33 ++ .../client/beacon-api/propose_attestation.go | 90 ++++-- .../beacon-api/propose_attestation_test.go | 254 ++++++++++++++- .../submit_aggregate_selection_proof.go | 123 ++++++-- .../submit_aggregate_selection_proof_test.go | 298 +++++++++++++++++- .../submit_signed_aggregate_proof.go | 41 ++- .../submit_signed_aggregate_proof_test.go | 146 ++++++++- 12 files changed, 976 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87ba927faad1..98206ce591b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve - Added SubmitAttestationsV2 endpoint. - Validator REST mode Electra block support - Added validator index label to `validator_statuses` metric +- Added Validator REST mode use of Attestation V2 endpoints and Electra attestations ### Changed diff --git a/validator/client/aggregate.go b/validator/client/aggregate.go index 43e54475a39c..380a78990d39 100644 --- a/validator/client/aggregate.go +++ b/validator/client/aggregate.go @@ -88,6 +88,7 @@ func (v *validator) SubmitAggregateAndProof(ctx context.Context, slot primitives PublicKey: pubKey[:], SlotSignature: slotSig, } + // TODO: look at renaming SubmitAggregateSelectionProof functions as they are GET beacon API var agg ethpb.AggregateAttAndProof if postElectra { res, err := v.validatorClient.SubmitAggregateSelectionProofElectra(ctx, aggSelectionRequest, duty.ValidatorIndex, uint64(len(duty.Committee))) diff --git a/validator/client/beacon-api/BUILD.bazel b/validator/client/beacon-api/BUILD.bazel index e03b86e8517e..cc25603b9877 100644 --- a/validator/client/beacon-api/BUILD.bazel +++ b/validator/client/beacon-api/BUILD.bazel @@ -129,6 +129,7 @@ go_test( "//network/httputil:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", "//time/slots:go_default_library", diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 3a202083cdfb..b37f810fedb4 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -155,7 +155,12 @@ func (c *beaconApiValidatorClient) ProposeAttestation(ctx context.Context, in *e } func (c *beaconApiValidatorClient) ProposeAttestationElectra(ctx context.Context, in *ethpb.AttestationElectra) (*ethpb.AttestResponse, error) { - return nil, errors.New("ProposeAttestationElectra is not implemented") + ctx, span := trace.StartSpan(ctx, "beacon-api.ProposeAttestationElectra") + defer span.End() + + return wrapInMetrics[*ethpb.AttestResponse]("ProposeAttestationElectra", func() (*ethpb.AttestResponse, error) { + return c.proposeAttestationElectra(ctx, in) + }) } func (c *beaconApiValidatorClient) ProposeBeaconBlock(ctx context.Context, in *ethpb.GenericSignedBeaconBlock) (*ethpb.ProposeResponse, error) { @@ -190,7 +195,12 @@ func (c *beaconApiValidatorClient) SubmitAggregateSelectionProof(ctx context.Con } func (c *beaconApiValidatorClient) SubmitAggregateSelectionProofElectra(ctx context.Context, in *ethpb.AggregateSelectionRequest, index primitives.ValidatorIndex, committeeLength uint64) (*ethpb.AggregateSelectionElectraResponse, error) { - return nil, errors.New("SubmitAggregateSelectionProofElectra is not implemented") + ctx, span := trace.StartSpan(ctx, "beacon-api.SubmitAggregateSelectionProofElectra") + defer span.End() + + return wrapInMetrics[*ethpb.AggregateSelectionElectraResponse]("SubmitAggregateSelectionProofElectra", func() (*ethpb.AggregateSelectionElectraResponse, error) { + return c.submitAggregateSelectionProofElectra(ctx, in, index, committeeLength) + }) } func (c *beaconApiValidatorClient) SubmitSignedAggregateSelectionProof(ctx context.Context, in *ethpb.SignedAggregateSubmitRequest) (*ethpb.SignedAggregateSubmitResponse, error) { @@ -203,7 +213,12 @@ func (c *beaconApiValidatorClient) SubmitSignedAggregateSelectionProof(ctx conte } func (c *beaconApiValidatorClient) SubmitSignedAggregateSelectionProofElectra(ctx context.Context, in *ethpb.SignedAggregateSubmitElectraRequest) (*ethpb.SignedAggregateSubmitResponse, error) { - return nil, errors.New("SubmitSignedAggregateSelectionProofElectra is not implemented") + ctx, span := trace.StartSpan(ctx, "beacon-api.SubmitSignedAggregateSelectionProofElectra") + defer span.End() + + return wrapInMetrics[*ethpb.SignedAggregateSubmitResponse]("SubmitSignedAggregateSelectionProofElectra", func() (*ethpb.SignedAggregateSubmitResponse, error) { + return c.submitSignedAggregateSelectionProofElectra(ctx, in) + }) } func (c *beaconApiValidatorClient) SubmitSignedContributionAndProof(ctx context.Context, in *ethpb.SignedContributionAndProof) (*empty.Empty, error) { diff --git a/validator/client/beacon-api/beacon_block_json_helpers.go b/validator/client/beacon-api/beacon_block_json_helpers.go index a72de11e2e91..f5ecf3c73909 100644 --- a/validator/client/beacon-api/beacon_block_json_helpers.go +++ b/validator/client/beacon-api/beacon_block_json_helpers.go @@ -51,6 +51,14 @@ func jsonifyAttestations(attestations []*ethpb.Attestation) []*structs.Attestati return jsonAttestations } +func jsonifyAttestationsElectra(attestations []*ethpb.AttestationElectra) []*structs.AttestationElectra { + jsonAttestations := make([]*structs.AttestationElectra, len(attestations)) + for index, attestation := range attestations { + jsonAttestations[index] = jsonifyAttestationElectra(attestation) + } + return jsonAttestations +} + func jsonifyAttesterSlashings(attesterSlashings []*ethpb.AttesterSlashing) []*structs.AttesterSlashing { jsonAttesterSlashings := make([]*structs.AttesterSlashing, len(attesterSlashings)) for index, attesterSlashing := range attesterSlashings { @@ -164,6 +172,15 @@ func jsonifyAttestation(attestation *ethpb.Attestation) *structs.Attestation { } } +func jsonifyAttestationElectra(attestation *ethpb.AttestationElectra) *structs.AttestationElectra { + return &structs.AttestationElectra{ + AggregationBits: hexutil.Encode(attestation.AggregationBits), + Data: jsonifyAttestationData(attestation.Data), + Signature: hexutil.Encode(attestation.Signature), + CommitteeBits: hexutil.Encode(attestation.CommitteeBits), + } +} + func jsonifySignedAggregateAndProof(signedAggregateAndProof *ethpb.SignedAggregateAttestationAndProof) *structs.SignedAggregateAttestationAndProof { return &structs.SignedAggregateAttestationAndProof{ Message: &structs.AggregateAttestationAndProof{ @@ -175,6 +192,17 @@ func jsonifySignedAggregateAndProof(signedAggregateAndProof *ethpb.SignedAggrega } } +func jsonifySignedAggregateAndProofElectra(signedAggregateAndProof *ethpb.SignedAggregateAttestationAndProofElectra) *structs.SignedAggregateAttestationAndProofElectra { + return &structs.SignedAggregateAttestationAndProofElectra{ + Message: &structs.AggregateAttestationAndProofElectra{ + AggregatorIndex: uint64ToString(signedAggregateAndProof.Message.AggregatorIndex), + Aggregate: jsonifyAttestationElectra(signedAggregateAndProof.Message.Aggregate), + SelectionProof: hexutil.Encode(signedAggregateAndProof.Message.SelectionProof), + }, + Signature: hexutil.Encode(signedAggregateAndProof.Signature), + } +} + func jsonifyWithdrawals(withdrawals []*enginev1.Withdrawal) []*structs.Withdrawal { jsonWithdrawals := make([]*structs.Withdrawal, len(withdrawals)) for index, withdrawal := range withdrawals { diff --git a/validator/client/beacon-api/beacon_block_proto_helpers.go b/validator/client/beacon-api/beacon_block_proto_helpers.go index dc5c40d0e1cf..52d6bd4d1e87 100644 --- a/validator/client/beacon-api/beacon_block_proto_helpers.go +++ b/validator/client/beacon-api/beacon_block_proto_helpers.go @@ -197,6 +197,39 @@ func convertAttestationToProto(jsonAttestation *structs.Attestation) (*ethpb.Att }, nil } +func convertAttestationElectraToProto(jsonAttestation *structs.AttestationElectra) (*ethpb.AttestationElectra, error) { + if jsonAttestation == nil { + return nil, errors.New("json attestation is nil") + } + + aggregationBits, err := hexutil.Decode(jsonAttestation.AggregationBits) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode aggregation bits `%s`", jsonAttestation.AggregationBits) + } + + attestationData, err := convertAttestationDataToProto(jsonAttestation.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to get attestation data") + } + + signature, err := hexutil.Decode(jsonAttestation.Signature) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode attestation signature `%s`", jsonAttestation.Signature) + } + + committeeBits, err := hexutil.Decode(jsonAttestation.CommitteeBits) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode committee bits `%s`", jsonAttestation.CommitteeBits) + } + + return ðpb.AttestationElectra{ + AggregationBits: aggregationBits, + Data: attestationData, + Signature: signature, + CommitteeBits: committeeBits, + }, nil +} + func convertAttestationsToProto(jsonAttestations []*structs.Attestation) ([]*ethpb.Attestation, error) { var attestations []*ethpb.Attestation for index, jsonAttestation := range jsonAttestations { diff --git a/validator/client/beacon-api/propose_attestation.go b/validator/client/beacon-api/propose_attestation.go index be543d14ed6f..6dd32975c024 100644 --- a/validator/client/beacon-api/propose_attestation.go +++ b/validator/client/beacon-api/propose_attestation.go @@ -4,25 +4,73 @@ import ( "bytes" "context" "encoding/json" + "net/http" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" ) func (c *beaconApiValidatorClient) proposeAttestation(ctx context.Context, attestation *ethpb.Attestation) (*ethpb.AttestResponse, error) { - if err := checkNilAttestation(attestation); err != nil { + if err := validateNilAttestation(attestation); err != nil { return nil, err } - marshalledAttestation, err := json.Marshal(jsonifyAttestations([]*ethpb.Attestation{attestation})) if err != nil { return nil, err } - if err = c.jsonRestHandler.Post( + headers := map[string]string{"Eth-Consensus-Version": version.String(attestation.Version())} + err = c.jsonRestHandler.Post( ctx, - "/eth/v1/beacon/pool/attestations", + "/eth/v2/beacon/pool/attestations", + headers, + bytes.NewBuffer(marshalledAttestation), nil, + ) + errJson := &httputil.DefaultJsonError{} + if err != nil { + // TODO: remove this when v2 becomes default + if !errors.As(err, &errJson) { + return nil, err + } + if errJson.Code != http.StatusNotFound { + return nil, errJson + } + log.Debug("Endpoint /eth/v2/beacon/pool/attestations is not supported, falling back to older endpoints for submit attestation.") + if err = c.jsonRestHandler.Post( + ctx, + "/eth/v1/beacon/pool/attestations", + nil, + bytes.NewBuffer(marshalledAttestation), + nil, + ); err != nil { + return nil, err + } + } + + attestationDataRoot, err := attestation.Data.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "failed to compute attestation data root") + } + + return ðpb.AttestResponse{AttestationDataRoot: attestationDataRoot[:]}, nil +} + +func (c *beaconApiValidatorClient) proposeAttestationElectra(ctx context.Context, attestation *ethpb.AttestationElectra) (*ethpb.AttestResponse, error) { + if err := validateNilAttestation(attestation); err != nil { + return nil, err + } + marshalledAttestation, err := json.Marshal(jsonifyAttestationsElectra([]*ethpb.AttestationElectra{attestation})) + if err != nil { + return nil, err + } + headers := map[string]string{"Eth-Consensus-Version": version.String(attestation.Version())} + if err = c.jsonRestHandler.Post( + ctx, + "/eth/v2/beacon/pool/attestations", + headers, bytes.NewBuffer(marshalledAttestation), nil, ); err != nil { @@ -37,27 +85,27 @@ func (c *beaconApiValidatorClient) proposeAttestation(ctx context.Context, attes return ðpb.AttestResponse{AttestationDataRoot: attestationDataRoot[:]}, nil } -// checkNilAttestation returns error if attestation or any field of attestation is nil. -func checkNilAttestation(attestation *ethpb.Attestation) error { - if attestation == nil { - return errors.New("attestation is nil") +func validateNilAttestation(attestation ethpb.Att) error { + if attestation == nil || attestation.IsNil() { + return errors.New("attestation can't be nil") } - - if attestation.Data == nil { - return errors.New("attestation data is nil") + if attestation.GetData().Source == nil { + return errors.New("attestation's source can't be nil") } - - if attestation.Data.Source == nil || attestation.Data.Target == nil { - return errors.New("source/target in attestation data is nil") + if attestation.GetData().Target == nil { + return errors.New("attestation's target can't be nil") } - - if len(attestation.AggregationBits) == 0 { - return errors.New("attestation aggregation bits is empty") + v := attestation.Version() + if len(attestation.GetAggregationBits()) == 0 { + return errors.New("attestation's bitfield can't be nil") } - - if len(attestation.Signature) == 0 { - return errors.New("attestation signature is empty") + if len(attestation.GetSignature()) == 0 { + return errors.New("attestation signature can't be nil") + } + if v >= version.Electra { + if len(attestation.CommitteeBitsVal().BitIndices()) == 0 { + return errors.New("attestation committee bits can't be nil") + } } - return nil } diff --git a/validator/client/beacon-api/propose_attestation_test.go b/validator/client/beacon-api/propose_attestation_test.go index be6064a0323e..3ead120bc3a4 100644 --- a/validator/client/beacon-api/propose_attestation_test.go +++ b/validator/client/beacon-api/propose_attestation_test.go @@ -5,9 +5,12 @@ import ( "context" "encoding/json" "errors" + "net/http" "testing" + "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" "github.com/prysmaticlabs/prysm/v5/validator/client/beacon-api/mock" @@ -48,7 +51,7 @@ func TestProposeAttestation(t *testing.T) { }, { name: "nil attestation", - expectedErrorMessage: "attestation is nil", + expectedErrorMessage: "attestation can't be nil", }, { name: "nil attestation data", @@ -56,7 +59,7 @@ func TestProposeAttestation(t *testing.T) { AggregationBits: testhelpers.FillByteSlice(4, 74), Signature: testhelpers.FillByteSlice(96, 82), }, - expectedErrorMessage: "attestation data is nil", + expectedErrorMessage: "attestation can't be nil", }, { name: "nil source checkpoint", @@ -67,7 +70,7 @@ func TestProposeAttestation(t *testing.T) { }, Signature: testhelpers.FillByteSlice(96, 82), }, - expectedErrorMessage: "source/target in attestation data is nil", + expectedErrorMessage: "attestation's source can't be nil", }, { name: "nil target checkpoint", @@ -78,7 +81,7 @@ func TestProposeAttestation(t *testing.T) { }, Signature: testhelpers.FillByteSlice(96, 82), }, - expectedErrorMessage: "source/target in attestation data is nil", + expectedErrorMessage: "attestation's target can't be nil", }, { name: "nil aggregation bits", @@ -89,7 +92,7 @@ func TestProposeAttestation(t *testing.T) { }, Signature: testhelpers.FillByteSlice(96, 82), }, - expectedErrorMessage: "attestation aggregation bits is empty", + expectedErrorMessage: "attestation's bitfield can't be nil", }, { name: "nil signature", @@ -100,7 +103,7 @@ func TestProposeAttestation(t *testing.T) { Target: ðpb.Checkpoint{}, }, }, - expectedErrorMessage: "attestation signature is empty", + expectedErrorMessage: "attestation signature can't be nil", }, { name: "bad request", @@ -117,7 +120,7 @@ func TestProposeAttestation(t *testing.T) { jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) var marshalledAttestations []byte - if checkNilAttestation(test.attestation) == nil { + if validateNilAttestation(test.attestation) == nil { b, err := json.Marshal(jsonifyAttestations([]*ethpb.Attestation{test.attestation})) require.NoError(t, err) marshalledAttestations = b @@ -125,10 +128,11 @@ func TestProposeAttestation(t *testing.T) { ctx := context.Background() + headers := map[string]string{"Eth-Consensus-Version": version.String(test.attestation.Version())} jsonRestHandler.EXPECT().Post( gomock.Any(), - "/eth/v1/beacon/pool/attestations", - nil, + "/eth/v2/beacon/pool/attestations", + headers, bytes.NewBuffer(marshalledAttestations), nil, ).Return( @@ -153,3 +157,235 @@ func TestProposeAttestation(t *testing.T) { }) } } + +func TestProposeAttestationFallBack(t *testing.T) { + attestation := ðpb.Attestation{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Slot: 75, + CommitteeIndex: 76, + BeaconBlockRoot: testhelpers.FillByteSlice(32, 38), + Source: ðpb.Checkpoint{ + Epoch: 78, + Root: testhelpers.FillByteSlice(32, 79), + }, + Target: ðpb.Checkpoint{ + Epoch: 80, + Root: testhelpers.FillByteSlice(32, 81), + }, + }, + Signature: testhelpers.FillByteSlice(96, 82), + } + + ctrl := gomock.NewController(t) + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + + var marshalledAttestations []byte + if validateNilAttestation(attestation) == nil { + b, err := json.Marshal(jsonifyAttestations([]*ethpb.Attestation{attestation})) + require.NoError(t, err) + marshalledAttestations = b + } + + ctx := context.Background() + headers := map[string]string{"Eth-Consensus-Version": version.String(attestation.Version())} + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v2/beacon/pool/attestations", + headers, + bytes.NewBuffer(marshalledAttestations), + nil, + ).Return( + &httputil.DefaultJsonError{ + Code: http.StatusNotFound, + }, + ).Times(1) + + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v1/beacon/pool/attestations", + nil, + bytes.NewBuffer(marshalledAttestations), + nil, + ).Return( + nil, + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + proposeResponse, err := validatorClient.proposeAttestation(ctx, attestation) + + require.NoError(t, err) + require.NotNil(t, proposeResponse) + + expectedAttestationDataRoot, err := attestation.Data.HashTreeRoot() + require.NoError(t, err) + + // Make sure that the attestation data root is set + assert.DeepEqual(t, expectedAttestationDataRoot[:], proposeResponse.AttestationDataRoot) +} + +func TestProposeAttestationElectra(t *testing.T) { + attestation := ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Slot: 75, + CommitteeIndex: 76, + BeaconBlockRoot: testhelpers.FillByteSlice(32, 38), + Source: ðpb.Checkpoint{ + Epoch: 78, + Root: testhelpers.FillByteSlice(32, 79), + }, + Target: ðpb.Checkpoint{ + Epoch: 80, + Root: testhelpers.FillByteSlice(32, 81), + }, + }, + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + } + + tests := []struct { + name string + attestation *ethpb.AttestationElectra + expectedErrorMessage string + endpointError error + endpointCall int + }{ + { + name: "valid", + attestation: attestation, + endpointCall: 1, + }, + { + name: "nil attestation", + expectedErrorMessage: "attestation can't be nil", + }, + { + name: "nil attestation data", + attestation: ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + }, + expectedErrorMessage: "attestation can't be nil", + }, + { + name: "nil source checkpoint", + attestation: ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Target: ðpb.Checkpoint{}, + }, + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + }, + expectedErrorMessage: "attestation's source can't be nil", + }, + { + name: "nil target checkpoint", + attestation: ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{}, + }, + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + }, + expectedErrorMessage: "attestation's target can't be nil", + }, + { + name: "nil aggregation bits", + attestation: ðpb.AttestationElectra{ + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{}, + Target: ðpb.Checkpoint{}, + }, + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + }, + expectedErrorMessage: "attestation's bitfield can't be nil", + }, + { + name: "nil signature", + attestation: ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Source: ðpb.Checkpoint{}, + Target: ðpb.Checkpoint{}, + }, + CommitteeBits: testhelpers.FillByteSlice(8, 83), + }, + expectedErrorMessage: "attestation signature can't be nil", + }, + { + name: "nil committee bits", + attestation: ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Slot: 75, + CommitteeIndex: 76, + BeaconBlockRoot: testhelpers.FillByteSlice(32, 38), + Source: ðpb.Checkpoint{ + Epoch: 78, + Root: testhelpers.FillByteSlice(32, 79), + }, + Target: ðpb.Checkpoint{ + Epoch: 80, + Root: testhelpers.FillByteSlice(32, 81), + }, + }, + Signature: testhelpers.FillByteSlice(96, 82), + }, + expectedErrorMessage: "attestation committee bits can't be nil", + }, + { + name: "bad request", + attestation: attestation, + expectedErrorMessage: "bad request", + endpointError: errors.New("bad request"), + endpointCall: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + + var marshalledAttestations []byte + if validateNilAttestation(test.attestation) == nil { + b, err := json.Marshal(jsonifyAttestationsElectra([]*ethpb.AttestationElectra{test.attestation})) + require.NoError(t, err) + marshalledAttestations = b + } + + ctx := context.Background() + headers := map[string]string{"Eth-Consensus-Version": version.String(test.attestation.Version())} + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v2/beacon/pool/attestations", + headers, + bytes.NewBuffer(marshalledAttestations), + nil, + ).Return( + test.endpointError, + ).Times(test.endpointCall) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + proposeResponse, err := validatorClient.proposeAttestationElectra(ctx, test.attestation) + if test.expectedErrorMessage != "" { + require.ErrorContains(t, test.expectedErrorMessage, err) + return + } + + require.NoError(t, err) + require.NotNil(t, proposeResponse) + + expectedAttestationDataRoot, err := attestation.Data.HashTreeRoot() + require.NoError(t, err) + + // Make sure that the attestation data root is set + assert.DeepEqual(t, expectedAttestationDataRoot[:], proposeResponse.AttestationDataRoot) + }) + } +} diff --git a/validator/client/beacon-api/submit_aggregate_selection_proof.go b/validator/client/beacon-api/submit_aggregate_selection_proof.go index 1d7269f0277f..da0ae21843f7 100644 --- a/validator/client/beacon-api/submit_aggregate_selection_proof.go +++ b/validator/client/beacon-api/submit_aggregate_selection_proof.go @@ -3,6 +3,7 @@ package beacon_api import ( "context" "encoding/json" + "net/http" "net/url" "strconv" @@ -11,6 +12,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/api/server/structs" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" ) @@ -20,6 +22,71 @@ func (c *beaconApiValidatorClient) submitAggregateSelectionProof( index primitives.ValidatorIndex, committeeLength uint64, ) (*ethpb.AggregateSelectionResponse, error) { + attestationDataRoot, err := c.getAttestationDataRootFromRequest(ctx, in, committeeLength) + if err != nil { + return nil, err + } + + aggregateAttestationResponse, err := c.aggregateAttestation(ctx, in.Slot, attestationDataRoot, in.CommitteeIndex) + if err != nil { + return nil, err + } + + 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") + } + + return ðpb.AggregateSelectionResponse{ + AggregateAndProof: ðpb.AggregateAttestationAndProof{ + AggregatorIndex: index, + Aggregate: aggregatedAttestation, + SelectionProof: in.SlotSignature, + }, + }, nil +} + +func (c *beaconApiValidatorClient) submitAggregateSelectionProofElectra( + ctx context.Context, + in *ethpb.AggregateSelectionRequest, + index primitives.ValidatorIndex, + committeeLength uint64, +) (*ethpb.AggregateSelectionElectraResponse, error) { + attestationDataRoot, err := c.getAttestationDataRootFromRequest(ctx, in, committeeLength) + if err != nil { + return nil, err + } + + aggregateAttestationResponse, err := c.aggregateAttestationElectra(ctx, in.Slot, attestationDataRoot, in.CommitteeIndex) + if err != nil { + return nil, err + } + + var attData *structs.AttestationElectra + if err := json.Unmarshal(aggregateAttestationResponse.Data, &attData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal aggregate attestation electra data") + } + + aggregatedAttestation, err := convertAttestationElectraToProto(attData) + if err != nil { + return nil, errors.Wrap(err, "failed to convert aggregate attestation json to proto") + } + + return ðpb.AggregateSelectionElectraResponse{ + AggregateAndProof: ðpb.AggregateAttestationAndProofElectra{ + AggregatorIndex: index, + Aggregate: aggregatedAttestation, + SelectionProof: in.SlotSignature, + }, + }, nil +} + +func (c *beaconApiValidatorClient) getAttestationDataRootFromRequest(ctx context.Context, in *ethpb.AggregateSelectionRequest, committeeLength uint64) ([]byte, error) { isOptimistic, err := c.isOptimistic(ctx) if err != nil { return nil, err @@ -47,40 +114,56 @@ func (c *beaconApiValidatorClient) submitAggregateSelectionProof( if err != nil { return nil, errors.Wrap(err, "failed to calculate attestation data root") } + return attestationDataRoot[:], nil +} - aggregateAttestationResponse, err := c.aggregateAttestation(ctx, in.Slot, attestationDataRoot[:]) - if err != nil { - return nil, err - } - - var attData *structs.Attestation - if err := json.Unmarshal(aggregateAttestationResponse.Data, &attData); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal aggregate attestation data") - } +func (c *beaconApiValidatorClient) aggregateAttestation( + ctx context.Context, + slot primitives.Slot, + attestationDataRoot []byte, + committeeIndex primitives.CommitteeIndex, +) (*structs.AggregateAttestationResponse, error) { + params := url.Values{} + params.Add("slot", strconv.FormatUint(uint64(slot), 10)) + params.Add("attestation_data_root", hexutil.Encode(attestationDataRoot)) + params.Add("committee_index", strconv.FormatUint(uint64(committeeIndex), 10)) + endpoint := buildURL("/eth/v2/validator/aggregate_attestation", params) - aggregatedAttestation, err := convertAttestationToProto(attData) + var aggregateAttestationResponse structs.AggregateAttestationResponse + err := c.jsonRestHandler.Get(ctx, endpoint, &aggregateAttestationResponse) + errJson := &httputil.DefaultJsonError{} if err != nil { - return nil, errors.Wrap(err, "failed to convert aggregate attestation json to proto") + // TODO: remove this when v2 becomes default + if !errors.As(err, &errJson) { + return nil, err + } + if errJson.Code != http.StatusNotFound { + return nil, errJson + } + log.Debug("Endpoint /eth/v2/validator/aggregate_attestation is not supported, falling back to older endpoints for get aggregated attestation.") + params = url.Values{} + params.Add("slot", strconv.FormatUint(uint64(slot), 10)) + params.Add("attestation_data_root", hexutil.Encode(attestationDataRoot)) + oldEndpoint := buildURL("/eth/v1/validator/aggregate_attestation", params) + if err = c.jsonRestHandler.Get(ctx, oldEndpoint, &aggregateAttestationResponse); err != nil { + return nil, err + } } - return ðpb.AggregateSelectionResponse{ - AggregateAndProof: ðpb.AggregateAttestationAndProof{ - AggregatorIndex: index, - Aggregate: aggregatedAttestation, - SelectionProof: in.SlotSignature, - }, - }, nil + return &aggregateAttestationResponse, nil } -func (c *beaconApiValidatorClient) aggregateAttestation( +func (c *beaconApiValidatorClient) aggregateAttestationElectra( ctx context.Context, slot primitives.Slot, attestationDataRoot []byte, + committeeIndex primitives.CommitteeIndex, ) (*structs.AggregateAttestationResponse, error) { params := url.Values{} params.Add("slot", strconv.FormatUint(uint64(slot), 10)) params.Add("attestation_data_root", hexutil.Encode(attestationDataRoot)) - endpoint := buildURL("/eth/v1/validator/aggregate_attestation", params) + params.Add("committee_index", strconv.FormatUint(uint64(committeeIndex), 10)) + endpoint := buildURL("/eth/v2/validator/aggregate_attestation", params) var aggregateAttestationResponse structs.AggregateAttestationResponse if err := c.jsonRestHandler.Get(ctx, endpoint, &aggregateAttestationResponse); err != nil { 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 6fa1abebed41..e5a3a0797567 100644 --- a/validator/client/beacon-api/submit_aggregate_selection_proof_test.go +++ b/validator/client/beacon-api/submit_aggregate_selection_proof_test.go @@ -5,11 +5,13 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "testing" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/prysmaticlabs/prysm/v5/api/server/structs" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" @@ -23,7 +25,7 @@ func TestSubmitAggregateSelectionProof(t *testing.T) { pubkeyStr = "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13" syncingEndpoint = "/eth/v1/node/syncing" attestationDataEndpoint = "/eth/v1/validator/attestation_data" - aggregateAttestationEndpoint = "/eth/v1/validator/aggregate_attestation" + aggregateAttestationEndpoint = "/eth/v2/validator/aggregate_attestation" validatorIndex = primitives.ValidatorIndex(55293) slotSignature = "0x8776a37d6802c4797d113169c5fcfda50e68a32058eb6356a6f00d06d7da64c841a00c7c38b9b94a204751eca53707bd03523ce4797827d9bacff116a6e776a20bbccff4b683bf5201b610797ed0502557a58a65c8395f8a1649b976c3112d15" slot = primitives.Slot(123) @@ -131,7 +133,7 @@ func TestSubmitAggregateSelectionProof(t *testing.T) { // Call attestation data to get attestation data root to query aggregate attestation. jsonRestHandler.EXPECT().Get( gomock.Any(), - fmt.Sprintf("%s?attestation_data_root=%s&slot=%d", aggregateAttestationEndpoint, hexutil.Encode(attestationDataRootBytes[:]), slot), + fmt.Sprintf("%s?attestation_data_root=%s&committee_index=%d&slot=%d", aggregateAttestationEndpoint, hexutil.Encode(attestationDataRootBytes[:]), committeeIndex, slot), &structs.AggregateAttestationResponse{}, ).SetArg( 2, @@ -185,3 +187,295 @@ func TestSubmitAggregateSelectionProof(t *testing.T) { }) } } + +func TestSubmitAggregateSelectionProofFallBack(t *testing.T) { + const ( + pubkeyStr = "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13" + syncingEndpoint = "/eth/v1/node/syncing" + attestationDataEndpoint = "/eth/v1/validator/attestation_data" + aggregateAttestationEndpoint = "/eth/v1/validator/aggregate_attestation" + aggregateAttestationV2Endpoint = "/eth/v2/validator/aggregate_attestation" + validatorIndex = primitives.ValidatorIndex(55293) + slotSignature = "0x8776a37d6802c4797d113169c5fcfda50e68a32058eb6356a6f00d06d7da64c841a00c7c38b9b94a204751eca53707bd03523ce4797827d9bacff116a6e776a20bbccff4b683bf5201b610797ed0502557a58a65c8395f8a1649b976c3112d15" + slot = primitives.Slot(123) + committeeIndex = primitives.CommitteeIndex(1) + committeesAtSlot = uint64(1) + ) + + attestationDataResponse := generateValidAttestation(uint64(slot), uint64(committeeIndex)) + attestationDataProto, err := attestationDataResponse.Data.ToConsensus() + require.NoError(t, err) + attestationDataRootBytes, err := attestationDataProto.HashTreeRoot() + require.NoError(t, err) + + aggregateAttestation := ðpb.Attestation{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: attestationDataProto, + Signature: testhelpers.FillByteSlice(96, 82), + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + + // Call node syncing endpoint to check if head is optimistic. + jsonRestHandler.EXPECT().Get( + gomock.Any(), + syncingEndpoint, + &structs.SyncStatusResponse{}, + ).SetArg( + 2, + structs.SyncStatusResponse{ + Data: &structs.SyncStatusResponseData{ + IsOptimistic: false, + }, + }, + ).Return( + nil, + ).Times(1) + + // Call attestation data to get attestation data root to query aggregate attestation. + jsonRestHandler.EXPECT().Get( + gomock.Any(), + fmt.Sprintf("%s?committee_index=%d&slot=%d", attestationDataEndpoint, committeeIndex, slot), + &structs.GetAttestationDataResponse{}, + ).SetArg( + 2, + attestationDataResponse, + ).Return( + nil, + ).Times(1) + + 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(), + fmt.Sprintf("%s?attestation_data_root=%s&committee_index=%d&slot=%d", aggregateAttestationV2Endpoint, hexutil.Encode(attestationDataRootBytes[:]), committeeIndex, slot), + &structs.AggregateAttestationResponse{}, + ).Return( + &httputil.DefaultJsonError{ + Code: http.StatusNotFound, + }, + ).Times(1) + + // Call attestation data to get attestation data root to query aggregate attestation. + jsonRestHandler.EXPECT().Get( + gomock.Any(), + fmt.Sprintf("%s?attestation_data_root=%s&slot=%d", aggregateAttestationEndpoint, hexutil.Encode(attestationDataRootBytes[:]), slot), + &structs.AggregateAttestationResponse{}, + ).SetArg( + 2, + structs.AggregateAttestationResponse{ + Data: attestationJSON, + }, + ).Return( + nil, + ).Times(1) + + pubkey, err := hexutil.Decode(pubkeyStr) + require.NoError(t, err) + + slotSignatureBytes, err := hexutil.Decode(slotSignature) + require.NoError(t, err) + + expectedResponse := ðpb.AggregateSelectionResponse{ + AggregateAndProof: ðpb.AggregateAttestationAndProof{ + AggregatorIndex: primitives.ValidatorIndex(55293), + Aggregate: aggregateAttestation, + SelectionProof: slotSignatureBytes, + }, + } + + validatorClient := &beaconApiValidatorClient{ + jsonRestHandler: jsonRestHandler, + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + dutiesProvider: beaconApiDutiesProvider{ + jsonRestHandler: jsonRestHandler, + }, + } + + actualResponse, err := validatorClient.submitAggregateSelectionProof(ctx, ðpb.AggregateSelectionRequest{ + Slot: slot, + CommitteeIndex: committeeIndex, + PublicKey: pubkey, + SlotSignature: slotSignatureBytes, + }, validatorIndex, committeesAtSlot) + + require.NoError(t, err) + assert.DeepEqual(t, expectedResponse, actualResponse) + +} + +func TestSubmitAggregateSelectionProofElectra(t *testing.T) { + const ( + pubkeyStr = "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13" + syncingEndpoint = "/eth/v1/node/syncing" + attestationDataEndpoint = "/eth/v1/validator/attestation_data" + aggregateAttestationEndpoint = "/eth/v2/validator/aggregate_attestation" + validatorIndex = primitives.ValidatorIndex(55293) + slotSignature = "0x8776a37d6802c4797d113169c5fcfda50e68a32058eb6356a6f00d06d7da64c841a00c7c38b9b94a204751eca53707bd03523ce4797827d9bacff116a6e776a20bbccff4b683bf5201b610797ed0502557a58a65c8395f8a1649b976c3112d15" + slot = primitives.Slot(123) + committeeIndex = primitives.CommitteeIndex(1) + committeesAtSlot = uint64(1) + ) + + attestationDataResponse := generateValidAttestation(uint64(slot), uint64(committeeIndex)) + attestationDataProto, err := attestationDataResponse.Data.ToConsensus() + require.NoError(t, err) + attestationDataRootBytes, err := attestationDataProto.HashTreeRoot() + require.NoError(t, err) + + aggregateAttestation := ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: attestationDataProto, + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + tests := []struct { + name string + isOptimistic bool + syncingErr error + attestationDataErr error + aggregateAttestationErr error + attestationDataCalled int + aggregateAttestationCalled int + expectedErrorMsg string + committeesAtSlot uint64 + }{ + { + name: "success", + attestationDataCalled: 1, + aggregateAttestationCalled: 1, + }, + { + name: "head is optimistic", + isOptimistic: true, + expectedErrorMsg: "the node is currently optimistic and cannot serve validators", + }, + { + name: "syncing error", + syncingErr: errors.New("bad request"), + expectedErrorMsg: "failed to get syncing status", + }, + { + name: "attestation data error", + attestationDataCalled: 1, + attestationDataErr: errors.New("bad request"), + expectedErrorMsg: fmt.Sprintf("failed to get attestation data for slot=%d and committee_index=%d", slot, committeeIndex), + }, + { + name: "aggregate attestation error", + attestationDataCalled: 1, + aggregateAttestationCalled: 1, + aggregateAttestationErr: errors.New("bad request"), + expectedErrorMsg: "bad request", + }, + { + name: "validator is not an aggregator", + committeesAtSlot: 64, + expectedErrorMsg: "validator is not an aggregator", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + + // Call node syncing endpoint to check if head is optimistic. + jsonRestHandler.EXPECT().Get( + gomock.Any(), + syncingEndpoint, + &structs.SyncStatusResponse{}, + ).SetArg( + 2, + structs.SyncStatusResponse{ + Data: &structs.SyncStatusResponseData{ + IsOptimistic: test.isOptimistic, + }, + }, + ).Return( + test.syncingErr, + ).Times(1) + + // Call attestation data to get attestation data root to query aggregate attestation. + jsonRestHandler.EXPECT().Get( + gomock.Any(), + fmt.Sprintf("%s?committee_index=%d&slot=%d", attestationDataEndpoint, committeeIndex, slot), + &structs.GetAttestationDataResponse{}, + ).SetArg( + 2, + attestationDataResponse, + ).Return( + test.attestationDataErr, + ).Times(test.attestationDataCalled) + + attestationJSON, err := json.Marshal(jsonifyAttestationElectra(aggregateAttestation)) + require.NoError(t, err) + + // Call attestation data to get attestation data root to query aggregate attestation. + jsonRestHandler.EXPECT().Get( + gomock.Any(), + fmt.Sprintf("%s?attestation_data_root=%s&committee_index=%d&slot=%d", aggregateAttestationEndpoint, hexutil.Encode(attestationDataRootBytes[:]), committeeIndex, slot), + &structs.AggregateAttestationResponse{}, + ).SetArg( + 2, + structs.AggregateAttestationResponse{ + Data: attestationJSON, + }, + ).Return( + test.aggregateAttestationErr, + ).Times(test.aggregateAttestationCalled) + + pubkey, err := hexutil.Decode(pubkeyStr) + require.NoError(t, err) + + slotSignatureBytes, err := hexutil.Decode(slotSignature) + require.NoError(t, err) + + expectedResponse := ðpb.AggregateSelectionElectraResponse{ + AggregateAndProof: ðpb.AggregateAttestationAndProofElectra{ + AggregatorIndex: primitives.ValidatorIndex(55293), + Aggregate: aggregateAttestation, + SelectionProof: slotSignatureBytes, + }, + } + + validatorClient := &beaconApiValidatorClient{ + jsonRestHandler: jsonRestHandler, + stateValidatorsProvider: beaconApiStateValidatorsProvider{ + jsonRestHandler: jsonRestHandler, + }, + dutiesProvider: beaconApiDutiesProvider{ + jsonRestHandler: jsonRestHandler, + }, + } + + committees := committeesAtSlot + if test.committeesAtSlot != 0 { + committees = test.committeesAtSlot + } + actualResponse, err := validatorClient.submitAggregateSelectionProofElectra(ctx, ðpb.AggregateSelectionRequest{ + Slot: slot, + CommitteeIndex: committeeIndex, + PublicKey: pubkey, + SlotSignature: slotSignatureBytes, + }, validatorIndex, committees) + if test.expectedErrorMsg == "" { + require.NoError(t, err) + assert.DeepEqual(t, expectedResponse, actualResponse) + } else { + require.ErrorContains(t, test.expectedErrorMsg, err) + } + }) + } +} diff --git a/validator/client/beacon-api/submit_signed_aggregate_proof.go b/validator/client/beacon-api/submit_signed_aggregate_proof.go index 9ebca667a63f..7f553d9b2be5 100644 --- a/validator/client/beacon-api/submit_signed_aggregate_proof.go +++ b/validator/client/beacon-api/submit_signed_aggregate_proof.go @@ -4,10 +4,13 @@ import ( "bytes" "context" "encoding/json" + "net/http" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/server/structs" + "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" ) func (c *beaconApiValidatorClient) submitSignedAggregateSelectionProof(ctx context.Context, in *ethpb.SignedAggregateSubmitRequest) (*ethpb.SignedAggregateSubmitResponse, error) { @@ -15,8 +18,44 @@ func (c *beaconApiValidatorClient) submitSignedAggregateSelectionProof(ctx conte if err != nil { return nil, errors.Wrap(err, "failed to marshal SignedAggregateAttestationAndProof") } + headers := map[string]string{"Eth-Consensus-Version": version.String(in.SignedAggregateAndProof.Version())} + err = c.jsonRestHandler.Post(ctx, "/eth/v2/validator/aggregate_and_proofs", headers, bytes.NewBuffer(body), nil) + errJson := &httputil.DefaultJsonError{} + if err != nil { + // TODO: remove this when v2 becomes default + if !errors.As(err, &errJson) { + return nil, err + } + if errJson.Code != http.StatusNotFound { + return nil, errJson + } + log.Debug("Endpoint /eth/v2/validator/aggregate_and_proofs is not supported, falling back to older endpoints for publish aggregate and proofs.") + if err = c.jsonRestHandler.Post( + ctx, + "/eth/v1/validator/aggregate_and_proofs", + nil, + bytes.NewBuffer(body), + nil, + ); err != nil { + return nil, err + } + } + + attestationDataRoot, err := in.SignedAggregateAndProof.Message.Aggregate.Data.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "failed to compute attestation data root") + } + + return ðpb.SignedAggregateSubmitResponse{AttestationDataRoot: attestationDataRoot[:]}, nil +} - if err = c.jsonRestHandler.Post(ctx, "/eth/v1/validator/aggregate_and_proofs", nil, bytes.NewBuffer(body), nil); err != nil { +func (c *beaconApiValidatorClient) submitSignedAggregateSelectionProofElectra(ctx context.Context, in *ethpb.SignedAggregateSubmitElectraRequest) (*ethpb.SignedAggregateSubmitResponse, error) { + body, err := json.Marshal([]*structs.SignedAggregateAttestationAndProofElectra{jsonifySignedAggregateAndProofElectra(in.SignedAggregateAndProof)}) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal SignedAggregateAttestationAndProofElectra") + } + headers := map[string]string{"Eth-Consensus-Version": version.String(in.SignedAggregateAndProof.Version())} + if err = c.jsonRestHandler.Post(ctx, "/eth/v2/validator/aggregate_and_proofs", headers, bytes.NewBuffer(body), nil); err != nil { return nil, err } diff --git a/validator/client/beacon-api/submit_signed_aggregate_proof_test.go b/validator/client/beacon-api/submit_signed_aggregate_proof_test.go index 1251d3d4d604..c25822d5cfa2 100644 --- a/validator/client/beacon-api/submit_signed_aggregate_proof_test.go +++ b/validator/client/beacon-api/submit_signed_aggregate_proof_test.go @@ -4,11 +4,14 @@ import ( "bytes" "context" "encoding/json" + "net/http" "testing" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/server/structs" + "github.com/prysmaticlabs/prysm/v5/network/httputil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" "github.com/prysmaticlabs/prysm/v5/validator/client/beacon-api/mock" @@ -25,12 +28,12 @@ func TestSubmitSignedAggregateSelectionProof_Valid(t *testing.T) { require.NoError(t, err) ctx := context.Background() - + headers := map[string]string{"Eth-Consensus-Version": version.String(signedAggregateAndProof.Message.Version())} jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) jsonRestHandler.EXPECT().Post( gomock.Any(), - "/eth/v1/validator/aggregate_and_proofs", - nil, + "/eth/v2/validator/aggregate_and_proofs", + headers, bytes.NewBuffer(marshalledSignedAggregateSignedAndProof), nil, ).Return( @@ -57,11 +60,12 @@ func TestSubmitSignedAggregateSelectionProof_BadRequest(t *testing.T) { require.NoError(t, err) ctx := context.Background() + headers := map[string]string{"Eth-Consensus-Version": version.String(signedAggregateAndProof.Message.Version())} jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) jsonRestHandler.EXPECT().Post( gomock.Any(), - "/eth/v1/validator/aggregate_and_proofs", - nil, + "/eth/v2/validator/aggregate_and_proofs", + headers, bytes.NewBuffer(marshalledSignedAggregateSignedAndProof), nil, ).Return( @@ -75,6 +79,110 @@ func TestSubmitSignedAggregateSelectionProof_BadRequest(t *testing.T) { assert.ErrorContains(t, "bad request", err) } +func TestSubmitSignedAggregateSelectionProof_Fallback(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + signedAggregateAndProof := generateSignedAggregateAndProofJson() + marshalledSignedAggregateSignedAndProof, err := json.Marshal([]*structs.SignedAggregateAttestationAndProof{jsonifySignedAggregateAndProof(signedAggregateAndProof)}) + require.NoError(t, err) + + ctx := context.Background() + + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + headers := map[string]string{"Eth-Consensus-Version": version.String(signedAggregateAndProof.Message.Version())} + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v2/validator/aggregate_and_proofs", + headers, + bytes.NewBuffer(marshalledSignedAggregateSignedAndProof), + nil, + ).Return( + &httputil.DefaultJsonError{ + Code: http.StatusNotFound, + }, + ).Times(1) + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v1/validator/aggregate_and_proofs", + nil, + bytes.NewBuffer(marshalledSignedAggregateSignedAndProof), + nil, + ).Return( + nil, + ).Times(1) + + attestationDataRoot, err := signedAggregateAndProof.Message.Aggregate.Data.HashTreeRoot() + require.NoError(t, err) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + resp, err := validatorClient.submitSignedAggregateSelectionProof(ctx, ðpb.SignedAggregateSubmitRequest{ + SignedAggregateAndProof: signedAggregateAndProof, + }) + require.NoError(t, err) + assert.DeepEqual(t, attestationDataRoot[:], resp.AttestationDataRoot) +} + +func TestSubmitSignedAggregateSelectionProofElectra_Valid(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + signedAggregateAndProofElectra := generateSignedAggregateAndProofElectraJson() + marshalledSignedAggregateSignedAndProofElectra, err := json.Marshal([]*structs.SignedAggregateAttestationAndProofElectra{jsonifySignedAggregateAndProofElectra(signedAggregateAndProofElectra)}) + require.NoError(t, err) + + ctx := context.Background() + headers := map[string]string{"Eth-Consensus-Version": version.String(signedAggregateAndProofElectra.Message.Version())} + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v2/validator/aggregate_and_proofs", + headers, + bytes.NewBuffer(marshalledSignedAggregateSignedAndProofElectra), + nil, + ).Return( + nil, + ).Times(1) + + attestationDataRoot, err := signedAggregateAndProofElectra.Message.Aggregate.Data.HashTreeRoot() + require.NoError(t, err) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + resp, err := validatorClient.submitSignedAggregateSelectionProofElectra(ctx, ðpb.SignedAggregateSubmitElectraRequest{ + SignedAggregateAndProof: signedAggregateAndProofElectra, + }) + require.NoError(t, err) + assert.DeepEqual(t, attestationDataRoot[:], resp.AttestationDataRoot) +} + +func TestSubmitSignedAggregateSelectionProofElectra_BadRequest(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + signedAggregateAndProofElectra := generateSignedAggregateAndProofElectraJson() + marshalledSignedAggregateSignedAndProofElectra, err := json.Marshal([]*structs.SignedAggregateAttestationAndProofElectra{jsonifySignedAggregateAndProofElectra(signedAggregateAndProofElectra)}) + require.NoError(t, err) + + ctx := context.Background() + headers := map[string]string{"Eth-Consensus-Version": version.String(signedAggregateAndProofElectra.Message.Version())} + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().Post( + gomock.Any(), + "/eth/v2/validator/aggregate_and_proofs", + headers, + bytes.NewBuffer(marshalledSignedAggregateSignedAndProofElectra), + nil, + ).Return( + errors.New("bad request"), + ).Times(1) + + validatorClient := &beaconApiValidatorClient{jsonRestHandler: jsonRestHandler} + _, err = validatorClient.submitSignedAggregateSelectionProofElectra(ctx, ðpb.SignedAggregateSubmitElectraRequest{ + SignedAggregateAndProof: signedAggregateAndProofElectra, + }) + assert.ErrorContains(t, "bad request", err) +} + func generateSignedAggregateAndProofJson() *ethpb.SignedAggregateAttestationAndProof { return ðpb.SignedAggregateAttestationAndProof{ Message: ðpb.AggregateAttestationAndProof{ @@ -101,3 +209,31 @@ func generateSignedAggregateAndProofJson() *ethpb.SignedAggregateAttestationAndP Signature: testhelpers.FillByteSlice(96, 82), } } + +func generateSignedAggregateAndProofElectraJson() *ethpb.SignedAggregateAttestationAndProofElectra { + return ðpb.SignedAggregateAttestationAndProofElectra{ + Message: ðpb.AggregateAttestationAndProofElectra{ + AggregatorIndex: 72, + Aggregate: ðpb.AttestationElectra{ + AggregationBits: testhelpers.FillByteSlice(4, 74), + Data: ðpb.AttestationData{ + Slot: 75, + CommitteeIndex: 76, + BeaconBlockRoot: testhelpers.FillByteSlice(32, 38), + Source: ðpb.Checkpoint{ + Epoch: 78, + Root: testhelpers.FillByteSlice(32, 79), + }, + Target: ðpb.Checkpoint{ + Epoch: 80, + Root: testhelpers.FillByteSlice(32, 81), + }, + }, + Signature: testhelpers.FillByteSlice(96, 82), + CommitteeBits: testhelpers.FillByteSlice(8, 83), + }, + SelectionProof: testhelpers.FillByteSlice(96, 84), + }, + Signature: testhelpers.FillByteSlice(96, 85), + } +}