Compare commits

..

19 Commits

Author SHA1 Message Date
james-prysm
3ea57deff0 Merge branch 'develop' into gloas-proposer 2026-02-12 14:13:21 -08:00
james-prysm
16e2b42990 changelog 2026-02-11 14:34:04 -06:00
james-prysm
9622ab90ea Merge branch 'develop' into gloas-proposer 2026-02-11 12:31:26 -08:00
james-prysm
9a5c709e20 Merge branch 'develop' into gloas-proposer 2026-02-10 14:07:58 -08:00
james-prysm
358a2537f0 more self review 2026-02-10 11:56:39 -06:00
james-prysm
c69c7a8c42 Merge branch 'develop' into gloas-proposer 2026-02-10 09:04:19 -08:00
james-prysm
d7d8941a65 making data columns TODO 2026-02-10 10:45:22 -06:00
james-prysm
da5db1b4e5 removing unneeded structs for this pr 2026-02-10 08:56:46 -06:00
james-prysm
367e4d49d3 more self review 2026-02-09 17:01:11 -06:00
james-prysm
afd5935c24 updating comment for clarity 2026-02-09 14:24:30 -06:00
james-prysm
2b02648391 Merge branch 'develop' into gloas-proposer 2026-02-09 11:17:43 -08:00
james-prysm
ec0c47396c cleanup with self review 2026-02-09 11:08:18 -06:00
james-prysm
513b699036 changing kzg root to commitments 2026-02-09 09:01:45 -06:00
james-prysm
08dfeae0d9 Merge branch 'develop' into gloas-proposer 2026-02-09 06:30:38 -08:00
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
james-prysm
048ea521b6 wip 2026-02-06 10:39:39 -06:00
29 changed files with 2906 additions and 1381 deletions

View File

