Compare commits

...

6 Commits

Author SHA1 Message Date
james-prysm
5bc2653bc7 fixing some of payload envelope population 2026-02-06 22:31:17 -06:00
james-prysm
fdfbe3bfa8 Merge branch 'develop' into gloas-proposer 2026-02-06 14:51:21 -08:00
james-prysm
6d4bc0e3bd updating publish proposer envelope to also broadcast blobs from cache 2026-02-06 16:12:14 -06:00
james-prysm
6173c290d7 adding logic to get block and propose block for gloas 2026-02-06 13:53:52 -06:00
Potuz
c3e74e4a5d Remove unused method in forkchoice (#16337) 2026-02-06 19:21:58 +00:00
james-prysm
048ea521b6 wip 2026-02-06 10:39:39 -06:00
32 changed files with 3086 additions and 1240 deletions

View File

@@ -150,3 +150,36 @@ type ActiveSetChanges struct {
EjectedPublicKeys []string `json:"ejected_public_keys"`
EjectedIndices []string `json:"ejected_indices"`
}
// =============================================================================
// GLOAS Fork Types - Execution Payload Envelope
// =============================================================================
// Note: Block-related GLOAS types (BeaconBlockGloas, ExecutionPayloadBid, etc.)
// are defined in block.go. This file contains only the envelope types used for
// the validator API endpoints.
// ExecutionPayloadEnvelope represents an execution payload envelope in the GLOAS fork.
// This wraps the full execution payload with builder metadata for separate signing
// and broadcasting by validators.
type ExecutionPayloadEnvelope struct {
Payload json.RawMessage `json:"payload"` // ExecutionPayloadDeneb
ExecutionRequests json.RawMessage `json:"execution_requests"` // ExecutionRequests
BuilderIndex string `json:"builder_index"` // uint64 as string
BeaconBlockRoot string `json:"beacon_block_root"` // hex encoded 32 bytes
Slot string `json:"slot"` // uint64 as string
BlobKzgCommitments []string `json:"blob_kzg_commitments"` // list of hex encoded 48-byte commitments
StateRoot string `json:"state_root"` // hex encoded 32 bytes
}
// SignedExecutionPayloadEnvelope wraps an execution payload envelope with a BLS signature.
// The signature is provided by the validator after retrieving the envelope from the beacon node.
type SignedExecutionPayloadEnvelope struct {
Message *ExecutionPayloadEnvelope `json:"message"`
Signature string `json:"signature"` // hex encoded 96-byte BLS signature
}
// GetExecutionPayloadEnvelopeResponse is the response for retrieving a cached execution payload envelope.
type GetExecutionPayloadEnvelopeResponse struct {
Version string `json:"version"`
Data *ExecutionPayloadEnvelope `json:"data"`
}

View File

@@ -15,6 +15,7 @@ go_library(
"common.go",
"doc.go",
"error.go",
"execution_payload_envelope.go",
"interfaces.go",
"log.go",
"payload_id.go",
@@ -51,6 +52,7 @@ go_library(
"//encoding/bytesutil:go_default_library",
"//math:go_default_library",
"//monitoring/tracing/trace:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//proto/prysm/v1alpha1/attestation:go_default_library",
"//runtime/version:go_default_library",
@@ -76,6 +78,7 @@ go_test(
"checkpoint_state_test.go",
"committee_fuzz_test.go",
"committee_test.go",
"execution_payload_envelope_test.go",
"payload_id_test.go",
"private_access_test.go",
"proposer_indices_test.go",
@@ -97,6 +100,7 @@ go_test(
"//consensus-types/primitives:go_default_library",
"//crypto/bls/blst:go_default_library",
"//encoding/bytesutil:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//proto/prysm/v1alpha1/attestation:go_default_library",
"//testing/assert:go_default_library",

View File

@@ -0,0 +1,111 @@
package cache
import (
"sync"
"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"
)
// ExecutionPayloadEnvelopeKey uniquely identifies a cached execution payload envelope.
type ExecutionPayloadEnvelopeKey struct {
Slot primitives.Slot
BuilderIndex primitives.BuilderIndex
}
// executionPayloadEnvelopeCacheEntry holds an execution payload envelope and
// the associated blobs bundle from the EL. The blobs bundle is needed later
// when proposing the block to build and broadcast blob sidecars.
type executionPayloadEnvelopeCacheEntry struct {
envelope *ethpb.ExecutionPayloadEnvelope
blobsBundle enginev1.BlobsBundler
}
// ExecutionPayloadEnvelopeCache stores execution payload envelopes produced during
// GLOAS block building for later retrieval by validators. When a beacon node
// produces a GLOAS block, it caches the execution payload envelope so the validator
// can retrieve it, sign it, and broadcast it separately from the beacon block.
// The blobs bundle from the EL is also cached alongside, since blobs are only
// persisted to the DB after they are broadcast as sidecars during block proposal.
type ExecutionPayloadEnvelopeCache struct {
cache map[ExecutionPayloadEnvelopeKey]*executionPayloadEnvelopeCacheEntry
sync.RWMutex
}
// NewExecutionPayloadEnvelopeCache creates a new execution payload envelope cache.
func NewExecutionPayloadEnvelopeCache() *ExecutionPayloadEnvelopeCache {
return &ExecutionPayloadEnvelopeCache{
cache: make(map[ExecutionPayloadEnvelopeKey]*executionPayloadEnvelopeCacheEntry),
}
}
// Get retrieves an execution payload envelope by slot and builder index.
// Returns the envelope and true if found, nil and false otherwise.
func (c *ExecutionPayloadEnvelopeCache) Get(slot primitives.Slot, builderIndex primitives.BuilderIndex) (*ethpb.ExecutionPayloadEnvelope, bool) {
c.RLock()
defer c.RUnlock()
key := ExecutionPayloadEnvelopeKey{
Slot: slot,
BuilderIndex: builderIndex,
}
entry, ok := c.cache[key]
if !ok {
return nil, false
}
return entry.envelope, true
}
// GetBlobsBundle retrieves a cached blobs bundle by slot and builder index.
// Returns the blobs bundle and true if found, nil and false otherwise.
func (c *ExecutionPayloadEnvelopeCache) GetBlobsBundle(slot primitives.Slot, builderIndex primitives.BuilderIndex) (enginev1.BlobsBundler, bool) {
c.RLock()
defer c.RUnlock()
key := ExecutionPayloadEnvelopeKey{
Slot: slot,
BuilderIndex: builderIndex,
}
entry, ok := c.cache[key]
if !ok {
return nil, false
}
return entry.blobsBundle, true
}
// Set stores an execution payload envelope and its associated blobs bundle in the cache.
// The envelope's slot and builder_index fields are used as the cache key.
// Old entries are automatically pruned to prevent unbounded growth.
func (c *ExecutionPayloadEnvelopeCache) Set(envelope *ethpb.ExecutionPayloadEnvelope, blobsBundle enginev1.BlobsBundler) {
if envelope == nil {
return
}
c.Lock()
defer c.Unlock()
slot := envelope.Slot
if slot > 2 {
c.prune(slot - 2)
}
key := ExecutionPayloadEnvelopeKey{
Slot: slot,
BuilderIndex: envelope.BuilderIndex,
}
c.cache[key] = &executionPayloadEnvelopeCacheEntry{
envelope: envelope,
blobsBundle: blobsBundle,
}
}
// prune removes all entries with slots older than the given slot.
// Must be called with the lock held.
func (c *ExecutionPayloadEnvelopeCache) prune(slot primitives.Slot) {
for key := range c.cache {
if key.Slot < slot {
delete(c.cache, key)
}
}
}

View File

@@ -0,0 +1,146 @@
package cache
import (
"testing"
"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"
)
func TestExecutionPayloadEnvelopeCache_GetSet(t *testing.T) {
cache := NewExecutionPayloadEnvelopeCache()
// Test empty cache returns false
_, ok := cache.Get(1, 0)
require.Equal(t, false, ok, "expected empty cache to return false")
// Create test envelope
envelope := &ethpb.ExecutionPayloadEnvelope{
Slot: primitives.Slot(100),
BuilderIndex: primitives.BuilderIndex(5),
StateRoot: make([]byte, 32),
}
// Set and retrieve
cache.Set(envelope, nil)
retrieved, ok := cache.Get(100, 5)
require.Equal(t, true, ok, "expected to find cached envelope")
require.Equal(t, envelope.Slot, retrieved.Slot)
require.Equal(t, envelope.BuilderIndex, retrieved.BuilderIndex)
// Different builder index should not find it
_, ok = cache.Get(100, 6)
require.Equal(t, false, ok, "expected different builder index to return false")
// Different slot should not find it
_, ok = cache.Get(101, 5)
require.Equal(t, false, ok, "expected different slot to return false")
}
func TestExecutionPayloadEnvelopeCache_Prune(t *testing.T) {
cache := NewExecutionPayloadEnvelopeCache()
// Add envelopes at slots 10, 11, 12 (close enough that none get pruned).
// Prune removes entries with slot < (new_slot - 2), so inserting 10, 11, 12
// keeps all three: slot 12 prunes < 10, but 10 is not < 10.
for i := primitives.Slot(10); i <= 12; i++ {
envelope := &ethpb.ExecutionPayloadEnvelope{
Slot: i,
BuilderIndex: 0,
StateRoot: make([]byte, 32),
}
cache.Set(envelope, nil)
}
// Verify all are present
for i := primitives.Slot(10); i <= 12; i++ {
_, ok := cache.Get(i, 0)
require.Equal(t, true, ok, "expected envelope at slot %d", i)
}
// Add envelope at slot 20, should prune slots < 18
envelope := &ethpb.ExecutionPayloadEnvelope{
Slot: 20,
BuilderIndex: 0,
StateRoot: make([]byte, 32),
}
cache.Set(envelope, nil)
// Slots 10-12 should be pruned (all < 18)
for i := primitives.Slot(10); i <= 12; i++ {
_, ok := cache.Get(i, 0)
require.Equal(t, false, ok, "expected envelope at slot %d to be pruned", i)
}
// Slot 20 should still be present
_, ok := cache.Get(20, 0)
require.Equal(t, true, ok, "expected envelope at slot 20 to be present")
}
func TestExecutionPayloadEnvelopeCache_NilEnvelope(t *testing.T) {
cache := NewExecutionPayloadEnvelopeCache()
// Setting nil should not panic or add entry
cache.Set(nil, nil)
// Cache should still be empty
require.Equal(t, 0, len(cache.cache))
}
func TestExecutionPayloadEnvelopeCache_MultipleBuilders(t *testing.T) {
cache := NewExecutionPayloadEnvelopeCache()
slot := primitives.Slot(100)
// Add envelopes from multiple builders at same slot
for i := range primitives.BuilderIndex(3) {
envelope := &ethpb.ExecutionPayloadEnvelope{
Slot: slot,
BuilderIndex: i,
StateRoot: make([]byte, 32),
}
cache.Set(envelope, nil)
}
// All should be retrievable
for i := range primitives.BuilderIndex(3) {
retrieved, ok := cache.Get(slot, i)
require.Equal(t, true, ok, "expected to find envelope for builder %d", i)
require.Equal(t, i, retrieved.BuilderIndex)
}
}
func TestExecutionPayloadEnvelopeCache_BlobsBundle(t *testing.T) {
cache := NewExecutionPayloadEnvelopeCache()
slot := primitives.Slot(100)
builderIndex := primitives.BuilderIndex(5)
envelope := &ethpb.ExecutionPayloadEnvelope{
Slot: slot,
BuilderIndex: builderIndex,
StateRoot: make([]byte, 32),
}
bundle := &enginev1.BlobsBundle{
KzgCommitments: [][]byte{{1, 2, 3}},
Proofs: [][]byte{{4, 5, 6}},
Blobs: [][]byte{{7, 8, 9}},
}
cache.Set(envelope, bundle)
// Retrieve blobs bundle
retrieved, ok := cache.GetBlobsBundle(slot, builderIndex)
require.Equal(t, true, ok, "expected to find cached blobs bundle")
require.NotNil(t, retrieved)
b, ok := retrieved.(*enginev1.BlobsBundle)
require.Equal(t, true, ok)
require.Equal(t, 1, len(b.KzgCommitments))
require.DeepEqual(t, []byte{1, 2, 3}, b.KzgCommitments[0])
// Nil blobs bundle for missing key
_, ok = cache.GetBlobsBundle(slot, 99)
require.Equal(t, false, ok, "expected missing key to return false")
}

View File

@@ -23,11 +23,13 @@ var (
var (
_ ConstructionPopulator = (*BlockReconstructionSource)(nil)
_ ConstructionPopulator = (*SidecarReconstructionSource)(nil)
_ ConstructionPopulator = (*EnvelopeReconstructionSource)(nil)
)
const (
BlockType = "BeaconBlock"
SidecarType = "DataColumnSidecar"
BlockType = "BeaconBlock"
SidecarType = "DataColumnSidecar"
EnvelopeType = "ExecutionPayloadEnvelope"
)
type (
@@ -49,11 +51,20 @@ type (
blocks.ROBlock
}
// DataColumnSidecar is a ConstructionPopulator that uses a data column sidecar as the source of data
// SidecarReconstructionSource is a ConstructionPopulator that uses a data column sidecar as the source of data
SidecarReconstructionSource struct {
blocks.VerifiedRODataColumn
}
// EnvelopeReconstructionSource is a ConstructionPopulator for GLOAS+ where
// blob KZG commitments are in the execution payload envelope rather than the
// block body. It uses the block for the header and root, but overrides
// commitments with those from the envelope.
EnvelopeReconstructionSource struct {
blocks.ROBlock
commitments [][]byte
}
blockInfo struct {
signedBlockHeader *ethpb.SignedBeaconBlockHeader
kzgCommitments [][]byte
@@ -71,6 +82,13 @@ func PopulateFromSidecar(sidecar blocks.VerifiedRODataColumn) *SidecarReconstruc
return &SidecarReconstructionSource{VerifiedRODataColumn: sidecar}
}
// PopulateFromEnvelope creates an EnvelopeReconstructionSource from a block and
// the KZG commitments from the execution payload envelope. This is used for GLOAS+
// where commitments are in the envelope rather than the block body.
func PopulateFromEnvelope(block blocks.ROBlock, commitments [][]byte) *EnvelopeReconstructionSource {
return &EnvelopeReconstructionSource{ROBlock: block, commitments: commitments}
}
// ValidatorsCustodyRequirement returns the number of custody groups regarding the validator indices attached to the beacon node.
// https://github.com/ethereum/consensus-specs/blob/master/specs/fulu/validator.md#validator-custody
func ValidatorsCustodyRequirement(state beaconState.ReadOnlyBeaconState, validatorsIndex map[primitives.ValidatorIndex]bool) (uint64, error) {
@@ -254,3 +272,46 @@ func (s *SidecarReconstructionSource) extract() (*blockInfo, error) {
return info, nil
}
// Slot returns the slot of the source
func (s *EnvelopeReconstructionSource) Slot() primitives.Slot {
return s.Block().Slot()
}
// ProposerIndex returns the proposer index of the source
func (s *EnvelopeReconstructionSource) ProposerIndex() primitives.ValidatorIndex {
return s.Block().ProposerIndex()
}
// Commitments returns the blob KZG commitments from the envelope.
func (s *EnvelopeReconstructionSource) Commitments() ([][]byte, error) {
return s.commitments, nil
}
// Type returns the type of the source
func (s *EnvelopeReconstructionSource) Type() string {
return EnvelopeType
}
// extract extracts the block information from the source, using commitments
// from the envelope rather than the block body.
func (s *EnvelopeReconstructionSource) extract() (*blockInfo, error) {
header, err := s.Header()
if err != nil {
return nil, errors.Wrap(err, "header")
}
// TODO: Implement proper merkle proof for envelope commitments.
// In GLOAS, commitments are in the envelope not the block body,
// so the inclusion proof structure differs from pre-GLOAS forks.
inclusionProof := make([][]byte, 4)
for i := range inclusionProof {
inclusionProof[i] = make([]byte, 32)
}
return &blockInfo{
signedBlockHeader: header,
kzgCommitments: s.commitments,
kzgInclusionProof: inclusionProof,
}, nil
}

View File

@@ -270,21 +270,6 @@ func (f *ForkChoice) HighestReceivedBlockSlot() primitives.Slot {
return f.store.highestReceivedNode.slot
}
// HighestReceivedBlockDelay returns the number of slots that the highest
// received block was late when receiving it. For example, a block was late by 12 slots,
// then this method is expected to return 12.
func (f *ForkChoice) HighestReceivedBlockDelay() primitives.Slot {
n := f.store.highestReceivedNode
if n == nil {
return 0
}
sss, err := slots.SinceSlotStart(n.slot, f.store.genesisTime, n.timestamp)
if err != nil {
return 0
}
return primitives.Slot(uint64(sss/time.Second) / params.BeaconConfig().SecondsPerSlot)
}
// ReceivedBlocksLastEpoch returns the number of blocks received in the last epoch
func (f *ForkChoice) ReceivedBlocksLastEpoch() (uint64, error) {
count := uint64(0)

View File

@@ -335,7 +335,6 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, uint64(1), count)
require.Equal(t, primitives.Slot(1), f.HighestReceivedBlockSlot())
require.Equal(t, primitives.Slot(0), f.HighestReceivedBlockDelay())
// 64
// Received block last epoch is 1
@@ -348,7 +347,6 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, uint64(1), count)
require.Equal(t, primitives.Slot(64), f.HighestReceivedBlockSlot())
require.Equal(t, primitives.Slot(0), f.HighestReceivedBlockDelay())
// 64 65
// Received block last epoch is 2
@@ -361,7 +359,6 @@ func TestForkChoice_ReceivedBlocksLastEpoch(t *testing.T) {
require.NoError(t, err)
require.Equal(t, uint64(2), count)
require.Equal(t, primitives.Slot(65), f.HighestReceivedBlockSlot())
require.Equal(t, primitives.Slot(1), f.HighestReceivedBlockDelay())
// 64 65 66
// Received block last epoch is 3
@@ -713,17 +710,3 @@ func TestStore_CleanupInserting(t *testing.T) {
require.NotNil(t, f.InsertNode(ctx, st, blk))
require.Equal(t, false, f.HasNode(blk.Root()))
}
func TestStore_HighestReceivedBlockDelay(t *testing.T) {
f := ForkChoice{
store: &Store{
genesisTime: time.Unix(0, 0),
highestReceivedNode: &Node{
slot: 10,
timestamp: time.Unix(int64(((10 + 12) * params.BeaconConfig().SecondsPerSlot)), 0), // 12 slots late
},
},
}
require.Equal(t, primitives.Slot(12), f.HighestReceivedBlockDelay())
}

View File

@@ -67,7 +67,6 @@ type FastGetter interface {
HasNode([32]byte) bool
HighestReceivedBlockSlot() primitives.Slot
HighestReceivedBlockRoot() [32]byte
HighestReceivedBlockDelay() primitives.Slot
IsCanonical(root [32]byte) bool
IsOptimistic(root [32]byte) (bool, error)
IsViableForCheckpoint(*forkchoicetypes.Checkpoint) (bool, error)

View File

@@ -121,13 +121,6 @@ func (ro *ROForkChoice) HighestReceivedBlockRoot() [32]byte {
return ro.getter.HighestReceivedBlockRoot()
}
// HighestReceivedBlockDelay delegates to the underlying forkchoice call, under a lock.
func (ro *ROForkChoice) HighestReceivedBlockDelay() primitives.Slot {
ro.l.RLock()
defer ro.l.RUnlock()
return ro.getter.HighestReceivedBlockDelay()
}
// ReceivedBlocksLastEpoch delegates to the underlying forkchoice call, under a lock.
func (ro *ROForkChoice) ReceivedBlocksLastEpoch() (uint64, error) {
ro.l.RLock()

View File

@@ -30,7 +30,6 @@ const (
nodeCountCalled
highestReceivedBlockSlotCalled
highestReceivedBlockRootCalled
highestReceivedBlockDelayCalled
receivedBlocksLastEpochCalled
weightCalled
isOptimisticCalled
@@ -118,11 +117,6 @@ func TestROLocking(t *testing.T) {
call: highestReceivedBlockSlotCalled,
cb: func(g FastGetter) { g.HighestReceivedBlockSlot() },
},
{
name: "highestReceivedBlockDelayCalled",
call: highestReceivedBlockDelayCalled,
cb: func(g FastGetter) { g.HighestReceivedBlockDelay() },
},
{
name: "receivedBlocksLastEpochCalled",
call: receivedBlocksLastEpochCalled,
@@ -260,11 +254,6 @@ func (ro *mockROForkchoice) HighestReceivedBlockRoot() [32]byte {
return [32]byte{}
}
func (ro *mockROForkchoice) HighestReceivedBlockDelay() primitives.Slot {
ro.calls = append(ro.calls, highestReceivedBlockDelayCalled)
return 0
}
func (ro *mockROForkchoice) ReceivedBlocksLastEpoch() (uint64, error) {
ro.calls = append(ro.calls, receivedBlocksLastEpochCalled)
return 0, nil

View File

@@ -5,6 +5,7 @@ go_library(
srcs = [
"handlers.go",
"handlers_block.go",
"handlers_gloas.go",
"log.go",
"server.go",
],

View File

@@ -0,0 +1,276 @@
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.
//
// 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.
//
// 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 := &eth.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)
}
// PublishExecutionPayloadEnvelope broadcasts a signed execution payload envelope.
// Validators call this after signing the envelope to broadcast it to the network.
//
// 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")
}

View File

@@ -19,23 +19,24 @@ import (
// Server defines a server implementation of the gRPC Validator service,
// providing RPC endpoints intended for validator clients.
type Server struct {
HeadFetcher blockchain.HeadFetcher
TimeFetcher blockchain.TimeFetcher
SyncChecker sync.Checker
AttestationCache *cache.AttestationCache
AttestationsPool attestations.Pool
PeerManager p2p.PeerManager
Broadcaster p2p.Broadcaster
Stater lookup.Stater
OptimisticModeFetcher blockchain.OptimisticModeFetcher
SyncCommitteePool synccommittee.Pool
V1Alpha1Server eth.BeaconNodeValidatorServer
ChainInfoFetcher blockchain.ChainInfoFetcher
BeaconDB db.HeadAccessDatabase
BlockBuilder builder.BlockBuilder
OperationNotifier operation.Notifier
CoreService *core.Service
BlockRewardFetcher rewards.BlockRewardsFetcher
TrackedValidatorsCache *cache.TrackedValidatorsCache
PayloadIDCache *cache.PayloadIDCache
HeadFetcher blockchain.HeadFetcher
TimeFetcher blockchain.TimeFetcher
SyncChecker sync.Checker
AttestationCache *cache.AttestationCache
AttestationsPool attestations.Pool
PeerManager p2p.PeerManager
Broadcaster p2p.Broadcaster
Stater lookup.Stater
OptimisticModeFetcher blockchain.OptimisticModeFetcher
SyncCommitteePool synccommittee.Pool
V1Alpha1Server eth.BeaconNodeValidatorServer
ChainInfoFetcher blockchain.ChainInfoFetcher
BeaconDB db.HeadAccessDatabase
BlockBuilder builder.BlockBuilder
OperationNotifier operation.Notifier
CoreService *core.Service
BlockRewardFetcher rewards.BlockRewardsFetcher
TrackedValidatorsCache *cache.TrackedValidatorsCache
PayloadIDCache *cache.PayloadIDCache
ExecutionPayloadEnvelopeCache *cache.ExecutionPayloadEnvelopeCache // GLOAS: Cache for execution payload envelopes
}

View File

@@ -26,6 +26,7 @@ go_library(
"proposer_eth1data.go",
"proposer_execution_payload.go",
"proposer_exits.go",
"proposer_gloas.go",
"proposer_slashings.go",
"proposer_sync_aggregate.go",
"server.go",

View File

@@ -57,6 +57,8 @@ func (vs *Server) constructGenericBeaconBlock(
return nil, fmt.Errorf("expected *BlobsBundleV2, got %T", blobsBundler)
}
return vs.constructFuluBlock(blockProto, isBlinded, bidStr, bundle), nil
case version.Gloas:
return vs.constructGloasBlock(blockProto), nil
default:
return nil, fmt.Errorf("unknown block version: %d", sBlk.Version())
}
@@ -109,6 +111,13 @@ func (vs *Server) constructElectraBlock(blockProto proto.Message, isBlinded bool
return &ethpb.GenericBeaconBlock{Block: &ethpb.GenericBeaconBlock_Electra{Electra: electraContents}, IsBlinded: false, PayloadValue: payloadValue}
}
func (vs *Server) constructGloasBlock(blockProto proto.Message) *ethpb.GenericBeaconBlock {
// GLOAS blocks do not carry a separate payload value — the bid is part of the block body.
return &ethpb.GenericBeaconBlock{
Block: &ethpb.GenericBeaconBlock_Gloas{Gloas: blockProto.(*ethpb.BeaconBlockGloas)},
}
}
func (vs *Server) constructFuluBlock(blockProto proto.Message, isBlinded bool, payloadValue string, bundle *enginev1.BlobsBundleV2) *ethpb.GenericBeaconBlock {
if isBlinded {
return &ethpb.GenericBeaconBlock{Block: &ethpb.GenericBeaconBlock_BlindedFulu{BlindedFulu: blockProto.(*ethpb.BlindedBeaconBlockFulu)}, IsBlinded: true, PayloadValue: payloadValue}

View File

@@ -231,34 +231,50 @@ func (vs *Server) BuildBlockParallel(ctx context.Context, sBlk interfaces.Signed
// Set bls to execution change. New in Capella.
vs.setBlsToExecData(sBlk, head)
// Set payload attestations. New in GLOAS.
if sBlk.Version() >= version.Gloas {
if err := sBlk.SetPayloadAttestations(vs.getPayloadAttestations(ctx, head, sBlk.Block().Slot())); err != nil {
log.WithError(err).Error("Could not set payload attestations")
}
}
})
winningBid := primitives.ZeroWei()
var bundle enginev1.BlobsBundler
var local *blocks.GetPayloadResponse
if sBlk.Version() >= version.Bellatrix {
local, err := vs.getLocalPayload(ctx, sBlk.Block(), head)
var err error
local, err = vs.getLocalPayload(ctx, sBlk.Block(), head)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get local payload: %v", err)
}
// There's no reason to try to get a builder bid if local override is true.
var builderBid builderapi.Bid
if !(local.OverrideBuilder || skipMevBoost) {
latestHeader, err := head.LatestExecutionPayloadHeader()
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get latest execution payload header: %v", err)
switch {
case sBlk.Version() >= version.Gloas:
if err := vs.setGloasExecutionData(ctx, sBlk, local); err != nil {
return nil, status.Errorf(codes.Internal, "Could not set GLOAS execution data: %v", err)
}
parentGasLimit := latestHeader.GasLimit()
builderBid, err = vs.getBuilderPayloadAndBlobs(ctx, sBlk.Block().Slot(), sBlk.Block().ProposerIndex(), parentGasLimit)
if err != nil {
builderGetPayloadMissCount.Inc()
log.WithError(err).Error("Could not get builder payload")
default:
// There's no reason to try to get a builder bid if local override is true.
var builderBid builderapi.Bid
if !(local.OverrideBuilder || skipMevBoost) {
latestHeader, err := head.LatestExecutionPayloadHeader()
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get latest execution payload header: %v", err)
}
parentGasLimit := latestHeader.GasLimit()
builderBid, err = vs.getBuilderPayloadAndBlobs(ctx, sBlk.Block().Slot(), sBlk.Block().ProposerIndex(), parentGasLimit)
if err != nil {
builderGetPayloadMissCount.Inc()
log.WithError(err).Error("Could not get builder payload")
}
}
}
winningBid, bundle, err = setExecutionData(ctx, sBlk, local, builderBid, builderBoostFactor)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not set execution data: %v", err)
winningBid, bundle, err = setExecutionData(ctx, sBlk, local, builderBid, builderBoostFactor)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not set execution data: %v", err)
}
}
}
@@ -270,6 +286,15 @@ func (vs *Server) BuildBlockParallel(ctx context.Context, sBlk interfaces.Signed
}
sBlk.SetStateRoot(sr)
// For GLOAS, build and cache the execution payload envelope now that the block
// is fully built (state root set). The envelope needs the final block HTR as
// BeaconBlockRoot and the post-payload state root as StateRoot.
if sBlk.Version() >= version.Gloas {
if err := vs.buildExecutionPayloadEnvelope(ctx, sBlk, head, local); err != nil {
return nil, status.Errorf(codes.Internal, "Could not build execution payload envelope: %v", err)
}
}
return vs.constructGenericBeaconBlock(sBlk, bundle, winningBid)
}
@@ -277,11 +302,6 @@ func (vs *Server) BuildBlockParallel(ctx context.Context, sBlk interfaces.Signed
//
// ProposeBeaconBlock handles the proposal of beacon blocks.
func (vs *Server) ProposeBeaconBlock(ctx context.Context, req *ethpb.GenericSignedBeaconBlock) (*ethpb.ProposeResponse, error) {
var (
blobSidecars []*ethpb.BlobSidecar
dataColumnSidecars []blocks.RODataColumn
)
ctx, span := trace.StartSpan(ctx, "ProposerServer.ProposeBeaconBlock")
defer span.End()
@@ -298,15 +318,58 @@ func (vs *Server) ProposeBeaconBlock(ctx context.Context, req *ethpb.GenericSign
return nil, status.Errorf(codes.Internal, "Could not hash tree root: %v", err)
}
// For post-Fulu blinded blocks, submit to relay and return early
if block.IsBlinded() && slots.ToEpoch(block.Block().Slot()) >= params.BeaconConfig().FuluForkEpoch {
err := vs.BlockBuilder.SubmitBlindedBlockPostFulu(ctx, block)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not submit blinded block post-Fulu: %v", err)
if block.Version() < version.Gloas {
// For post-Fulu blinded blocks, submit to relay and return early.
if block.IsBlinded() && slots.ToEpoch(block.Block().Slot()) >= params.BeaconConfig().FuluForkEpoch {
err := vs.BlockBuilder.SubmitBlindedBlockPostFulu(ctx, block)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not submit blinded block post-Fulu: %v", err)
}
return &ethpb.ProposeResponse{BlockRoot: root[:]}, nil
}
return &ethpb.ProposeResponse{BlockRoot: root[:]}, nil
return vs.proposeBlockWithSidecars(ctx, block, root, req)
}
return vs.proposeBlock(ctx, block, root)
}
// proposeBlock broadcasts and receives a beacon block without sidecars.
// Used for GLOAS and beyond where execution data is delivered via a separate envelope.
func (vs *Server) proposeBlock(
ctx context.Context,
block interfaces.SignedBeaconBlock,
root [fieldparams.RootLength]byte,
) (*ethpb.ProposeResponse, error) {
protoBlock, err := block.Proto()
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not convert block to proto: %v", err)
}
if err := vs.P2P.Broadcast(ctx, protoBlock); err != nil {
return nil, status.Errorf(codes.Internal, "Could not broadcast block: %v", err)
}
vs.BlockNotifier.BlockFeed().Send(&feed.Event{
Type: blockfeed.ReceivedBlock,
Data: &blockfeed.ReceivedBlockData{SignedBlock: block},
})
if err := vs.BlockReceiver.ReceiveBlock(ctx, block, root, nil); err != nil {
return nil, status.Errorf(codes.Internal, "Could not receive block: %v", err)
}
return &ethpb.ProposeResponse{BlockRoot: root[:]}, nil
}
// proposeBlockWithSidecars handles block proposal for forks that carry blob or
// data column sidecars alongside the block (Bellatrix through Fulu).
func (vs *Server) proposeBlockWithSidecars(
ctx context.Context,
block interfaces.SignedBeaconBlock,
root [fieldparams.RootLength]byte,
req *ethpb.GenericSignedBeaconBlock,
) (*ethpb.ProposeResponse, error) {
var (
blobSidecars []*ethpb.BlobSidecar
dataColumnSidecars []blocks.RODataColumn
)
rob, err := blocks.NewROBlockWithRoot(block, root)
if block.IsBlinded() {
block, blobSidecars, err = vs.handleBlindedBlock(ctx, block)
@@ -410,18 +473,10 @@ func (vs *Server) handleUnblindedBlock(
}
if block.Version() >= version.Fulu {
// Compute cells and proofs from the blobs and cell proofs.
cellsPerBlob, proofsPerBlob, err := peerdas.ComputeCellsAndProofsFromFlat(rawBlobs, proofs)
roDataColumnSidecars, err := buildDataColumnSidecars(rawBlobs, proofs, peerdas.PopulateFromBlock(block))
if err != nil {
return nil, nil, errors.Wrap(err, "compute cells and proofs")
return nil, nil, err
}
// Construct data column sidecars from the signed block and cells and proofs.
roDataColumnSidecars, err := peerdas.DataColumnSidecars(cellsPerBlob, proofsPerBlob, peerdas.PopulateFromBlock(block))
if err != nil {
return nil, nil, errors.Wrap(err, "data column sidcars")
}
return nil, roDataColumnSidecars, nil
}
@@ -433,6 +488,22 @@ func (vs *Server) handleUnblindedBlock(
return blobSidecars, nil, nil
}
// buildDataColumnSidecars computes cells and proofs from blobs and constructs
// data column sidecars using the given ConstructionPopulator source.
func buildDataColumnSidecars(blobs, proofs [][]byte, src peerdas.ConstructionPopulator) ([]blocks.RODataColumn, error) {
cellsPerBlob, proofsPerBlob, err := peerdas.ComputeCellsAndProofsFromFlat(blobs, proofs)
if err != nil {
return nil, errors.Wrap(err, "compute cells and proofs")
}
roDataColumnSidecars, err := peerdas.DataColumnSidecars(cellsPerBlob, proofsPerBlob, src)
if err != nil {
return nil, errors.Wrap(err, "data column sidecars")
}
return roDataColumnSidecars, nil
}
// broadcastReceiveBlock broadcasts a block and handles its reception.
func (vs *Server) broadcastReceiveBlock(ctx context.Context, wg *sync.WaitGroup, block interfaces.SignedBeaconBlock, root [fieldparams.RootLength]byte) error {
if err := vs.broadcastBlock(ctx, wg, block, root); err != nil {

View File

@@ -16,6 +16,11 @@ func getEmptyBlock(slot primitives.Slot) (interfaces.SignedBeaconBlock, error) {
var err error
epoch := slots.ToEpoch(slot)
switch {
case epoch >= params.BeaconConfig().GloasForkEpoch:
sBlk, err = blocks.NewSignedBeaconBlock(&ethpb.SignedBeaconBlockGloas{Block: &ethpb.BeaconBlockGloas{Body: &ethpb.BeaconBlockBodyGloas{}}})
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not initialize block for proposal: %v", err)
}
case epoch >= params.BeaconConfig().FuluForkEpoch:
sBlk, err = blocks.NewSignedBeaconBlock(&ethpb.SignedBeaconBlockFulu{Block: &ethpb.BeaconBlockElectra{Body: &ethpb.BeaconBlockBodyElectra{}}})
if err != nil {

View File

@@ -0,0 +1,413 @@
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"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
consensusblocks "github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/container/trie"
"github.com/OffchainLabs/prysm/v7/crypto/bls/common"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// setGloasExecutionData creates an execution payload bid from the local payload
// and sets it on the block body. The envelope is created and cached later by
// buildExecutionPayloadEnvelope once the block is fully built.
func (vs *Server) setGloasExecutionData(
ctx context.Context,
sBlk interfaces.SignedBeaconBlock,
local *consensusblocks.GetPayloadResponse,
) error {
_, span := trace.StartSpan(ctx, "ProposerServer.setGloasExecutionData")
defer span.End()
if local == nil || local.ExecutionData == nil {
return errors.New("local execution payload is nil")
}
// Create execution payload bid from the local payload.
parentRoot := sBlk.Block().ParentRoot()
bid, err := vs.createSelfBuildExecutionPayloadBid(
local.ExecutionData,
primitives.BuilderIndex(sBlk.Block().ProposerIndex()),
parentRoot[:],
sBlk.Block().Slot(),
local.BlobsBundler,
)
if err != nil {
return errors.Wrap(err, "could not create execution payload bid")
}
// Per spec, self-build bids must use G2 point-at-infinity as the signature.
// Only the execution payload envelope requires a real signature from the proposer.
signedBid := &ethpb.SignedExecutionPayloadBid{
Message: bid,
Signature: common.InfiniteSignature[:],
}
if err := sBlk.SetSignedExecutionPayloadBid(signedBid); err != nil {
return errors.Wrap(err, "could not set signed execution payload bid")
}
return nil
}
// buildExecutionPayloadEnvelope creates and caches the execution payload envelope
// after the block is fully built (state root set). This allows setting the
// BeaconBlockRoot from the final block HTR and computing the post-payload state root.
func (vs *Server) buildExecutionPayloadEnvelope(
ctx context.Context,
sBlk interfaces.SignedBeaconBlock,
head state.BeaconState,
local *consensusblocks.GetPayloadResponse,
) error {
blockRoot, err := sBlk.Block().HashTreeRoot()
if err != nil {
return errors.Wrap(err, "could not compute block hash tree root")
}
// Extract the underlying ExecutionPayloadDeneb proto.
var payload *enginev1.ExecutionPayloadDeneb
if local.ExecutionData != nil && !local.ExecutionData.IsNil() {
if p, ok := local.ExecutionData.Proto().(*enginev1.ExecutionPayloadDeneb); ok {
payload = p
}
}
// TODO: Compute post-payload state root. This requires:
// 1. Run state transition on head state with the block to get post-block state
// 2. Run ProcessPayloadStateTransition(ctx, postBlockState, envelope) to apply
// execution payload effects (deposits, withdrawals, consolidations, etc.)
// 3. Set stateRoot = postPayloadState.HashTreeRoot()
// For now, the state root remains zeroed until ProcessPayloadStateTransition
// is implemented in beacon-chain/core/gloas.
envelope := &ethpb.ExecutionPayloadEnvelope{
Payload: payload,
ExecutionRequests: local.ExecutionRequests,
BuilderIndex: primitives.BuilderIndex(sBlk.Block().ProposerIndex()),
BeaconBlockRoot: blockRoot[:],
Slot: sBlk.Block().Slot(),
BlobKzgCommitments: extractKzgCommitments(local.BlobsBundler),
StateRoot: make([]byte, 32),
}
vs.cacheExecutionPayloadEnvelope(envelope, local.BlobsBundler)
return nil
}
// getPayloadAttestations returns payload attestations for inclusion in a GLOAS block.
// These attest to the payload timeliness from the previous slot's PTC.
func (vs *Server) getPayloadAttestations(ctx context.Context, head state.BeaconState, slot primitives.Slot) []*ethpb.PayloadAttestation {
// TODO: Implement payload attestation retrieval from pool.
// This requires:
// 1. A PayloadAttestationPool to collect PTC votes
// 2. Aggregation of individual PayloadAttestationMessages into PayloadAttestations
// For now, return empty - blocks are valid without payload attestations.
return []*ethpb.PayloadAttestation{}
}
// createSelfBuildExecutionPayloadBid creates an ExecutionPayloadBid for self-building,
// where the proposer acts as its own builder. Value and payment are zero, and the
// bid fields are derived directly from the local execution payload.
func (vs *Server) createSelfBuildExecutionPayloadBid(
executionData interfaces.ExecutionData,
builderIndex primitives.BuilderIndex,
parentBlockRoot []byte,
slot primitives.Slot,
blobsBundler enginev1.BlobsBundler,
) (*ethpb.ExecutionPayloadBid, error) {
if executionData == nil || executionData.IsNil() {
return nil, errors.New("execution data is nil")
}
// Compute blob_kzg_commitments_root from the blobs bundle.
// This is hash_tree_root(List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]).
kzgCommitmentsRoot := make([]byte, 32)
if blobsBundler != nil {
commitments := extractKzgCommitments(blobsBundler)
if len(commitments) > 0 {
leaves := consensusblocks.LeavesFromCommitments(commitments)
commitmentsTree, err := trie.GenerateTrieFromItems(leaves, fieldparams.LogMaxBlobCommitments)
if err != nil {
return nil, errors.Wrap(err, "could not generate kzg commitments trie")
}
root, err := commitmentsTree.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "could not compute kzg commitments root")
}
kzgCommitmentsRoot = root[:]
}
}
return &ethpb.ExecutionPayloadBid{
ParentBlockHash: executionData.ParentHash(),
ParentBlockRoot: bytesutil.SafeCopyBytes(parentBlockRoot),
BlockHash: executionData.BlockHash(),
PrevRandao: executionData.PrevRandao(),
FeeRecipient: executionData.FeeRecipient(),
GasLimit: executionData.GasLimit(),
BuilderIndex: builderIndex,
Slot: slot,
Value: 0,
ExecutionPayment: 0,
BlobKzgCommitmentsRoot: kzgCommitmentsRoot,
}, nil
}
// extractKzgCommitments pulls KZG commitments from a blobs bundler.
func extractKzgCommitments(blobsBundler enginev1.BlobsBundler) [][]byte {
if blobsBundler == nil {
return nil
}
switch b := blobsBundler.(type) {
case *enginev1.BlobsBundle:
if b != nil {
return b.KzgCommitments
}
case *enginev1.BlobsBundleV2:
if b != nil {
return b.KzgCommitments
}
}
return nil
}
// cacheExecutionPayloadEnvelope stores an envelope and its blobs bundle for later retrieval.
// The blobs bundle is cached alongside the envelope because blobs from the EL are only
// held in memory until they are broadcast as sidecars during block proposal.
func (vs *Server) cacheExecutionPayloadEnvelope(envelope *ethpb.ExecutionPayloadEnvelope, blobsBundle enginev1.BlobsBundler) {
if vs.ExecutionPayloadEnvelopeCache == nil {
log.Warn("ExecutionPayloadEnvelopeCache is nil, envelope will not be cached")
return
}
vs.ExecutionPayloadEnvelopeCache.Set(envelope, blobsBundle)
}
// GetExecutionPayloadEnvelope retrieves a cached execution payload envelope.
// This is called by validators after receiving a GLOAS block to get the envelope
// they need to sign and broadcast.
//
// gRPC endpoint: /eth/v1alpha1/validator/execution_payload_envelope/{slot}/{builder_index}
func (vs *Server) GetExecutionPayloadEnvelope(
ctx context.Context,
req *ethpb.ExecutionPayloadEnvelopeRequest,
) (*ethpb.ExecutionPayloadEnvelopeResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "request cannot be nil")
}
if slots.ToEpoch(req.Slot) < params.BeaconConfig().GloasForkEpoch {
return nil, status.Errorf(codes.InvalidArgument,
"execution payload envelopes are not supported before GLOAS fork (slot %d)", req.Slot)
}
if vs.ExecutionPayloadEnvelopeCache == nil {
return nil, status.Error(codes.Internal, "execution payload envelope cache not initialized")
}
envelope, found := vs.ExecutionPayloadEnvelopeCache.Get(req.Slot, req.BuilderIndex)
if !found {
return nil, status.Errorf(
codes.NotFound,
"execution payload envelope not found for slot %d builder %d",
req.Slot,
req.BuilderIndex,
)
}
return &ethpb.ExecutionPayloadEnvelopeResponse{
Envelope: envelope,
}, 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,
req *ethpb.SignedExecutionPayloadEnvelope,
) (*emptypb.Empty, error) {
if req == nil || req.Message == nil {
return nil, status.Error(codes.InvalidArgument, "signed envelope cannot be nil")
}
if slots.ToEpoch(req.Message.Slot) < params.BeaconConfig().GloasForkEpoch {
return nil, status.Errorf(codes.InvalidArgument,
"execution payload envelopes are not supported before GLOAS fork (slot %d)", req.Message.Slot)
}
beaconBlockRoot := bytesutil.ToBytes32(req.Message.BeaconBlockRoot)
log := log.WithFields(logrus.Fields{
"slot": req.Message.Slot,
"builderIndex": req.Message.BuilderIndex,
"beaconBlockRoot": fmt.Sprintf("%#x", beaconBlockRoot[:8]),
})
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)
// }
// Build data column sidecars from the cached blobs bundle before broadcasting.
// In GLOAS, blob data is delivered alongside the execution payload envelope
// rather than with the beacon block (which only carries the bid).
dataColumnSidecars, err := vs.buildEnvelopeDataColumns(ctx, req.Message, beaconBlockRoot)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to build data column sidecars: %v", err)
}
// Broadcast envelope and data column sidecars concurrently.
eg, eCtx := errgroup.WithContext(ctx)
eg.Go(func() error {
if err := vs.P2P.Broadcast(eCtx, req); err != nil {
return errors.Wrap(err, "broadcast signed execution payload envelope")
}
// TODO: Receive the envelope locally following the broadcastReceiveBlock pattern.
// This requires:
// 1. blocks.WrappedROSignedExecutionPayloadEnvelope wrapper
// 2. BlockReceiver.ReceiveExecutionPayloadEnvelope method
// See epbs branch's receive_execution_payload_envelope.go for reference.
return nil
})
if len(dataColumnSidecars) > 0 {
eg.Go(func() error {
return vs.broadcastAndReceiveDataColumns(eCtx, dataColumnSidecars)
})
}
if err := eg.Wait(); err != nil {
return nil, status.Errorf(codes.Internal, "failed to publish execution payload envelope: %v", err)
}
log.Info("Successfully published execution payload envelope")
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(
ctx context.Context,
envelope *ethpb.ExecutionPayloadEnvelope,
blockRoot [32]byte,
) ([]consensusblocks.RODataColumn, error) {
if vs.ExecutionPayloadEnvelopeCache == nil {
return nil, nil
}
blobsBundle, found := vs.ExecutionPayloadEnvelopeCache.GetBlobsBundle(envelope.Slot, envelope.BuilderIndex)
if !found || blobsBundle == nil {
return nil, nil
}
blobs := blobsBundle.GetBlobs()
proofs := blobsBundle.GetProofs()
commitments := envelope.BlobKzgCommitments
if len(blobs) == 0 {
return nil, nil
}
// Retrieve the beacon block to build the signed block header for sidecars.
blk, err := vs.BeaconDB.Block(ctx, blockRoot)
if err != nil {
return nil, errors.Wrap(err, "could not get block for data column sidecars")
}
if blk == nil {
return nil, errors.New("block not found for data column sidecars")
}
roBlock, err := consensusblocks.NewROBlockWithRoot(blk, blockRoot)
if err != nil {
return nil, errors.Wrap(err, "could not create ROBlock")
}
return buildDataColumnSidecars(blobs, proofs, peerdas.PopulateFromEnvelope(roBlock, commitments))
}

View File

@@ -0,0 +1,91 @@
package validator
import (
"testing"
consensusblocks "github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/crypto/bls/common"
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"
)
func TestSetGloasExecutionData(t *testing.T) {
parentRoot := [32]byte{1, 2, 3}
slot := primitives.Slot(100)
proposerIndex := primitives.ValidatorIndex(42)
sBlk, err := consensusblocks.NewSignedBeaconBlock(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: slot,
ProposerIndex: proposerIndex,
ParentRoot: parentRoot[:],
Body: &ethpb.BeaconBlockBodyGloas{},
},
})
require.NoError(t, err)
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),
}
ed, err := consensusblocks.WrappedExecutionPayloadDeneb(payload)
require.NoError(t, err)
local := &consensusblocks.GetPayloadResponse{
ExecutionData: ed,
Bid: primitives.ZeroWei(),
BlobsBundler: nil,
ExecutionRequests: &enginev1.ExecutionRequests{},
}
vs := &Server{}
err = vs.setGloasExecutionData(t.Context(), sBlk, local)
require.NoError(t, err)
// Verify the signed bid was set on the block.
signedBid, err := sBlk.Block().Body().SignedExecutionPayloadBid()
require.NoError(t, err)
require.NotNil(t, signedBid)
require.NotNil(t, signedBid.Message)
// Per spec (process_execution_payload_bid): for self-builds,
// signature must be G2 point-at-infinity.
require.DeepEqual(t, common.InfiniteSignature[:], signedBid.Signature)
// Verify bid fields.
bid := signedBid.Message
require.Equal(t, slot, bid.Slot)
require.Equal(t, primitives.BuilderIndex(proposerIndex), bid.BuilderIndex)
require.DeepEqual(t, parentRoot[:], bid.ParentBlockRoot)
require.Equal(t, uint64(0), bid.Value)
require.Equal(t, uint64(0), bid.ExecutionPayment)
}
func TestSetGloasExecutionData_NilPayload(t *testing.T) {
sBlk, err := consensusblocks.NewSignedBeaconBlock(&ethpb.SignedBeaconBlockGloas{
Block: &ethpb.BeaconBlockGloas{
Slot: 1,
ParentRoot: make([]byte, 32),
Body: &ethpb.BeaconBlockBodyGloas{},
},
})
require.NoError(t, err)
vs := &Server{}
err = vs.setGloasExecutionData(t.Context(), sBlk, nil)
require.ErrorContains(t, "local execution payload is nil", err)
err = vs.setGloasExecutionData(t.Context(), sBlk, &consensusblocks.GetPayloadResponse{})
require.ErrorContains(t, "local execution payload is nil", err)
}

View File

@@ -44,45 +44,46 @@ import (
// and committees in which particular validators need to perform their responsibilities,
// and more.
type Server struct {
Ctx context.Context
PayloadIDCache *cache.PayloadIDCache
TrackedValidatorsCache *cache.TrackedValidatorsCache
HeadFetcher blockchain.HeadFetcher
ForkFetcher blockchain.ForkFetcher
ForkchoiceFetcher blockchain.ForkchoiceFetcher
GenesisFetcher blockchain.GenesisFetcher
FinalizationFetcher blockchain.FinalizationFetcher
TimeFetcher blockchain.TimeFetcher
BlockFetcher execution.POWBlockFetcher
DepositFetcher cache.DepositFetcher
ChainStartFetcher execution.ChainStartFetcher
Eth1InfoFetcher execution.ChainInfoFetcher
OptimisticModeFetcher blockchain.OptimisticModeFetcher
SyncChecker sync.Checker
StateNotifier statefeed.Notifier
BlockNotifier blockfeed.Notifier
P2P p2p.Broadcaster
AttestationCache *cache.AttestationCache
AttPool attestations.Pool
SlashingsPool slashings.PoolManager
ExitPool voluntaryexits.PoolManager
SyncCommitteePool synccommittee.Pool
BlockReceiver blockchain.BlockReceiver
BlobReceiver blockchain.BlobReceiver
DataColumnReceiver blockchain.DataColumnReceiver
MockEth1Votes bool
Eth1BlockFetcher execution.POWBlockFetcher
PendingDepositsFetcher depositsnapshot.PendingDepositsFetcher
OperationNotifier opfeed.Notifier
StateGen stategen.StateManager
ReplayerBuilder stategen.ReplayerBuilder
BeaconDB db.HeadAccessDatabase
ExecutionEngineCaller execution.EngineCaller
BlockBuilder builder.BlockBuilder
BLSChangesPool blstoexec.PoolManager
ClockWaiter startup.ClockWaiter
CoreService *core.Service
AttestationStateFetcher blockchain.AttestationStateFetcher
Ctx context.Context
PayloadIDCache *cache.PayloadIDCache
TrackedValidatorsCache *cache.TrackedValidatorsCache
ExecutionPayloadEnvelopeCache *cache.ExecutionPayloadEnvelopeCache // GLOAS: Cache for execution payload envelopes
HeadFetcher blockchain.HeadFetcher
ForkFetcher blockchain.ForkFetcher
ForkchoiceFetcher blockchain.ForkchoiceFetcher
GenesisFetcher blockchain.GenesisFetcher
FinalizationFetcher blockchain.FinalizationFetcher
TimeFetcher blockchain.TimeFetcher
BlockFetcher execution.POWBlockFetcher
DepositFetcher cache.DepositFetcher
ChainStartFetcher execution.ChainStartFetcher
Eth1InfoFetcher execution.ChainInfoFetcher
OptimisticModeFetcher blockchain.OptimisticModeFetcher
SyncChecker sync.Checker
StateNotifier statefeed.Notifier
BlockNotifier blockfeed.Notifier
P2P p2p.Broadcaster
AttestationCache *cache.AttestationCache
AttPool attestations.Pool
SlashingsPool slashings.PoolManager
ExitPool voluntaryexits.PoolManager
SyncCommitteePool synccommittee.Pool
BlockReceiver blockchain.BlockReceiver
BlobReceiver blockchain.BlobReceiver
DataColumnReceiver blockchain.DataColumnReceiver
MockEth1Votes bool
Eth1BlockFetcher execution.POWBlockFetcher
PendingDepositsFetcher depositsnapshot.PendingDepositsFetcher
OperationNotifier opfeed.Notifier
StateGen stategen.StateManager
ReplayerBuilder stategen.ReplayerBuilder
BeaconDB db.HeadAccessDatabase
ExecutionEngineCaller execution.EngineCaller
BlockBuilder builder.BlockBuilder
BLSChangesPool blstoexec.PoolManager
ClockWaiter startup.ClockWaiter
CoreService *core.Service
AttestationStateFetcher blockchain.AttestationStateFetcher
}
// Deprecated: The gRPC API will remain the default and fully supported through v8 (expected in 2026) but will be eventually removed in favor of REST API.

View File

@@ -0,0 +1,2 @@
### Ignored
- Remove unused `HighestBlockDelay` method in forkchoice.

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ import "proto/prysm/v1alpha1/beacon_block.proto";
import "proto/prysm/v1alpha1/beacon_core_types.proto";
import "proto/prysm/v1alpha1/sync_committee.proto";
import "proto/prysm/v1alpha1/attestation.proto";
import "proto/prysm/v1alpha1/gloas.proto";
option csharp_namespace = "Ethereum.Eth.V1";
option go_package = "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1;eth";
@@ -439,6 +440,39 @@ service BeaconNodeValidator {
get : "/eth/v1alpha1/validator/blocks/aggregated_sig_and_aggregation_bits"
};
}
// ==========================================================================
// GLOAS Fork Endpoints
// ==========================================================================
// GetExecutionPayloadEnvelope retrieves a cached 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.
//
// The envelope is cached by the beacon node during block production and
// contains the full execution payload that corresponds to the bid in the block.
rpc GetExecutionPayloadEnvelope(ExecutionPayloadEnvelopeRequest)
returns (ExecutionPayloadEnvelopeResponse) {
option deprecated = true;
option (google.api.http) = {
get : "/eth/v1alpha1/validator/execution_payload_envelope/{slot}/{builder_index}"
};
}
// PublishExecutionPayloadEnvelope broadcasts a signed execution payload envelope
// to the P2P network. This is called by validators after signing the envelope
// retrieved from GetExecutionPayloadEnvelope.
//
// The beacon node validates the envelope signature and broadcasts it to peers
// via the execution_payload_envelope gossip topic.
rpc PublishExecutionPayloadEnvelope(SignedExecutionPayloadEnvelope)
returns (google.protobuf.Empty) {
option deprecated = true;
option (google.api.http) = {
post : "/eth/v1alpha1/validator/execution_payload_envelope"
body : "*"
};
}
}
// SyncMessageBlockRootResponse for beacon chain validator to retrieve and
@@ -1134,3 +1168,34 @@ message AggregatedSigAndAggregationBitsResponse {
bytes aggregated_sig = 1;
bytes bits = 2;
}
// =============================================================================
// GLOAS Fork Messages
// =============================================================================
// ExecutionPayloadEnvelopeRequest is the request for retrieving a cached
// execution payload envelope from the beacon node.
message ExecutionPayloadEnvelopeRequest {
option deprecated = true;
// The slot for which to retrieve the execution payload envelope.
uint64 slot = 1 [
(ethereum.eth.ext.cast_type) =
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.Slot"
];
// The builder index that created the payload envelope.
uint64 builder_index = 2 [
(ethereum.eth.ext.cast_type) =
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.BuilderIndex"
];
}
// ExecutionPayloadEnvelopeResponse is the response containing the cached
// execution payload envelope.
message ExecutionPayloadEnvelopeResponse {
option deprecated = true;
// The execution payload envelope for the requested slot and builder.
ExecutionPayloadEnvelope envelope = 1;
}

View File

@@ -683,6 +683,46 @@ func (mr *MockBeaconNodeValidatorClientMockRecorder) WaitForChainStart(arg0, arg
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForChainStart", reflect.TypeOf((*MockBeaconNodeValidatorClient)(nil).WaitForChainStart), varargs...)
}
// GetExecutionPayloadEnvelope mocks base method.
func (m *MockBeaconNodeValidatorClient) GetExecutionPayloadEnvelope(arg0 context.Context, arg1 *eth.ExecutionPayloadEnvelopeRequest, arg2 ...grpc.CallOption) (*eth.ExecutionPayloadEnvelopeResponse, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecutionPayloadEnvelope", varargs...)
ret0, _ := ret[0].(*eth.ExecutionPayloadEnvelopeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetExecutionPayloadEnvelope indicates an expected call of GetExecutionPayloadEnvelope.
func (mr *MockBeaconNodeValidatorClientMockRecorder) GetExecutionPayloadEnvelope(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionPayloadEnvelope", reflect.TypeOf((*MockBeaconNodeValidatorClient)(nil).GetExecutionPayloadEnvelope), varargs...)
}
// PublishExecutionPayloadEnvelope mocks base method.
func (m *MockBeaconNodeValidatorClient) PublishExecutionPayloadEnvelope(arg0 context.Context, arg1 *eth.SignedExecutionPayloadEnvelope, arg2 ...grpc.CallOption) (*emptypb.Empty, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PublishExecutionPayloadEnvelope", varargs...)
ret0, _ := ret[0].(*emptypb.Empty)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishExecutionPayloadEnvelope indicates an expected call of PublishExecutionPayloadEnvelope.
func (mr *MockBeaconNodeValidatorClientMockRecorder) PublishExecutionPayloadEnvelope(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishExecutionPayloadEnvelope", reflect.TypeOf((*MockBeaconNodeValidatorClient)(nil).PublishExecutionPayloadEnvelope), varargs...)
}
// MockBeaconNodeValidator_WaitForChainStartClient is a mock of BeaconNodeValidator_WaitForChainStartClient interface.
type MockBeaconNodeValidator_WaitForChainStartClient struct {
ctrl *gomock.Controller

View File

@@ -518,6 +518,36 @@ func (mr *MockBeaconNodeValidatorServerMockRecorder) WaitForChainStart(arg0, arg
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForChainStart", reflect.TypeOf((*MockBeaconNodeValidatorServer)(nil).WaitForChainStart), arg0, arg1)
}
// GetExecutionPayloadEnvelope mocks base method.
func (m *MockBeaconNodeValidatorServer) GetExecutionPayloadEnvelope(arg0 context.Context, arg1 *eth.ExecutionPayloadEnvelopeRequest) (*eth.ExecutionPayloadEnvelopeResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecutionPayloadEnvelope", arg0, arg1)
ret0, _ := ret[0].(*eth.ExecutionPayloadEnvelopeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetExecutionPayloadEnvelope indicates an expected call of GetExecutionPayloadEnvelope.
func (mr *MockBeaconNodeValidatorServerMockRecorder) GetExecutionPayloadEnvelope(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionPayloadEnvelope", reflect.TypeOf((*MockBeaconNodeValidatorServer)(nil).GetExecutionPayloadEnvelope), arg0, arg1)
}
// PublishExecutionPayloadEnvelope mocks base method.
func (m *MockBeaconNodeValidatorServer) PublishExecutionPayloadEnvelope(arg0 context.Context, arg1 *eth.SignedExecutionPayloadEnvelope) (*emptypb.Empty, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishExecutionPayloadEnvelope", arg0, arg1)
ret0, _ := ret[0].(*emptypb.Empty)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishExecutionPayloadEnvelope indicates an expected call of PublishExecutionPayloadEnvelope.
func (mr *MockBeaconNodeValidatorServerMockRecorder) PublishExecutionPayloadEnvelope(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishExecutionPayloadEnvelope", reflect.TypeOf((*MockBeaconNodeValidatorServer)(nil).PublishExecutionPayloadEnvelope), arg0, arg1)
}
// MockBeaconNodeValidator_WaitForActivationServer is a mock of BeaconNodeValidator_WaitForActivationServer interface.
type MockBeaconNodeValidator_WaitForActivationServer struct {
ctrl *gomock.Controller

View File

@@ -518,3 +518,33 @@ func (mr *MockValidatorClientMockRecorder) WaitForChainStart(ctx, in any) *gomoc
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForChainStart", reflect.TypeOf((*MockValidatorClient)(nil).WaitForChainStart), ctx, in)
}
// GetExecutionPayloadEnvelope mocks base method.
func (m *MockValidatorClient) ExecutionPayloadEnvelope(ctx context.Context, slot primitives.Slot, builderIndex primitives.BuilderIndex) (*eth.ExecutionPayloadEnvelope, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecutionPayloadEnvelope", ctx, slot, builderIndex)
ret0, _ := ret[0].(*eth.ExecutionPayloadEnvelope)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetExecutionPayloadEnvelope indicates an expected call of GetExecutionPayloadEnvelope.
func (mr *MockValidatorClientMockRecorder) GetExecutionPayloadEnvelope(ctx, slot, builderIndex any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecutionPayloadEnvelope", reflect.TypeOf((*MockValidatorClient)(nil).ExecutionPayloadEnvelope), ctx, slot, builderIndex)
}
// PublishExecutionPayloadEnvelope mocks base method.
func (m *MockValidatorClient) PublishExecutionPayloadEnvelope(ctx context.Context, in *eth.SignedExecutionPayloadEnvelope) (*emptypb.Empty, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishExecutionPayloadEnvelope", ctx, in)
ret0, _ := ret[0].(*emptypb.Empty)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishExecutionPayloadEnvelope indicates an expected call of PublishExecutionPayloadEnvelope.
func (mr *MockValidatorClientMockRecorder) PublishExecutionPayloadEnvelope(ctx, in any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishExecutionPayloadEnvelope", reflect.TypeOf((*MockValidatorClient)(nil).PublishExecutionPayloadEnvelope), ctx, in)
}

View File

@@ -17,6 +17,7 @@ go_library(
"duties.go",
"genesis.go",
"get_beacon_block.go",
"gloas.go",
"index.go",
"log.go",
"metrics.go",

View File

@@ -342,3 +342,23 @@ func (c *beaconApiValidatorClient) Host() string {
func (c *beaconApiValidatorClient) EnsureReady(ctx context.Context) bool {
return fallback.EnsureReady(ctx, c.restProvider, c.nodeClient)
}
// GLOAS Fork Methods
func (c *beaconApiValidatorClient) ExecutionPayloadEnvelope(ctx context.Context, slot primitives.Slot, builderIndex primitives.BuilderIndex) (*ethpb.ExecutionPayloadEnvelope, error) {
ctx, span := trace.StartSpan(ctx, "beacon-api.ExecutionPayloadEnvelope")
defer span.End()
return wrapInMetrics[*ethpb.ExecutionPayloadEnvelope]("ExecutionPayloadEnvelope", func() (*ethpb.ExecutionPayloadEnvelope, error) {
return c.getExecutionPayloadEnvelope(ctx, slot, builderIndex)
})
}
func (c *beaconApiValidatorClient) PublishExecutionPayloadEnvelope(ctx context.Context, in *ethpb.SignedExecutionPayloadEnvelope) (*empty.Empty, error) {
ctx, span := trace.StartSpan(ctx, "beacon-api.PublishExecutionPayloadEnvelope")
defer span.End()
return wrapInMetrics[*empty.Empty]("PublishExecutionPayloadEnvelope", func() (*empty.Empty, error) {
return c.publishExecutionPayloadEnvelope(ctx, in)
})
}

View File

@@ -0,0 +1,122 @@
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}
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
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")
}

View File

@@ -391,3 +391,24 @@ func (c *grpcValidatorClient) EnsureReady(ctx context.Context) bool {
provider := c.grpcClientManager.conn.GetGrpcConnectionProvider()
return fallback.EnsureReady(ctx, provider, c.nodeClient)
}
// GLOAS Fork Methods
func (c *grpcValidatorClient) ExecutionPayloadEnvelope(ctx context.Context, slot primitives.Slot, builderIndex primitives.BuilderIndex) (*ethpb.ExecutionPayloadEnvelope, error) {
req := &ethpb.ExecutionPayloadEnvelopeRequest{
Slot: slot,
BuilderIndex: builderIndex,
}
resp, err := c.getClient().GetExecutionPayloadEnvelope(ctx, req)
if err != nil {
return nil, errors.Wrap(
client.ErrConnectionIssue,
errors.Wrap(err, "ExecutionPayloadEnvelope").Error(),
)
}
return resp.Envelope, nil
}
func (c *grpcValidatorClient) PublishExecutionPayloadEnvelope(ctx context.Context, in *ethpb.SignedExecutionPayloadEnvelope) (*empty.Empty, error) {
return c.getClient().PublishExecutionPayloadEnvelope(ctx, in)
}

View File

@@ -153,4 +153,6 @@ type ValidatorClient interface {
AggregatedSyncSelections(ctx context.Context, selections []SyncCommitteeSelection) ([]SyncCommitteeSelection, error)
Host() string
EnsureReady(ctx context.Context) bool
ExecutionPayloadEnvelope(ctx context.Context, slot primitives.Slot, builderIndex primitives.BuilderIndex) (*ethpb.ExecutionPayloadEnvelope, error)
PublishExecutionPayloadEnvelope(ctx context.Context, in *ethpb.SignedExecutionPayloadEnvelope) (*empty.Empty, error)
}

View File

@@ -128,7 +128,8 @@ func (v *validator) ProposeBlock(ctx context.Context, slot primitives.Slot, pubK
var genericSignedBlock *ethpb.GenericSignedBeaconBlock
// Special handling for Deneb blocks and later version because of blob side cars.
if blk.Version() >= version.Deneb && !blk.IsBlinded() {
// GLOAS blocks are handled differently - no blobs in block, execution payload is separate.
if blk.Version() >= version.Deneb && blk.Version() < version.Gloas && !blk.IsBlinded() {
pb, err := blk.Proto()
if err != nil {
log.WithError(err).Error("Failed to get deneb block")
@@ -176,6 +177,14 @@ 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
}
}
span.SetAttributes(
trace.StringAttribute("blockRoot", fmt.Sprintf("%#x", blkResp.BlockRoot)),
trace.Int64Attribute("numDeposits", int64(len(blk.Block().Body().Deposits()))),
@@ -583,3 +592,98 @@ 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 &ethpb.SignedExecutionPayloadEnvelope{
Message: envelope,
Signature: sig.Marshal(),
}, nil
}