From 267a2e5c111279ff1677553de598933113a24139 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 14 Oct 2024 09:22:39 -0300 Subject: [PATCH] Signed execution payload header for sync (#14363) * Signed execution payload header for sync * Use RO state * SignedExecutionPayloadHeader by hash and root * Fix execution headers cache --- beacon-chain/blockchain/testing/mock.go | 5 + beacon-chain/cache/BUILD.bazel | 4 + beacon-chain/cache/signed_execution_header.go | 76 +++++ .../cache/signed_execution_header_test.go | 243 +++++++++++++++ beacon-chain/node/node.go | 3 + beacon-chain/rpc/eth/config/handlers_test.go | 4 +- beacon-chain/sync/BUILD.bazel | 2 + .../sync/execution_payload_envelope.go | 2 +- beacon-chain/sync/execution_payload_header.go | 144 +++++++++ .../sync/execution_payload_header_test.go | 289 ++++++++++++++++++ beacon-chain/sync/options.go | 7 + beacon-chain/sync/payload_attestations.go | 2 +- beacon-chain/sync/service.go | 3 + beacon-chain/sync/subscriber.go | 6 + beacon-chain/sync/validate_beacon_blocks.go | 5 + beacon-chain/verification/BUILD.bazel | 3 + beacon-chain/verification/epbs.go | 16 + .../execution_payload_envelope.go | 1 - .../verification/execution_payload_header.go | 243 +++++++++++++++ .../execution_payload_header_mock.go | 40 +++ .../execution_payload_header_test.go | 266 ++++++++++++++++ beacon-chain/verification/initializer_epbs.go | 13 + config/params/config.go | 3 + consensus-types/blocks/BUILD.bazel | 2 +- .../blocks/signed_execution_payload_header.go | 6 + .../signed_execution_payload_header.go | 1 + testing/util/BUILD.bazel | 1 + testing/util/execution_payload_header.go | 26 ++ 28 files changed, 1411 insertions(+), 5 deletions(-) create mode 100644 beacon-chain/cache/signed_execution_header.go create mode 100644 beacon-chain/cache/signed_execution_header_test.go create mode 100644 beacon-chain/sync/execution_payload_header.go create mode 100644 beacon-chain/sync/execution_payload_header_test.go create mode 100644 beacon-chain/verification/execution_payload_header.go create mode 100644 beacon-chain/verification/execution_payload_header_mock.go create mode 100644 beacon-chain/verification/execution_payload_header_test.go create mode 100644 testing/util/execution_payload_header.go diff --git a/beacon-chain/blockchain/testing/mock.go b/beacon-chain/blockchain/testing/mock.go index 825d2f31c143..2809eb03db6b 100644 --- a/beacon-chain/blockchain/testing/mock.go +++ b/beacon-chain/blockchain/testing/mock.go @@ -708,3 +708,8 @@ func (c *ChainService) ReceiveBlob(_ context.Context, b blocks.VerifiedROBlob) e func (c *ChainService) TargetRootForEpoch(_ [32]byte, _ primitives.Epoch) ([32]byte, error) { return c.TargetRoot, nil } + +// HashInForkchoice mocks the same method in the chain service +func (c *ChainService) HashInForkchoice([32]byte) bool { + return false +} diff --git a/beacon-chain/cache/BUILD.bazel b/beacon-chain/cache/BUILD.bazel index f64137c13251..1c186487a67a 100644 --- a/beacon-chain/cache/BUILD.bazel +++ b/beacon-chain/cache/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "proposer_indices_disabled.go", # keep "proposer_indices_type.go", "registration.go", + "signed_execution_header.go", "skip_slot_cache.go", "subnet_ids.go", "sync_committee.go", @@ -49,6 +50,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", "//runtime/version:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", @@ -77,6 +79,7 @@ go_test( "private_access_test.go", "proposer_indices_test.go", "registration_test.go", + "signed_execution_header_test.go", "skip_slot_cache_test.go", "subnet_ids_test.go", "sync_committee_head_state_test.go", @@ -93,6 +96,7 @@ go_test( "//consensus-types/primitives:go_default_library", "//crypto/bls:go_default_library", "//encoding/bytesutil:go_default_library", + "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", diff --git a/beacon-chain/cache/signed_execution_header.go b/beacon-chain/cache/signed_execution_header.go new file mode 100644 index 000000000000..921ab15a09c9 --- /dev/null +++ b/beacon-chain/cache/signed_execution_header.go @@ -0,0 +1,76 @@ +package cache + +import ( + "bytes" + "sync" + + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" +) + +// ExecutionPayloadHeaders is used by the sync service to store signed execution payload headers after they pass validation, +// and filter out subsequent headers with lower value. +// The signed header from this cache could be used by the proposer when proposing the next slot. +type ExecutionPayloadHeaders struct { + headers map[primitives.Slot][]*enginev1.SignedExecutionPayloadHeader + sync.RWMutex +} + +func NewExecutionPayloadHeaders() *ExecutionPayloadHeaders { + return &ExecutionPayloadHeaders{ + headers: make(map[primitives.Slot][]*enginev1.SignedExecutionPayloadHeader), + } +} + +// SaveSignedExecutionPayloadHeader saves the signed execution payload header to the cache. +// The cache stores headers for up to two slots. If the input slot is higher than the lowest slot +// currently in the cache, the lowest slot is removed to make space for the new header. +// Only the highest value header for a given parent block hash will be stored. +// This function assumes caller already checks header's slot is current or next slot, it doesn't account for slot validation. +func (c *ExecutionPayloadHeaders) SaveSignedExecutionPayloadHeader(header *enginev1.SignedExecutionPayloadHeader) { + c.Lock() + defer c.Unlock() + + for s := range c.headers { + if s+1 < header.Message.Slot { + delete(c.headers, s) + } + } + + // Add or update the header in the map + if _, ok := c.headers[header.Message.Slot]; !ok { + c.headers[header.Message.Slot] = []*enginev1.SignedExecutionPayloadHeader{header} + } else { + found := false + for i, h := range c.headers[header.Message.Slot] { + if bytes.Equal(h.Message.ParentBlockHash, header.Message.ParentBlockHash) && bytes.Equal(h.Message.ParentBlockRoot, header.Message.ParentBlockRoot) { + if header.Message.Value > h.Message.Value { + c.headers[header.Message.Slot][i] = header + } + found = true + break + } + } + if !found { + c.headers[header.Message.Slot] = append(c.headers[header.Message.Slot], header) + } + } +} + +// SignedExecutionPayloadHeader returns the signed payload header for the given slot and parent block hash and block root. +// Returns nil if the header is not found. +// This should be used when the caller wants the header to match parent block hash and parent block root such as proposer choosing a header to propose. +func (c *ExecutionPayloadHeaders) SignedExecutionPayloadHeader(slot primitives.Slot, parentBlockHash []byte, parentBlockRoot []byte) *enginev1.SignedExecutionPayloadHeader { + c.RLock() + defer c.RUnlock() + + if headers, ok := c.headers[slot]; ok { + for _, header := range headers { + if bytes.Equal(header.Message.ParentBlockHash, parentBlockHash) && bytes.Equal(header.Message.ParentBlockRoot, parentBlockRoot) { + return header + } + } + } + + return nil +} diff --git a/beacon-chain/cache/signed_execution_header_test.go b/beacon-chain/cache/signed_execution_header_test.go new file mode 100644 index 000000000000..9a65eba7bcb2 --- /dev/null +++ b/beacon-chain/cache/signed_execution_header_test.go @@ -0,0 +1,243 @@ +package cache + +import ( + "testing" + + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + "github.com/prysmaticlabs/prysm/v5/testing/require" +) + +func Test_SaveSignedExecutionPayloadHeader(t *testing.T) { + t.Run("First header should be added to cache", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + Value: 100, + }, + } + c.SaveSignedExecutionPayloadHeader(header) + require.Equal(t, 1, len(c.headers)) + require.Equal(t, header, c.headers[1][0]) + }) + + t.Run("Second header with higher slot should be added, and both slots should be in cache", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header1 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + Value: 100, + }, + } + header2 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 100, + }, + } + c.SaveSignedExecutionPayloadHeader(header1) + c.SaveSignedExecutionPayloadHeader(header2) + require.Equal(t, 2, len(c.headers)) + require.Equal(t, header1, c.headers[1][0]) + require.Equal(t, header2, c.headers[2][0]) + }) + + t.Run("Third header with higher slot should replace the oldest slot", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header1 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + Value: 100, + }, + } + header2 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 100, + }, + } + header3 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 3, + ParentBlockHash: []byte("parent3"), + Value: 100, + }, + } + c.SaveSignedExecutionPayloadHeader(header1) + c.SaveSignedExecutionPayloadHeader(header2) + c.SaveSignedExecutionPayloadHeader(header3) + require.Equal(t, 2, len(c.headers)) + require.Equal(t, header2, c.headers[2][0]) + require.Equal(t, header3, c.headers[3][0]) + }) + + t.Run("Header with same slot but higher value should replace the existing one", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header1 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 100, + }, + } + header2 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 200, + }, + } + c.SaveSignedExecutionPayloadHeader(header1) + c.SaveSignedExecutionPayloadHeader(header2) + require.Equal(t, 1, len(c.headers[2])) + require.Equal(t, header2, c.headers[2][0]) + }) + + t.Run("Header with different parent block hash should be appended to the same slot", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header1 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent1"), + Value: 100, + }, + } + header2 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 200, + }, + } + c.SaveSignedExecutionPayloadHeader(header1) + c.SaveSignedExecutionPayloadHeader(header2) + require.Equal(t, 2, len(c.headers[2])) + require.Equal(t, header1, c.headers[2][0]) + require.Equal(t, header2, c.headers[2][1]) + }) +} + +func TestSignedExecutionPayloadHeader(t *testing.T) { + t.Run("Return header when slot and parentBlockHash match", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + ParentBlockRoot: []byte("root1"), + Value: 100, + }, + } + c.SaveSignedExecutionPayloadHeader(header) + result := c.SignedExecutionPayloadHeader(1, []byte("parent1"), []byte("root1")) + require.NotNil(t, result) + require.Equal(t, header, result) + }) + + t.Run("Return nil when no matching slot and parentBlockHash", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + ParentBlockRoot: []byte("root1"), + Value: 100, + }, + } + c.SaveSignedExecutionPayloadHeader(header) + result := c.SignedExecutionPayloadHeader(2, []byte("parent2"), []byte("root1")) + require.IsNil(t, result) + }) + + t.Run("Return nil when no matching slot and parentBlockRoot", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + ParentBlockRoot: []byte("root1"), + Value: 100, + }, + } + c.SaveSignedExecutionPayloadHeader(header) + result := c.SignedExecutionPayloadHeader(2, []byte("parent1"), []byte("root2")) + require.IsNil(t, result) + }) + + t.Run("Return header when there are two slots in the cache and a match is found", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header1 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + Value: 100, + }, + } + header2 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 200, + }, + } + c.SaveSignedExecutionPayloadHeader(header1) + c.SaveSignedExecutionPayloadHeader(header2) + + // Check for the first header + result1 := c.SignedExecutionPayloadHeader(1, []byte("parent1"), []byte{}) + require.NotNil(t, result1) + require.Equal(t, header1, result1) + + // Check for the second header + result2 := c.SignedExecutionPayloadHeader(2, []byte("parent2"), []byte{}) + require.NotNil(t, result2) + require.Equal(t, header2, result2) + }) + + t.Run("Return nil when slot is evicted from cache", func(t *testing.T) { + c := NewExecutionPayloadHeaders() + header1 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + ParentBlockHash: []byte("parent1"), + Value: 100, + }, + } + header2 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + ParentBlockHash: []byte("parent2"), + Value: 200, + }, + } + header3 := &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 3, + ParentBlockHash: []byte("parent3"), + Value: 300, + }, + } + c.SaveSignedExecutionPayloadHeader(header1) + c.SaveSignedExecutionPayloadHeader(header2) + c.SaveSignedExecutionPayloadHeader(header3) + + // The first slot should be evicted, so result should be nil + result := c.SignedExecutionPayloadHeader(1, []byte("parent1"), []byte{}) + require.IsNil(t, result) + + // The second slot should still be present + result = c.SignedExecutionPayloadHeader(2, []byte("parent2"), []byte{}) + require.NotNil(t, result) + require.Equal(t, header2, result) + + // The third slot should be present + result = c.SignedExecutionPayloadHeader(3, []byte("parent3"), []byte{}) + require.NotNil(t, result) + require.Equal(t, header3, result) + }) +} diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index 80cb543f9bb3..f638632d8d2f 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -103,6 +103,7 @@ type BeaconNode struct { trackedValidatorsCache *cache.TrackedValidatorsCache payloadAttestationCache *cache.PayloadAttestationCache payloadEnvelopeCache *sync.Map + executionHeaderCache *cache.ExecutionPayloadHeaders payloadIDCache *cache.PayloadIDCache stateFeed *event.Feed blockFeed *event.Feed @@ -155,6 +156,7 @@ func New(cliCtx *cli.Context, cancel context.CancelFunc, opts ...Option) (*Beaco trackedValidatorsCache: cache.NewTrackedValidatorsCache(), payloadAttestationCache: &cache.PayloadAttestationCache{}, payloadEnvelopeCache: &sync.Map{}, + executionHeaderCache: cache.NewExecutionPayloadHeaders(), payloadIDCache: cache.NewPayloadIDCache(), slasherBlockHeadersFeed: new(event.Feed), slasherAttestationsFeed: new(event.Feed), @@ -846,6 +848,7 @@ func (b *BeaconNode) registerSyncService(initialSyncComplete chan struct{}, bFil regularsync.WithSlasherAttestationsFeed(b.slasherAttestationsFeed), regularsync.WithSlasherBlockHeadersFeed(b.slasherBlockHeadersFeed), regularsync.WithPayloadAttestationCache(b.payloadAttestationCache), + regularsync.WithExecutionPayloadHeaderCache(b.executionHeaderCache), regularsync.WithPayloadEnvelopeCache(b.payloadEnvelopeCache), regularsync.WithPayloadReconstructor(web3Service), regularsync.WithClockWaiter(b.clockWaiter), diff --git a/beacon-chain/rpc/eth/config/handlers_test.go b/beacon-chain/rpc/eth/config/handlers_test.go index 19dd568b3ad2..ecd409953770 100644 --- a/beacon-chain/rpc/eth/config/handlers_test.go +++ b/beacon-chain/rpc/eth/config/handlers_test.go @@ -193,7 +193,7 @@ func TestGetSpec(t *testing.T) { data, ok := resp.Data.(map[string]interface{}) require.Equal(t, true, ok) - assert.Equal(t, 157, len(data)) + assert.Equal(t, 158, len(data)) for k, v := range data { t.Run(k, func(t *testing.T) { switch k { @@ -530,6 +530,8 @@ func TestGetSpec(t *testing.T) { assert.Equal(t, "93", v) case "MAX_PENDING_DEPOSITS_PER_EPOCH": assert.Equal(t, "94", v) + case "MIN_BUILDER_BALANCE": + assert.Equal(t, "0", v) default: for _, pf := range placeholderFields { if k == pf { diff --git a/beacon-chain/sync/BUILD.bazel b/beacon-chain/sync/BUILD.bazel index d51842c24c25..848518a6c099 100644 --- a/beacon-chain/sync/BUILD.bazel +++ b/beacon-chain/sync/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "doc.go", "error.go", "execution_payload_envelope.go", + "execution_payload_header.go", "fork_watcher.go", "fuzz_exports.go", # keep "log.go", @@ -160,6 +161,7 @@ go_test( "decode_pubsub_test.go", "error_test.go", "execution_payload_envelope_test.go", + "execution_payload_header_test.go", "fork_watcher_test.go", "payload_attestations_test.go", "pending_attestations_queue_test.go", diff --git a/beacon-chain/sync/execution_payload_envelope.go b/beacon-chain/sync/execution_payload_envelope.go index 11fcf83a35d1..40c89f4a55c4 100644 --- a/beacon-chain/sync/execution_payload_envelope.go +++ b/beacon-chain/sync/execution_payload_envelope.go @@ -9,8 +9,8 @@ import ( "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" v1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" - "go.opencensus.io/trace" "google.golang.org/protobuf/proto" ) diff --git a/beacon-chain/sync/execution_payload_header.go b/beacon-chain/sync/execution_payload_header.go new file mode 100644 index 000000000000..19583ec9aa69 --- /dev/null +++ b/beacon-chain/sync/execution_payload_header.go @@ -0,0 +1,144 @@ +package sync + +import ( + "context" + "fmt" + "sync" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/verification" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" + v1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + "google.golang.org/protobuf/proto" +) + +func (s *Service) validateExecutionPayloadHeader(ctx context.Context, pid peer.ID, msg *pubsub.Message) (pubsub.ValidationResult, error) { + if pid == s.cfg.p2p.PeerID() { + return pubsub.ValidationAccept, nil + } + + if s.cfg.initialSync.Syncing() { + return pubsub.ValidationIgnore, nil + } + + ctx, span := trace.StartSpan(ctx, "sync.validateExecutionPayloadHeader") + defer span.End() + + if msg.Topic == nil { + return pubsub.ValidationReject, errInvalidTopic + } + + m, err := s.decodePubsubMessage(msg) + if err != nil { + tracing.AnnotateError(span, err) + return pubsub.ValidationReject, err + } + + signedHeader, ok := m.(*v1.SignedExecutionPayloadHeader) + if !ok { + return pubsub.ValidationReject, errWrongMessage + } + shm := signedHeader.Message + slot := shm.Slot + builderIndex := shm.BuilderIndex + + if seenBuilderBySlot(slot, builderIndex) { + return pubsub.ValidationIgnore, fmt.Errorf("builder %d has already been seen in slot %d", builderIndex, slot) + } + + highestValueHeader := s.executionPayloadHeaderCache.SignedExecutionPayloadHeader(slot, shm.ParentBlockHash, shm.ParentBlockRoot) + if highestValueHeader != nil && highestValueHeader.Message.Value >= shm.Value { + return pubsub.ValidationIgnore, fmt.Errorf("received header has lower value than cached header") + } + + h, err := blocks.WrappedROSignedExecutionPayloadHeader(signedHeader) + if err != nil { + log.WithError(err).Error("failed to create read only signed execution payload header") + return pubsub.ValidationIgnore, err + } + + roState, err := s.cfg.chain.HeadStateReadOnly(ctx) + if err != nil { + log.WithError(err).Error("failed to get head state to validate execution payload header") + return pubsub.ValidationIgnore, err + } + v := s.newExecutionPayloadHeaderVerifier(h, roState, verification.GossipExecutionPayloadHeaderRequirements) + + if err := v.VerifyCurrentOrNextSlot(); err != nil { + return pubsub.ValidationIgnore, err + } + + if err := v.VerifyParentBlockRootSeen(s.seenBlockRoot); err != nil { + return pubsub.ValidationIgnore, err + } + + if err := v.VerifyParentBlockHashSeen(s.seenBlockHash); err != nil { + return pubsub.ValidationIgnore, err + } + + if err := v.VerifySignature(); err != nil { + return pubsub.ValidationReject, err + } + addBuilderBySlot(slot, builderIndex) + + if err := v.VerifyBuilderActiveNotSlashed(); err != nil { + return pubsub.ValidationReject, err + } + + if err := v.VerifyBuilderSufficientBalance(); err != nil { + return pubsub.ValidationReject, err + } + + return pubsub.ValidationAccept, nil +} + +func (s *Service) subscribeExecutionPayloadHeader(ctx context.Context, msg proto.Message) error { + e, ok := msg.(*v1.SignedExecutionPayloadHeader) + if !ok { + return errWrongMessage + } + + s.executionPayloadHeaderCache.SaveSignedExecutionPayloadHeader(e) + + return nil +} + +var ( + // builderBySlot is a map of slots to a set of builder that have been seen in that slot. + builderBySlot = make(map[primitives.Slot]map[primitives.ValidatorIndex]struct{}) + builderBySlotLock sync.RWMutex +) + +func addBuilderBySlot(slot primitives.Slot, index primitives.ValidatorIndex) { + builderBySlotLock.Lock() + defer builderBySlotLock.Unlock() + + // Remove old slots: p2p allows current and next slot, so we allow two slots to be seen + for k := range builderBySlot { + if k+1 < slot { + delete(builderBySlot, k) + } + } + + if _, ok := builderBySlot[slot]; !ok { + builderBySlot[slot] = make(map[primitives.ValidatorIndex]struct{}) + } + + builderBySlot[slot][index] = struct{}{} +} + +func seenBuilderBySlot(slot primitives.Slot, index primitives.ValidatorIndex) bool { + builderBySlotLock.RLock() + defer builderBySlotLock.RUnlock() + + if _, ok := builderBySlot[slot]; !ok { + return false + } + + _, ok := builderBySlot[slot][index] + return ok +} diff --git a/beacon-chain/sync/execution_payload_header_test.go b/beacon-chain/sync/execution_payload_header_test.go new file mode 100644 index 000000000000..cf2975824e2c --- /dev/null +++ b/beacon-chain/sync/execution_payload_header_test.go @@ -0,0 +1,289 @@ +package sync + +import ( + "bytes" + "context" + "fmt" + "reflect" + "testing" + "time" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + pb "github.com/libp2p/go-libp2p-pubsub/pb" + "github.com/pkg/errors" + mock "github.com/prysmaticlabs/prysm/v5/beacon-chain/blockchain/testing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/cache" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p" + p2ptest "github.com/prysmaticlabs/prysm/v5/beacon-chain/p2p/testing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + mockSync "github.com/prysmaticlabs/prysm/v5/beacon-chain/sync/initial-sync/testing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/verification" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util/random" +) + +func TestValidateExecutionPayloadHeader_IncorrectTopic(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + chainService := &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0)} + s := &Service{ + cfg: &config{chain: chainService, p2p: p, initialSync: &mockSync.Sync{}, clock: startup.NewClock(chainService.Genesis, chainService.ValidatorsRoot)}} + + msg := random.ExecutionPayloadHeader(t) + buf := new(bytes.Buffer) + _, err := p.Encoding().EncodeGossip(buf, msg) + require.NoError(t, err) + + topic := p2p.GossipTypeMapping[reflect.TypeOf(msg)] + digest, err := s.currentForkDigest() + require.NoError(t, err) + topic = s.addDigestToTopic(topic, digest) + + result, err := s.validateExecutionPayloadHeader(ctx, "", &pubsub.Message{ + Message: &pb.Message{ + Data: buf.Bytes(), + Topic: &topic, + }}) + require.ErrorContains(t, "extraction failed for topic", err) + require.Equal(t, result, pubsub.ValidationReject) +} + +func TestValidateExecutionPayloadHeader_MockErrorPath(t *testing.T) { + tests := []struct { + error error + verifier verification.NewExecutionPayloadHeaderVerifier + result pubsub.ValidationResult + }{ + { + error: errors.New("incorrect slot"), + verifier: func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{ErrIncorrectSlot: errors.New("incorrect slot")} + }, + result: pubsub.ValidationIgnore, + }, + { + error: errors.New("unknown block root"), + verifier: func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{ErrUnknownParentBlockRoot: errors.New("unknown block root")} + }, + result: pubsub.ValidationIgnore, + }, + { + error: errors.New("unknown block hash"), + verifier: func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{ErrUnknownParentBlockHash: errors.New("unknown block hash")} + }, + result: pubsub.ValidationIgnore, + }, + { + error: errors.New("invalid signature"), + verifier: func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{ErrInvalidSignature: errors.New("invalid signature")} + }, + result: pubsub.ValidationReject, + }, + { + error: errors.New("builder slashed"), + verifier: func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{ErrBuilderSlashed: errors.New("builder slashed")} + }, + result: pubsub.ValidationReject, + }, + { + error: errors.New("insufficient balance"), + verifier: func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{ErrBuilderInsufficientBalance: errors.New("insufficient balance")} + }, + result: pubsub.ValidationReject, + }, + } + for _, tt := range tests { + t.Run(tt.error.Error(), func(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + chainService := &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0)} + s := &Service{ + executionPayloadHeaderCache: cache.NewExecutionPayloadHeaders(), + cfg: &config{chain: chainService, p2p: p, initialSync: &mockSync.Sync{}, clock: startup.NewClock(chainService.Genesis, chainService.ValidatorsRoot)}} + s.newExecutionPayloadHeaderVerifier = tt.verifier + + msg := random.SignedExecutionPayloadHeader(t) + buf := new(bytes.Buffer) + _, err := p.Encoding().EncodeGossip(buf, msg) + require.NoError(t, err) + + topic := p2p.GossipTypeMapping[reflect.TypeOf(msg)] + digest, err := s.currentForkDigest() + require.NoError(t, err) + topic = s.addDigestToTopic(topic, digest) + + result, err := s.validateExecutionPayloadHeader(ctx, "", &pubsub.Message{ + Message: &pb.Message{ + Data: buf.Bytes(), + Topic: &topic, + }}) + + require.ErrorContains(t, tt.error.Error(), err) + require.Equal(t, result, tt.result) + }) + } +} + +func TestValidateExecutionPayloadHeader_Accept(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + chainService := &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0)} + s := &Service{ + executionPayloadHeaderCache: cache.NewExecutionPayloadHeaders(), + cfg: &config{chain: chainService, p2p: p, initialSync: &mockSync.Sync{}, clock: startup.NewClock(chainService.Genesis, chainService.ValidatorsRoot)}} + s.newExecutionPayloadHeaderVerifier = func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{} + } + + msg := random.SignedExecutionPayloadHeader(t) + buf := new(bytes.Buffer) + _, err := p.Encoding().EncodeGossip(buf, msg) + require.NoError(t, err) + + topic := p2p.GossipTypeMapping[reflect.TypeOf(msg)] + digest, err := s.currentForkDigest() + require.NoError(t, err) + topic = s.addDigestToTopic(topic, digest) + + result, err := s.validateExecutionPayloadHeader(ctx, "", &pubsub.Message{ + Message: &pb.Message{ + Data: buf.Bytes(), + Topic: &topic, + }}) + require.NoError(t, err) + require.Equal(t, result, pubsub.ValidationAccept) +} + +func TestValidateExecutionPayloadHeader_MoreThanOneSameBuilder(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + chainService := &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0)} + s := &Service{ + executionPayloadHeaderCache: cache.NewExecutionPayloadHeaders(), + cfg: &config{chain: chainService, p2p: p, initialSync: &mockSync.Sync{}, clock: startup.NewClock(chainService.Genesis, chainService.ValidatorsRoot)}} + s.newExecutionPayloadHeaderVerifier = func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{} + } + + msg := random.SignedExecutionPayloadHeader(t) + buf := new(bytes.Buffer) + _, err := p.Encoding().EncodeGossip(buf, msg) + require.NoError(t, err) + + topic := p2p.GossipTypeMapping[reflect.TypeOf(msg)] + digest, err := s.currentForkDigest() + require.NoError(t, err) + topic = s.addDigestToTopic(topic, digest) + + result, err := s.validateExecutionPayloadHeader(ctx, "", &pubsub.Message{ + Message: &pb.Message{ + Data: buf.Bytes(), + Topic: &topic, + }}) + require.NoError(t, err) + require.Equal(t, result, pubsub.ValidationAccept) + + result, err = s.validateExecutionPayloadHeader(ctx, "", &pubsub.Message{ + Message: &pb.Message{ + Data: buf.Bytes(), + Topic: &topic, + }}) + require.ErrorContains(t, fmt.Sprintf("builder %d has already been seen in slot %d", msg.Message.BuilderIndex, msg.Message.Slot), err) + require.Equal(t, result, pubsub.ValidationIgnore) +} + +func TestValidateExecutionPayloadHeader_LowerValue(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + chainService := &mock.ChainService{Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0)} + s := &Service{ + executionPayloadHeaderCache: cache.NewExecutionPayloadHeaders(), + cfg: &config{chain: chainService, p2p: p, initialSync: &mockSync.Sync{}, clock: startup.NewClock(chainService.Genesis, chainService.ValidatorsRoot)}} + s.newExecutionPayloadHeaderVerifier = func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []verification.Requirement) verification.ExecutionPayloadHeaderVerifier { + return &verification.MockExecutionPayloadHeader{} + } + + msg := random.SignedExecutionPayloadHeader(t) + buf := new(bytes.Buffer) + _, err := p.Encoding().EncodeGossip(buf, msg) + require.NoError(t, err) + + topic := p2p.GossipTypeMapping[reflect.TypeOf(msg)] + digest, err := s.currentForkDigest() + require.NoError(t, err) + topic = s.addDigestToTopic(topic, digest) + + m := &pubsub.Message{ + Message: &pb.Message{ + Data: buf.Bytes(), + Topic: &topic, + }} + result, err := s.validateExecutionPayloadHeader(ctx, "", m) + require.NoError(t, err) + require.Equal(t, result, pubsub.ValidationAccept) + + require.NoError(t, s.subscribeExecutionPayloadHeader(ctx, msg)) + + // Different builder but lower value should fail + newMsg := eth.CopySignedExecutionPayloadHeader(msg) + newMsg.Message.BuilderIndex = newMsg.Message.BuilderIndex - 1 + newMsg.Message.Value = newMsg.Message.Value - 1 + newBuf := new(bytes.Buffer) + _, err = p.Encoding().EncodeGossip(newBuf, newMsg) + require.NoError(t, err) + + result, err = s.validateExecutionPayloadHeader(ctx, "", &pubsub.Message{ + Message: &pb.Message{ + Data: newBuf.Bytes(), + Topic: &topic, + }}) + require.ErrorContains(t, "received header has lower value than cached header", err) + require.Equal(t, result, pubsub.ValidationIgnore) +} + +func TestAddAndSeenBuilderBySlot(t *testing.T) { + resetBuilderBySlot() + + // Add builder to slot 1 + addBuilderBySlot(1, 100) + require.Equal(t, true, seenBuilderBySlot(1, 100), "Builder 100 should be seen in slot 1") + require.Equal(t, false, seenBuilderBySlot(1, 101), "Builder 101 should not be seen in slot 1") + + // Add builder to slot 2 + addBuilderBySlot(2, 200) + require.Equal(t, true, seenBuilderBySlot(2, 200), "Builder 200 should be seen in slot 2") + + // Slot 3 should not have any builders yet + require.Equal(t, false, seenBuilderBySlot(3, 300), "Builder 300 should not be seen in slot 3") + + // Add builder to slot 3 + addBuilderBySlot(3, 300) + require.Equal(t, true, seenBuilderBySlot(3, 300), "Builder 300 should be seen in slot 3") + + // Now slot 1 should be removed (assuming the current slot is 3) + require.Equal(t, false, seenBuilderBySlot(1, 100), "Builder 100 should no longer be seen in slot 1") + + // Slot 2 should still be valid + require.Equal(t, true, seenBuilderBySlot(2, 200), "Builder 200 should still be seen in slot 2") + + // Add builder to slot 4, slot 2 should now be removed + addBuilderBySlot(4, 400) + require.Equal(t, true, seenBuilderBySlot(4, 400), "Builder 400 should be seen in slot 4") + require.Equal(t, false, seenBuilderBySlot(2, 200), "Builder 200 should no longer be seen in slot 2") +} + +func resetBuilderBySlot() { + builderBySlotLock.Lock() + defer builderBySlotLock.Unlock() + builderBySlot = make(map[primitives.Slot]map[primitives.ValidatorIndex]struct{}) +} diff --git a/beacon-chain/sync/options.go b/beacon-chain/sync/options.go index 40693110a6ee..b87e4dad6bdd 100644 --- a/beacon-chain/sync/options.go +++ b/beacon-chain/sync/options.go @@ -137,6 +137,13 @@ func WithPayloadAttestationCache(r *cache.PayloadAttestationCache) Option { } } +func WithExecutionPayloadHeaderCache(r *cache.ExecutionPayloadHeaders) Option { + return func(s *Service) error { + s.executionPayloadHeaderCache = r + return nil + } +} + func WithPayloadEnvelopeCache(r *sync.Map) Option { return func(s *Service) error { s.payloadEnvelopeCache = r diff --git a/beacon-chain/sync/payload_attestations.go b/beacon-chain/sync/payload_attestations.go index cfb121b12e1f..b3bff26bc09d 100644 --- a/beacon-chain/sync/payload_attestations.go +++ b/beacon-chain/sync/payload_attestations.go @@ -12,8 +12,8 @@ import ( payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/monitoring/tracing" + "github.com/prysmaticlabs/prysm/v5/monitoring/tracing/trace" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" - "go.opencensus.io/trace" "google.golang.org/protobuf/proto" ) diff --git a/beacon-chain/sync/service.go b/beacon-chain/sync/service.go index 699a8e08d5ef..3001c8bc38d5 100644 --- a/beacon-chain/sync/service.go +++ b/beacon-chain/sync/service.go @@ -114,6 +114,7 @@ type blockchainService interface { blockchain.OptimisticModeFetcher blockchain.SlashingReceiver blockchain.ForkchoiceFetcher + blockchain.ExecutionPayloadFetcher } // Service is responsible for handling all run time p2p related operations as the @@ -127,6 +128,7 @@ type Service struct { blkRootToPendingAtts map[[32]byte][]ethpb.SignedAggregateAttAndProof subHandler *subTopicHandler payloadAttestationCache *cache.PayloadAttestationCache + executionPayloadHeaderCache *cache.ExecutionPayloadHeaders payloadEnvelopeCache *sync.Map pendingAttsLock sync.RWMutex pendingQueueLock sync.RWMutex @@ -162,6 +164,7 @@ type Service struct { newBlobVerifier verification.NewBlobVerifier newPayloadAttestationVerifier verification.NewPayloadAttestationMsgVerifier newExecutionPayloadEnvelopeVerifier verification.NewExecutionPayloadEnvelopeVerifier + newExecutionPayloadHeaderVerifier verification.NewExecutionPayloadHeaderVerifier availableBlocker coverage.AvailableBlocker ctxMap ContextByteVersions } diff --git a/beacon-chain/sync/subscriber.go b/beacon-chain/sync/subscriber.go index 5fd9f43dd342..0000ba3a72a1 100644 --- a/beacon-chain/sync/subscriber.go +++ b/beacon-chain/sync/subscriber.go @@ -160,6 +160,12 @@ func (s *Service) registerSubscribers(epoch primitives.Epoch, digest [4]byte) { s.executionPayloadEnvelopeSubscriber, digest, ) + s.subscribe( + p2p.SignedExecutionPayloadHeaderTopicFormat, + s.validateExecutionPayloadHeader, + s.subscribeExecutionPayloadHeader, + digest, + ) } } diff --git a/beacon-chain/sync/validate_beacon_blocks.go b/beacon-chain/sync/validate_beacon_blocks.go index 4a5e854a8167..d930703b3b6c 100644 --- a/beacon-chain/sync/validate_beacon_blocks.go +++ b/beacon-chain/sync/validate_beacon_blocks.go @@ -414,6 +414,11 @@ func (s *Service) seenBlockRoot(root [32]byte) bool { return s.cfg.chain.InForkchoice(root) } +// seenBlockHash returns true if the block hash is seen in the fork choice store. +func (s *Service) seenBlockHash(hash [32]byte) bool { + return s.cfg.chain.HashInForkchoice(hash) +} + // Set bad block in the cache. func (s *Service) setBadBlock(ctx context.Context, root [32]byte) { s.badBlockLock.Lock() diff --git a/beacon-chain/verification/BUILD.bazel b/beacon-chain/verification/BUILD.bazel index fb22a3363fb3..e3e9dd78a1e3 100644 --- a/beacon-chain/verification/BUILD.bazel +++ b/beacon-chain/verification/BUILD.bazel @@ -10,6 +10,8 @@ go_library( "error.go", "execution_payload_envelope.go", "execution_payload_envelope_mock.go", + "execution_payload_header.go", + "execution_payload_header_mock.go", "fake.go", "initializer.go", "initializer_epbs.go", @@ -58,6 +60,7 @@ go_test( "blob_test.go", "cache_test.go", "execution_payload_envelope_test.go", + "execution_payload_header_test.go", "initializer_test.go", "payload_attestation_test.go", "result_test.go", diff --git a/beacon-chain/verification/epbs.go b/beacon-chain/verification/epbs.go index e2b869d4078b..71ad88f752ce 100644 --- a/beacon-chain/verification/epbs.go +++ b/beacon-chain/verification/epbs.go @@ -41,3 +41,19 @@ type ExecutionPayloadEnvelopeVerifier interface { // NewExecutionPayloadEnvelopeVerifier is a function signature that can be used by code that needs to be // able to mock Initializer.NewExecutionPayloadEnvelopeVerifier without complex setup. type NewExecutionPayloadEnvelopeVerifier func(e interfaces.ROSignedExecutionPayloadEnvelope, reqs []Requirement) ExecutionPayloadEnvelopeVerifier + +// ExecutionPayloadHeaderVerifier defines the methods implemented by the ROSignedExecutionPayloadHeader. +// It is similar to BlobVerifier, but for signed execution payload header. +type ExecutionPayloadHeaderVerifier interface { + VerifyBuilderActiveNotSlashed() error + VerifyBuilderSufficientBalance() error + VerifyParentBlockHashSeen(func([32]byte) bool) error + VerifyParentBlockRootSeen(seen func([32]byte) bool) (err error) + VerifyCurrentOrNextSlot() error + VerifySignature() error + SatisfyRequirement(Requirement) +} + +// NewExecutionPayloadHeaderVerifier is a function signature that can be used by code that needs to be +// able to mock Initializer.NewExecutionPayloadHeaderVerifier without complex setup. +type NewExecutionPayloadHeaderVerifier func(e interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []Requirement) ExecutionPayloadHeaderVerifier diff --git a/beacon-chain/verification/execution_payload_envelope.go b/beacon-chain/verification/execution_payload_envelope.go index deea713dabbd..7721d90af3af 100644 --- a/beacon-chain/verification/execution_payload_envelope.go +++ b/beacon-chain/verification/execution_payload_envelope.go @@ -37,7 +37,6 @@ var ( ErrEnvelopeBlockRootInvalid = errors.New("block root invalid") ErrIncorrectEnvelopeBuilder = errors.New("builder index does not match committed header") ErrIncorrectEnvelopeBlockHash = errors.New("block hash does not match committed header") - ErrInvalidEnvelope = errors.New("invalid payload attestation message") ) var _ ExecutionPayloadEnvelopeVerifier = &EnvelopeVerifier{} diff --git a/beacon-chain/verification/execution_payload_header.go b/beacon-chain/verification/execution_payload_header.go new file mode 100644 index 000000000000..99a84e8989ca --- /dev/null +++ b/beacon-chain/verification/execution_payload_header.go @@ -0,0 +1,243 @@ +package verification + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" + "github.com/prysmaticlabs/prysm/v5/network/forks" + "github.com/prysmaticlabs/prysm/v5/time/slots" + log "github.com/sirupsen/logrus" +) + +const ( + RequireBuilderActiveNotSlashed Requirement = iota + RequireBuilderSufficientBalance + RequireKnownParentBlockHash + RequireKnownParentBlockRoot + RequireCurrentOrNextSlot +) + +// ExecutionPayloadHeaderGossipRequirements defines the list of requirements for gossip +// signed execution payload header. +var ExecutionPayloadHeaderGossipRequirements = []Requirement{ + RequireBuilderActiveNotSlashed, + RequireBuilderSufficientBalance, + RequireKnownParentBlockHash, + RequireBlockRootSeen, + RequireCurrentOrNextSlot, + RequireSignatureValid, +} + +// GossipExecutionPayloadHeaderRequirements is a requirement list for gossip execution payload header messages. +var GossipExecutionPayloadHeaderRequirements = RequirementList(PayloadAttGossipRequirements) + +var ( + ErrBuilderSlashed = errors.New("builder is slashed") + ErrBuilderInactive = errors.New("builder is inactive") + ErrBuilderInsufficientBalance = errors.New("insufficient builder balance") + ErrUnknownParentBlockHash = errors.New("unknown parent block hash") + ErrUnknownParentBlockRoot = errors.New("unknown parent block root") + ErrIncorrectPayloadHeaderSlot = errors.New("incorrect payload header slot") +) + +// HeaderVerifier is a verifier for execution payload headers. +type HeaderVerifier struct { + *sharedResources + results *results + h interfaces.ROSignedExecutionPayloadHeader + st state.ReadOnlyBeaconState +} + +var _ ExecutionPayloadHeaderVerifier = &HeaderVerifier{} + +// VerifyBuilderActiveNotSlashed verifies that the builder is active and not slashed. +func (v *HeaderVerifier) VerifyBuilderActiveNotSlashed() (err error) { + defer v.record(RequireBuilderActiveNotSlashed, &err) + + h, err := v.h.Header() + if err != nil { + return err + } + val, err := v.st.ValidatorAtIndexReadOnly(h.BuilderIndex()) + if err != nil { + return err + } + + if val.Slashed() { + log.WithFields(headerLogFields(h)).Error(ErrBuilderSlashed.Error()) + return ErrBuilderSlashed + } + + t := slots.ToEpoch(v.clock.CurrentSlot()) + if !helpers.IsActiveValidatorUsingTrie(val, t) { + log.WithFields(headerLogFields(h)).Error(ErrBuilderInactive.Error()) + return ErrBuilderInactive + } + + return nil +} + +// VerifyBuilderSufficientBalance verifies that the builder has a sufficient balance with respect to MinBuilderBalance. +func (v *HeaderVerifier) VerifyBuilderSufficientBalance() (err error) { + defer v.record(RequireBuilderSufficientBalance, &err) + + h, err := v.h.Header() + if err != nil { + return err + } + bal, err := v.st.BalanceAtIndex(h.BuilderIndex()) + if err != nil { + return err + } + + minBuilderBalance := params.BeaconConfig().MinBuilderBalance + if uint64(h.Value())+minBuilderBalance > bal { + log.WithFields(headerLogFields(h)).Errorf("insufficient builder balance %d - minimal builder balance %d", bal, minBuilderBalance) + return ErrBuilderInsufficientBalance + } + return nil +} + +// VerifyParentBlockHashSeen verifies that the parent block hash is known. +func (v *HeaderVerifier) VerifyParentBlockHashSeen(seen func([32]byte) bool) (err error) { + defer v.record(RequireKnownParentBlockHash, &err) + + h, err := v.h.Header() + if err != nil { + return err + } + + if seen != nil && seen(h.ParentBlockHash()) { + return nil + } + + log.WithFields(headerLogFields(h)).Error(ErrUnknownParentBlockHash.Error()) + return ErrUnknownParentBlockHash +} + +// VerifyParentBlockRootSeen verifies that the parent block root is known. +func (v *HeaderVerifier) VerifyParentBlockRootSeen(seen func([32]byte) bool) (err error) { + defer v.record(RequireKnownParentBlockRoot, &err) + + h, err := v.h.Header() + if err != nil { + return err + } + + if seen != nil && seen(h.ParentBlockRoot()) { + return nil + } + + log.WithFields(headerLogFields(h)).Error(ErrUnknownParentBlockRoot.Error()) + return ErrUnknownParentBlockRoot +} + +// VerifySignature verifies the signature of the execution payload header taking in validator and the genesis root. +// It uses header's slot for fork version. +func (v *HeaderVerifier) VerifySignature() (err error) { + defer v.record(RequireSignatureValid, &err) + + err = validatePayloadHeaderSignature(v.st, v.h) + if err != nil { + h, envErr := v.h.Header() + if envErr != nil { + return err + } + if errors.Is(err, signing.ErrSigFailedToVerify) { + log.WithFields(headerLogFields(h)).Error("signature failed to validate") + } else { + log.WithFields(headerLogFields(h)).WithError(err).Error("could not validate signature") + } + return err + } + return nil +} + +// VerifyCurrentOrNextSlot verifies that the header slot is either the current slot or the next slot. +func (v *HeaderVerifier) VerifyCurrentOrNextSlot() (err error) { + defer v.record(RequireCurrentOrNextSlot, &err) + + h, err := v.h.Header() + if err != nil { + return err + } + if h.Slot() == v.clock.CurrentSlot()+1 || h.Slot() == v.clock.CurrentSlot() { + return nil + } + + log.WithFields(headerLogFields(h)).Errorf("does not match current or next slot %d", v.clock.CurrentSlot()) + return ErrIncorrectPayloadHeaderSlot +} + +// SatisfyRequirement satisfies a requirement. +func (v *HeaderVerifier) SatisfyRequirement(req Requirement) { + v.record(req, nil) +} + +// record records the result of a requirement verification. +func (v *HeaderVerifier) record(req Requirement, err *error) { + if err == nil || *err == nil { + v.results.record(req, nil) + return + } + + v.results.record(req, *err) +} + +// headerLogFields returns log fields for a ROExecutionPayloadHeader instance. +func headerLogFields(h interfaces.ROExecutionPayloadHeaderEPBS) log.Fields { + return log.Fields{ + "builderIndex": h.BuilderIndex(), + "blockHash": fmt.Sprintf("%#x", h.BlockHash()), + "parentBlockHash": fmt.Sprintf("%#x", h.ParentBlockHash()), + "parentBlockRoot": fmt.Sprintf("%#x", h.ParentBlockRoot()), + "slot": h.Slot(), + "value": h.Value(), + } +} + +// validatePayloadHeaderSignature validates the signature of the execution payload header. +func validatePayloadHeaderSignature(st state.ReadOnlyBeaconState, sh interfaces.ROSignedExecutionPayloadHeader) error { + h, err := sh.Header() + if err != nil { + return err + } + + pubkey := st.PubkeyAtIndex(h.BuilderIndex()) + pub, err := bls.PublicKeyFromBytes(pubkey[:]) + if err != nil { + return err + } + + s := sh.Signature() + sig, err := bls.SignatureFromBytes(s[:]) + if err != nil { + return err + } + + currentEpoch := slots.ToEpoch(h.Slot()) + f, err := forks.Fork(currentEpoch) + if err != nil { + return err + } + + domain, err := signing.Domain(f, currentEpoch, params.BeaconConfig().DomainBeaconBuilder, st.GenesisValidatorsRoot()) + if err != nil { + return err + } + root, err := sh.SigningRoot(domain) + if err != nil { + return err + } + if !sig.Verify(pub, root[:]) { + return signing.ErrSigFailedToVerify + } + + return nil +} diff --git a/beacon-chain/verification/execution_payload_header_mock.go b/beacon-chain/verification/execution_payload_header_mock.go new file mode 100644 index 000000000000..46f46c061660 --- /dev/null +++ b/beacon-chain/verification/execution_payload_header_mock.go @@ -0,0 +1,40 @@ +package verification + +type MockExecutionPayloadHeader struct { + ErrBuilderSlashed error + ErrBuilderInsufficientBalance error + ErrUnknownParentBlockHash error + ErrUnknownParentBlockRoot error + ErrIncorrectSlot error + ErrInvalidSignature error +} + +var _ ExecutionPayloadHeaderVerifier = &MockExecutionPayloadHeader{} + +func (e *MockExecutionPayloadHeader) VerifyBuilderActiveNotSlashed() error { + return e.ErrBuilderSlashed +} + +func (e *MockExecutionPayloadHeader) VerifyBuilderSufficientBalance() error { + return e.ErrBuilderInsufficientBalance +} + +func (e *MockExecutionPayloadHeader) VerifyParentBlockHashSeen(func([32]byte) bool) error { + return e.ErrUnknownParentBlockHash +} + +func (e *MockExecutionPayloadHeader) VerifyParentBlockRootSeen(func([32]byte) bool) error { + return e.ErrUnknownParentBlockRoot +} + +func (e *MockExecutionPayloadHeader) VerifyCurrentOrNextSlot() error { + return e.ErrIncorrectSlot +} + +func (e *MockExecutionPayloadHeader) VerifySignature() error { + return e.ErrInvalidSignature +} + +func (e *MockExecutionPayloadHeader) SatisfyRequirement(requirement Requirement) { + +} diff --git a/beacon-chain/verification/execution_payload_header_test.go b/beacon-chain/verification/execution_payload_header_test.go new file mode 100644 index 000000000000..a210bd81ec09 --- /dev/null +++ b/beacon-chain/verification/execution_payload_header_test.go @@ -0,0 +1,266 @@ +package verification + +import ( + "testing" + "time" + + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/startup" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" + state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + "github.com/prysmaticlabs/prysm/v5/crypto/bls" + enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" + "github.com/prysmaticlabs/prysm/v5/time/slots" +) + +func TestHeaderVerifier_VerifyBuilderNotSlashedInactive(t *testing.T) { + st, _ := util.DeterministicGenesisState(t, 3) + val, err := st.ValidatorAtIndex(1) + require.NoError(t, err) + val.Slashed = true + require.NoError(t, st.UpdateValidatorAtIndex(1, val)) + + val, err = st.ValidatorAtIndex(2) + require.NoError(t, err) + val.ExitEpoch = 0 + require.NoError(t, st.UpdateValidatorAtIndex(2, val)) + + now := time.Now() + genesis := now.Add(-1 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + clock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now })) + init := Initializer{shared: &sharedResources{clock: clock}} + + t.Run("not slashed and active", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + BuilderIndex: 0, + }, + }, st, init) + require.NoError(t, h.VerifyBuilderActiveNotSlashed()) + require.Equal(t, true, h.results.executed(RequireBuilderActiveNotSlashed)) + require.NoError(t, h.results.result(RequireBuilderActiveNotSlashed)) + }) + + t.Run("slashed", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + BuilderIndex: 1, + }, + }, st, init) + require.ErrorIs(t, h.VerifyBuilderActiveNotSlashed(), ErrBuilderSlashed) + require.Equal(t, true, h.results.executed(RequireBuilderActiveNotSlashed)) + require.Equal(t, ErrBuilderSlashed, h.results.result(RequireBuilderActiveNotSlashed)) + }) + + t.Run("inactive", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + BuilderIndex: 2, + }, + }, st, init) + require.ErrorIs(t, h.VerifyBuilderActiveNotSlashed(), ErrBuilderInactive) + require.Equal(t, true, h.results.executed(RequireBuilderActiveNotSlashed)) + require.Equal(t, ErrBuilderInactive, h.results.result(RequireBuilderActiveNotSlashed)) + }) +} + +func TestHeaderVerifier_VerifyBuilderSufficientBalance(t *testing.T) { + st, _ := util.DeterministicGenesisState(t, 1) + mbb := params.BeaconConfig().MinBuilderBalance + require.NoError(t, st.SetBalances([]uint64{mbb, mbb + 1})) + + init := Initializer{shared: &sharedResources{}} + + t.Run("happy case", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + BuilderIndex: 1, + Value: 1, + }, + }, st, init) + require.NoError(t, h.VerifyBuilderSufficientBalance()) + require.Equal(t, true, h.results.executed(RequireBuilderSufficientBalance)) + require.NoError(t, h.results.result(RequireBuilderSufficientBalance)) + }) + + t.Run("insufficient balance", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + BuilderIndex: 0, + Value: 1, + }, + }, st, init) + require.ErrorIs(t, h.VerifyBuilderSufficientBalance(), ErrBuilderInsufficientBalance) + require.Equal(t, true, h.results.executed(RequireBuilderSufficientBalance)) + require.Equal(t, ErrBuilderInsufficientBalance, h.results.result(RequireBuilderSufficientBalance)) + }) +} + +func TestHeaderVerifier_VerifyCurrentOrNextSlot(t *testing.T) { + now := time.Now() + genesis := now.Add(-1 * time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + clock := startup.NewClock(genesis, [32]byte{}, startup.WithNower(func() time.Time { return now })) + + init := Initializer{shared: &sharedResources{clock: clock}} + + t.Run("current slot", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 1, + }, + }, nil, init) + require.NoError(t, h.VerifyCurrentOrNextSlot()) + require.Equal(t, true, h.results.executed(RequireCurrentOrNextSlot)) + require.NoError(t, h.results.result(RequireCurrentOrNextSlot)) + }) + + t.Run("next slot", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 2, + }, + }, nil, init) + require.NoError(t, h.VerifyCurrentOrNextSlot()) + require.Equal(t, true, h.results.executed(RequireCurrentOrNextSlot)) + require.NoError(t, h.results.result(RequireCurrentOrNextSlot)) + }) + + t.Run("incorrect slot", func(t *testing.T) { + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: &enginev1.ExecutionPayloadHeaderEPBS{ + Slot: 3, + }, + }, nil, init) + require.ErrorIs(t, h.VerifyCurrentOrNextSlot(), ErrIncorrectPayloadHeaderSlot) + require.Equal(t, true, h.results.executed(RequireCurrentOrNextSlot)) + require.Equal(t, ErrIncorrectPayloadHeaderSlot, h.results.result(RequireCurrentOrNextSlot)) + }) +} + +func TestHeaderVerifier_VerifyParentBlockHashSeen(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{}, nil, init) + require.NoError(t, h.VerifyParentBlockHashSeen( + func(_ [32]byte) bool { + return true + }, + )) + require.Equal(t, true, h.results.executed(RequireKnownParentBlockHash)) + require.NoError(t, h.results.result(RequireKnownParentBlockHash)) + }) + + t.Run("unknown parent hash", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{}, nil, init) + require.ErrorIs(t, h.VerifyParentBlockHashSeen( + func(_ [32]byte) bool { + return false + }, + ), ErrUnknownParentBlockHash) + require.Equal(t, true, h.results.executed(RequireKnownParentBlockHash)) + require.Equal(t, ErrUnknownParentBlockHash, h.results.result(RequireKnownParentBlockHash)) + }) +} + +func TestHeaderVerifier_VerifyParentBlockRootSeen(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{}, nil, init) + require.NoError(t, h.VerifyParentBlockRootSeen( + func(_ [32]byte) bool { + return true + }, + )) + require.Equal(t, true, h.results.executed(RequireKnownParentBlockRoot)) + require.NoError(t, h.results.result(RequireKnownParentBlockRoot)) + }) + + t.Run("unknown parent root", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + h := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{}, nil, init) + require.ErrorIs(t, h.VerifyParentBlockRootSeen( + func(_ [32]byte) bool { + return false + }, + ), ErrUnknownParentBlockRoot) + require.Equal(t, true, h.results.executed(RequireKnownParentBlockRoot)) + require.Equal(t, ErrUnknownParentBlockRoot, h.results.result(RequireKnownParentBlockRoot)) + }) +} + +func TestHeaderVerifier_VerifySignature(t *testing.T) { + _, secretKeys, err := util.DeterministicDepositsAndKeys(2) + require.NoError(t, err) + + st, err := state_native.InitializeFromProtoEpbs(ðpb.BeaconStateEPBS{ + Validators: []*ethpb.Validator{{PublicKey: secretKeys[0].PublicKey().Marshal()}, + {PublicKey: secretKeys[1].PublicKey().Marshal()}}, + Fork: ðpb.Fork{ + CurrentVersion: params.BeaconConfig().GenesisForkVersion, + PreviousVersion: params.BeaconConfig().GenesisForkVersion, + }, + }) + require.NoError(t, err) + + t.Run("valid signature", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + sh := util.HydrateSignedExecutionPayloadHeader(&enginev1.SignedExecutionPayloadHeader{}) + h := sh.Message + + signedBytes, err := signing.ComputeDomainAndSign( + st, + slots.ToEpoch(h.Slot), + h, + params.BeaconConfig().DomainBeaconBuilder, + secretKeys[0], + ) + require.NoError(t, err) + sig, err := bls.SignatureFromBytes(signedBytes) + require.NoError(t, err) + pa := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: h, + Signature: sig.Marshal(), + }, st, init) + + require.NoError(t, pa.VerifySignature()) + require.Equal(t, true, pa.results.executed(RequireSignatureValid)) + require.NoError(t, pa.results.result(RequireSignatureValid)) + }) + + t.Run("invalid signature", func(t *testing.T) { + init := Initializer{shared: &sharedResources{}} + sh := util.HydrateSignedExecutionPayloadHeader(&enginev1.SignedExecutionPayloadHeader{}) + h := sh.Message + signedBytes, err := signing.ComputeDomainAndSign( + st, + slots.ToEpoch(h.Slot), + h, + params.BeaconConfig().DomainBeaconBuilder, + secretKeys[1], + ) + require.NoError(t, err) + sig, err := bls.SignatureFromBytes(signedBytes) + require.NoError(t, err) + pa := newExecutionPayloadHeader(t, &enginev1.SignedExecutionPayloadHeader{ + Message: h, + Signature: sig.Marshal(), + }, st, init) + + require.ErrorIs(t, pa.VerifySignature(), signing.ErrSigFailedToVerify) + require.Equal(t, true, pa.results.executed(RequireSignatureValid)) + require.Equal(t, signing.ErrSigFailedToVerify, pa.results.result(RequireSignatureValid)) + }) +} + +func newExecutionPayloadHeader(t *testing.T, h *enginev1.SignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, init Initializer) *HeaderVerifier { + h = util.HydrateSignedExecutionPayloadHeader(h) + ro, err := blocks.WrappedROSignedExecutionPayloadHeader(h) + require.NoError(t, err) + return init.NewHeaderVerifier(ro, st, GossipExecutionPayloadHeaderRequirements) +} diff --git a/beacon-chain/verification/initializer_epbs.go b/beacon-chain/verification/initializer_epbs.go index b6886ba423ae..e1374b0e83a0 100644 --- a/beacon-chain/verification/initializer_epbs.go +++ b/beacon-chain/verification/initializer_epbs.go @@ -1,7 +1,9 @@ package verification import ( + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" payloadattestation "github.com/prysmaticlabs/prysm/v5/consensus-types/epbs/payload-attestation" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" ) // NewPayloadAttestationMsgVerifier creates a PayloadAttestationMsgVerifier for a single payload attestation message, @@ -13,3 +15,14 @@ func (ini *Initializer) NewPayloadAttestationMsgVerifier(pa payloadattestation.R pa: pa, } } + +// NewHeaderVerifier creates a SignedExecutionPayloadHeaderVerifier for a single signed execution payload header, +// with the given set of requirements. +func (ini *Initializer) NewHeaderVerifier(eh interfaces.ROSignedExecutionPayloadHeader, st state.ReadOnlyBeaconState, reqs []Requirement) *HeaderVerifier { + return &HeaderVerifier{ + sharedResources: ini.shared, + results: newResults(reqs...), + h: eh, + st: st, + } +} diff --git a/config/params/config.go b/config/params/config.go index 8e9ae40f55a9..3c5ff9590562 100644 --- a/config/params/config.go +++ b/config/params/config.go @@ -281,6 +281,9 @@ type BeaconChainConfig struct { // PeerDAS NumberOfColumns uint64 `yaml:"NUMBER_OF_COLUMNS" spec:"true"` // NumberOfColumns in the extended data matrix. MaxCellsInExtendedMatrix uint64 `yaml:"MAX_CELLS_IN_EXTENDED_MATRIX" spec:"true"` // MaxCellsInExtendedMatrix is the full data of one-dimensional erasure coding extended blobs (in row major format). + + // Builder + MinBuilderBalance uint64 `yaml:"MIN_BUILDER_BALANCE" spec:"true"` // MinBuilderBalance defines the minimal builder balance to accept bid from. } // InitializeForkSchedule initializes the schedules forks baked into the config. diff --git a/consensus-types/blocks/BUILD.bazel b/consensus-types/blocks/BUILD.bazel index 92aa367902c2..f87d2ff3ff72 100644 --- a/consensus-types/blocks/BUILD.bazel +++ b/consensus-types/blocks/BUILD.bazel @@ -22,8 +22,8 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks", visibility = ["//visibility:public"], deps = [ - "//beacon-chain/state/stateutil:go_default_library", "//beacon-chain/core/signing:go_default_library", + "//beacon-chain/state/stateutil:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types:go_default_library", diff --git a/consensus-types/blocks/signed_execution_payload_header.go b/consensus-types/blocks/signed_execution_payload_header.go index 31dc66bc369d..846550220989 100644 --- a/consensus-types/blocks/signed_execution_payload_header.go +++ b/consensus-types/blocks/signed_execution_payload_header.go @@ -1,6 +1,7 @@ package blocks import ( + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/signing" consensus_types "github.com/prysmaticlabs/prysm/v5/consensus-types" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -69,6 +70,11 @@ func (s signedExecutionPayloadHeader) Header() (interfaces.ROExecutionPayloadHea return WrappedROExecutionPayloadHeaderEPBS(s.s.Message) } +// SigningRoot returns the signing root for the given domain +func (s signedExecutionPayloadHeader) SigningRoot(domain []byte) (root [32]byte, err error) { + return signing.ComputeSigningRoot(s.s.Message, domain) +} + // Signature returns the wrapped signature func (s signedExecutionPayloadHeader) Signature() [96]byte { return [96]byte(s.s.Signature) diff --git a/consensus-types/interfaces/signed_execution_payload_header.go b/consensus-types/interfaces/signed_execution_payload_header.go index c67bb40eab6d..9ef4d0ab1d6c 100644 --- a/consensus-types/interfaces/signed_execution_payload_header.go +++ b/consensus-types/interfaces/signed_execution_payload_header.go @@ -8,6 +8,7 @@ import ( type ROSignedExecutionPayloadHeader interface { Header() (ROExecutionPayloadHeaderEPBS, error) Signature() [field_params.BLSSignatureLength]byte + SigningRoot([]byte) ([32]byte, error) IsNil() bool } diff --git a/testing/util/BUILD.bazel b/testing/util/BUILD.bazel index 22bdb1f9affb..7fc91294d2e3 100644 --- a/testing/util/BUILD.bazel +++ b/testing/util/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "electra_state.go", "epbs_block.go", "epbs_state.go", + "execution_payload_header.go", "helpers.go", "lightclient.go", "logging.go", diff --git a/testing/util/execution_payload_header.go b/testing/util/execution_payload_header.go new file mode 100644 index 000000000000..bb36d3db3371 --- /dev/null +++ b/testing/util/execution_payload_header.go @@ -0,0 +1,26 @@ +package util + +import enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" + +// HydrateSignedExecutionPayloadHeader hydrates a SignedExecutionPayloadHeader. +func HydrateSignedExecutionPayloadHeader(h *enginev1.SignedExecutionPayloadHeader) *enginev1.SignedExecutionPayloadHeader { + if h.Message == nil { + h.Message = &enginev1.ExecutionPayloadHeaderEPBS{} + } + if h.Signature == nil { + h.Signature = make([]byte, 96) + } + if h.Message.ParentBlockRoot == nil { + h.Message.ParentBlockRoot = make([]byte, 32) + } + if h.Message.ParentBlockHash == nil { + h.Message.ParentBlockHash = make([]byte, 32) + } + if h.Message.BlockHash == nil { + h.Message.BlockHash = make([]byte, 32) + } + if h.Message.BlobKzgCommitmentsRoot == nil { + h.Message.BlobKzgCommitmentsRoot = make([]byte, 32) + } + return h +}