@@ -15,6 +15,7 @@ go_library(
"common.go",
"doc.go",
"error.go",
"execution_payload_envelope.go",
"interfaces.go",
"log.go",
"payload_attestation.go",
@@ -52,6 +53,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",
@@ -77,6 +79,7 @@ go_test(
"checkpoint_state_test.go",
"committee_fuzz_test.go",
"committee_test.go",
"execution_payload_envelope_test.go",
"payload_attestation_test.go",
"payload_id_test.go",
"private_access_test.go",
@@ -99,6 +102,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

@@ -87,49 +87,50 @@ type serviceFlagOpts struct {
// full PoS node. It handles the lifecycle of the entire system and registers
// services to a service registry.
type BeaconNode struct {
cliCtx *cli.Context
ctx context.Context
cancel context.CancelFunc
services *runtime.ServiceRegistry
lock sync.RWMutex
stop chan struct{} // Channel to wait for termination notifications.
db db.Database
slasherDB db.SlasherDatabase
attestationCache *cache.AttestationCache
attestationPool attestations.Pool
exitPool voluntaryexits.PoolManager
slashingsPool slashings.PoolManager
syncCommitteePool synccommittee.Pool
blsToExecPool blstoexec.PoolManager
depositCache cache.DepositCache
trackedValidatorsCache *cache.TrackedValidatorsCache
payloadIDCache *cache.PayloadIDCache
stateFeed *event.Feed
blockFeed *event.Feed
opFeed *event.Feed
stateGen *stategen.State
collector *bcnodeCollector
slasherBlockHeadersFeed *event.Feed
slasherAttestationsFeed *event.Feed
finalizedStateAtStartUp state.BeaconState
serviceFlagOpts *serviceFlagOpts
GenesisProviders []genesis.Provider
CheckpointInitializer checkpoint.Initializer
forkChoicer forkchoice.ForkChoicer
ClockWaiter startup.ClockWaiter
BackfillOpts []backfill.ServiceOption
initialSyncComplete chan struct{}
BlobStorage *filesystem.BlobStorage
BlobStorageOptions []filesystem.BlobStorageOption
DataColumnStorage *filesystem.DataColumnStorage
DataColumnStorageOptions []filesystem.DataColumnStorageOption
verifyInitWaiter *verification.InitializerWaiter
lhsp *verification.LazyHeadStateProvider
syncChecker *initialsync.SyncChecker
slasherEnabled bool
lcStore *lightclient.Store
ConfigOptions []params.Option
SyncNeedsWaiter func() (das.SyncNeeds, error)
cliCtx *cli.Context
ctx context.Context
cancel context.CancelFunc
services *runtime.ServiceRegistry
lock sync.RWMutex
stop chan struct{} // Channel to wait for termination notifications.
db db.Database
slasherDB db.SlasherDatabase
attestationCache *cache.AttestationCache
attestationPool attestations.Pool
exitPool voluntaryexits.PoolManager
slashingsPool slashings.PoolManager
syncCommitteePool synccommittee.Pool
blsToExecPool blstoexec.PoolManager
depositCache cache.DepositCache
trackedValidatorsCache *cache.TrackedValidatorsCache
payloadIDCache *cache.PayloadIDCache
executionPayloadEnvelopeCache *cache.ExecutionPayloadEnvelopeCache
stateFeed *event.Feed
blockFeed *event.Feed
opFeed *event.Feed
stateGen *stategen.State
collector *bcnodeCollector
slasherBlockHeadersFeed *event.Feed
slasherAttestationsFeed *event.Feed
finalizedStateAtStartUp state.BeaconState
serviceFlagOpts *serviceFlagOpts
GenesisProviders []genesis.Provider
CheckpointInitializer checkpoint.Initializer
forkChoicer forkchoice.ForkChoicer
ClockWaiter startup.ClockWaiter
BackfillOpts []backfill.ServiceOption
initialSyncComplete chan struct{}
BlobStorage *filesystem.BlobStorage
BlobStorageOptions []filesystem.BlobStorageOption
DataColumnStorage *filesystem.DataColumnStorage
DataColumnStorageOptions []filesystem.DataColumnStorageOption
verifyInitWaiter *verification.InitializerWaiter
lhsp *verification.LazyHeadStateProvider
syncChecker *initialsync.SyncChecker
slasherEnabled bool
lcStore *lightclient.Store
ConfigOptions []params.Option
SyncNeedsWaiter func() (das.SyncNeeds, error)
}
// New creates a new node instance, sets up configuration options, and registers
@@ -151,28 +152,29 @@ func New(cliCtx *cli.Context, cancel context.CancelFunc, optFuncs []func(*cli.Co
ctx := cliCtx.Context
beacon := &BeaconNode{
cliCtx: cliCtx,
ctx: ctx,
cancel: cancel,
services: runtime.NewServiceRegistry(),
stop: make(chan struct{}),
stateFeed: new(event.Feed),
blockFeed: new(event.Feed),
opFeed: new(event.Feed),
attestationCache: cache.NewAttestationCache(),
attestationPool: attestations.NewPool(),
exitPool: voluntaryexits.NewPool(),
slashingsPool: slashings.NewPool(),
syncCommitteePool: synccommittee.NewPool(),
blsToExecPool: blstoexec.NewPool(),
trackedValidatorsCache: cache.NewTrackedValidatorsCache(),
payloadIDCache: cache.NewPayloadIDCache(),
slasherBlockHeadersFeed: new(event.Feed),
slasherAttestationsFeed: new(event.Feed),
serviceFlagOpts: &serviceFlagOpts{},
initialSyncComplete: make(chan struct{}),
syncChecker: &initialsync.SyncChecker{},
slasherEnabled: cliCtx.Bool(flags.SlasherFlag.Name),
cliCtx: cliCtx,
ctx: ctx,
cancel: cancel,
services: runtime.NewServiceRegistry(),
stop: make(chan struct{}),
stateFeed: new(event.Feed),
blockFeed: new(event.Feed),
opFeed: new(event.Feed),
attestationCache: cache.NewAttestationCache(),
attestationPool: attestations.NewPool(),
exitPool: voluntaryexits.NewPool(),
slashingsPool: slashings.NewPool(),
syncCommitteePool: synccommittee.NewPool(),
blsToExecPool: blstoexec.NewPool(),
trackedValidatorsCache: cache.NewTrackedValidatorsCache(),
payloadIDCache: cache.NewPayloadIDCache(),
executionPayloadEnvelopeCache: cache.NewExecutionPayloadEnvelopeCache(),
slasherBlockHeadersFeed: new(event.Feed),
slasherAttestationsFeed: new(event.Feed),
serviceFlagOpts: &serviceFlagOpts{},
initialSyncComplete: make(chan struct{}),
syncChecker: &initialsync.SyncChecker{},
slasherEnabled: cliCtx.Bool(flags.SlasherFlag.Name),
}
for _, opt := range opts {
@@ -954,60 +956,61 @@ func (b *BeaconNode) registerRPCService(router *http.ServeMux) error {
p2pService := b.fetchP2P()
rpcService := rpc.NewService(b.ctx, &rpc.Config{
ExecutionEngineCaller: web3Service,
ExecutionReconstructor: web3Service,
Host: host,
Port: port,
BeaconMonitoringHost: beaconMonitoringHost,
BeaconMonitoringPort: beaconMonitoringPort,
CertFlag: cert,
KeyFlag: key,
BeaconDB: b.db,
Broadcaster: p2pService,
PeersFetcher: p2pService,
PeerManager: p2pService,
MetadataProvider: p2pService,
ChainInfoFetcher: chainService,
HeadFetcher: chainService,
CanonicalFetcher: chainService,
ForkFetcher: chainService,
ForkchoiceFetcher: chainService,
FinalizationFetcher: chainService,
BlockReceiver: chainService,
BlobReceiver: chainService,
DataColumnReceiver: chainService,
AttestationReceiver: chainService,
GenesisTimeFetcher: chainService,
GenesisFetcher: chainService,
OptimisticModeFetcher: chainService,
AttestationCache: b.attestationCache,
AttestationsPool: b.attestationPool,
ExitPool: b.exitPool,
SlashingsPool: b.slashingsPool,
BLSChangesPool: b.blsToExecPool,
SyncCommitteeObjectPool: b.syncCommitteePool,
ExecutionChainService: web3Service,
ExecutionChainInfoFetcher: web3Service,
ChainStartFetcher: chainStartFetcher,
MockEth1Votes: mockEth1DataVotes,
SyncService: syncService,
DepositFetcher: depositFetcher,
PendingDepositFetcher: b.depositCache,
BlockNotifier: b,
StateNotifier: b,
OperationNotifier: b,
StateGen: b.stateGen,
EnableDebugRPCEndpoints: enableDebugRPCEndpoints,
MaxMsgSize: maxMsgSize,
BlockBuilder: b.fetchBuilderService(),
Router: router,
ClockWaiter: b.ClockWaiter,
BlobStorage: b.BlobStorage,
DataColumnStorage: b.DataColumnStorage,
TrackedValidatorsCache: b.trackedValidatorsCache,
PayloadIDCache: b.payloadIDCache,
LCStore: b.lcStore,
GraffitiInfo: web3Service.GraffitiInfo(),
ExecutionEngineCaller: web3Service,
ExecutionReconstructor: web3Service,
Host: host,
Port: port,
BeaconMonitoringHost: beaconMonitoringHost,
BeaconMonitoringPort: beaconMonitoringPort,
CertFlag: cert,
KeyFlag: key,
BeaconDB: b.db,
Broadcaster: p2pService,
PeersFetcher: p2pService,
PeerManager: p2pService,
MetadataProvider: p2pService,
ChainInfoFetcher: chainService,
HeadFetcher: chainService,
CanonicalFetcher: chainService,
ForkFetcher: chainService,
ForkchoiceFetcher: chainService,
FinalizationFetcher: chainService,
BlockReceiver: chainService,
BlobReceiver: chainService,
DataColumnReceiver: chainService,
AttestationReceiver: chainService,
GenesisTimeFetcher: chainService,
GenesisFetcher: chainService,
OptimisticModeFetcher: chainService,
AttestationCache: b.attestationCache,
AttestationsPool: b.attestationPool,
ExitPool: b.exitPool,
SlashingsPool: b.slashingsPool,
BLSChangesPool: b.blsToExecPool,
SyncCommitteeObjectPool: b.syncCommitteePool,
ExecutionChainService: web3Service,
ExecutionChainInfoFetcher: web3Service,
ChainStartFetcher: chainStartFetcher,
MockEth1Votes: mockEth1DataVotes,
SyncService: syncService,
DepositFetcher: depositFetcher,
PendingDepositFetcher: b.depositCache,
BlockNotifier: b,
StateNotifier: b,
OperationNotifier: b,
StateGen: b.stateGen,
EnableDebugRPCEndpoints: enableDebugRPCEndpoints,
MaxMsgSize: maxMsgSize,
BlockBuilder: b.fetchBuilderService(),
Router: router,
ClockWaiter: b.ClockWaiter,
BlobStorage: b.BlobStorage,
DataColumnStorage: b.DataColumnStorage,
TrackedValidatorsCache: b.trackedValidatorsCache,
PayloadIDCache: b.payloadIDCache,
ExecutionPayloadEnvelopeCache: b.executionPayloadEnvelopeCache,
LCStore: b.lcStore,
GraffitiInfo: web3Service.GraffitiInfo(),
})
return b.services.RegisterService(rpcService)

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,31 @@
package validator
import (
"net/http"
"github.com/OffchainLabs/prysm/v7/network/httputil"
)
// ProduceBlockV4 requests a beacon node to produce a valid GLOAS block.
//
// TODO: Implement GLOAS-specific block production.
// Endpoint: GET /eth/v4/validator/blocks/{slot}
func (s *Server) ProduceBlockV4(w http.ResponseWriter, r *http.Request) {
httputil.HandleError(w, "ProduceBlockV4 not yet implemented", http.StatusNotImplemented)
}
// GetExecutionPayloadEnvelope retrieves a cached execution payload envelope.
//
// 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) {
httputil.HandleError(w, "GetExecutionPayloadEnvelope not yet implemented", http.StatusNotImplemented)
}
// PublishExecutionPayloadEnvelope broadcasts a signed execution payload envelope.
//
// TODO: Implement envelope validation and broadcast.
// Endpoint: POST /eth/v1/beacon/execution_payload_envelope
func (s *Server) PublishExecutionPayloadEnvelope(w http.ResponseWriter, r *http.Request) {
httputil.HandleError(w, "PublishExecutionPayloadEnvelope not yet implemented", http.StatusNotImplemented)
}

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,11 @@ func (vs *Server) constructGenericBeaconBlock(
return nil, fmt.Errorf("expected *BlobsBundleV2, got %T", blobsBundler)
}
return vs.constructFuluBlock(blockProto, isBlinded, bidStr, bundle), nil
case version.Gloas:
// 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)},
}, nil
default:
return nil, fmt.Errorf("unknown block version: %d", sBlk.Version())
}

View File

@@ -237,34 +237,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)
}
}
}
@@ -276,6 +292,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)
}
@@ -283,11 +308,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()
@@ -304,15 +324,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)
@@ -416,18 +479,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
}
@@ -439,6 +494,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,251 @@
package validator
import (
"context"
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"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/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"
"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,
primitives.BuilderIndex(sBlk.Block().ProposerIndex()),
parentRoot[:],
sBlk.Block().Slot(),
)
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(),
StateRoot: make([]byte, 32), // TODO: computed state root
}
vs.cacheExecutionPayloadEnvelope(envelope, local.BlobsBundler)
return nil
}
// getPayloadAttestations returns payload attestations for inclusion in a GLOAS block.
// PTC members broadcast PayloadAttestationMessages via P2P gossip during slot N.
// All nodes collect these in a pool. The slot N+1 proposer retrieves and aggregates
// them into PayloadAttestations for block inclusion.
func (vs *Server) getPayloadAttestations(ctx context.Context, head state.BeaconState, slot primitives.Slot) []*ethpb.PayloadAttestation {
// TODO: Retrieve and aggregate PayloadAttestationMessages from the pool
// for the previous slot. Blocks are valid without payload attestations.
return []*ethpb.PayloadAttestation{}
}
// createSelfBuildExecutionPayloadBid creates an ExecutionPayloadBid for self-building,
// where the proposer acts as its own builder. The value is the block value from the
// execution layer, and execution payment is zero since the proposer doesn't pay themselves.
func (vs *Server) createSelfBuildExecutionPayloadBid(
local *consensusblocks.GetPayloadResponse,
builderIndex primitives.BuilderIndex,
parentBlockRoot []byte,
slot primitives.Slot,
) (*ethpb.ExecutionPayloadBid, error) {
ed := local.ExecutionData
if ed == nil || ed.IsNil() {
return nil, errors.New("execution data is nil")
}
return &ethpb.ExecutionPayloadBid{
ParentBlockHash: ed.ParentHash(),
ParentBlockRoot: bytesutil.SafeCopyBytes(parentBlockRoot),
BlockHash: ed.BlockHash(),
PrevRandao: ed.PrevRandao(),
FeeRecipient: ed.FeeRecipient(),
GasLimit: ed.GasLimit(),
BuilderIndex: builderIndex,
Slot: slot,
Value: primitives.WeiToGwei(local.Bid),
ExecutionPayment: 0, // Self-build: proposer doesn't pay themselves.
BlobKzgCommitments: extractKzgCommitments(local.BlobsBundler),
}, 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)
}
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
}
// PublishExecutionPayloadEnvelope validates and broadcasts a signed execution payload envelope.
// This is called by validators after signing the envelope retrieved from GetExecutionPayloadEnvelope.
//
// 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")
// TODO: Validate envelope signature before broadcasting.
if err := vs.P2P.Broadcast(ctx, req); err != nil {
return nil, status.Errorf(codes.Internal, "failed to broadcast execution payload envelope: %v", err)
}
// TODO: Receive the envelope locally following the broadcastReceiveBlock pattern.
// TODO: Build and broadcast data column sidecars from the cached blobs bundle.
// In GLOAS, blob data is delivered alongside the execution payload envelope
// rather than with the beacon block (which only carries the bid). Not needed
// for devnet-0.
log.Info("Successfully published execution payload envelope")
return &emptypb.Empty{}, nil
}

