diff --git a/api/server/structs/block.go b/api/server/structs/block.go index 5393088aa03c..19ef7bde7f5b 100644 --- a/api/server/structs/block.go +++ b/api/server/structs/block.go @@ -540,6 +540,12 @@ type PayloadAttestation struct { Signature string `json:"signature"` } +type PayloadAttestationMessage struct { + ValidatorIndex string `json:"validator_index"` + Data *PayloadAttestationData `json:"data"` + Signature string `json:"signature"` +} + type BeaconBlockBodyGloas struct { RandaoReveal string `json:"randao_reveal"` Eth1Data *Eth1Data `json:"eth1_data"` diff --git a/api/server/structs/conversions_block.go b/api/server/structs/conversions_block.go index 1effc2b0b8fc..2a936fff71ea 100644 --- a/api/server/structs/conversions_block.go +++ b/api/server/structs/conversions_block.go @@ -3263,3 +3263,34 @@ func (d *PayloadAttestationData) ToConsensus() (*eth.PayloadAttestationData, err BlobDataAvailable: d.BlobDataAvailable, }, nil } + +func PayloadAttestationMessageFromConsensus(msg *eth.PayloadAttestationMessage) *PayloadAttestationMessage { + return &PayloadAttestationMessage{ + ValidatorIndex: fmt.Sprintf("%d", msg.ValidatorIndex), + Data: PayloadAttestationDataFromConsensus(msg.Data), + Signature: hexutil.Encode(msg.Signature), + } +} + +func (p *PayloadAttestationMessage) ToConsensus() (*eth.PayloadAttestationMessage, error) { + if p == nil { + return nil, errNilValue + } + validatorIndex, err := strconv.ParseUint(p.ValidatorIndex, 10, 64) + if err != nil { + return nil, server.NewDecodeError(err, "ValidatorIndex") + } + data, err := p.Data.ToConsensus() + if err != nil { + return nil, server.NewDecodeError(err, "Data") + } + sig, err := bytesutil.DecodeHexWithLength(p.Signature, fieldparams.BLSSignatureLength) + if err != nil { + return nil, server.NewDecodeError(err, "Signature") + } + return ð.PayloadAttestationMessage{ + ValidatorIndex: primitives.ValidatorIndex(validatorIndex), + Data: data, + Signature: sig, + }, nil +} diff --git a/api/server/structs/endpoints_beacon.go b/api/server/structs/endpoints_beacon.go index 511d452cc0c8..b78637d05908 100644 --- a/api/server/structs/endpoints_beacon.go +++ b/api/server/structs/endpoints_beacon.go @@ -188,6 +188,11 @@ type BLSToExecutionChangesPoolResponse struct { Data []*SignedBLSToExecutionChange `json:"data"` } +type GetPoolPayloadAttestationsResponse struct { + Version string `json:"version"` + Data []*PayloadAttestation `json:"data"` +} + type GetAttesterSlashingsResponse struct { Version string `json:"version,omitempty"` Data json.RawMessage `json:"data"` // Accepts both `[]*AttesterSlashing` and `[]*AttesterSlashingElectra` types diff --git a/api/server/structs/endpoints_validator.go b/api/server/structs/endpoints_validator.go index 71778fe62e4f..8f1841b8f5c2 100644 --- a/api/server/structs/endpoints_validator.go +++ b/api/server/structs/endpoints_validator.go @@ -31,6 +31,11 @@ type GetAttestationDataResponse struct { Data *AttestationData `json:"data"` } +type GetPayloadAttestationDataResponse struct { + Version string `json:"version"` + Data *PayloadAttestationData `json:"data"` +} + type ProduceSyncCommitteeContributionResponse struct { Data *SyncCommitteeContribution `json:"data"` } diff --git a/beacon-chain/node/BUILD.bazel b/beacon-chain/node/BUILD.bazel index f1cae794fee0..5b8cfebca544 100644 --- a/beacon-chain/node/BUILD.bazel +++ b/beacon-chain/node/BUILD.bazel @@ -37,6 +37,7 @@ go_library( "//beacon-chain/node/registration:go_default_library", "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", + "//beacon-chain/operations/payloadattestation:go_default_library", "//beacon-chain/operations/slashings:go_default_library", "//beacon-chain/operations/synccommittee:go_default_library", "//beacon-chain/operations/voluntaryexits:go_default_library", diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index ce0b2f28e501..b8d086848810 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -40,6 +40,7 @@ import ( "github.com/OffchainLabs/prysm/v7/beacon-chain/node/registration" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/attestations" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/blstoexec" + "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/slashings" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/synccommittee" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/voluntaryexits" @@ -101,6 +102,7 @@ type BeaconNode struct { slashingsPool slashings.PoolManager syncCommitteePool synccommittee.Pool blsToExecPool blstoexec.PoolManager + payloadAttestationPool payloadattestation.PoolManager depositCache cache.DepositCache trackedValidatorsCache *cache.TrackedValidatorsCache payloadIDCache *cache.PayloadIDCache @@ -141,20 +143,21 @@ func New(cliCtx *cli.Context, cancel context.CancelFunc, opts ...Option) (*Beaco ctx := cliCtx.Context beacon := &BeaconNode{ - cliCtx: cliCtx, - ctx: ctx, - cancel: cancel, - services: runtime.NewServiceRegistry(), - stop: make(chan struct{}), - stateFeed: new(event.Feed), - blockFeed: new(event.Feed), - opFeed: new(event.Feed), - attestationCache: cache.NewAttestationCache(), - attestationPool: attestations.NewPool(), - exitPool: voluntaryexits.NewPool(), - slashingsPool: slashings.NewPool(), - syncCommitteePool: synccommittee.NewPool(), - blsToExecPool: blstoexec.NewPool(), + cliCtx: cliCtx, + ctx: ctx, + cancel: cancel, + services: runtime.NewServiceRegistry(), + stop: make(chan struct{}), + stateFeed: new(event.Feed), + blockFeed: new(event.Feed), + opFeed: new(event.Feed), + attestationCache: cache.NewAttestationCache(), + attestationPool: attestations.NewPool(), + exitPool: voluntaryexits.NewPool(), + slashingsPool: slashings.NewPool(), + syncCommitteePool: synccommittee.NewPool(), + blsToExecPool: blstoexec.NewPool(), + // TODO payloadAttestationPool: set once a PoolManager implementation exists. trackedValidatorsCache: cache.NewTrackedValidatorsCache(), payloadIDCache: cache.NewPayloadIDCache(), slasherBlockHeadersFeed: new(event.Feed), @@ -972,6 +975,7 @@ func (b *BeaconNode) registerRPCService(router *http.ServeMux) error { SlashingsPool: b.slashingsPool, BLSChangesPool: b.blsToExecPool, SyncCommitteeObjectPool: b.syncCommitteePool, + PayloadAttestationPool: b.payloadAttestationPool, ExecutionChainService: web3Service, ExecutionChainInfoFetcher: web3Service, ChainStartFetcher: chainStartFetcher, diff --git a/beacon-chain/operations/payloadattestation/BUILD.bazel b/beacon-chain/operations/payloadattestation/BUILD.bazel new file mode 100644 index 000000000000..1c68eb7169bc --- /dev/null +++ b/beacon-chain/operations/payloadattestation/BUILD.bazel @@ -0,0 +1,12 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["pool.go"], + importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation", + visibility = ["//visibility:public"], + deps = [ + "//consensus-types/primitives:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + ], +) diff --git a/beacon-chain/operations/payloadattestation/mock/BUILD.bazel b/beacon-chain/operations/payloadattestation/mock/BUILD.bazel new file mode 100644 index 000000000000..c9a73ab235dd --- /dev/null +++ b/beacon-chain/operations/payloadattestation/mock/BUILD.bazel @@ -0,0 +1,9 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["mock.go"], + importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation/mock", + visibility = ["//visibility:public"], + deps = ["//proto/prysm/v1alpha1:go_default_library"], +) diff --git a/beacon-chain/operations/payloadattestation/mock/mock.go b/beacon-chain/operations/payloadattestation/mock/mock.go new file mode 100644 index 000000000000..820164a74a51 --- /dev/null +++ b/beacon-chain/operations/payloadattestation/mock/mock.go @@ -0,0 +1,28 @@ +package mock + +import ( + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +// PoolMock is a fake implementation of PoolManager. +type PoolMock struct { + Attestations []*ethpb.PayloadAttestation +} + +// PendingPayloadAttestations -- +func (m *PoolMock) PendingPayloadAttestations() []*ethpb.PayloadAttestation { + return m.Attestations +} + +// InsertPayloadAttestation -- +func (m *PoolMock) InsertPayloadAttestation(msg *ethpb.PayloadAttestationMessage) error { + m.Attestations = append(m.Attestations, ðpb.PayloadAttestation{ + Data: msg.Data, + Signature: msg.Signature, + }) + return nil +} + +// MarkIncluded -- +func (*PoolMock) MarkIncluded(_ *ethpb.PayloadAttestation) { +} diff --git a/beacon-chain/operations/payloadattestation/pool.go b/beacon-chain/operations/payloadattestation/pool.go new file mode 100644 index 000000000000..2d8a332a29d6 --- /dev/null +++ b/beacon-chain/operations/payloadattestation/pool.go @@ -0,0 +1,20 @@ +package payloadattestation + +import ( + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +// PoolManager maintains pending payload attestations. +// This pool is used by proposers to insert payload attestations into new blocks. +type PoolManager interface { + PendingPayloadAttestations() []*ethpb.PayloadAttestation + InsertPayloadAttestation(msg *ethpb.PayloadAttestationMessage) error + MarkIncluded(att *ethpb.PayloadAttestation) +} + +// PayloadStatusFetcher determines the payload presence and blob data availability +// for a given slot. This is used by PTC validators to produce PayloadAttestationData. +type PayloadStatusFetcher interface { + PayloadStatus(slot primitives.Slot) (payloadPresent bool, blobDataAvailable bool, err error) +} diff --git a/beacon-chain/rpc/BUILD.bazel b/beacon-chain/rpc/BUILD.bazel index 404e378181c0..c2500d753ad7 100644 --- a/beacon-chain/rpc/BUILD.bazel +++ b/beacon-chain/rpc/BUILD.bazel @@ -26,6 +26,7 @@ go_library( "//beacon-chain/light-client:go_default_library", "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", + "//beacon-chain/operations/payloadattestation:go_default_library", "//beacon-chain/operations/slashings:go_default_library", "//beacon-chain/operations/synccommittee:go_default_library", "//beacon-chain/operations/voluntaryexits:go_default_library", diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 4b953a1b0726..9da319471f8c 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -217,6 +217,7 @@ func (s *Service) validatorEndpoints( OperationNotifier: s.cfg.OperationNotifier, TrackedValidatorsCache: s.cfg.TrackedValidatorsCache, PayloadIDCache: s.cfg.PayloadIDCache, + PayloadAttestationPool: s.cfg.PayloadAttestationPool, CoreService: coreService, BlockRewardFetcher: rewardFetcher, } @@ -390,6 +391,16 @@ func (s *Service) validatorEndpoints( handler: server.SyncCommitteeSelections, methods: []string{http.MethodPost}, }, + { + template: "/eth/v1/validator/payload_attestation_data/{slot}", + name: namespace + ".GetPayloadAttestationData", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType, api.OctetStreamMediaType}), + middleware.AcceptEncodingHeaderHandler(), + }, + handler: server.GetPayloadAttestationData, + methods: []string{http.MethodGet}, + }, } } @@ -512,6 +523,7 @@ func (s *Service) beaconEndpoints( SyncChecker: s.cfg.SyncService, ExecutionReconstructor: s.cfg.ExecutionReconstructor, BLSChangesPool: s.cfg.BLSChangesPool, + PayloadAttestationPool: s.cfg.PayloadAttestationPool, FinalizationFetcher: s.cfg.FinalizationFetcher, ForkchoiceFetcher: s.cfg.ForkchoiceFetcher, CoreService: coreService, @@ -869,6 +881,27 @@ func (s *Service) beaconEndpoints( handler: server.GetProposerLookahead, methods: []string{http.MethodGet}, }, + { + template: "/eth/v1/beacon/pool/payload_attestations", + name: namespace + ".ListPayloadAttestations", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + middleware.AcceptEncodingHeaderHandler(), + }, + handler: server.ListPayloadAttestations, + methods: []string{http.MethodGet}, + }, + { + template: "/eth/v1/beacon/pool/payload_attestations", + name: namespace + ".SubmitPayloadAttestations", + middleware: []middleware.Middleware{ + middleware.ContentTypeHandler([]string{api.JsonMediaType}), + middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + middleware.AcceptEncodingHeaderHandler(), + }, + handler: server.SubmitPayloadAttestations, + methods: []string{http.MethodPost}, + }, } } diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index 7967e3c592fe..8b0d92442cb6 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -48,6 +48,7 @@ func Test_endpoints(t *testing.T) { "/eth/v1/beacon/pool/sync_committees": {http.MethodPost}, "/eth/v1/beacon/pool/voluntary_exits": {http.MethodGet, http.MethodPost}, "/eth/v1/beacon/pool/bls_to_execution_changes": {http.MethodGet, http.MethodPost}, + "/eth/v1/beacon/pool/payload_attestations": {http.MethodGet, http.MethodPost}, "/prysm/v1/beacon/individual_votes": {http.MethodPost}, } @@ -91,22 +92,23 @@ func Test_endpoints(t *testing.T) { } validatorRoutes := map[string][]string{ - "/eth/v1/validator/duties/attester/{epoch}": {http.MethodPost}, - "/eth/v1/validator/duties/proposer/{epoch}": {http.MethodGet}, - "/eth/v1/validator/duties/sync/{epoch}": {http.MethodPost}, - "/eth/v3/validator/blocks/{slot}": {http.MethodGet}, - "/eth/v1/validator/attestation_data": {http.MethodGet}, - "/eth/v2/validator/aggregate_attestation": {http.MethodGet}, - "/eth/v2/validator/aggregate_and_proofs": {http.MethodPost}, - "/eth/v1/validator/beacon_committee_subscriptions": {http.MethodPost}, - "/eth/v1/validator/sync_committee_subscriptions": {http.MethodPost}, - "/eth/v1/validator/beacon_committee_selections": {http.MethodPost}, - "/eth/v1/validator/sync_committee_selections": {http.MethodPost}, - "/eth/v1/validator/sync_committee_contribution": {http.MethodGet}, - "/eth/v1/validator/contribution_and_proofs": {http.MethodPost}, - "/eth/v1/validator/prepare_beacon_proposer": {http.MethodPost}, - "/eth/v1/validator/register_validator": {http.MethodPost}, - "/eth/v1/validator/liveness/{epoch}": {http.MethodPost}, + "/eth/v1/validator/duties/attester/{epoch}": {http.MethodPost}, + "/eth/v1/validator/duties/proposer/{epoch}": {http.MethodGet}, + "/eth/v1/validator/duties/sync/{epoch}": {http.MethodPost}, + "/eth/v3/validator/blocks/{slot}": {http.MethodGet}, + "/eth/v1/validator/attestation_data": {http.MethodGet}, + "/eth/v2/validator/aggregate_attestation": {http.MethodGet}, + "/eth/v2/validator/aggregate_and_proofs": {http.MethodPost}, + "/eth/v1/validator/beacon_committee_subscriptions": {http.MethodPost}, + "/eth/v1/validator/sync_committee_subscriptions": {http.MethodPost}, + "/eth/v1/validator/beacon_committee_selections": {http.MethodPost}, + "/eth/v1/validator/sync_committee_selections": {http.MethodPost}, + "/eth/v1/validator/sync_committee_contribution": {http.MethodGet}, + "/eth/v1/validator/contribution_and_proofs": {http.MethodPost}, + "/eth/v1/validator/prepare_beacon_proposer": {http.MethodPost}, + "/eth/v1/validator/register_validator": {http.MethodPost}, + "/eth/v1/validator/liveness/{epoch}": {http.MethodPost}, + "/eth/v1/validator/payload_attestation_data/{slot}": {http.MethodGet}, } prysmBeaconRoutes := map[string][]string{ diff --git a/beacon-chain/rpc/eth/beacon/BUILD.bazel b/beacon-chain/rpc/eth/beacon/BUILD.bazel index ccd0f52407da..4616365fe53b 100644 --- a/beacon-chain/rpc/eth/beacon/BUILD.bazel +++ b/beacon-chain/rpc/eth/beacon/BUILD.bazel @@ -32,6 +32,7 @@ go_library( "//beacon-chain/execution:go_default_library", "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", + "//beacon-chain/operations/payloadattestation:go_default_library", "//beacon-chain/operations/slashings:go_default_library", "//beacon-chain/operations/voluntaryexits:go_default_library", "//beacon-chain/p2p:go_default_library", @@ -94,6 +95,7 @@ go_test( "//beacon-chain/operations/attestations:go_default_library", "//beacon-chain/operations/blstoexec:go_default_library", "//beacon-chain/operations/blstoexec/mock:go_default_library", + "//beacon-chain/operations/payloadattestation/mock:go_default_library", "//beacon-chain/operations/slashings/mock:go_default_library", "//beacon-chain/operations/synccommittee:go_default_library", "//beacon-chain/operations/voluntaryexits/mock:go_default_library", diff --git a/beacon-chain/rpc/eth/beacon/handlers_pool.go b/beacon-chain/rpc/eth/beacon/handlers_pool.go index 8b7be09d059d..b0926bef041b 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_pool.go +++ b/beacon-chain/rpc/eth/beacon/handlers_pool.go @@ -893,3 +893,99 @@ func (s *Server) SubmitProposerSlashing(w http.ResponseWriter, r *http.Request) } } } + +// SubmitPayloadAttestations submits payload attestation messages to the node's pool. +func (s *Server) SubmitPayloadAttestations(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "beacon.SubmitPayloadAttestations") + defer span.End() + + currentEpoch := slots.ToEpoch(s.TimeFetcher.CurrentSlot()) + if currentEpoch < params.BeaconConfig().GloasForkEpoch { + httputil.HandleError(w, fmt.Sprintf("payload attestations require the Gloas fork, current epoch %d, Gloas epoch %d", currentEpoch, params.BeaconConfig().GloasForkEpoch), http.StatusBadRequest) + return + } + + if shared.IsSyncing(ctx, w, s.SyncChecker, s.HeadFetcher, s.TimeFetcher, s.OptimisticModeFetcher) { + return + } + + versionHeader := r.Header.Get(api.VersionHeader) + if versionHeader == "" { + httputil.HandleError(w, api.VersionHeader+" header is required", http.StatusBadRequest) + return + } + + var msgs []*structs.PayloadAttestationMessage + if err := json.NewDecoder(r.Body).Decode(&msgs); err != nil { + httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest) + return + } + + var failures []*server.IndexedError + for i, msg := range msgs { + consensusMsg, err := msg.ToConsensus() + if err != nil { + failures = append(failures, &server.IndexedError{ + Index: i, + Message: "Could not convert message: " + err.Error(), + }) + continue + } + + // TODO: Add full gossip validation (BLS signatures, PTC membership). + if err := s.PayloadAttestationPool.InsertPayloadAttestation(consensusMsg); err != nil { + failures = append(failures, &server.IndexedError{ + Index: i, + Message: "Could not insert payload attestation: " + err.Error(), + }) + continue + } + + if err := s.Broadcaster.Broadcast(ctx, consensusMsg); err != nil { + log.WithError(err).Error("Could not broadcast payload attestation message") + } + } + + if len(failures) > 0 { + failuresErr := &server.IndexedErrorContainer{ + Code: http.StatusBadRequest, + Message: server.ErrIndexedValidationFail, + Failures: failures, + } + httputil.WriteError(w, failuresErr) + return + } +} + +// ListPayloadAttestations retrieves payload attestations from the pool. +func (s *Server) ListPayloadAttestations(w http.ResponseWriter, r *http.Request) { + _, span := trace.StartSpan(r.Context(), "beacon.ListPayloadAttestations") + defer span.End() + + currentEpoch := slots.ToEpoch(s.TimeFetcher.CurrentSlot()) + if currentEpoch < params.BeaconConfig().GloasForkEpoch { + httputil.HandleError(w, fmt.Sprintf("payload attestations require the Gloas fork, current epoch %d, Gloas epoch %d", currentEpoch, params.BeaconConfig().GloasForkEpoch), http.StatusBadRequest) + return + } + + rawSlot, slot, ok := shared.UintFromQuery(w, r, "slot", false) + if !ok { + return + } + + allAtts := s.PayloadAttestationPool.PendingPayloadAttestations() + + var data []*structs.PayloadAttestation + for _, att := range allAtts { + if rawSlot != "" && att.Data.Slot != primitives.Slot(slot) { + continue + } + data = append(data, structs.PayloadAttestationFromConsensus(att)) + } + + w.Header().Set(api.VersionHeader, version.String(version.Gloas)) + httputil.WriteJson(w, &structs.GetPoolPayloadAttestationsResponse{ + Version: version.String(version.Gloas), + Data: data, + }) +} diff --git a/beacon-chain/rpc/eth/beacon/handlers_pool_test.go b/beacon-chain/rpc/eth/beacon/handlers_pool_test.go index ba8a14e780f4..30483b483c08 100644 --- a/beacon-chain/rpc/eth/beacon/handlers_pool_test.go +++ b/beacon-chain/rpc/eth/beacon/handlers_pool_test.go @@ -21,6 +21,7 @@ import ( "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/attestations" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/blstoexec" blstoexecmock "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/blstoexec/mock" + payloadattestationmock "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation/mock" slashingsmock "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/slashings/mock" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/synccommittee" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/voluntaryexits/mock" @@ -2547,3 +2548,253 @@ var ( } }` ) + +func TestSubmitPayloadAttestations(t *testing.T) { + t.Run("pre-gloas fork returns error", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 100 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(0) + chainService := &blockchainmock.ChainService{Slot: &slot} + s := &Server{ + TimeFetcher: chainService, + } + + request := httptest.NewRequest(http.MethodPost, "http://example.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.SubmitPayloadAttestations(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + assert.StringContains(t, "Gloas fork", writer.Body.String()) + }) + t.Run("no version header", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(0) + chainService := &blockchainmock.ChainService{Slot: &slot} + s := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + HeadFetcher: chainService, + TimeFetcher: chainService, + OptimisticModeFetcher: chainService, + } + + request := httptest.NewRequest(http.MethodPost, "http://example.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.SubmitPayloadAttestations(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + assert.StringContains(t, "Eth-Consensus-Version", writer.Body.String()) + }) + t.Run("ok", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(1) + chainService := &blockchainmock.ChainService{Slot: &slot} + broadcaster := &p2pMock.MockBroadcaster{} + pool := &payloadattestationmock.PoolMock{} + s := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + HeadFetcher: chainService, + TimeFetcher: chainService, + OptimisticModeFetcher: chainService, + Broadcaster: broadcaster, + PayloadAttestationPool: pool, + } + + body := `[{ + "validator_index": "1", + "data": { + "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "slot": "1", + "payload_present": true, + "blob_data_available": true + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + }]` + request := httptest.NewRequest(http.MethodPost, "http://example.com", strings.NewReader(body)) + request.Header.Set(api.VersionHeader, "gloas") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.SubmitPayloadAttestations(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + assert.Equal(t, 1, len(pool.Attestations)) + assert.Equal(t, true, broadcaster.BroadcastCalled.Load()) + }) + t.Run("invalid body", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(1) + chainService := &blockchainmock.ChainService{Slot: &slot} + s := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + HeadFetcher: chainService, + TimeFetcher: chainService, + OptimisticModeFetcher: chainService, + } + + request := httptest.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("invalid")) + request.Header.Set(api.VersionHeader, "gloas") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.SubmitPayloadAttestations(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + }) +} + +func TestListPayloadAttestations(t *testing.T) { + t.Run("pre-gloas fork returns error", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 100 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(0) + chainService := &blockchainmock.ChainService{Slot: &slot} + s := &Server{ + TimeFetcher: chainService, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.ListPayloadAttestations(writer, request) + assert.Equal(t, http.StatusBadRequest, writer.Code) + assert.StringContains(t, "Gloas fork", writer.Body.String()) + }) + t.Run("empty pool", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(0) + chainService := &blockchainmock.ChainService{Slot: &slot} + pool := &payloadattestationmock.PoolMock{} + s := &Server{ + TimeFetcher: chainService, + PayloadAttestationPool: pool, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.ListPayloadAttestations(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.GetPoolPayloadAttestationsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + assert.Equal(t, 0, len(resp.Data)) + }) + t.Run("returns attestations", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(0) + chainService := &blockchainmock.ChainService{Slot: &slot} + pool := &payloadattestationmock.PoolMock{ + Attestations: []*ethpbv1alpha1.PayloadAttestation{ + { + Data: ðpbv1alpha1.PayloadAttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("root1"), 32), + Slot: 1, + PayloadPresent: true, + BlobDataAvailable: true, + }, + Signature: bytesutil.PadTo([]byte("sig1"), 96), + }, + { + Data: ðpbv1alpha1.PayloadAttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("root2"), 32), + Slot: 2, + PayloadPresent: false, + BlobDataAvailable: false, + }, + Signature: bytesutil.PadTo([]byte("sig2"), 96), + }, + }, + } + s := &Server{ + TimeFetcher: chainService, + PayloadAttestationPool: pool, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.ListPayloadAttestations(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.GetPoolPayloadAttestationsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + assert.Equal(t, 2, len(resp.Data)) + assert.Equal(t, "gloas", resp.Version) + }) + t.Run("filter by slot", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 0 + params.OverrideBeaconConfig(cfg) + + slot := primitives.Slot(0) + chainService := &blockchainmock.ChainService{Slot: &slot} + pool := &payloadattestationmock.PoolMock{ + Attestations: []*ethpbv1alpha1.PayloadAttestation{ + { + Data: ðpbv1alpha1.PayloadAttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("root1"), 32), + Slot: 1, + PayloadPresent: true, + BlobDataAvailable: true, + }, + Signature: bytesutil.PadTo([]byte("sig1"), 96), + }, + { + Data: ðpbv1alpha1.PayloadAttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("root2"), 32), + Slot: 2, + PayloadPresent: false, + BlobDataAvailable: false, + }, + Signature: bytesutil.PadTo([]byte("sig2"), 96), + }, + }, + } + s := &Server{ + TimeFetcher: chainService, + PayloadAttestationPool: pool, + } + + request := httptest.NewRequest(http.MethodGet, "http://example.com?slot=1", nil) + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.ListPayloadAttestations(writer, request) + assert.Equal(t, http.StatusOK, writer.Code) + + resp := &structs.GetPoolPayloadAttestationsResponse{} + require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp)) + assert.Equal(t, 1, len(resp.Data)) + assert.Equal(t, "1", resp.Data[0].Data.Slot) + }) +} diff --git a/beacon-chain/rpc/eth/beacon/server.go b/beacon-chain/rpc/eth/beacon/server.go index bf6fca748a05..51d74de4fd8f 100644 --- a/beacon-chain/rpc/eth/beacon/server.go +++ b/beacon-chain/rpc/eth/beacon/server.go @@ -12,6 +12,7 @@ import ( "github.com/OffchainLabs/prysm/v7/beacon-chain/execution" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/attestations" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/blstoexec" + "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/slashings" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/voluntaryexits" "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p" @@ -48,6 +49,7 @@ type Server struct { ExecutionReconstructor execution.Reconstructor FinalizationFetcher blockchain.FinalizationFetcher BLSChangesPool blstoexec.PoolManager + PayloadAttestationPool payloadattestation.PoolManager ForkchoiceFetcher blockchain.ForkchoiceFetcher CoreService *core.Service AttestationStateFetcher blockchain.AttestationStateFetcher diff --git a/beacon-chain/rpc/eth/validator/BUILD.bazel b/beacon-chain/rpc/eth/validator/BUILD.bazel index 962d5c05d290..487c3f3de833 100644 --- a/beacon-chain/rpc/eth/validator/BUILD.bazel +++ b/beacon-chain/rpc/eth/validator/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/db:go_default_library", "//beacon-chain/operations/attestations:go_default_library", + "//beacon-chain/operations/payloadattestation:go_default_library", "//beacon-chain/operations/synccommittee:go_default_library", "//beacon-chain/p2p:go_default_library", "//beacon-chain/rpc/core:go_default_library", diff --git a/beacon-chain/rpc/eth/validator/handlers.go b/beacon-chain/rpc/eth/validator/handlers.go index bf2d6851c6fe..dfc3e188cf2a 100644 --- a/beacon-chain/rpc/eth/validator/handlers.go +++ b/beacon-chain/rpc/eth/validator/handlers.go @@ -1427,3 +1427,61 @@ func sortProposerDuties(duties []*structs.ProposerDuty) error { }) return err } + +// GetPayloadAttestationData produces payload attestation data for the requested slot. +func (s *Server) GetPayloadAttestationData(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "validator.GetPayloadAttestationData") + defer span.End() + + currentEpoch := slots.ToEpoch(s.TimeFetcher.CurrentSlot()) + if currentEpoch < params.BeaconConfig().GloasForkEpoch { + httputil.HandleError(w, fmt.Sprintf("payload attestation data requires the Gloas fork, current epoch %d, Gloas epoch %d", currentEpoch, params.BeaconConfig().GloasForkEpoch), http.StatusBadRequest) + return + } + + if shared.IsSyncing(ctx, w, s.SyncChecker, s.HeadFetcher, s.TimeFetcher, s.OptimisticModeFetcher) { + return + } + + _, slot, ok := shared.UintFromRoute(w, r, "slot") + if !ok { + return + } + + headRoot, err := s.HeadFetcher.HeadRoot(ctx) + if err != nil { + httputil.HandleError(w, "Could not get head root: "+err.Error(), http.StatusInternalServerError) + return + } + + payloadPresent, blobDataAvailable, err := s.PayloadStatusFetcher.PayloadStatus(primitives.Slot(slot)) + if err != nil { + httputil.HandleError(w, "Could not get payload status: "+err.Error(), http.StatusInternalServerError) + return + } + + data := ðpbalpha.PayloadAttestationData{ + BeaconBlockRoot: headRoot, + Slot: primitives.Slot(slot), + PayloadPresent: payloadPresent, + BlobDataAvailable: blobDataAvailable, + } + + if httputil.RespondWithSsz(r) { + sszData, err := data.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "Could not marshal payload attestation data: "+err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set(api.VersionHeader, version.String(version.Gloas)) + httputil.WriteSsz(w, sszData) + return + } + + response := &structs.GetPayloadAttestationDataResponse{ + Version: version.String(version.Gloas), + Data: structs.PayloadAttestationDataFromConsensus(data), + } + w.Header().Set(api.VersionHeader, version.String(version.Gloas)) + httputil.WriteJson(w, response) +} diff --git a/beacon-chain/rpc/eth/validator/server.go b/beacon-chain/rpc/eth/validator/server.go index c8f456bed313..c73ddf296df4 100644 --- a/beacon-chain/rpc/eth/validator/server.go +++ b/beacon-chain/rpc/eth/validator/server.go @@ -7,6 +7,7 @@ import ( "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation" "github.com/OffchainLabs/prysm/v7/beacon-chain/db" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/attestations" + "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/synccommittee" "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core" @@ -38,4 +39,6 @@ type Server struct { BlockRewardFetcher rewards.BlockRewardsFetcher TrackedValidatorsCache *cache.TrackedValidatorsCache PayloadIDCache *cache.PayloadIDCache + PayloadAttestationPool payloadattestation.PoolManager + PayloadStatusFetcher payloadattestation.PayloadStatusFetcher } diff --git a/beacon-chain/rpc/service.go b/beacon-chain/rpc/service.go index 032ee2e46524..a56e80fee773 100644 --- a/beacon-chain/rpc/service.go +++ b/beacon-chain/rpc/service.go @@ -22,6 +22,7 @@ import ( lightClient "github.com/OffchainLabs/prysm/v7/beacon-chain/light-client" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/attestations" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/blstoexec" + "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/payloadattestation" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/slashings" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/synccommittee" "github.com/OffchainLabs/prysm/v7/beacon-chain/operations/voluntaryexits" @@ -103,6 +104,7 @@ type Config struct { SlashingsPool slashings.PoolManager SyncCommitteeObjectPool synccommittee.Pool BLSChangesPool blstoexec.PoolManager + PayloadAttestationPool payloadattestation.PoolManager SyncService chainSync.Checker Broadcaster p2p.Broadcaster PeersFetcher p2p.PeersProvider diff --git a/changelog/james-prysm_gloas-ptc-api.md b/changelog/james-prysm_gloas-ptc-api.md new file mode 100644 index 000000000000..52338419323a --- /dev/null +++ b/changelog/james-prysm_gloas-ptc-api.md @@ -0,0 +1,4 @@ +### Added + +- GET /eth/v1/validator/payload_attestation_data/{slot} +- POST & GET /eth/v1/beacon/pool/payload_attestations \ No newline at end of file