diff --git a/api/server/structs/block.go b/api/server/structs/block.go index 1bc56f7a387a..337adf19e1eb 100644 --- a/api/server/structs/block.go +++ b/api/server/structs/block.go @@ -577,3 +577,17 @@ func (s *SignedBeaconBlockGloas) MessageRawJson() ([]byte, error) { func (s *SignedBeaconBlockGloas) SigString() string { return s.Signature } + +type ExecutionPayloadEnvelope struct { + Payload *ExecutionPayloadDeneb `json:"payload"` + ExecutionRequests *ExecutionRequests `json:"execution_requests"` + BuilderIndex string `json:"builder_index"` + BeaconBlockRoot string `json:"beacon_block_root"` + Slot string `json:"slot"` + StateRoot string `json:"state_root"` +} + +type SignedExecutionPayloadEnvelope struct { + Message *ExecutionPayloadEnvelope `json:"message"` + Signature string `json:"signature"` +} diff --git a/api/server/structs/conversions_block.go b/api/server/structs/conversions_block.go index 77080d5b075b..a93b250ff66b 100644 --- a/api/server/structs/conversions_block.go +++ b/api/server/structs/conversions_block.go @@ -3275,3 +3275,26 @@ func (d *PayloadAttestationData) ToConsensus() (*eth.PayloadAttestationData, err BlobDataAvailable: d.BlobDataAvailable, }, nil } + +// SignedExecutionPayloadEnvelopeFromConsensus converts a proto envelope to the API struct. +func SignedExecutionPayloadEnvelopeFromConsensus(e *eth.SignedExecutionPayloadEnvelope) (*SignedExecutionPayloadEnvelope, error) { + payload, err := ExecutionPayloadDenebFromConsensus(e.Message.Payload) + if err != nil { + return nil, err + } + var requests *ExecutionRequests + if e.Message.ExecutionRequests != nil { + requests = ExecutionRequestsFromConsensus(e.Message.ExecutionRequests) + } + return &SignedExecutionPayloadEnvelope{ + Message: &ExecutionPayloadEnvelope{ + Payload: payload, + ExecutionRequests: requests, + BuilderIndex: fmt.Sprintf("%d", e.Message.BuilderIndex), + BeaconBlockRoot: hexutil.Encode(e.Message.BeaconBlockRoot), + Slot: fmt.Sprintf("%d", e.Message.Slot), + StateRoot: hexutil.Encode(e.Message.StateRoot), + }, + Signature: hexutil.Encode(e.Signature), + }, nil +} diff --git a/api/server/structs/endpoints_beacon.go b/api/server/structs/endpoints_beacon.go index 511d452cc0c8..de12948bc95d 100644 --- a/api/server/structs/endpoints_beacon.go +++ b/api/server/structs/endpoints_beacon.go @@ -285,6 +285,13 @@ type GetBlobsResponse struct { Data []string `json:"data"` //blobs } +type GetExecutionPayloadEnvelopeResponse struct { + Version string `json:"version"` + ExecutionOptimistic bool `json:"execution_optimistic"` + Finalized bool `json:"finalized"` + Data *SignedExecutionPayloadEnvelope `json:"data"` +} + type SSZQueryRequest struct { Query string `json:"query"` IncludeProof bool `json:"include_proof,omitempty"` diff --git a/api/server/structs/endpoints_events.go b/api/server/structs/endpoints_events.go index 2ff2bb8a6098..e39ffac62991 100644 --- a/api/server/structs/endpoints_events.go +++ b/api/server/structs/endpoints_events.go @@ -112,3 +112,8 @@ type LightClientOptimisticUpdateEvent struct { Version string `json:"version"` Data *LightClientOptimisticUpdate `json:"data"` } + +type PayloadEvent struct { + Slot string `json:"slot"` + BlockRoot string `json:"block_root"` +} diff --git a/beacon-chain/blockchain/receive_execution_payload_envelope.go b/beacon-chain/blockchain/receive_execution_payload_envelope.go index 322bb37e94e5..9d12a1e21048 100644 --- a/beacon-chain/blockchain/receive_execution_payload_envelope.go +++ b/beacon-chain/blockchain/receive_execution_payload_envelope.go @@ -5,6 +5,8 @@ import ( "context" "fmt" + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed" + statefeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/state" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas" "github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition" "github.com/OffchainLabs/prysm/v7/beacon-chain/execution" @@ -102,6 +104,14 @@ func (s *Service) ReceiveExecutionPayloadEnvelope(ctx context.Context, signed in return err } + s.cfg.StateNotifier.StateFeed().Send(&feed.Event{ + Type: statefeed.PayloadProcessed, + Data: &statefeed.PayloadProcessedData{ + Slot: envelope.Slot(), + BlockRoot: root, + }, + }) + execution, err := envelope.Execution() if err != nil { log.WithError(err).Error("Could not get execution payload from envelope for logging") diff --git a/beacon-chain/core/feed/state/events.go b/beacon-chain/core/feed/state/events.go index 425c00318e31..217cf26ed42f 100644 --- a/beacon-chain/core/feed/state/events.go +++ b/beacon-chain/core/feed/state/events.go @@ -33,6 +33,8 @@ const ( LightClientOptimisticUpdate // PayloadAttributes events are fired upon a missed slot or new head. PayloadAttributes + // PayloadProcessed is sent after a payload envelope has been processed. + PayloadProcessed ) // BlockProcessedData is the data sent with BlockProcessed events. @@ -72,3 +74,9 @@ type InitializedData struct { // GenesisValidatorsRoot represents state.validators.HashTreeRoot(). GenesisValidatorsRoot []byte } + +// PayloadProcessedData is the data sent with PayloadProcessed events. +type PayloadProcessedData struct { + Slot primitives.Slot + BlockRoot [32]byte +} diff --git a/beacon-chain/execution/engine_client.go b/beacon-chain/execution/engine_client.go index 86d65a57b8f5..ccede2c1eba5 100644 --- a/beacon-chain/execution/engine_client.go +++ b/beacon-chain/execution/engine_client.go @@ -142,6 +142,7 @@ type Reconstructor interface { ) (map[[32]byte]*pb.ExecutionPayloadDeneb, error) ReconstructBlobSidecars(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [fieldparams.RootLength]byte, hi func(uint64) bool) ([]blocks.VerifiedROBlob, error) ConstructDataColumnSidecars(ctx context.Context, populator peerdas.ConstructionPopulator) ([]blocks.VerifiedRODataColumn, error) + ReconstructExecutionPayloadEnvelope(ctx context.Context, envelope *ethpb.SignedBlindedExecutionPayloadEnvelope) (*ethpb.SignedExecutionPayloadEnvelope, error) } // EngineCaller defines a client that can interact with an Ethereum @@ -652,6 +653,33 @@ func (s *Service) ReconstructFullBellatrixBlockBatch( return unb, nil } +// ReconstructExecutionPayloadEnvelope takes a blinded execution payload envelope and +// reconstructs the full envelope by fetching the execution payload from the EL via +// eth_getBlockByHash. +func (s *Service) ReconstructExecutionPayloadEnvelope( + ctx context.Context, envelope *ethpb.SignedBlindedExecutionPayloadEnvelope, +) (*ethpb.SignedExecutionPayloadEnvelope, error) { + if envelope == nil || envelope.Message == nil { + return nil, errors.New("nil blinded execution payload envelope") + } + blockHash := bytesutil.ToBytes32(envelope.Message.BlockHash) + payload, err := s.ReconstructFullExecutionPayloadByHash(ctx, blockHash) + if err != nil { + return nil, errors.Wrap(err, "could not reconstruct execution payload") + } + return ðpb.SignedExecutionPayloadEnvelope{ + Message: ðpb.ExecutionPayloadEnvelope{ + Payload: payload, + ExecutionRequests: envelope.Message.ExecutionRequests, + BuilderIndex: envelope.Message.BuilderIndex, + BeaconBlockRoot: envelope.Message.BeaconBlockRoot, + Slot: envelope.Message.Slot, + StateRoot: envelope.Message.StateRoot, + }, + Signature: envelope.Signature, + }, nil +} + // ReconstructFullExecutionPayloadByHash reconstructs a full deneb payload from EL data by block hash. func (s *Service) ReconstructFullExecutionPayloadByHash( ctx context.Context, blockHash [32]byte, diff --git a/beacon-chain/execution/testing/mock_engine_client.go b/beacon-chain/execution/testing/mock_engine_client.go index 14ef6827079f..29d5f31f92c0 100644 --- a/beacon-chain/execution/testing/mock_engine_client.go +++ b/beacon-chain/execution/testing/mock_engine_client.go @@ -14,6 +14,7 @@ import ( "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" pb "github.com/OffchainLabs/prysm/v7/proto/engine/v1" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/holiman/uint256" @@ -171,6 +172,49 @@ func (e *EngineClient) ConstructDataColumnSidecars(context.Context, peerdas.Cons return e.DataColumnSidecars, e.ErrorDataColumnSidecars } +// ReconstructExecutionPayloadEnvelope -- +func (e *EngineClient) ReconstructExecutionPayloadEnvelope( + _ context.Context, envelope *ethpb.SignedBlindedExecutionPayloadEnvelope, +) (*ethpb.SignedExecutionPayloadEnvelope, error) { + if e.Err != nil { + return nil, e.Err + } + payload, ok := e.ExecutionPayloadByBlockHash[bytesutil.ToBytes32(envelope.Message.BlockHash)] + if !ok { + return nil, errors.New("execution payload not found for block hash") + } + return ðpb.SignedExecutionPayloadEnvelope{ + Message: ðpb.ExecutionPayloadEnvelope{ + Payload: payloadToPayloadDeneb(payload), + ExecutionRequests: envelope.Message.ExecutionRequests, + BuilderIndex: envelope.Message.BuilderIndex, + BeaconBlockRoot: envelope.Message.BeaconBlockRoot, + Slot: envelope.Message.Slot, + StateRoot: envelope.Message.StateRoot, + }, + Signature: envelope.Signature, + }, nil +} + +func payloadToPayloadDeneb(p *pb.ExecutionPayload) *pb.ExecutionPayloadDeneb { + return &pb.ExecutionPayloadDeneb{ + ParentHash: p.ParentHash, + FeeRecipient: p.FeeRecipient, + StateRoot: p.StateRoot, + ReceiptsRoot: p.ReceiptsRoot, + LogsBloom: p.LogsBloom, + PrevRandao: p.PrevRandao, + BlockNumber: p.BlockNumber, + GasLimit: p.GasLimit, + GasUsed: p.GasUsed, + Timestamp: p.Timestamp, + ExtraData: p.ExtraData, + BaseFeePerGas: p.BaseFeePerGas, + BlockHash: p.BlockHash, + Transactions: p.Transactions, + } +} + // GetTerminalBlockHash -- func (e *EngineClient) GetTerminalBlockHash(ctx context.Context, transitionTime uint64) ([]byte, bool, error) { ttd := new(big.Int) diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index aa58d70a83f3..4fca3349df2c 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -891,6 +891,15 @@ func (s *Service) beaconEndpoints( handler: server.GetProposerLookahead, methods: []string{http.MethodGet}, }, + { + template: "/eth/v1/beacon/execution_payload_envelope/{block_root}", + name: namespace + ".GetExecutionPayloadEnvelope", + middleware: []middleware.Middleware{ + middleware.AcceptHeaderHandler([]string{api.JsonMediaType, api.OctetStreamMediaType}), + }, + handler: server.GetExecutionPayloadEnvelope, + methods: []string{http.MethodGet}, + }, } } diff --git a/beacon-chain/rpc/endpoints_test.go b/beacon-chain/rpc/endpoints_test.go index 3e2b7c438fc3..7bb0a8ec99c2 100644 --- a/beacon-chain/rpc/endpoints_test.go +++ b/beacon-chain/rpc/endpoints_test.go @@ -33,6 +33,7 @@ func Test_endpoints(t *testing.T) { "/eth/v1/beacon/states/{state_id}/pending_partial_withdrawals": {http.MethodGet}, "/eth/v1/beacon/states/{state_id}/pending_consolidations": {http.MethodGet}, "/eth/v1/beacon/states/{state_id}/proposer_lookahead": {http.MethodGet}, + "/eth/v1/beacon/execution_payload_envelope/{block_root}": {http.MethodGet}, "/eth/v1/beacon/headers": {http.MethodGet}, "/eth/v1/beacon/headers/{block_id}": {http.MethodGet}, "/eth/v2/beacon/blinded_blocks": {http.MethodPost}, diff --git a/beacon-chain/rpc/eth/beacon/BUILD.bazel b/beacon-chain/rpc/eth/beacon/BUILD.bazel index ccd0f52407da..91482c9dd363 100644 --- a/beacon-chain/rpc/eth/beacon/BUILD.bazel +++ b/beacon-chain/rpc/eth/beacon/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "handlers.go", + "handlers_gloas.go", "handlers_pool.go", "handlers_state.go", "handlers_validator.go", diff --git a/beacon-chain/rpc/eth/beacon/handlers_gloas.go b/beacon-chain/rpc/eth/beacon/handlers_gloas.go new file mode 100644 index 000000000000..ada20f7b11a6 --- /dev/null +++ b/beacon-chain/rpc/eth/beacon/handlers_gloas.go @@ -0,0 +1,74 @@ +package beacon + +import ( + "net/http" + + "github.com/OffchainLabs/prysm/v7/api" + "github.com/OffchainLabs/prysm/v7/api/server/structs" + "github.com/OffchainLabs/prysm/v7/beacon-chain/db" + "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" + "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" + "github.com/OffchainLabs/prysm/v7/network/httputil" + "github.com/OffchainLabs/prysm/v7/runtime/version" + "github.com/pkg/errors" +) + +// GetExecutionPayloadEnvelope retrieves a full execution payload envelope by beacon block root. +// The blinded envelope is fetched from the DB and the full execution payload is reconstructed +// from the EL via eth_getBlockByHash. +func (s *Server) GetExecutionPayloadEnvelope(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "beacon.GetExecutionPayloadEnvelope") + defer span.End() + + rootBytes, err := bytesutil.DecodeHexWithLength(r.PathValue("block_root"), 32) + if err != nil { + httputil.HandleError(w, "Could not decode block root: "+err.Error(), http.StatusBadRequest) + return + } + root := [32]byte(rootBytes) + blinded, err := s.BeaconDB.ExecutionPayloadEnvelope(ctx, root) + if err != nil { + if errors.Is(err, db.ErrNotFound) { + httputil.HandleError(w, "execution payload envelope not found", http.StatusNotFound) + return + } + httputil.HandleError(w, "could not retrieve execution payload envelope: "+err.Error(), http.StatusInternalServerError) + return + } + full, err := s.ExecutionReconstructor.ReconstructExecutionPayloadEnvelope(ctx, blinded) + if err != nil { + httputil.HandleError(w, "could not reconstruct execution payload envelope: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set(api.VersionHeader, version.String(version.Gloas)) + + if httputil.RespondWithSsz(r) { + sszBytes, err := full.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "could not marshal envelope to SSZ: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, sszBytes) + return + } + + isOptimistic, err := s.OptimisticModeFetcher.IsOptimisticForRoot(ctx, root) + if err != nil { + httputil.HandleError(w, "could not check optimistic status: "+err.Error(), http.StatusInternalServerError) + return + } + finalized := s.FinalizationFetcher.IsFinalized(ctx, root) + + jsonEnvelope, err := structs.SignedExecutionPayloadEnvelopeFromConsensus(full) + if err != nil { + httputil.HandleError(w, "could not convert envelope to JSON: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteJson(w, &structs.GetExecutionPayloadEnvelopeResponse{ + Version: version.String(version.Gloas), + ExecutionOptimistic: isOptimistic, + Finalized: finalized, + Data: jsonEnvelope, + }) +} diff --git a/beacon-chain/rpc/eth/events/events.go b/beacon-chain/rpc/eth/events/events.go index 92fd0f91cbfe..08996be6a8ac 100644 --- a/beacon-chain/rpc/eth/events/events.go +++ b/beacon-chain/rpc/eth/events/events.go @@ -74,6 +74,11 @@ const ( LightClientOptimisticUpdateTopic = "light_client_optimistic_update" // DataColumnTopic represents a data column sidecar event topic DataColumnTopic = "data_column_sidecar" + // ExecutionPayloadTopic represents a new execution payload envelope event topic + ExecutionPayloadTopic = "execution_payload_available" + // ExecutionPayloadBidTopic represents a new execution payload bid event topic. + // This topic is currently not triggered but is recognized to avoid client subscription errors. + ExecutionPayloadBidTopic = "execution_payload_bid" ) var ( @@ -118,9 +123,14 @@ var stateFeedEventTopics = map[feed.EventType]string{ statefeed.Reorg: ChainReorgTopic, statefeed.BlockProcessed: BlockTopic, statefeed.PayloadAttributes: PayloadAttributesTopic, + statefeed.PayloadProcessed: ExecutionPayloadTopic, } -var topicsForStateFeed = topicsForFeed(stateFeedEventTopics) +var topicsForStateFeed = func() map[string]bool { + m := topicsForFeed(stateFeedEventTopics) + m[ExecutionPayloadBidTopic] = true + return m +}() var topicsForOpsFeed = topicsForFeed(opsFeedEventTopics) func topicsForFeed(em map[feed.EventType]string) map[string]bool { @@ -466,6 +476,8 @@ func topicForEvent(event *feed.Event) string { return PayloadAttributesTopic case *operation.DataColumnReceivedData: return DataColumnTopic + case *statefeed.PayloadProcessedData: + return ExecutionPayloadTopic default: return InvalidTopic } @@ -638,6 +650,13 @@ func (s *Server) lazyReaderForEvent(ctx context.Context, event *feed.Event, topi } return jsonMarshalReader(eventName, blk) }, nil + case *statefeed.PayloadProcessedData: + return func() io.Reader { + return jsonMarshalReader(eventName, &structs.PayloadEvent{ + Slot: fmt.Sprintf("%d", v.Slot), + BlockRoot: hexutil.Encode(v.BlockRoot[:]), + }) + }, nil default: return nil, errors.Wrapf(errUnhandledEventData, "event data type %T unsupported", v) } diff --git a/beacon-chain/rpc/eth/events/events_test.go b/beacon-chain/rpc/eth/events/events_test.go index 284c33190540..a67a8cd41033 100644 --- a/beacon-chain/rpc/eth/events/events_test.go +++ b/beacon-chain/rpc/eth/events/events_test.go @@ -393,6 +393,7 @@ func TestStreamEvents_OperationsEvents(t *testing.T) { FinalizedCheckpointTopic, ChainReorgTopic, BlockTopic, + ExecutionPayloadTopic, }) require.NoError(t, err) request := topics.testHttpRequest(testSync.ctx, t) @@ -445,6 +446,13 @@ func TestStreamEvents_OperationsEvents(t *testing.T) { ExecutionOptimistic: false, }, }, + { + Type: statefeed.PayloadProcessed, + Data: &statefeed.PayloadProcessedData{ + Slot: 10, + BlockRoot: [32]byte{0x9a}, + }, + }, } go func() { diff --git a/changelog/potuz_envelope_endpoints.md b/changelog/potuz_envelope_endpoints.md new file mode 100644 index 000000000000..28f5c28e6cde --- /dev/null +++ b/changelog/potuz_envelope_endpoints.md @@ -0,0 +1,2 @@ +### Added +- Add Gloas payload_envelope endpoint and some event streams. diff --git a/proto/engine/v1/json_marshal_unmarshal.go b/proto/engine/v1/json_marshal_unmarshal.go index 34a7db3700e0..bb5c919e2c61 100644 --- a/proto/engine/v1/json_marshal_unmarshal.go +++ b/proto/engine/v1/json_marshal_unmarshal.go @@ -123,10 +123,7 @@ func (e *ExecutionBlock) UnmarshalJSON(enc []byte) error { return err } e.Hash = common.BytesToHash(decodedHash) - e.TotalDifficulty, ok = decoded["totalDifficulty"].(string) - if !ok { - return errors.New("expected `totalDifficulty` field in JSON response") - } + e.TotalDifficulty, _ = decoded["totalDifficulty"].(string) rawWithdrawals, ok := decoded["withdrawals"] if !ok || rawWithdrawals == nil {