View File

@@ -0,0 +1,94 @@
package validator
import (
"math/big"
"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)
// 5 Gwei = 5,000,000,000 Wei
bidValue := big.NewInt(5_000_000_000)
local := &consensusblocks.GetPayloadResponse{
ExecutionData: ed,
Bid: bidValue,
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, primitives.Gwei(5), bid.Value)
require.Equal(t, primitives.Gwei(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,46 +44,47 @@ 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
GraffitiInfo *execution.GraffitiInfo
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
GraffitiInfo *execution.GraffitiInfo
}
// 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

@@ -72,60 +72,61 @@ type Service struct {
// Config options for the beacon node RPC server.
type Config struct {
ExecutionReconstructor execution.Reconstructor
Host string
Port string
CertFlag string
KeyFlag string
BeaconMonitoringHost string
BeaconMonitoringPort int
BeaconDB db.HeadAccessDatabase
ChainInfoFetcher blockchain.ChainInfoFetcher
HeadFetcher blockchain.HeadFetcher
CanonicalFetcher blockchain.CanonicalFetcher
ForkFetcher blockchain.ForkFetcher
ForkchoiceFetcher blockchain.ForkchoiceFetcher
FinalizationFetcher blockchain.FinalizationFetcher
AttestationReceiver blockchain.AttestationReceiver
BlockReceiver blockchain.BlockReceiver
BlobReceiver blockchain.BlobReceiver
DataColumnReceiver blockchain.DataColumnReceiver
ExecutionChainService execution.Chain
ChainStartFetcher execution.ChainStartFetcher
ExecutionChainInfoFetcher execution.ChainInfoFetcher
GenesisTimeFetcher blockchain.TimeFetcher
GenesisFetcher blockchain.GenesisFetcher
MockEth1Votes bool
EnableDebugRPCEndpoints bool
AttestationCache *cache.AttestationCache
AttestationsPool attestations.Pool
ExitPool voluntaryexits.PoolManager
SlashingsPool slashings.PoolManager
SyncCommitteeObjectPool synccommittee.Pool
BLSChangesPool blstoexec.PoolManager
SyncService chainSync.Checker
Broadcaster p2p.Broadcaster
PeersFetcher p2p.PeersProvider
PeerManager p2p.PeerManager
MetadataProvider p2p.MetadataProvider
DepositFetcher cache.DepositFetcher
PendingDepositFetcher depositsnapshot.PendingDepositsFetcher
StateNotifier statefeed.Notifier
BlockNotifier blockfeed.Notifier
OperationNotifier opfeed.Notifier
StateGen *stategen.State
MaxMsgSize int
ExecutionEngineCaller execution.EngineCaller
OptimisticModeFetcher blockchain.OptimisticModeFetcher
BlockBuilder builder.BlockBuilder
Router *http.ServeMux
ClockWaiter startup.ClockWaiter
BlobStorage *filesystem.BlobStorage
DataColumnStorage *filesystem.DataColumnStorage
TrackedValidatorsCache *cache.TrackedValidatorsCache
PayloadIDCache *cache.PayloadIDCache
LCStore *lightClient.Store
GraffitiInfo *execution.GraffitiInfo
ExecutionReconstructor execution.Reconstructor
Host string
Port string
CertFlag string
KeyFlag string
BeaconMonitoringHost string
BeaconMonitoringPort int
BeaconDB db.HeadAccessDatabase
ChainInfoFetcher blockchain.ChainInfoFetcher
HeadFetcher blockchain.HeadFetcher
CanonicalFetcher blockchain.CanonicalFetcher
ForkFetcher blockchain.ForkFetcher
ForkchoiceFetcher blockchain.ForkchoiceFetcher
FinalizationFetcher blockchain.FinalizationFetcher
AttestationReceiver blockchain.AttestationReceiver
BlockReceiver blockchain.BlockReceiver
BlobReceiver blockchain.BlobReceiver
DataColumnReceiver blockchain.DataColumnReceiver
ExecutionChainService execution.Chain
ChainStartFetcher execution.ChainStartFetcher
ExecutionChainInfoFetcher execution.ChainInfoFetcher
GenesisTimeFetcher blockchain.TimeFetcher
GenesisFetcher blockchain.GenesisFetcher
MockEth1Votes bool
EnableDebugRPCEndpoints bool
AttestationCache *cache.AttestationCache
AttestationsPool attestations.Pool
ExitPool voluntaryexits.PoolManager
SlashingsPool slashings.PoolManager
SyncCommitteeObjectPool synccommittee.Pool
BLSChangesPool blstoexec.PoolManager
SyncService chainSync.Checker
Broadcaster p2p.Broadcaster
PeersFetcher p2p.PeersProvider
PeerManager p2p.PeerManager
MetadataProvider p2p.MetadataProvider
DepositFetcher cache.DepositFetcher
PendingDepositFetcher depositsnapshot.PendingDepositsFetcher
StateNotifier statefeed.Notifier
BlockNotifier blockfeed.Notifier
OperationNotifier opfeed.Notifier
StateGen *stategen.State
MaxMsgSize int
ExecutionEngineCaller execution.EngineCaller
OptimisticModeFetcher blockchain.OptimisticModeFetcher
BlockBuilder builder.BlockBuilder
Router *http.ServeMux
ClockWaiter startup.ClockWaiter
BlobStorage *filesystem.BlobStorage
DataColumnStorage *filesystem.DataColumnStorage
TrackedValidatorsCache *cache.TrackedValidatorsCache
PayloadIDCache *cache.PayloadIDCache
ExecutionPayloadEnvelopeCache *cache.ExecutionPayloadEnvelopeCache
LCStore *lightClient.Store
GraffitiInfo *execution.GraffitiInfo
}
// NewService instantiates a new RPC service instance that will
@@ -218,46 +219,47 @@ func NewService(ctx context.Context, cfg *Config) *Service {
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
}
validatorServer := &validatorv1alpha1.Server{
Ctx: s.ctx,
AttestationCache: s.cfg.AttestationCache,
AttPool: s.cfg.AttestationsPool,
ExitPool: s.cfg.ExitPool,
HeadFetcher: s.cfg.HeadFetcher,
ForkFetcher: s.cfg.ForkFetcher,
ForkchoiceFetcher: s.cfg.ForkchoiceFetcher,
GenesisFetcher: s.cfg.GenesisFetcher,
FinalizationFetcher: s.cfg.FinalizationFetcher,
TimeFetcher: s.cfg.GenesisTimeFetcher,
BlockFetcher: s.cfg.ExecutionChainService,
DepositFetcher: s.cfg.DepositFetcher,
ChainStartFetcher: s.cfg.ChainStartFetcher,
Eth1InfoFetcher: s.cfg.ExecutionChainService,
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
SyncChecker: s.cfg.SyncService,
StateNotifier: s.cfg.StateNotifier,
BlockNotifier: s.cfg.BlockNotifier,
OperationNotifier: s.cfg.OperationNotifier,
P2P: s.cfg.Broadcaster,
BlockReceiver: s.cfg.BlockReceiver,
BlobReceiver: s.cfg.BlobReceiver,
DataColumnReceiver: s.cfg.DataColumnReceiver,
MockEth1Votes: s.cfg.MockEth1Votes,
Eth1BlockFetcher: s.cfg.ExecutionChainService,
PendingDepositsFetcher: s.cfg.PendingDepositFetcher,
SlashingsPool: s.cfg.SlashingsPool,
StateGen: s.cfg.StateGen,
SyncCommitteePool: s.cfg.SyncCommitteeObjectPool,
ReplayerBuilder: ch,
ExecutionEngineCaller: s.cfg.ExecutionEngineCaller,
BeaconDB: s.cfg.BeaconDB,
BlockBuilder: s.cfg.BlockBuilder,
BLSChangesPool: s.cfg.BLSChangesPool,
ClockWaiter: s.cfg.ClockWaiter,
CoreService: coreService,
TrackedValidatorsCache: s.cfg.TrackedValidatorsCache,
PayloadIDCache: s.cfg.PayloadIDCache,
AttestationStateFetcher: s.cfg.AttestationReceiver,
GraffitiInfo: s.cfg.GraffitiInfo,
Ctx: s.ctx,
AttestationCache: s.cfg.AttestationCache,
AttPool: s.cfg.AttestationsPool,
ExitPool: s.cfg.ExitPool,
HeadFetcher: s.cfg.HeadFetcher,
ForkFetcher: s.cfg.ForkFetcher,
ForkchoiceFetcher: s.cfg.ForkchoiceFetcher,
GenesisFetcher: s.cfg.GenesisFetcher,
FinalizationFetcher: s.cfg.FinalizationFetcher,
TimeFetcher: s.cfg.GenesisTimeFetcher,
BlockFetcher: s.cfg.ExecutionChainService,
DepositFetcher: s.cfg.DepositFetcher,
ChainStartFetcher: s.cfg.ChainStartFetcher,
Eth1InfoFetcher: s.cfg.ExecutionChainService,
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
SyncChecker: s.cfg.SyncService,
StateNotifier: s.cfg.StateNotifier,
BlockNotifier: s.cfg.BlockNotifier,
OperationNotifier: s.cfg.OperationNotifier,
P2P: s.cfg.Broadcaster,
BlockReceiver: s.cfg.BlockReceiver,
BlobReceiver: s.cfg.BlobReceiver,
DataColumnReceiver: s.cfg.DataColumnReceiver,
MockEth1Votes: s.cfg.MockEth1Votes,
Eth1BlockFetcher: s.cfg.ExecutionChainService,
PendingDepositsFetcher: s.cfg.PendingDepositFetcher,
SlashingsPool: s.cfg.SlashingsPool,
StateGen: s.cfg.StateGen,
SyncCommitteePool: s.cfg.SyncCommitteeObjectPool,
ReplayerBuilder: ch,
ExecutionEngineCaller: s.cfg.ExecutionEngineCaller,
BeaconDB: s.cfg.BeaconDB,
BlockBuilder: s.cfg.BlockBuilder,
BLSChangesPool: s.cfg.BLSChangesPool,
ClockWaiter: s.cfg.ClockWaiter,
CoreService: coreService,
TrackedValidatorsCache: s.cfg.TrackedValidatorsCache,
PayloadIDCache: s.cfg.PayloadIDCache,
AttestationStateFetcher: s.cfg.AttestationReceiver,
ExecutionPayloadEnvelopeCache: s.cfg.ExecutionPayloadEnvelopeCache,
GraffitiInfo: s.cfg.GraffitiInfo,
}
s.validatorServer = validatorServer
nodeServer := &nodev1alpha1.Server{

View File

@@ -0,0 +1,4 @@
### Added
- gRPC endpoints for get block, propose block for gloas - excludes PTC attestations
- gRPC endpoints for get payload envelope, and propose payload envelope - excludes blob propogation

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

@@ -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",

View File

@@ -15,6 +15,7 @@ go_library(
"domain_data.go",
"doppelganger.go",
"duties.go",
"execution_payload_envelope.go",
"genesis.go",
"get_beacon_block.go",
"index.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,29 @@
package beacon_api
import (
"context"
"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"
)
// 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) {
return nil, errors.New("getExecutionPayloadEnvelope not yet implemented")
}
// publishExecutionPayloadEnvelope broadcasts a signed execution payload envelope.
func (c *beaconApiValidatorClient) publishExecutionPayloadEnvelope(
ctx context.Context,
envelope *ethpb.SignedExecutionPayloadEnvelope,
) (*empty.Empty, error) {
return nil, errors.New("publishExecutionPayloadEnvelope 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")
@@ -167,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")
@@ -176,6 +200,13 @@ func (v *validator) ProposeBlock(ctx context.Context, slot primitives.Slot, pubK
return
}
// 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")
}
}
span.SetAttributes(
trace.StringAttribute("blockRoot", fmt.Sprintf("%#x", blkResp.BlockRoot)),
trace.Int64Attribute("numDeposits", int64(len(blk.Block().Body().Deposits()))),

View File

@@ -0,0 +1,82 @@
package client
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
validatorpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/validator-client"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
// getExecutionPayloadEnvelope retrieves the execution payload envelope from the
// beacon node for the given block's builder index and slot.
func (v *validator) getExecutionPayloadEnvelope(
ctx context.Context,
slot primitives.Slot,
b *ethpb.GenericBeaconBlock,
) (*ethpb.ExecutionPayloadEnvelope, error) {
ctx, span := trace.StartSpan(ctx, "validator.getExecutionPayloadEnvelope")
defer span.End()
gloasBlock := b.GetGloas()
if gloasBlock == nil {
return nil, errors.New("expected GLOAS block but got nil")
}
if gloasBlock.Body == nil || gloasBlock.Body.SignedExecutionPayloadBid == nil || gloasBlock.Body.SignedExecutionPayloadBid.Message == nil {
return nil, errors.New("block missing signed execution payload bid")
}
builderIndex := gloasBlock.Body.SignedExecutionPayloadBid.Message.BuilderIndex
return v.validatorClient.ExecutionPayloadEnvelope(ctx, slot, builderIndex)
}
// signExecutionPayloadEnvelope signs the execution payload envelope using the
// builder's key. The envelope is signed with DomainBeaconBuilder since it is
// a builder artifact — even in the self-build case where the proposer acts as
// their own builder.
func (v *validator) signExecutionPayloadEnvelope(
ctx context.Context,
pubKey [fieldparams.BLSPubkeyLength]byte,
slot primitives.Slot,
envelope *ethpb.ExecutionPayloadEnvelope,
) (*ethpb.SignedExecutionPayloadEnvelope, error) {
ctx, span := trace.StartSpan(ctx, "validator.signExecutionPayloadEnvelope")
defer span.End()
epoch := slots.ToEpoch(slot)
domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainBeaconBuilder[:])
if err != nil {
return nil, errors.Wrap(err, "could not get domain data")
}
if domain == nil {
return nil, errors.New("nil domain data")
}
signingRoot, err := signing.ComputeSigningRoot(envelope, domain.SignatureDomain)
if err != nil {
return nil, errors.Wrap(err, "could not compute signing root")
}
sig, err := v.km.Sign(ctx, &validatorpb.SignRequest{
PublicKey: pubKey[:],
SigningRoot: signingRoot[:],
SignatureDomain: domain.SignatureDomain,
Object: &validatorpb.SignRequest_Slot{Slot: slot},
SigningSlot: slot,
})
if err != nil {
return nil, errors.Wrap(err, "could not sign execution payload envelope")
}
return &ethpb.SignedExecutionPayloadEnvelope{
Message: envelope,
Signature: sig.Marshal(),
}, nil
}

View File

@@ -0,0 +1,205 @@
package client
import (
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/pkg/errors"
"go.uber.org/mock/gomock"
)
func testExecutionPayloadEnvelope(slot primitives.Slot, builderIndex primitives.BuilderIndex) *ethpb.ExecutionPayloadEnvelope {
return &ethpb.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 := &ethpb.GenericBeaconBlock{
Block: &ethpb.GenericBeaconBlock_Gloas{
Gloas: &ethpb.BeaconBlockGloas{
Slot: slot,
Body: &ethpb.BeaconBlockBodyGloas{
SignedExecutionPayloadBid: &ethpb.SignedExecutionPayloadBid{
Message: &ethpb.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 := &ethpb.GenericBeaconBlock{
Block: &ethpb.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 := &ethpb.GenericBeaconBlock{
Block: &ethpb.GenericBeaconBlock_Gloas{
Gloas: &ethpb.BeaconBlockGloas{
Slot: 1,
Body: &ethpb.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 := &ethpb.GenericBeaconBlock{
Block: &ethpb.GenericBeaconBlock_Gloas{
Gloas: &ethpb.BeaconBlockGloas{
Slot: 1,
Body: &ethpb.BeaconBlockBodyGloas{
SignedExecutionPayloadBid: &ethpb.SignedExecutionPayloadBid{
Message: &ethpb.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(&ethpb.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 &ethpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil
})
envelope := testExecutionPayloadEnvelope(100, 0)
_, err := validator.signExecutionPayloadEnvelope(t.Context(), kp.pub, 100, envelope)
require.NoError(t, err)
}