From 367e4d49d338cd5de760716e9836ff2a216eaec4 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Mon, 9 Feb 2026 17:01:11 -0600 Subject: [PATCH] more self review --- .../rpc/eth/validator/handlers_gloas.go | 253 +----------------- .../v1alpha1/validator/proposer_gloas.go | 70 ----- validator/client/BUILD.bazel | 3 + validator/client/beacon-api/gloas.go | 101 +------ validator/client/propose.go | 127 ++------- validator/client/propose_gloas.go | 82 ++++++ validator/client/propose_gloas_test.go | 205 ++++++++++++++ 7 files changed, 325 insertions(+), 516 deletions(-) create mode 100644 validator/client/propose_gloas.go create mode 100644 validator/client/propose_gloas_test.go diff --git a/beacon-chain/rpc/eth/validator/handlers_gloas.go b/beacon-chain/rpc/eth/validator/handlers_gloas.go index 0372163de6..98ea9e648b 100644 --- a/beacon-chain/rpc/eth/validator/handlers_gloas.go +++ b/beacon-chain/rpc/eth/validator/handlers_gloas.go @@ -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") -} diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_gloas.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_gloas.go index 59595cc1b9..91cb253e2d 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_gloas.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_gloas.go @@ -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( diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index 7959419e1f..fa437cea3a 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -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", diff --git a/validator/client/beacon-api/gloas.go b/validator/client/beacon-api/gloas.go index 9fdade4b12..89993a9da5 100644 --- a/validator/client/beacon-api/gloas.go +++ b/validator/client/beacon-api/gloas.go @@ -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") -} diff --git a/validator/client/propose.go b/validator/client/propose.go index 9236edb2c2..1fa5f583dc 100644 --- a/validator/client/propose.go +++ b/validator/client/propose.go @@ -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 -} diff --git a/validator/client/propose_gloas.go b/validator/client/propose_gloas.go new file mode 100644 index 0000000000..b05f241027 --- /dev/null +++ b/validator/client/propose_gloas.go @@ -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 +} diff --git a/validator/client/propose_gloas_test.go b/validator/client/propose_gloas_test.go new file mode 100644 index 0000000000..d885a43b9c --- /dev/null +++ b/validator/client/propose_gloas_test.go @@ -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) +}