mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-07 03:25:05 -05:00
Compare commits
6 Commits
gloas/fork
...
gloas-prop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bc2653bc7 | ||
|
|
fdfbe3bfa8 | ||
|
|
6d4bc0e3bd | ||
|
|
6173c290d7 | ||
|
|
c3e74e4a5d | ||
|
|
048ea521b6 |
@@ -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"`
|
||||
}
|
||||
|
||||
4
beacon-chain/cache/BUILD.bazel
vendored
4
beacon-chain/cache/BUILD.bazel
vendored
@@ -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",
|
||||
|
||||
111
beacon-chain/cache/execution_payload_envelope.go
vendored
Normal file
111
beacon-chain/cache/execution_payload_envelope.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
146
beacon-chain/cache/execution_payload_envelope_test.go
vendored
Normal file
146
beacon-chain/cache/execution_payload_envelope_test.go
vendored
Normal 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 := ðpb.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 := ðpb.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 := ðpb.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 := ðpb.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 := ðpb.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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ go_library(
|
||||
srcs = [
|
||||
"handlers.go",
|
||||
"handlers_block.go",
|
||||
"handlers_gloas.go",
|
||||
"log.go",
|
||||
"server.go",
|
||||
],
|
||||
|
||||
276
beacon-chain/rpc/eth/validator/handlers_gloas.go
Normal file
276
beacon-chain/rpc/eth/validator/handlers_gloas.go
Normal 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 := ð.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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ðpb.GenericBeaconBlock{Block: ðpb.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 ðpb.GenericBeaconBlock{
|
||||
Block: ðpb.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 ðpb.GenericBeaconBlock{Block: ðpb.GenericBeaconBlock_BlindedFulu{BlindedFulu: blockProto.(*ethpb.BlindedBeaconBlockFulu)}, IsBlinded: true, PayloadValue: payloadValue}
|
||||
|
||||
@@ -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 ðpb.ProposeResponse{BlockRoot: root[:]}, nil
|
||||
}
|
||||
return ðpb.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 ðpb.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 {
|
||||
|
||||
@@ -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(ðpb.SignedBeaconBlockGloas{Block: ðpb.BeaconBlockGloas{Body: ðpb.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(ðpb.SignedBeaconBlockFulu{Block: ðpb.BeaconBlockElectra{Body: ðpb.BeaconBlockBodyElectra{}}})
|
||||
if err != nil {
|
||||
|
||||
413
beacon-chain/rpc/prysm/v1alpha1/validator/proposer_gloas.go
Normal file
413
beacon-chain/rpc/prysm/v1alpha1/validator/proposer_gloas.go
Normal 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 := ðpb.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 := ðpb.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 ðpb.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 ðpb.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))
|
||||
}
|
||||
@@ -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(ðpb.SignedBeaconBlockGloas{
|
||||
Block: ðpb.BeaconBlockGloas{
|
||||
Slot: slot,
|
||||
ProposerIndex: proposerIndex,
|
||||
ParentRoot: parentRoot[:],
|
||||
Body: ðpb.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(ðpb.SignedBeaconBlockGloas{
|
||||
Block: ðpb.BeaconBlockGloas{
|
||||
Slot: 1,
|
||||
ParentRoot: make([]byte, 32),
|
||||
Body: ðpb.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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
2
changelog/potuz_forkchoice_unused_highestblockdelay.md
Normal file
2
changelog/potuz_forkchoice_unused_highestblockdelay.md
Normal file
@@ -0,0 +1,2 @@
|
||||
### Ignored
|
||||
- Remove unused `HighestBlockDelay` method in forkchoice.
|
||||
2416
proto/prysm/v1alpha1/validator.pb.go
generated
2416
proto/prysm/v1alpha1/validator.pb.go
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
40
testing/mock/beacon_validator_client_mock.go
generated
40
testing/mock/beacon_validator_client_mock.go
generated
@@ -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
|
||||
|
||||
30
testing/mock/beacon_validator_server_mock.go
generated
30
testing/mock/beacon_validator_server_mock.go
generated
@@ -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
|
||||
|
||||
30
testing/validator-mock/validator_client_mock.go
generated
30
testing/validator-mock/validator_client_mock.go
generated
@@ -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)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ go_library(
|
||||
"duties.go",
|
||||
"genesis.go",
|
||||
"get_beacon_block.go",
|
||||
"gloas.go",
|
||||
"index.go",
|
||||
"log.go",
|
||||
"metrics.go",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
122
validator/client/beacon-api/gloas.go
Normal file
122
validator/client/beacon-api/gloas.go
Normal 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")
|
||||
}
|
||||
@@ -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 := ðpb.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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 ðpb.SignedExecutionPayloadEnvelope{
|
||||
Message: envelope,
|
||||
Signature: sig.Marshal(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user