mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-12 05:54:57 -05:00
more self review
This commit is contained in:
@@ -1,276 +1,31 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
|
||||
"github.com/OffchainLabs/prysm/v7/network/httputil"
|
||||
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
// ProduceBlockV4 requests a beacon node to produce a valid GLOAS block.
|
||||
// This is the GLOAS-specific block production endpoint that returns a block
|
||||
// containing a signed execution payload bid instead of the full payload.
|
||||
//
|
||||
// The execution payload envelope is cached by the beacon node and can be
|
||||
// retrieved via GetExecutionPayloadEnvelope.
|
||||
//
|
||||
// TODO: Implement GLOAS-specific block production.
|
||||
// Endpoint: GET /eth/v4/validator/blocks/{slot}
|
||||
func (s *Server) ProduceBlockV4(w http.ResponseWriter, r *http.Request) {
|
||||
_, span := trace.StartSpan(r.Context(), "validator.ProduceBlockV4")
|
||||
defer span.End()
|
||||
|
||||
if shared.IsSyncing(r.Context(), w, s.SyncChecker, s.HeadFetcher, s.TimeFetcher, s.OptimisticModeFetcher) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse path parameters
|
||||
segments := strings.Split(r.URL.Path, "/")
|
||||
rawSlot := segments[len(segments)-1]
|
||||
|
||||
slot, valid := shared.ValidateUint(w, "slot", rawSlot)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
rawRandaoReveal := r.URL.Query().Get("randao_reveal")
|
||||
rawGraffiti := r.URL.Query().Get("graffiti")
|
||||
rawSkipRandaoVerification := r.URL.Query().Get("skip_randao_verification")
|
||||
|
||||
var bbFactor *wrapperspb.UInt64Value
|
||||
rawBbFactor, bbValue, ok := shared.UintFromQuery(w, r, "builder_boost_factor", false)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if rawBbFactor != "" {
|
||||
bbFactor = &wrapperspb.UInt64Value{Value: bbValue}
|
||||
}
|
||||
|
||||
// Parse randao reveal
|
||||
var randaoReveal []byte
|
||||
if rawSkipRandaoVerification == "true" {
|
||||
// TODO: Use infinite signature constant
|
||||
randaoReveal = make([]byte, 96)
|
||||
} else {
|
||||
// TODO: Decode randao reveal from hex
|
||||
_ = rawRandaoReveal
|
||||
}
|
||||
|
||||
// Parse graffiti
|
||||
var graffiti []byte
|
||||
if rawGraffiti != "" {
|
||||
// TODO: Decode graffiti from hex
|
||||
}
|
||||
|
||||
// TODO: Implement GLOAS-specific block production
|
||||
//
|
||||
// This handler should:
|
||||
// 1. Verify the slot is in the GLOAS fork
|
||||
// 2. Call v1alpha1 server's getGloasBeaconBlock
|
||||
// 3. Format response with GLOAS-specific headers
|
||||
// 4. Return the block (the envelope is cached server-side)
|
||||
|
||||
_ = bbFactor
|
||||
_ = graffiti
|
||||
_ = randaoReveal
|
||||
_ = slot
|
||||
|
||||
httputil.HandleError(w, "ProduceBlockV4 not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// handleProduceGloasV4 handles the response formatting for GLOAS blocks.
|
||||
func handleProduceGloasV4(w http.ResponseWriter, isSSZ bool, block *eth.BeaconBlockGloas, payloadValue, consensusBlockValue string) {
|
||||
// TODO: Implement GLOAS response handling
|
||||
//
|
||||
// Similar to handleProduceFuluV3 but for GLOAS blocks.
|
||||
// The response should NOT include the execution payload envelope,
|
||||
// as that is retrieved separately.
|
||||
|
||||
if isSSZ {
|
||||
// TODO: SSZ serialize the GLOAS block
|
||||
httputil.HandleError(w, "SSZ response not yet implemented for GLOAS", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response
|
||||
// TODO: Convert GLOAS block to JSON struct
|
||||
resp := &structs.ProduceBlockV3Response{
|
||||
Version: version.String(version.Gloas),
|
||||
ExecutionPayloadBlinded: false, // GLOAS blocks don't have blinded concept in same way
|
||||
ExecutionPayloadValue: payloadValue,
|
||||
ConsensusBlockValue: consensusBlockValue,
|
||||
Data: nil, // TODO: Marshal block to JSON
|
||||
}
|
||||
|
||||
httputil.WriteJson(w, resp)
|
||||
}
|
||||
|
||||
// GetExecutionPayloadEnvelope retrieves a cached execution payload envelope.
|
||||
// Validators call this after receiving a GLOAS block to get the envelope
|
||||
// they need to sign and broadcast.
|
||||
//
|
||||
// TODO: Implement envelope retrieval from cache.
|
||||
// Endpoint: GET /eth/v1/validator/execution_payload_envelope/{slot}/{builder_index}
|
||||
func (s *Server) GetExecutionPayloadEnvelope(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := trace.StartSpan(r.Context(), "validator.ExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
// Parse path parameters
|
||||
segments := strings.Split(r.URL.Path, "/")
|
||||
if len(segments) < 2 {
|
||||
httputil.HandleError(w, "missing slot and builder_index in path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rawSlot := segments[len(segments)-2]
|
||||
rawBuilderIndex := segments[len(segments)-1]
|
||||
|
||||
slot, valid := shared.ValidateUint(w, "slot", rawSlot)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
builderIndex, err := strconv.ParseUint(rawBuilderIndex, 10, 64)
|
||||
if err != nil {
|
||||
httputil.HandleError(w, errors.Wrap(err, "invalid builder_index").Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Build gRPC request
|
||||
req := ð.ExecutionPayloadEnvelopeRequest{
|
||||
Slot: primitives.Slot(slot),
|
||||
BuilderIndex: primitives.BuilderIndex(builderIndex),
|
||||
}
|
||||
|
||||
// TODO: The V1Alpha1Server needs to implement the ExecutionPayloadEnvelope method
|
||||
// from the BeaconNodeValidatorServer interface. Currently it's defined but the
|
||||
// interface may need updating to include this method.
|
||||
//
|
||||
// Once implemented, uncomment:
|
||||
// resp, err := s.V1Alpha1Server.ExecutionPayloadEnvelope(ctx, req)
|
||||
// if err != nil {
|
||||
// // Map gRPC error codes to HTTP status codes
|
||||
// if status.Code(err) == codes.NotFound {
|
||||
// httputil.HandleError(w, err.Error(), http.StatusNotFound)
|
||||
// } else {
|
||||
// httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Format and return response
|
||||
// // - Support both JSON and SSZ based on Accept header
|
||||
// // - Set version header
|
||||
// w.Header().Set(api.VersionHeader, version.String(version.Gloas))
|
||||
// httputil.WriteJson(w, &structs.GetExecutionPayloadEnvelopeResponse{
|
||||
// Version: version.String(version.Gloas),
|
||||
// Data: envelopeProtoToJSON(resp.Envelope),
|
||||
// })
|
||||
|
||||
_ = ctx
|
||||
_ = req
|
||||
|
||||
httputil.HandleError(w, "ExecutionPayloadEnvelope not yet implemented", http.StatusNotImplemented)
|
||||
httputil.HandleError(w, "GetExecutionPayloadEnvelope not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// PublishExecutionPayloadEnvelope broadcasts a signed execution payload envelope.
|
||||
// Validators call this after signing the envelope to broadcast it to the network.
|
||||
//
|
||||
// TODO: Implement envelope validation and broadcast.
|
||||
// Endpoint: POST /eth/v1/beacon/execution_payload_envelope
|
||||
func (s *Server) PublishExecutionPayloadEnvelope(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := trace.StartSpan(r.Context(), "validator.PublishExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
// Parse request body
|
||||
var signedEnvelope structs.SignedExecutionPayloadEnvelope
|
||||
if err := json.NewDecoder(r.Body).Decode(&signedEnvelope); err != nil {
|
||||
httputil.HandleError(w, errors.Wrap(err, "failed to decode request body").Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Convert JSON struct to proto
|
||||
// protoEnvelope, err := signedEnvelope.ToProto()
|
||||
// if err != nil {
|
||||
// httputil.HandleError(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// TODO: Call gRPC server
|
||||
// _, err = s.V1Alpha1Server.PublishExecutionPayloadEnvelope(ctx, protoEnvelope)
|
||||
// if err != nil {
|
||||
// // Handle different error types (validation errors vs internal errors)
|
||||
// httputil.HandleError(w, err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
_ = ctx
|
||||
_ = signedEnvelope
|
||||
|
||||
httputil.HandleError(w, "PublishExecutionPayloadEnvelope not yet implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// ExecutionPayloadEnvelopeJSON represents the JSON structure for an execution payload envelope.
|
||||
// This is used for REST API serialization.
|
||||
type ExecutionPayloadEnvelopeJSON struct {
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
ExecutionRequests json.RawMessage `json:"execution_requests"`
|
||||
BuilderIndex string `json:"builder_index"`
|
||||
BeaconBlockRoot string `json:"beacon_block_root"`
|
||||
Slot string `json:"slot"`
|
||||
BlobKzgCommitments []string `json:"blob_kzg_commitments"`
|
||||
StateRoot string `json:"state_root"`
|
||||
}
|
||||
|
||||
// SignedExecutionPayloadEnvelopeJSON represents the JSON structure for a signed envelope.
|
||||
type SignedExecutionPayloadEnvelopeJSON struct {
|
||||
Message *ExecutionPayloadEnvelopeJSON `json:"message"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// ExecutionPayloadEnvelopeResponseJSON is the response wrapper for envelope retrieval.
|
||||
type ExecutionPayloadEnvelopeResponseJSON struct {
|
||||
Version string `json:"version"`
|
||||
Data *ExecutionPayloadEnvelopeJSON `json:"data"`
|
||||
}
|
||||
|
||||
// envelopeProtoToJSON converts a proto envelope to JSON representation.
|
||||
func envelopeProtoToJSON(envelope *eth.ExecutionPayloadEnvelope) (*ExecutionPayloadEnvelopeJSON, error) {
|
||||
// TODO: Implement conversion
|
||||
//
|
||||
// Convert each field:
|
||||
// - payload: Marshal ExecutionPayloadDeneb to JSON
|
||||
// - execution_requests: Marshal to JSON
|
||||
// - builder_index: Convert uint64 to string
|
||||
// - beacon_block_root: Hex encode
|
||||
// - slot: Convert uint64 to string
|
||||
// - blob_kzg_commitments: Hex encode each
|
||||
// - state_root: Hex encode
|
||||
|
||||
return nil, fmt.Errorf("envelopeProtoToJSON not yet implemented")
|
||||
}
|
||||
|
||||
// envelopeJSONToProto converts a JSON envelope to proto representation.
|
||||
func envelopeJSONToProto(envelope *ExecutionPayloadEnvelopeJSON) (*eth.ExecutionPayloadEnvelope, error) {
|
||||
// TODO: Implement conversion
|
||||
//
|
||||
// Parse each field:
|
||||
// - payload: Unmarshal from JSON
|
||||
// - execution_requests: Unmarshal from JSON
|
||||
// - builder_index: Parse uint64 from string
|
||||
// - beacon_block_root: Hex decode
|
||||
// - slot: Parse uint64 from string
|
||||
// - blob_kzg_commitments: Hex decode each
|
||||
// - state_root: Hex decode
|
||||
|
||||
return nil, fmt.Errorf("envelopeJSONToProto not yet implemented")
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ package validator
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed"
|
||||
blockfeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/block"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/peerdas"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
@@ -215,20 +212,9 @@ func (vs *Server) GetExecutionPayloadEnvelope(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// envelopeBlockWaitTimeout is the maximum time to wait for the associated beacon block
|
||||
// before giving up on publishing the execution payload envelope.
|
||||
const envelopeBlockWaitTimeout = 4 * time.Second
|
||||
|
||||
// envelopeBlockPollInterval is how often to check for the beacon block while waiting.
|
||||
const envelopeBlockPollInterval = 100 * time.Millisecond
|
||||
|
||||
// PublishExecutionPayloadEnvelope validates and broadcasts a signed execution payload envelope.
|
||||
// This is called by validators after signing the envelope retrieved from GetExecutionPayloadEnvelope.
|
||||
//
|
||||
// The function waits for the associated beacon block to be available before processing,
|
||||
// as the envelope references a beacon_block_root that must exist either from local
|
||||
// production or P2P gossip.
|
||||
//
|
||||
// gRPC endpoint: POST /eth/v1alpha1/validator/execution_payload_envelope
|
||||
func (vs *Server) PublishExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
@@ -252,13 +238,6 @@ func (vs *Server) PublishExecutionPayloadEnvelope(
|
||||
})
|
||||
log.Info("Publishing signed execution payload envelope")
|
||||
|
||||
// Wait for the associated beacon block to be available.
|
||||
// The block may come from local production or P2P gossip.
|
||||
if err := vs.waitForBeaconBlock(ctx, beaconBlockRoot); err != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition,
|
||||
"beacon block %#x not available: %v", beaconBlockRoot[:8], err)
|
||||
}
|
||||
|
||||
// TODO: Validate envelope signature before broadcasting
|
||||
// if err := vs.validateEnvelopeSignature(ctx, req); err != nil {
|
||||
// return nil, status.Errorf(codes.InvalidArgument, "invalid envelope signature: %v", err)
|
||||
@@ -299,55 +278,6 @@ func (vs *Server) PublishExecutionPayloadEnvelope(
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// waitForBeaconBlock waits for the beacon block with the given root to be available.
|
||||
// It first checks if the block already exists, then subscribes to block notifications
|
||||
// and polls periodically until the block arrives or the timeout is reached.
|
||||
func (vs *Server) waitForBeaconBlock(ctx context.Context, blockRoot [32]byte) error {
|
||||
// Fast path: check if block already exists
|
||||
if vs.BlockReceiver.HasBlock(ctx, blockRoot) {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.WithField("blockRoot", fmt.Sprintf("%#x", blockRoot[:8])).
|
||||
Debug("Waiting for beacon block to arrive")
|
||||
|
||||
waitCtx, cancel := context.WithTimeout(ctx, envelopeBlockWaitTimeout)
|
||||
defer cancel()
|
||||
|
||||
blocksChan := make(chan *feed.Event, 1)
|
||||
blockSub := vs.BlockNotifier.BlockFeed().Subscribe(blocksChan)
|
||||
defer blockSub.Unsubscribe()
|
||||
|
||||
ticker := time.NewTicker(envelopeBlockPollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-waitCtx.Done():
|
||||
return errors.Wrap(waitCtx.Err(), "timeout waiting for beacon block")
|
||||
|
||||
case blockEvent := <-blocksChan:
|
||||
if blockEvent.Type == blockfeed.ReceivedBlock {
|
||||
data, ok := blockEvent.Data.(*blockfeed.ReceivedBlockData)
|
||||
if ok && data != nil && data.SignedBlock != nil {
|
||||
root, err := data.SignedBlock.Block().HashTreeRoot()
|
||||
if err == nil && root == blockRoot {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
if vs.BlockReceiver.HasBlock(ctx, blockRoot) {
|
||||
return nil
|
||||
}
|
||||
|
||||
case <-blockSub.Err():
|
||||
return errors.New("block subscription closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildEnvelopeDataColumns retrieves the cached blobs bundle for the envelope's
|
||||
// slot/builder and builds data column sidecars. Returns nil if no blobs to broadcast.
|
||||
func (vs *Server) buildEnvelopeDataColumns(
|
||||
|
||||
@@ -11,6 +11,7 @@ go_library(
|
||||
"log_helpers.go",
|
||||
"metrics.go",
|
||||
"propose.go",
|
||||
"propose_gloas.go",
|
||||
"registration.go",
|
||||
"runner.go",
|
||||
"service.go",
|
||||
@@ -106,6 +107,7 @@ go_test(
|
||||
"key_reload_test.go",
|
||||
"log_test.go",
|
||||
"metrics_test.go",
|
||||
"propose_gloas_test.go",
|
||||
"propose_test.go",
|
||||
"registration_test.go",
|
||||
"runner_test.go",
|
||||
@@ -140,6 +142,7 @@ go_test(
|
||||
"//crypto/bls/common/mock:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//io/file:go_default_library",
|
||||
"//proto/engine/v1:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//proto/prysm/v1alpha1/validator-client:go_default_library",
|
||||
"//runtime:go_default_library",
|
||||
|
||||
@@ -2,121 +2,28 @@ package beacon_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
neturl "net/url"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/api/apiutil"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// getExecutionPayloadEnvelope retrieves the execution payload envelope for the given
|
||||
// slot and builder index. This is called by validators after receiving a GLOAS block
|
||||
// to get the envelope they need to sign and broadcast.
|
||||
//
|
||||
// REST endpoint: GET /eth/v1/validator/execution_payload_envelope/{slot}/{builder_index}
|
||||
// TODO: Implement GLOAS beacon API client methods.
|
||||
|
||||
// getExecutionPayloadEnvelope retrieves the execution payload envelope for the given slot and builder index.
|
||||
func (c *beaconApiValidatorClient) getExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
slot primitives.Slot,
|
||||
builderIndex primitives.BuilderIndex,
|
||||
) (*ethpb.ExecutionPayloadEnvelope, error) {
|
||||
// TODO: Implement execution payload envelope retrieval
|
||||
//
|
||||
// Implementation steps:
|
||||
// 1. Build URL with slot and builder_index path parameters
|
||||
// 2. Make GET request (support both JSON and SSZ based on Accept header)
|
||||
// 3. Parse response
|
||||
// 4. Convert to proto type
|
||||
// 5. Return envelope
|
||||
|
||||
queryUrl := apiutil.BuildURL(
|
||||
fmt.Sprintf("/eth/v1/validator/execution_payload_envelope/%d/%d", slot, builderIndex),
|
||||
neturl.Values{},
|
||||
)
|
||||
|
||||
_ = queryUrl
|
||||
|
||||
return nil, errors.New("getExecutionPayloadEnvelope not yet implemented")
|
||||
}
|
||||
|
||||
// publishExecutionPayloadEnvelope broadcasts a signed execution payload envelope
|
||||
// to the beacon node for P2P gossip.
|
||||
//
|
||||
// REST endpoint: POST /eth/v1/beacon/execution_payload_envelope
|
||||
// publishExecutionPayloadEnvelope broadcasts a signed execution payload envelope.
|
||||
func (c *beaconApiValidatorClient) publishExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
envelope *ethpb.SignedExecutionPayloadEnvelope,
|
||||
) (*empty.Empty, error) {
|
||||
// TODO: Implement envelope publishing
|
||||
//
|
||||
// Implementation steps:
|
||||
// 1. Convert proto envelope to JSON struct
|
||||
// 2. Serialize to JSON (or SSZ based on Content-Type)
|
||||
// 3. POST to /eth/v1/beacon/execution_payload_envelope
|
||||
// 4. Handle response (200 = success, 4xx = validation error)
|
||||
|
||||
if envelope == nil || envelope.Message == nil {
|
||||
return nil, errors.New("signed envelope cannot be nil")
|
||||
}
|
||||
|
||||
return nil, errors.New("publishExecutionPayloadEnvelope not yet implemented")
|
||||
}
|
||||
|
||||
// signedEnvelopeToJSON converts a proto SignedExecutionPayloadEnvelope to its JSON representation.
|
||||
func signedEnvelopeToJSON(envelope *ethpb.SignedExecutionPayloadEnvelope) (any, error) {
|
||||
// TODO: Implement conversion from proto to JSON struct
|
||||
//
|
||||
// Convert each field:
|
||||
// - message.payload: Marshal ExecutionPayloadDeneb to JSON
|
||||
// - message.execution_requests: Marshal to JSON
|
||||
// - message.builder_index: Format as decimal string
|
||||
// - message.beacon_block_root: Hex encode with 0x prefix
|
||||
// - message.slot: Format as decimal string
|
||||
// - message.blob_kzg_commitments: Hex encode each with 0x prefix
|
||||
// - message.state_root: Hex encode with 0x prefix
|
||||
// - signature: Hex encode with 0x prefix
|
||||
|
||||
return nil, errors.New("signedEnvelopeToJSON not yet implemented")
|
||||
}
|
||||
|
||||
// envelopeJSONToProto converts a JSON execution payload envelope to proto type.
|
||||
func envelopeJSONToProto(jsonEnvelope any) (*ethpb.ExecutionPayloadEnvelope, error) {
|
||||
// TODO: Implement conversion from JSON to proto
|
||||
//
|
||||
// Parse each field:
|
||||
// - payload: Unmarshal ExecutionPayloadDeneb from JSON
|
||||
// - execution_requests: Unmarshal from JSON
|
||||
// - builder_index: Parse uint64 from decimal string
|
||||
// - beacon_block_root: Hex decode (strip 0x prefix)
|
||||
// - slot: Parse uint64 from decimal string
|
||||
// - blob_kzg_commitments: Hex decode each (strip 0x prefix)
|
||||
// - state_root: Hex decode (strip 0x prefix)
|
||||
|
||||
return nil, errors.New("envelopeJSONToProto not yet implemented")
|
||||
}
|
||||
|
||||
// processGloasBlock handles GLOAS block responses from the beacon node.
|
||||
// This is called from processBlockJSONResponse when the version is "gloas".
|
||||
func processGloasBlock(jsonBlock any) (*ethpb.GenericBeaconBlock, error) {
|
||||
// TODO: Implement GLOAS block processing
|
||||
//
|
||||
// Convert the JSON block to proto BeaconBlockGloas:
|
||||
// 1. Parse BeaconBlockGloas fields
|
||||
// 2. Parse BeaconBlockBodyGloas with signed_execution_payload_bid
|
||||
// 3. Parse payload_attestations
|
||||
// 4. Return GenericBeaconBlock with Gloas variant
|
||||
|
||||
return nil, errors.New("processGloasBlock not yet implemented")
|
||||
}
|
||||
|
||||
// processBlockSSZResponseGloas handles SSZ-encoded GLOAS block responses.
|
||||
func processBlockSSZResponseGloas(data []byte) (*ethpb.GenericBeaconBlock, error) {
|
||||
// TODO: Implement SSZ deserialization for GLOAS blocks
|
||||
//
|
||||
// Note: GLOAS blocks don't have a "blinded" variant in the same way
|
||||
// as previous forks because the execution payload is always separate.
|
||||
|
||||
return nil, errors.New("processBlockSSZResponseGloas not yet implemented")
|
||||
}
|
||||
|
||||
@@ -168,6 +168,29 @@ func (v *validator) ProposeBlock(ctx context.Context, slot primitives.Slot, pubK
|
||||
}
|
||||
}
|
||||
|
||||
// For GLOAS, retrieve and sign the execution payload envelope before
|
||||
// broadcasting the block. This ensures we can bail out early if signing
|
||||
// fails, rather than broadcasting a block with no valid envelope to back it.
|
||||
var signedEnvelope *ethpb.SignedExecutionPayloadEnvelope
|
||||
if blk.Version() >= version.Gloas {
|
||||
envelope, err := v.getExecutionPayloadEnvelope(ctx, slot, b)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to get execution payload envelope")
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
return
|
||||
}
|
||||
signedEnvelope, err = v.signExecutionPayloadEnvelope(ctx, pubKey, slot, envelope)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to sign execution payload envelope")
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
blkResp, err := v.validatorClient.ProposeBeaconBlock(ctx, genericSignedBlock)
|
||||
if err != nil {
|
||||
log.WithField("slot", slot).WithError(err).Error("Failed to propose block")
|
||||
@@ -177,11 +200,10 @@ func (v *validator) ProposeBlock(ctx context.Context, slot primitives.Slot, pubK
|
||||
return
|
||||
}
|
||||
|
||||
// GLOAS: After proposing the beacon block, handle the execution payload envelope
|
||||
if blk.Version() >= version.Gloas {
|
||||
if err := v.handleGloasExecutionPayloadEnvelope(ctx, slot, pubKey, b); err != nil {
|
||||
log.WithError(err).Error("Failed to handle GLOAS execution payload envelope")
|
||||
// Don't return - the block was proposed successfully, envelope handling is secondary
|
||||
// Publish the signed envelope after block broadcast.
|
||||
if signedEnvelope != nil {
|
||||
if _, err := v.validatorClient.PublishExecutionPayloadEnvelope(ctx, signedEnvelope); err != nil {
|
||||
log.WithError(err).Error("Failed to publish execution payload envelope")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,98 +614,3 @@ func blockLogFields(pubKey [fieldparams.BLSPubkeyLength]byte, blk interfaces.Rea
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// handleGloasExecutionPayloadEnvelope retrieves, signs, and publishes the execution payload envelope
|
||||
// for GLOAS blocks. This is called after the beacon block has been proposed.
|
||||
func (v *validator) handleGloasExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
slot primitives.Slot,
|
||||
pubKey [fieldparams.BLSPubkeyLength]byte,
|
||||
b *ethpb.GenericBeaconBlock,
|
||||
) error {
|
||||
ctx, span := trace.StartSpan(ctx, "validator.handleGloasExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
log := log.WithFields(logrus.Fields{
|
||||
"slot": slot,
|
||||
"pubkey": fmt.Sprintf("%#x", bytesutil.Trunc(pubKey[:])),
|
||||
})
|
||||
|
||||
// Extract builder_index from the GLOAS block's signed execution payload bid
|
||||
gloasBlock := b.GetGloas()
|
||||
if gloasBlock == nil {
|
||||
return errors.New("expected GLOAS block but got nil")
|
||||
}
|
||||
if gloasBlock.Body == nil || gloasBlock.Body.SignedExecutionPayloadBid == nil || gloasBlock.Body.SignedExecutionPayloadBid.Message == nil {
|
||||
return errors.New("GLOAS block missing signed execution payload bid")
|
||||
}
|
||||
builderIndex := gloasBlock.Body.SignedExecutionPayloadBid.Message.BuilderIndex
|
||||
|
||||
log = log.WithField("builderIndex", builderIndex)
|
||||
log.Debug("Retrieving execution payload envelope")
|
||||
|
||||
// 1. Retrieve the execution payload envelope from the beacon node
|
||||
envelope, err := v.validatorClient.ExecutionPayloadEnvelope(ctx, slot, builderIndex)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get execution payload envelope")
|
||||
}
|
||||
|
||||
// 2. Sign the envelope
|
||||
signedEnvelope, err := v.signExecutionPayloadEnvelope(ctx, pubKey, slot, envelope)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to sign execution payload envelope")
|
||||
}
|
||||
|
||||
// 3. Publish the signed envelope
|
||||
log.Debug("Publishing signed execution payload envelope")
|
||||
if _, err := v.validatorClient.PublishExecutionPayloadEnvelope(ctx, signedEnvelope); err != nil {
|
||||
return errors.Wrap(err, "failed to publish execution payload envelope")
|
||||
}
|
||||
|
||||
log.Info("Successfully published execution payload envelope")
|
||||
return nil
|
||||
}
|
||||
|
||||
// signExecutionPayloadEnvelope signs the execution payload envelope using the validator's key.
|
||||
func (v *validator) signExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
pubKey [fieldparams.BLSPubkeyLength]byte,
|
||||
slot primitives.Slot,
|
||||
envelope *ethpb.ExecutionPayloadEnvelope,
|
||||
) (*ethpb.SignedExecutionPayloadEnvelope, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "validator.signExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
epoch := slots.ToEpoch(slot)
|
||||
|
||||
// Use DomainBeaconBuilder for execution payload envelope signing.
|
||||
// This domain is used for builder-related operations including envelope signing.
|
||||
domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainBeaconBuilder[:])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get domain data")
|
||||
}
|
||||
if domain == nil {
|
||||
return nil, errors.New("nil domain data")
|
||||
}
|
||||
|
||||
signingRoot, err := signing.ComputeSigningRoot(envelope, domain.SignatureDomain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not compute signing root")
|
||||
}
|
||||
|
||||
sig, err := v.km.Sign(ctx, &validatorpb.SignRequest{
|
||||
PublicKey: pubKey[:],
|
||||
SigningRoot: signingRoot[:],
|
||||
SignatureDomain: domain.SignatureDomain,
|
||||
Object: &validatorpb.SignRequest_Slot{Slot: slot},
|
||||
SigningSlot: slot,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not sign execution payload envelope")
|
||||
}
|
||||
|
||||
return ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: envelope,
|
||||
Signature: sig.Marshal(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
82
validator/client/propose_gloas.go
Normal file
82
validator/client/propose_gloas.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
|
||||
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
validatorpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/validator-client"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// getExecutionPayloadEnvelope retrieves the execution payload envelope from the
|
||||
// beacon node for the given block's builder index and slot.
|
||||
func (v *validator) getExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
slot primitives.Slot,
|
||||
b *ethpb.GenericBeaconBlock,
|
||||
) (*ethpb.ExecutionPayloadEnvelope, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "validator.getExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
gloasBlock := b.GetGloas()
|
||||
if gloasBlock == nil {
|
||||
return nil, errors.New("expected GLOAS block but got nil")
|
||||
}
|
||||
if gloasBlock.Body == nil || gloasBlock.Body.SignedExecutionPayloadBid == nil || gloasBlock.Body.SignedExecutionPayloadBid.Message == nil {
|
||||
return nil, errors.New("block missing signed execution payload bid")
|
||||
}
|
||||
builderIndex := gloasBlock.Body.SignedExecutionPayloadBid.Message.BuilderIndex
|
||||
|
||||
return v.validatorClient.ExecutionPayloadEnvelope(ctx, slot, builderIndex)
|
||||
}
|
||||
|
||||
// signExecutionPayloadEnvelope signs the execution payload envelope using the
|
||||
// builder's key. The envelope is signed with DomainBeaconBuilder since it is
|
||||
// a builder artifact — even in the self-build case where the proposer acts as
|
||||
// their own builder.
|
||||
func (v *validator) signExecutionPayloadEnvelope(
|
||||
ctx context.Context,
|
||||
pubKey [fieldparams.BLSPubkeyLength]byte,
|
||||
slot primitives.Slot,
|
||||
envelope *ethpb.ExecutionPayloadEnvelope,
|
||||
) (*ethpb.SignedExecutionPayloadEnvelope, error) {
|
||||
ctx, span := trace.StartSpan(ctx, "validator.signExecutionPayloadEnvelope")
|
||||
defer span.End()
|
||||
|
||||
epoch := slots.ToEpoch(slot)
|
||||
|
||||
domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainBeaconBuilder[:])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get domain data")
|
||||
}
|
||||
if domain == nil {
|
||||
return nil, errors.New("nil domain data")
|
||||
}
|
||||
|
||||
signingRoot, err := signing.ComputeSigningRoot(envelope, domain.SignatureDomain)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not compute signing root")
|
||||
}
|
||||
|
||||
sig, err := v.km.Sign(ctx, &validatorpb.SignRequest{
|
||||
PublicKey: pubKey[:],
|
||||
SigningRoot: signingRoot[:],
|
||||
SignatureDomain: domain.SignatureDomain,
|
||||
Object: &validatorpb.SignRequest_Slot{Slot: slot},
|
||||
SigningSlot: slot,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not sign execution payload envelope")
|
||||
}
|
||||
|
||||
return ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: envelope,
|
||||
Signature: sig.Marshal(),
|
||||
}, nil
|
||||
}
|
||||
205
validator/client/propose_gloas_test.go
Normal file
205
validator/client/propose_gloas_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/require"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func testExecutionPayloadEnvelope(slot primitives.Slot, builderIndex primitives.BuilderIndex) *ethpb.ExecutionPayloadEnvelope {
|
||||
return ðpb.ExecutionPayloadEnvelope{
|
||||
Payload: &enginev1.ExecutionPayloadDeneb{
|
||||
ParentHash: make([]byte, 32),
|
||||
FeeRecipient: make([]byte, 20),
|
||||
StateRoot: make([]byte, 32),
|
||||
ReceiptsRoot: make([]byte, 32),
|
||||
LogsBloom: make([]byte, 256),
|
||||
PrevRandao: make([]byte, 32),
|
||||
BaseFeePerGas: make([]byte, 32),
|
||||
BlockHash: make([]byte, 32),
|
||||
ExtraData: make([]byte, 0),
|
||||
},
|
||||
ExecutionRequests: &enginev1.ExecutionRequests{},
|
||||
Slot: slot,
|
||||
BuilderIndex: builderIndex,
|
||||
BeaconBlockRoot: make([]byte, 32),
|
||||
StateRoot: make([]byte, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExecutionPayloadEnvelope(t *testing.T) {
|
||||
validator, m, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
slot := primitives.Slot(100)
|
||||
builderIndex := primitives.BuilderIndex(42)
|
||||
|
||||
expectedEnvelope := testExecutionPayloadEnvelope(slot, builderIndex)
|
||||
|
||||
m.validatorClient.EXPECT().
|
||||
GetExecutionPayloadEnvelope(gomock.Any(), slot, builderIndex).
|
||||
Return(expectedEnvelope, nil)
|
||||
|
||||
b := ðpb.GenericBeaconBlock{
|
||||
Block: ðpb.GenericBeaconBlock_Gloas{
|
||||
Gloas: ðpb.BeaconBlockGloas{
|
||||
Slot: slot,
|
||||
Body: ðpb.BeaconBlockBodyGloas{
|
||||
SignedExecutionPayloadBid: ðpb.SignedExecutionPayloadBid{
|
||||
Message: ðpb.ExecutionPayloadBid{
|
||||
BuilderIndex: builderIndex,
|
||||
},
|
||||
Signature: make([]byte, 96),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
envelope, err := validator.getExecutionPayloadEnvelope(t.Context(), slot, b)
|
||||
require.NoError(t, err)
|
||||
require.DeepEqual(t, expectedEnvelope, envelope)
|
||||
}
|
||||
|
||||
func TestGetExecutionPayloadEnvelope_NilBlock(t *testing.T) {
|
||||
validator, _, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
b := ðpb.GenericBeaconBlock{
|
||||
Block: ðpb.GenericBeaconBlock_Gloas{},
|
||||
}
|
||||
|
||||
_, err := validator.getExecutionPayloadEnvelope(t.Context(), 1, b)
|
||||
require.ErrorContains(t, "expected GLOAS block but got nil", err)
|
||||
}
|
||||
|
||||
func TestGetExecutionPayloadEnvelope_MissingBid(t *testing.T) {
|
||||
validator, _, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
b := ðpb.GenericBeaconBlock{
|
||||
Block: ðpb.GenericBeaconBlock_Gloas{
|
||||
Gloas: ðpb.BeaconBlockGloas{
|
||||
Slot: 1,
|
||||
Body: ðpb.BeaconBlockBodyGloas{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := validator.getExecutionPayloadEnvelope(t.Context(), 1, b)
|
||||
require.ErrorContains(t, "block missing signed execution payload bid", err)
|
||||
}
|
||||
|
||||
func TestGetExecutionPayloadEnvelope_ClientError(t *testing.T) {
|
||||
validator, m, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
m.validatorClient.EXPECT().
|
||||
GetExecutionPayloadEnvelope(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("connection refused"))
|
||||
|
||||
b := ðpb.GenericBeaconBlock{
|
||||
Block: ðpb.GenericBeaconBlock_Gloas{
|
||||
Gloas: ðpb.BeaconBlockGloas{
|
||||
Slot: 1,
|
||||
Body: ðpb.BeaconBlockBodyGloas{
|
||||
SignedExecutionPayloadBid: ðpb.SignedExecutionPayloadBid{
|
||||
Message: ðpb.ExecutionPayloadBid{BuilderIndex: 1},
|
||||
Signature: make([]byte, 96),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := validator.getExecutionPayloadEnvelope(t.Context(), 1, b)
|
||||
require.ErrorContains(t, "connection refused", err)
|
||||
}
|
||||
|
||||
func TestSignExecutionPayloadEnvelope(t *testing.T) {
|
||||
validator, m, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
kp := testKeyFromBytes(t, []byte{1})
|
||||
validator.km = newMockKeymanager(t, kp)
|
||||
|
||||
builderDomain := make([]byte, 32)
|
||||
m.validatorClient.EXPECT().
|
||||
DomainData(gomock.Any(), gomock.Any()).
|
||||
Return(ðpb.DomainResponse{SignatureDomain: builderDomain}, nil)
|
||||
|
||||
envelope := testExecutionPayloadEnvelope(100, 42)
|
||||
|
||||
signed, err := validator.signExecutionPayloadEnvelope(t.Context(), kp.pub, 100, envelope)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signed)
|
||||
require.DeepEqual(t, envelope, signed.Message)
|
||||
require.NotNil(t, signed.Signature)
|
||||
|
||||
// Verify the signature was computed with the builder domain.
|
||||
expectedRoot, err := signing.ComputeSigningRoot(envelope, builderDomain)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, [32]byte{}, expectedRoot)
|
||||
}
|
||||
|
||||
func TestSignExecutionPayloadEnvelope_DomainDataError(t *testing.T) {
|
||||
validator, m, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
kp := testKeyFromBytes(t, []byte{1})
|
||||
validator.km = newMockKeymanager(t, kp)
|
||||
|
||||
m.validatorClient.EXPECT().
|
||||
DomainData(gomock.Any(), gomock.Any()).
|
||||
Return(nil, errors.New("domain data unavailable"))
|
||||
|
||||
envelope := testExecutionPayloadEnvelope(100, 0)
|
||||
|
||||
_, err := validator.signExecutionPayloadEnvelope(t.Context(), kp.pub, 100, envelope)
|
||||
require.ErrorContains(t, "could not get domain data", err)
|
||||
}
|
||||
|
||||
func TestSignExecutionPayloadEnvelope_NilDomain(t *testing.T) {
|
||||
validator, m, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
kp := testKeyFromBytes(t, []byte{1})
|
||||
validator.km = newMockKeymanager(t, kp)
|
||||
|
||||
m.validatorClient.EXPECT().
|
||||
DomainData(gomock.Any(), gomock.Any()).
|
||||
Return(nil, nil)
|
||||
|
||||
envelope := testExecutionPayloadEnvelope(100, 0)
|
||||
|
||||
_, err := validator.signExecutionPayloadEnvelope(t.Context(), kp.pub, 100, envelope)
|
||||
require.ErrorContains(t, "nil domain data", err)
|
||||
}
|
||||
|
||||
func TestSignExecutionPayloadEnvelope_UsesDomainBeaconBuilder(t *testing.T) {
|
||||
validator, m, _, finish := setup(t, false)
|
||||
defer finish()
|
||||
|
||||
kp := testKeyFromBytes(t, []byte{1})
|
||||
validator.km = newMockKeymanager(t, kp)
|
||||
|
||||
// Verify the correct domain type is requested.
|
||||
m.validatorClient.EXPECT().
|
||||
DomainData(gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(func(ctx any, req *ethpb.DomainRequest) (*ethpb.DomainResponse, error) {
|
||||
require.DeepEqual(t, params.BeaconConfig().DomainBeaconBuilder[:], req.Domain)
|
||||
return ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil
|
||||
})
|
||||
|
||||
envelope := testExecutionPayloadEnvelope(100, 0)
|
||||
|
||||
_, err := validator.signExecutionPayloadEnvelope(t.Context(), kp.pub, 100, envelope)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user