Compare commits

...

2 Commits

Author SHA1 Message Date
terence
2295c62f8f James feedback 2026-02-10 15:18:36 -08:00
terence
94091e5ebd Add gossip for payload envelope 2026-02-10 10:19:12 -08:00
17 changed files with 1065 additions and 54 deletions

View File

@@ -27,6 +27,7 @@ go_library(
"receive_blob.go",
"receive_block.go",
"receive_data_column.go",
"receive_execution_payload_envelope.go",
"receive_payload_attestation_message.go",
"service.go",
"setup_forkchoice.go",

View File

@@ -0,0 +1,19 @@
package blockchain
import (
"context"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
)
// ExecutionPayloadEnvelopeReceiver interface defines the methods of chain service for receiving
// validated execution payload envelopes.
type ExecutionPayloadEnvelopeReceiver interface {
ReceiveExecutionPayloadEnvelope(context.Context, interfaces.ROSignedExecutionPayloadEnvelope) error
}
// ReceiveExecutionPayloadEnvelope accepts a signed execution payload envelope.
func (s *Service) ReceiveExecutionPayloadEnvelope(_ context.Context, _ interfaces.ROSignedExecutionPayloadEnvelope) error {
// TODO: wire into execution payload envelope processing pipeline.
return nil
}

View File

@@ -762,6 +762,11 @@ func (c *ChainService) ReceivePayloadAttestationMessage(_ context.Context, _ *et
return nil
}
// ReceiveExecutionPayloadEnvelope implements the same method in the chain service.
func (c *ChainService) ReceiveExecutionPayloadEnvelope(_ context.Context, _ interfaces.ROSignedExecutionPayloadEnvelope) error {
return nil
}
// DependentRootForEpoch mocks the same method in the chain service
func (c *ChainService) DependentRootForEpoch(_ [32]byte, _ primitives.Epoch) ([32]byte, error) {
return c.TargetRoot, nil

View File

@@ -26,6 +26,7 @@ var gossipTopicMappings = map[string]func() proto.Message{
LightClientFinalityUpdateTopicFormat: func() proto.Message { return &ethpb.LightClientFinalityUpdateAltair{} },
DataColumnSubnetTopicFormat: func() proto.Message { return &ethpb.DataColumnSidecar{} },
PayloadAttestationMessageTopicFormat: func() proto.Message { return &ethpb.PayloadAttestationMessage{} },
ExecutionPayloadEnvelopeTopicFormat: func() proto.Message { return &ethpb.SignedExecutionPayloadEnvelope{} },
}
// GossipTopicMappings is a function to return the assigned data type

View File

@@ -48,6 +48,8 @@ const (
GossipDataColumnSidecarMessage = "data_column_sidecar"
// GossipPayloadAttestationMessage is the name for the payload attestation message type.
GossipPayloadAttestationMessage = "payload_attestation_message"
// GossipExecutionPayloadEnvelopeMessage is the name for the execution payload envelope message type.
GossipExecutionPayloadEnvelopeMessage = "execution_payload_envelope"
// Topic Formats
//
@@ -79,6 +81,8 @@ const (
DataColumnSubnetTopicFormat = GossipProtocolAndDigest + GossipDataColumnSidecarMessage + "_%d"
// PayloadAttestationMessageTopicFormat is the topic format for payload attestation messages.
PayloadAttestationMessageTopicFormat = GossipProtocolAndDigest + GossipPayloadAttestationMessage
// ExecutionPayloadEnvelopeTopicFormat is the topic format for execution payload envelopes.
ExecutionPayloadEnvelopeTopicFormat = GossipProtocolAndDigest + GossipExecutionPayloadEnvelopeMessage
)
// topic is a struct representing a single gossipsub topic.
@@ -163,6 +167,7 @@ func (s *Service) allTopics() []topic {
newTopic(altair, future, empty, GossipLightClientFinalityUpdateMessage),
newTopic(capella, future, empty, GossipBlsToExecutionChangeMessage),
newTopic(gloas, future, empty, GossipPayloadAttestationMessage),
newTopic(gloas, future, empty, GossipExecutionPayloadEnvelopeMessage),
}
last := params.GetNetworkScheduleEntry(genesis)
schedule := []params.NetworkScheduleEntry{last}

View File

@@ -58,6 +58,7 @@ go_library(
"validate_blob.go",
"validate_bls_to_execution_change.go",
"validate_data_column.go",
"validate_execution_payload_envelope.go",
"validate_light_client.go",
"validate_payload_attestation.go",
"validate_proposer_slashing.go",
@@ -214,6 +215,7 @@ go_test(
"validate_blob_test.go",
"validate_bls_to_execution_change_test.go",
"validate_data_column_test.go",
"validate_execution_payload_envelope_test.go",
"validate_light_client_test.go",
"validate_payload_attestation_test.go",
"validate_proposer_slashing_test.go",

View File

@@ -62,6 +62,7 @@ var _ runtime.Service = (*Service)(nil)
const (
rangeLimit uint64 = 1024
seenBlockSize = 1000
seenPayloadEnvelopeSize = 1000
seenDataColumnSize = seenBlockSize * 128 // Each block can have max 128 data columns.
seenUnaggregatedAttSize = 20000
seenAggregatedAttSize = 16384
@@ -118,6 +119,7 @@ type blockchainService interface {
blockchain.BlockReceiver
blockchain.BlobReceiver
blockchain.DataColumnReceiver
blockchain.ExecutionPayloadEnvelopeReceiver
blockchain.HeadFetcher
blockchain.FinalizationFetcher
blockchain.ForkFetcher
@@ -134,60 +136,63 @@ type blockchainService interface {
// Service is responsible for handling all run time p2p related operations as the
// main entry point for network messages.
type Service struct {
cfg *config
ctx context.Context
cancel context.CancelFunc
slotToPendingBlocks *gcache.Cache
seenPendingBlocks map[[32]byte]bool
blkRootToPendingAtts map[[32]byte][]any
subHandler *subTopicHandler
pendingAttsLock sync.RWMutex
pendingQueueLock sync.RWMutex
chainStarted *abool.AtomicBool
validateBlockLock sync.RWMutex
rateLimiter *limiter
seenBlockLock sync.RWMutex
seenBlockCache *lru.Cache
seenBlobLock sync.RWMutex
seenBlobCache *lru.Cache
seenDataColumnCache *slotAwareCache
seenAggregatedAttestationLock sync.RWMutex
seenAggregatedAttestationCache *lru.Cache
seenUnAggregatedAttestationLock sync.RWMutex
seenUnAggregatedAttestationCache *lru.Cache
seenExitLock sync.RWMutex
seenExitCache *lru.Cache
seenProposerSlashingLock sync.RWMutex
seenProposerSlashingCache *lru.Cache
seenAttesterSlashingLock sync.RWMutex
seenAttesterSlashingCache map[uint64]bool
seenSyncMessageLock sync.RWMutex
seenSyncMessageCache *lru.Cache
seenSyncContributionLock sync.RWMutex
seenSyncContributionCache *lru.Cache
badBlockCache *lru.Cache
badBlockLock sync.RWMutex
syncContributionBitsOverlapLock sync.RWMutex
syncContributionBitsOverlapCache *lru.Cache
signatureChan chan *signatureVerifier
clockWaiter startup.ClockWaiter
initialSyncComplete chan struct{}
verifierWaiter *verification.InitializerWaiter
newBlobVerifier verification.NewBlobVerifier
newColumnsVerifier verification.NewDataColumnsVerifier
newPayloadAttestationVerifier verification.NewPayloadAttestationMsgVerifier
columnSidecarsExecSingleFlight singleflight.Group
reconstructionSingleFlight singleflight.Group
availableBlocker coverage.AvailableBlocker
reconstructionRandGen *rand.Rand
trackedValidatorsCache *cache.TrackedValidatorsCache
ctxMap ContextByteVersions
slasherEnabled bool
lcStore *lightClient.Store
dataColumnLogCh chan dataColumnLogEntry
payloadAttestationCache *cache.PayloadAttestationCache
digestActions perDigestSet
subscriptionSpawner func(func()) // see Service.spawn for details
cfg *config
ctx context.Context
cancel context.CancelFunc
slotToPendingBlocks *gcache.Cache
seenPendingBlocks map[[32]byte]bool
blkRootToPendingAtts map[[32]byte][]any
subHandler *subTopicHandler
pendingAttsLock sync.RWMutex
pendingQueueLock sync.RWMutex
chainStarted *abool.AtomicBool
validateBlockLock sync.RWMutex
rateLimiter *limiter
seenBlockLock sync.RWMutex
seenBlockCache *lru.Cache
seenPayloadEnvelopeLock sync.RWMutex
seenPayloadEnvelopeCache *lru.Cache
seenBlobLock sync.RWMutex
seenBlobCache *lru.Cache
seenDataColumnCache *slotAwareCache
seenAggregatedAttestationLock sync.RWMutex
seenAggregatedAttestationCache *lru.Cache
seenUnAggregatedAttestationLock sync.RWMutex
seenUnAggregatedAttestationCache *lru.Cache
seenExitLock sync.RWMutex
seenExitCache *lru.Cache
seenProposerSlashingLock sync.RWMutex
seenProposerSlashingCache *lru.Cache
seenAttesterSlashingLock sync.RWMutex
seenAttesterSlashingCache map[uint64]bool
seenSyncMessageLock sync.RWMutex
seenSyncMessageCache *lru.Cache
seenSyncContributionLock sync.RWMutex
seenSyncContributionCache *lru.Cache
badBlockCache *lru.Cache
badBlockLock sync.RWMutex
syncContributionBitsOverlapLock sync.RWMutex
syncContributionBitsOverlapCache *lru.Cache
signatureChan chan *signatureVerifier
clockWaiter startup.ClockWaiter
initialSyncComplete chan struct{}
verifierWaiter *verification.InitializerWaiter
newBlobVerifier verification.NewBlobVerifier
newColumnsVerifier verification.NewDataColumnsVerifier
newPayloadAttestationVerifier verification.NewPayloadAttestationMsgVerifier
columnSidecarsExecSingleFlight singleflight.Group
reconstructionSingleFlight singleflight.Group
availableBlocker coverage.AvailableBlocker
reconstructionRandGen *rand.Rand
trackedValidatorsCache *cache.TrackedValidatorsCache
ctxMap ContextByteVersions
slasherEnabled bool
lcStore *lightClient.Store
dataColumnLogCh chan dataColumnLogEntry
payloadAttestationCache *cache.PayloadAttestationCache
digestActions perDigestSet
subscriptionSpawner func(func()) // see Service.spawn for details
newExecutionPayloadEnvelopeVerifier verification.NewExecutionPayloadEnvelopeVerifier
}
// NewService initializes new regular sync service.
@@ -271,6 +276,7 @@ func (s *Service) Start() {
s.newBlobVerifier = newBlobVerifierFromInitializer(v)
s.newColumnsVerifier = newDataColumnsVerifierFromInitializer(v)
s.newPayloadAttestationVerifier = newPayloadAttestationMessageFromInitializer(v)
s.newExecutionPayloadEnvelopeVerifier = newPayloadVerifierFromInitializer(v)
go s.verifierRoutine()
go s.startDiscoveryAndSubscriptions()
@@ -358,6 +364,7 @@ func (s *Service) Status() error {
// and prevent DoS.
func (s *Service) initCaches() {
s.seenBlockCache = lruwrpr.New(seenBlockSize)
s.seenPayloadEnvelopeCache = lruwrpr.New(seenPayloadEnvelopeSize)
s.seenBlobCache = lruwrpr.New(seenBlockSize * params.BeaconConfig().DeprecatedMaxBlobsPerBlockElectra)
s.seenDataColumnCache = newSlotAwareCache(seenDataColumnSize)
s.seenAggregatedAttestationCache = lruwrpr.New(seenAggregatedAttSize)
@@ -558,3 +565,9 @@ type Checker interface {
Status() error
Resync() error
}
func newPayloadVerifierFromInitializer(ini *verification.Initializer) verification.NewExecutionPayloadEnvelopeVerifier {
return func(e interfaces.ROSignedExecutionPayloadEnvelope, reqs []verification.Requirement) verification.ExecutionPayloadEnvelopeVerifier {
return ini.NewPayloadEnvelopeVerifier(e, reqs)
}
}

View File

@@ -341,6 +341,15 @@ func (s *Service) registerSubscribers(nse params.NetworkScheduleEntry) bool {
nse,
)
})
s.spawn(func() {
s.subscribe(
p2p.ExecutionPayloadEnvelopeTopicFormat,
s.validateExecutionPayloadEnvelope,
s.executionPayloadEnvelopeSubscriber,
nse,
)
})
}
return true
}

View File

@@ -0,0 +1,160 @@
package sync
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
)
func (s *Service) validateExecutionPayloadEnvelope(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.validateExecutionPayloadEnvelope")
defer span.End()
if msg.Topic == nil {
return pubsub.ValidationReject, p2p.ErrInvalidTopic
}
m, err := s.decodePubsubMessage(msg)
if err != nil {
tracing.AnnotateError(span, err)
return pubsub.ValidationReject, err
}
signedEnvelope, ok := m.(*ethpb.SignedExecutionPayloadEnvelope)
if !ok {
return pubsub.ValidationReject, errWrongMessage
}
e, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedEnvelope)
if err != nil {
log.WithError(err).Error("failed to create read only signed payload execution envelope")
return pubsub.ValidationIgnore, err
}
v := s.newExecutionPayloadEnvelopeVerifier(e, verification.GossipExecutionPayloadEnvelopeRequirements)
env, err := e.Envelope()
if err != nil {
return pubsub.ValidationIgnore, err
}
// [IGNORE] The envelope's block root envelope.block_root has been seen (via gossip or non-gossip sources)
// (a client MAY queue payload for processing once the block is retrieved).
if err := v.VerifyBlockRootSeen(func(root [32]byte) bool { return s.cfg.chain.HasBlock(ctx, root) }); err != nil {
return pubsub.ValidationIgnore, err
}
root := env.BeaconBlockRoot()
// [IGNORE] The node has not seen another valid SignedExecutionPayloadEnvelope for this block root from this builder.
if s.hasSeenPayloadEnvelope(root, env.BuilderIndex()) {
return pubsub.ValidationIgnore, nil
}
finalized := s.cfg.chain.FinalizedCheckpt()
if finalized == nil {
return pubsub.ValidationIgnore, errors.New("nil finalized checkpoint")
}
// [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot --
// i.e. validate that envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch).
if err := v.VerifySlotAboveFinalized(finalized.Epoch); err != nil {
return pubsub.ValidationIgnore, err
}
// [REJECT] block passes validation.
if err := v.VerifyBlockRootValid(s.hasBadBlock); err != nil {
return pubsub.ValidationReject, err
}
// Let block be the block with envelope.beacon_block_root.
block, err := s.cfg.beaconDB.Block(ctx, root)
if err != nil {
return pubsub.ValidationIgnore, err
}
// [REJECT] block.slot equals envelope.slot.
if err := v.VerifySlotMatchesBlock(block.Block().Slot()); err != nil {
return pubsub.ValidationReject, err
}
// Let bid alias block.body.signed_execution_payload_bid.message
// (notice that this can be obtained from the state.latest_execution_payload_bid).
signedBid, err := block.Block().Body().SignedExecutionPayloadBid()
if err != nil {
return pubsub.ValidationIgnore, err
}
wrappedBid, err := blocks.WrappedROSignedExecutionPayloadBid(signedBid)
if err != nil {
return pubsub.ValidationIgnore, err
}
bid, err := wrappedBid.Bid()
if err != nil {
return pubsub.ValidationIgnore, err
}
// [REJECT] envelope.builder_index == bid.builder_index.
if err := v.VerifyBuilderValid(bid); err != nil {
return pubsub.ValidationReject, err
}
// [REJECT] payload.block_hash == bid.block_hash.
if err := v.VerifyPayloadHash(bid); err != nil {
return pubsub.ValidationReject, err
}
// For self-build, the state is retrived via how we retrieve for beacon block optimization
// For builder index, the state is retrived via head state read only
st, err := s.blockVerifyingState(ctx, block)
if err != nil {
return pubsub.ValidationIgnore, err
}
// [REJECT] signed_execution_payload_envelope.signature is valid with respect to the builder's public key.
if err := v.VerifySignature(st); err != nil {
return pubsub.ValidationReject, err
}
s.setSeenPayloadEnvelope(root, env.BuilderIndex())
return pubsub.ValidationAccept, nil
}
func (s *Service) executionPayloadEnvelopeSubscriber(ctx context.Context, msg proto.Message) error {
e, ok := msg.(*ethpb.SignedExecutionPayloadEnvelope)
if !ok {
return errWrongMessage
}
env, err := blocks.WrappedROSignedExecutionPayloadEnvelope(e)
if err != nil {
return err
}
return s.cfg.chain.ReceiveExecutionPayloadEnvelope(ctx, env)
}
func (s *Service) hasSeenPayloadEnvelope(root [32]byte, builderIdx primitives.BuilderIndex) bool {
if s.seenPayloadEnvelopeCache == nil {
return false
}
s.seenPayloadEnvelopeLock.RLock()
defer s.seenPayloadEnvelopeLock.RUnlock()
b := append(bytesutil.Bytes32(uint64(builderIdx)), root[:]...)
_, seen := s.seenPayloadEnvelopeCache.Get(string(b))
return seen
}
func (s *Service) setSeenPayloadEnvelope(root [32]byte, builderIdx primitives.BuilderIndex) {
if s.seenPayloadEnvelopeCache == nil {
return
}
s.seenPayloadEnvelopeLock.Lock()
defer s.seenPayloadEnvelopeLock.Unlock()
b := append(bytesutil.Bytes32(uint64(builderIdx)), root[:]...)
s.seenPayloadEnvelopeCache.Add(string(b), true)
}

View File

@@ -0,0 +1,275 @@
package sync
import (
"bytes"
"context"
"reflect"
"testing"
"time"
mock "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
dbtest "github.com/OffchainLabs/prysm/v7/beacon-chain/db/testing"
doublylinkedtree "github.com/OffchainLabs/prysm/v7/beacon-chain/forkchoice/doubly-linked-tree"
"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
p2ptest "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/startup"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stategen"
mockSync "github.com/OffchainLabs/prysm/v7/beacon-chain/sync/initial-sync/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
lruwrpr "github.com/OffchainLabs/prysm/v7/cache/lru"
"github.com/OffchainLabs/prysm/v7/config/params"
"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/encoding/bytesutil"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
pubsub "github.com/libp2p/go-libp2p-pubsub"
pb "github.com/libp2p/go-libp2p-pubsub/pb"
"github.com/pkg/errors"
)
func TestValidateExecutionPayloadEnvelope_InvalidTopic(t *testing.T) {
ctx := context.Background()
p := p2ptest.NewTestP2P(t)
s := &Service{cfg: &config{p2p: p, initialSync: &mockSync.Sync{}}}
result, err := s.validateExecutionPayloadEnvelope(ctx, "", &pubsub.Message{
Message: &pb.Message{},
})
require.ErrorIs(t, p2p.ErrInvalidTopic, err)
require.Equal(t, result, pubsub.ValidationReject)
}
func TestValidateExecutionPayloadEnvelope_AlreadySeen(t *testing.T) {
ctx := context.Background()
s, msg, builderIdx, root := setupExecutionPayloadEnvelopeService(t, 1, 1)
s.newExecutionPayloadEnvelopeVerifier = testNewExecutionPayloadEnvelopeVerifier(mockExecutionPayloadEnvelopeVerifier{})
s.setSeenPayloadEnvelope(root, builderIdx)
result, err := s.validateExecutionPayloadEnvelope(ctx, "", msg)
require.NoError(t, err)
require.Equal(t, result, pubsub.ValidationIgnore)
}
func TestValidateExecutionPayloadEnvelope_ErrorPathsWithMock(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
verifier mockExecutionPayloadEnvelopeVerifier
result pubsub.ValidationResult
}{
{
name: "block root not seen",
verifier: mockExecutionPayloadEnvelopeVerifier{errBlockRootSeen: errors.New("not seen")},
result: pubsub.ValidationIgnore,
},
{
name: "slot below finalized",
verifier: mockExecutionPayloadEnvelopeVerifier{errSlotAboveFinalized: errors.New("below finalized")},
result: pubsub.ValidationIgnore,
},
{
name: "block root invalid",
verifier: mockExecutionPayloadEnvelopeVerifier{errBlockRootValid: errors.New("invalid block")},
result: pubsub.ValidationReject,
},
{
name: "slot mismatch",
verifier: mockExecutionPayloadEnvelopeVerifier{errSlotMatchesBlock: errors.New("slot mismatch")},
result: pubsub.ValidationReject,
},
{
name: "builder mismatch",
verifier: mockExecutionPayloadEnvelopeVerifier{errBuilderValid: errors.New("builder mismatch")},
result: pubsub.ValidationReject,
},
{
name: "payload hash mismatch",
verifier: mockExecutionPayloadEnvelopeVerifier{errPayloadHash: errors.New("payload hash mismatch")},
result: pubsub.ValidationReject,
},
{
name: "signature invalid",
verifier: mockExecutionPayloadEnvelopeVerifier{errSignature: errors.New("signature invalid")},
result: pubsub.ValidationReject,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
s, msg, _, _ := setupExecutionPayloadEnvelopeService(t, 1, 1)
s.newExecutionPayloadEnvelopeVerifier = testNewExecutionPayloadEnvelopeVerifier(tc.verifier)
result, err := s.validateExecutionPayloadEnvelope(ctx, "", msg)
require.NotNil(t, err)
require.Equal(t, result, tc.result)
})
}
}
func TestValidateExecutionPayloadEnvelope_HappyPath(t *testing.T) {
ctx := context.Background()
s, msg, builderIdx, root := setupExecutionPayloadEnvelopeService(t, 1, 1)
s.newExecutionPayloadEnvelopeVerifier = testNewExecutionPayloadEnvelopeVerifier(mockExecutionPayloadEnvelopeVerifier{})
require.Equal(t, false, s.hasSeenPayloadEnvelope(root, builderIdx))
result, err := s.validateExecutionPayloadEnvelope(ctx, "", msg)
require.NoError(t, err)
require.Equal(t, result, pubsub.ValidationAccept)
require.Equal(t, true, s.hasSeenPayloadEnvelope(root, builderIdx))
}
type mockExecutionPayloadEnvelopeVerifier struct {
errBlockRootSeen error
errBlockRootValid error
errSlotAboveFinalized error
errSlotMatchesBlock error
errBuilderValid error
errPayloadHash error
errSignature error
}
var _ verification.ExecutionPayloadEnvelopeVerifier = &mockExecutionPayloadEnvelopeVerifier{}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifyBlockRootSeen(_ func([32]byte) bool) error {
return m.errBlockRootSeen
}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifyBlockRootValid(_ func([32]byte) bool) error {
return m.errBlockRootValid
}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifySlotAboveFinalized(_ primitives.Epoch) error {
return m.errSlotAboveFinalized
}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifySlotMatchesBlock(_ primitives.Slot) error {
return m.errSlotMatchesBlock
}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifyBuilderValid(_ interfaces.ROExecutionPayloadBid) error {
return m.errBuilderValid
}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifyPayloadHash(_ interfaces.ROExecutionPayloadBid) error {
return m.errPayloadHash
}
func (m *mockExecutionPayloadEnvelopeVerifier) VerifySignature(_ state.ReadOnlyBeaconState) error {
return m.errSignature
}
func (*mockExecutionPayloadEnvelopeVerifier) SatisfyRequirement(_ verification.Requirement) {}
func testNewExecutionPayloadEnvelopeVerifier(m mockExecutionPayloadEnvelopeVerifier) verification.NewExecutionPayloadEnvelopeVerifier {
return func(_ interfaces.ROSignedExecutionPayloadEnvelope, _ []verification.Requirement) verification.ExecutionPayloadEnvelopeVerifier {
clone := m
return &clone
}
}
func setupExecutionPayloadEnvelopeService(t *testing.T, envelopeSlot, blockSlot primitives.Slot) (*Service, *pubsub.Message, primitives.BuilderIndex, [32]byte) {
t.Helper()
ctx := context.Background()
db := dbtest.SetupDB(t)
p := p2ptest.NewTestP2P(t)
chainService := &mock.ChainService{
Genesis: time.Unix(time.Now().Unix()-int64(params.BeaconConfig().SecondsPerSlot), 0),
FinalizedCheckPoint: &ethpb.Checkpoint{},
DB: db,
}
stateGen := stategen.New(db, doublylinkedtree.New())
s := &Service{
seenPayloadEnvelopeCache: lruwrpr.New(10),
cfg: &config{
p2p: p,
initialSync: &mockSync.Sync{},
chain: chainService,
beaconDB: db,
stateGen: stateGen,
clock: startup.NewClock(chainService.Genesis, chainService.ValidatorsRoot),
},
}
bid := util.GenerateTestSignedExecutionPayloadBid(blockSlot)
sb := util.NewBeaconBlockGloas()
sb.Block.Slot = blockSlot
sb.Block.Body.SignedExecutionPayloadBid = bid
signedBlock, err := blocks.NewSignedBeaconBlock(sb)
require.NoError(t, err)
root, err := signedBlock.Block().HashTreeRoot()
require.NoError(t, err)
require.NoError(t, db.SaveBlock(ctx, signedBlock))
state, err := util.NewBeaconStateFulu()
require.NoError(t, err)
require.NoError(t, db.SaveState(ctx, state, root))
blockHash := bytesutil.ToBytes32(bid.Message.BlockHash)
env := testSignedExecutionPayloadEnvelope(t, envelopeSlot, primitives.BuilderIndex(bid.Message.BuilderIndex), root, blockHash)
msg := envelopeToPubsub(t, s, p, env)
return s, msg, primitives.BuilderIndex(bid.Message.BuilderIndex), root
}
func envelopeToPubsub(t *testing.T, s *Service, p p2p.P2P, env *ethpb.SignedExecutionPayloadEnvelope) *pubsub.Message {
t.Helper()
buf := new(bytes.Buffer)
_, err := p.Encoding().EncodeGossip(buf, env)
require.NoError(t, err)
topic := p2p.GossipTypeMapping[reflect.TypeFor[*ethpb.SignedExecutionPayloadEnvelope]()]
digest, err := s.currentForkDigest()
require.NoError(t, err)
topic = s.addDigestToTopic(topic, digest)
return &pubsub.Message{
Message: &pb.Message{
Data: buf.Bytes(),
Topic: &topic,
},
}
}
func testSignedExecutionPayloadEnvelope(t *testing.T, slot primitives.Slot, builderIdx primitives.BuilderIndex, root, blockHash [32]byte) *ethpb.SignedExecutionPayloadEnvelope {
t.Helper()
payload := &enginev1.ExecutionPayloadDeneb{
ParentHash: bytes.Repeat([]byte{0x01}, 32),
FeeRecipient: bytes.Repeat([]byte{0x02}, 20),
StateRoot: bytes.Repeat([]byte{0x03}, 32),
ReceiptsRoot: bytes.Repeat([]byte{0x04}, 32),
LogsBloom: bytes.Repeat([]byte{0x05}, 256),
PrevRandao: bytes.Repeat([]byte{0x06}, 32),
BlockNumber: 1,
GasLimit: 2,
GasUsed: 3,
Timestamp: 4,
BaseFeePerGas: bytes.Repeat([]byte{0x07}, 32),
BlockHash: blockHash[:],
Transactions: [][]byte{},
Withdrawals: []*enginev1.Withdrawal{},
BlobGasUsed: 0,
ExcessBlobGas: 0,
}
return &ethpb.SignedExecutionPayloadEnvelope{
Message: &ethpb.ExecutionPayloadEnvelope{
Payload: payload,
ExecutionRequests: &enginev1.ExecutionRequests{
Deposits: []*enginev1.DepositRequest{},
},
BuilderIndex: builderIdx,
BeaconBlockRoot: root[:],
Slot: slot,
StateRoot: bytes.Repeat([]byte{0xBB}, 32),
},
Signature: bytes.Repeat([]byte{0xAA}, 96),
}
}

View File

@@ -8,6 +8,7 @@ go_library(
"cache.go",
"data_column.go",
"error.go",
"execution_payload_envelope.go",
"fake.go",
"filesystem.go",
"initializer.go",
@@ -36,6 +37,7 @@ go_library(
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/payload-attestation:go_default_library",
"//consensus-types/primitives:go_default_library",
"//crypto/bls:go_default_library",
@@ -60,6 +62,7 @@ go_test(
"blob_test.go",
"cache_test.go",
"data_column_test.go",
"execution_payload_envelope_test.go",
"filesystem_test.go",
"initializer_test.go",
"payload_attestation_test.go",
@@ -80,11 +83,13 @@ go_test(
"//config/fieldparams:go_default_library",
"//config/params:go_default_library",
"//consensus-types/blocks:go_default_library",
"//consensus-types/interfaces:go_default_library",
"//consensus-types/payload-attestation:go_default_library",
"//consensus-types/primitives:go_default_library",
"//crypto/bls:go_default_library",
"//crypto/bls/common:go_default_library",
"//encoding/bytesutil:go_default_library",
"//proto/engine/v1:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//runtime/interop:go_default_library",
"//testing/require:go_default_library",

View File

@@ -0,0 +1,229 @@
package verification
import (
"fmt"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/crypto/bls"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
// ExecutionPayloadEnvelopeVerifier defines the methods implemented by the ROSignedExecutionPayloadEnvelope.
type ExecutionPayloadEnvelopeVerifier interface {
VerifyBlockRootSeen(func([32]byte) bool) error
VerifyBlockRootValid(func([32]byte) bool) error
VerifySlotAboveFinalized(primitives.Epoch) error
VerifySlotMatchesBlock(primitives.Slot) error
VerifyBuilderValid(interfaces.ROExecutionPayloadBid) error
VerifyPayloadHash(interfaces.ROExecutionPayloadBid) error
VerifySignature(state.ReadOnlyBeaconState) error
SatisfyRequirement(Requirement)
}
// 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
// ExecutionPayloadEnvelopeGossipRequirements defines the list of requirements for gossip
// execution payload envelopes.
var ExecutionPayloadEnvelopeGossipRequirements = []Requirement{
RequireBlockRootSeen,
RequireBlockRootValid,
RequireEnvelopeSlotAboveFinalized,
RequireEnvelopeSlotMatchesBlock,
RequireBuilderValid,
RequirePayloadHashValid,
RequireBuilderSignatureValid,
}
// GossipExecutionPayloadEnvelopeRequirements is a requirement list for gossip execution payload envelopes.
var GossipExecutionPayloadEnvelopeRequirements = requirementList(ExecutionPayloadEnvelopeGossipRequirements)
var (
ErrEnvelopeBlockRootNotSeen = errors.New("block root not seen")
ErrEnvelopeBlockRootInvalid = errors.New("block root invalid")
ErrEnvelopeSlotBeforeFinalized = errors.New("envelope slot is before finalized checkpoint")
ErrEnvelopeSlotMismatch = errors.New("envelope slot does not match block slot")
ErrIncorrectEnvelopeBuilder = errors.New("builder index does not match committed header")
ErrIncorrectEnvelopeBlockHash = errors.New("block hash does not match committed header")
)
var _ ExecutionPayloadEnvelopeVerifier = &EnvelopeVerifier{}
// EnvelopeVerifier is a read-only verifier for execution payload envelopes.
type EnvelopeVerifier struct {
results *results
e interfaces.ROSignedExecutionPayloadEnvelope
}
// VerifyBlockRootSeen verifies if the block root has been seen before.
func (v *EnvelopeVerifier) VerifyBlockRootSeen(parentSeen func([32]byte) bool) (err error) {
defer v.record(RequireBlockRootSeen, &err)
env, err := v.e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
if parentSeen != nil && parentSeen(env.BeaconBlockRoot()) {
return nil
}
return fmt.Errorf("%w: root=%#x slot=%d builder=%d", ErrEnvelopeBlockRootNotSeen, env.BeaconBlockRoot(), env.Slot(), env.BuilderIndex())
}
// VerifyBlockRootValid verifies if the block root is valid.
func (v *EnvelopeVerifier) VerifyBlockRootValid(badBlock func([32]byte) bool) (err error) {
defer v.record(RequireBlockRootValid, &err)
env, err := v.e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
if badBlock != nil && badBlock(env.BeaconBlockRoot()) {
return fmt.Errorf("%w: root=%#x slot=%d builder=%d", ErrEnvelopeBlockRootInvalid, env.BeaconBlockRoot(), env.Slot(), env.BuilderIndex())
}
return nil
}
// VerifySlotAboveFinalized ensures the envelope slot is not before the latest finalized epoch start.
func (v *EnvelopeVerifier) VerifySlotAboveFinalized(finalizedEpoch primitives.Epoch) (err error) {
defer v.record(RequireEnvelopeSlotAboveFinalized, &err)
env, err := v.e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
startSlot, err := slots.EpochStart(finalizedEpoch)
if err != nil {
return errors.Wrapf(ErrEnvelopeSlotBeforeFinalized, "error computing epoch start slot for finalized checkpoint (%d) %s", finalizedEpoch, err.Error())
}
if env.Slot() < startSlot {
return fmt.Errorf("%w: slot=%d start=%d", ErrEnvelopeSlotBeforeFinalized, env.Slot(), startSlot)
}
return nil
}
// VerifySlotMatchesBlock ensures the envelope slot matches the block slot.
func (v *EnvelopeVerifier) VerifySlotMatchesBlock(blockSlot primitives.Slot) (err error) {
defer v.record(RequireEnvelopeSlotMatchesBlock, &err)
env, err := v.e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
if env.Slot() != blockSlot {
return fmt.Errorf("%w: envelope=%d block=%d", ErrEnvelopeSlotMismatch, env.Slot(), blockSlot)
}
return nil
}
// VerifyBuilderValid checks that the builder index matches the one in the bid.
func (v *EnvelopeVerifier) VerifyBuilderValid(bid interfaces.ROExecutionPayloadBid) (err error) {
defer v.record(RequireBuilderValid, &err)
env, err := v.e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
if bid.BuilderIndex() != env.BuilderIndex() {
return fmt.Errorf("%w: envelope=%d bid=%d", ErrIncorrectEnvelopeBuilder, env.BuilderIndex(), bid.BuilderIndex())
}
return nil
}
// VerifyPayloadHash checks that the payload blockhash matches the one in the bid.
func (v *EnvelopeVerifier) VerifyPayloadHash(bid interfaces.ROExecutionPayloadBid) (err error) {
defer v.record(RequirePayloadHashValid, &err)
env, err := v.e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
if env.IsBlinded() {
return nil
}
payload, err := env.Execution()
if err != nil {
return errors.Wrap(err, "failed to get payload execution")
}
if bid.BlockHash() != [32]byte(payload.BlockHash()) {
return fmt.Errorf("%w: payload=%#x bid=%#x", ErrIncorrectEnvelopeBlockHash, payload.BlockHash(), bid.BlockHash())
}
return nil
}
// VerifySignature verifies the signature of the execution payload envelope.
func (v *EnvelopeVerifier) VerifySignature(st state.ReadOnlyBeaconState) (err error) {
defer v.record(RequireBuilderSignatureValid, &err)
err = validatePayloadEnvelopeSignature(st, v.e)
if err != nil {
env, envErr := v.e.Envelope()
if envErr != nil {
return errors.Wrap(err, "failed to get envelope for signature validation")
}
return errors.Wrapf(err, "signature validation failed: root=%#x slot=%d builder=%d", env.BeaconBlockRoot(), env.Slot(), env.BuilderIndex())
}
return nil
}
// SatisfyRequirement allows the caller to manually mark a requirement as satisfied.
func (v *EnvelopeVerifier) SatisfyRequirement(req Requirement) {
v.record(req, nil)
}
// record records the result of a requirement verification.
func (v *EnvelopeVerifier) record(req Requirement, err *error) {
if err == nil || *err == nil {
v.results.record(req, nil)
return
}
v.results.record(req, *err)
}
// validatePayloadEnvelopeSignature verifies the signature of a signed execution payload envelope
func validatePayloadEnvelopeSignature(st state.ReadOnlyBeaconState, e interfaces.ROSignedExecutionPayloadEnvelope) error {
env, err := e.Envelope()
if err != nil {
return errors.Wrap(err, "failed to get envelope")
}
var pubkey []byte
if env.BuilderIndex() == params.BeaconConfig().BuilderIndexSelfBuild {
header := st.LatestBlockHeader()
if header == nil {
return errors.New("latest block header is nil")
}
val, err := st.ValidatorAtIndex(primitives.ValidatorIndex(header.ProposerIndex))
if err != nil {
return errors.Wrap(err, "failed to get proposer validator")
}
pubkey = val.PublicKey
} else {
builderPubkey, err := st.BuilderPubkey(env.BuilderIndex())
if err != nil {
return errors.Wrap(err, "failed to get builder pubkey")
}
pubkey = builderPubkey[:]
}
pub, err := bls.PublicKeyFromBytes(pubkey)
if err != nil {
return errors.Wrap(err, "invalid public key")
}
s := e.Signature()
sig, err := bls.SignatureFromBytes(s[:])
if err != nil {
return errors.Wrap(err, "invalid signature format")
}
currentEpoch := slots.ToEpoch(st.Slot())
domain, err := signing.Domain(st.Fork(), currentEpoch, params.BeaconConfig().DomainBeaconBuilder, st.GenesisValidatorsRoot())
if err != nil {
return errors.Wrap(err, "failed to compute signing domain")
}
root, err := e.SigningRoot(domain)
if err != nil {
return errors.Wrap(err, "failed to compute signing root")
}
if !sig.Verify(pub, root[:]) {
return signing.ErrSigFailedToVerify
}
return nil
}

View File

@@ -0,0 +1,258 @@
package verification
import (
"bytes"
"testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/crypto/bls"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
"github.com/OffchainLabs/prysm/v7/time/slots"
)
func TestEnvelopeVerifier_VerifySlotAboveFinalized(t *testing.T) {
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, 1, 1, root, blockHash)
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier := &EnvelopeVerifier{results: newResults(RequireEnvelopeSlotAboveFinalized), e: wrapped}
require.ErrorIs(t, verifier.VerifySlotAboveFinalized(1), ErrEnvelopeSlotBeforeFinalized)
verifier = &EnvelopeVerifier{results: newResults(RequireEnvelopeSlotAboveFinalized), e: wrapped}
require.NoError(t, verifier.VerifySlotAboveFinalized(0))
}
func TestEnvelopeVerifier_VerifySlotMatchesBlock(t *testing.T) {
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, 2, 1, root, blockHash)
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier := &EnvelopeVerifier{results: newResults(RequireEnvelopeSlotMatchesBlock), e: wrapped}
require.ErrorIs(t, verifier.VerifySlotMatchesBlock(3), ErrEnvelopeSlotMismatch)
verifier = &EnvelopeVerifier{results: newResults(RequireEnvelopeSlotMatchesBlock), e: wrapped}
require.NoError(t, verifier.VerifySlotMatchesBlock(2))
}
func TestEnvelopeVerifier_VerifyBlockRootSeen(t *testing.T) {
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, 1, 1, root, blockHash)
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier := &EnvelopeVerifier{results: newResults(RequireBlockRootSeen), e: wrapped}
require.ErrorIs(t, verifier.VerifyBlockRootSeen(func([32]byte) bool { return false }), ErrEnvelopeBlockRootNotSeen)
verifier = &EnvelopeVerifier{results: newResults(RequireBlockRootSeen), e: wrapped}
require.NoError(t, verifier.VerifyBlockRootSeen(func([32]byte) bool { return true }))
}
func TestEnvelopeVerifier_VerifyBlockRootValid(t *testing.T) {
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, 1, 1, root, blockHash)
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier := &EnvelopeVerifier{results: newResults(RequireBlockRootValid), e: wrapped}
require.ErrorIs(t, verifier.VerifyBlockRootValid(func([32]byte) bool { return true }), ErrEnvelopeBlockRootInvalid)
verifier = &EnvelopeVerifier{results: newResults(RequireBlockRootValid), e: wrapped}
require.NoError(t, verifier.VerifyBlockRootValid(func([32]byte) bool { return false }))
}
func TestEnvelopeVerifier_VerifyBuilderValid(t *testing.T) {
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, 1, 1, root, blockHash)
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
badBid := testExecutionPayloadBid(t, 1, 2, blockHash)
verifier := &EnvelopeVerifier{results: newResults(RequireBuilderValid), e: wrapped}
require.ErrorIs(t, verifier.VerifyBuilderValid(badBid), ErrIncorrectEnvelopeBuilder)
okBid := testExecutionPayloadBid(t, 1, 1, blockHash)
verifier = &EnvelopeVerifier{results: newResults(RequireBuilderValid), e: wrapped}
require.NoError(t, verifier.VerifyBuilderValid(okBid))
}
func TestEnvelopeVerifier_VerifyPayloadHash(t *testing.T) {
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, 1, 1, root, blockHash)
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
badHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xCC}, 32))
badBid := testExecutionPayloadBid(t, 1, 1, badHash)
verifier := &EnvelopeVerifier{results: newResults(RequirePayloadHashValid), e: wrapped}
require.ErrorIs(t, verifier.VerifyPayloadHash(badBid), ErrIncorrectEnvelopeBlockHash)
okBid := testExecutionPayloadBid(t, 1, 1, blockHash)
verifier = &EnvelopeVerifier{results: newResults(RequirePayloadHashValid), e: wrapped}
require.NoError(t, verifier.VerifyPayloadHash(okBid))
}
func TestEnvelopeVerifier_VerifySignature_Builder(t *testing.T) {
slot := primitives.Slot(1)
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, slot, 0, root, blockHash)
sk, err := bls.RandKey()
require.NoError(t, err)
builderPubkey := sk.PublicKey().Marshal()
st := newGloasState(t, slot, nil, nil, []*ethpb.Builder{{Pubkey: builderPubkey}})
sig := signEnvelope(t, sk, env.Message, st.Fork(), st.GenesisValidatorsRoot(), slot)
env.Signature = sig[:]
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier := &EnvelopeVerifier{results: newResults(RequireBuilderSignatureValid), e: wrapped}
require.NoError(t, verifier.VerifySignature(st))
sk2, err := bls.RandKey()
require.NoError(t, err)
badSig := signEnvelope(t, sk2, env.Message, st.Fork(), st.GenesisValidatorsRoot(), slot)
env.Signature = badSig[:]
wrapped, err = blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier = &EnvelopeVerifier{results: newResults(RequireBuilderSignatureValid), e: wrapped}
require.ErrorIs(t, verifier.VerifySignature(st), signing.ErrSigFailedToVerify)
}
func TestEnvelopeVerifier_VerifySignature_SelfBuild(t *testing.T) {
slot := primitives.Slot(2)
root := bytesutil.ToBytes32(bytes.Repeat([]byte{0xAA}, 32))
blockHash := bytesutil.ToBytes32(bytes.Repeat([]byte{0xBB}, 32))
env := testSignedExecutionPayloadEnvelope(t, slot, params.BeaconConfig().BuilderIndexSelfBuild, root, blockHash)
sk, err := bls.RandKey()
require.NoError(t, err)
validatorPubkey := sk.PublicKey().Marshal()
validators := []*ethpb.Validator{{PublicKey: validatorPubkey}}
balances := []uint64{0}
st := newGloasState(t, slot, validators, balances, nil)
sig := signEnvelope(t, sk, env.Message, st.Fork(), st.GenesisValidatorsRoot(), slot)
env.Signature = sig[:]
wrapped, err := blocks.WrappedROSignedExecutionPayloadEnvelope(env)
require.NoError(t, err)
verifier := &EnvelopeVerifier{results: newResults(RequireBuilderSignatureValid), e: wrapped}
require.NoError(t, verifier.VerifySignature(st))
}
func testSignedExecutionPayloadEnvelope(t *testing.T, slot primitives.Slot, builderIdx primitives.BuilderIndex, root, blockHash [32]byte) *ethpb.SignedExecutionPayloadEnvelope {
t.Helper()
payload := &enginev1.ExecutionPayloadDeneb{
ParentHash: bytes.Repeat([]byte{0x01}, 32),
FeeRecipient: bytes.Repeat([]byte{0x02}, 20),
StateRoot: bytes.Repeat([]byte{0x03}, 32),
ReceiptsRoot: bytes.Repeat([]byte{0x04}, 32),
LogsBloom: bytes.Repeat([]byte{0x05}, 256),
PrevRandao: bytes.Repeat([]byte{0x06}, 32),
BlockNumber: 1,
GasLimit: 2,
GasUsed: 3,
Timestamp: 4,
BaseFeePerGas: bytes.Repeat([]byte{0x07}, 32),
BlockHash: blockHash[:],
Transactions: [][]byte{},
Withdrawals: []*enginev1.Withdrawal{},
BlobGasUsed: 0,
ExcessBlobGas: 0,
}
return &ethpb.SignedExecutionPayloadEnvelope{
Message: &ethpb.ExecutionPayloadEnvelope{
Payload: payload,
ExecutionRequests: &enginev1.ExecutionRequests{
Deposits: []*enginev1.DepositRequest{},
},
BuilderIndex: builderIdx,
BeaconBlockRoot: root[:],
Slot: slot,
StateRoot: bytes.Repeat([]byte{0xBB}, 32),
},
Signature: bytes.Repeat([]byte{0xCC}, 96),
}
}
func testExecutionPayloadBid(t *testing.T, slot primitives.Slot, builderIdx primitives.BuilderIndex, blockHash [32]byte) interfaces.ROExecutionPayloadBid {
t.Helper()
signed := util.GenerateTestSignedExecutionPayloadBid(slot)
signed.Message.BuilderIndex = builderIdx
copy(signed.Message.BlockHash, blockHash[:])
wrapped, err := blocks.WrappedROSignedExecutionPayloadBid(signed)
require.NoError(t, err)
bid, err := wrapped.Bid()
require.NoError(t, err)
return bid
}
func newGloasState(
t *testing.T,
slot primitives.Slot,
validators []*ethpb.Validator,
balances []uint64,
builders []*ethpb.Builder,
) state.BeaconState {
t.Helper()
genesisRoot := bytes.Repeat([]byte{0x11}, 32)
st, err := util.NewBeaconStateGloas(func(s *ethpb.BeaconStateGloas) error {
s.Slot = slot
s.GenesisValidatorsRoot = genesisRoot
if validators != nil {
s.Validators = validators
}
if balances != nil {
s.Balances = balances
}
if s.LatestBlockHeader != nil {
s.LatestBlockHeader.ProposerIndex = 0
}
if builders != nil {
s.Builders = builders
}
return nil
})
require.NoError(t, err)
return st
}
func signEnvelope(t *testing.T, sk bls.SecretKey, env *ethpb.ExecutionPayloadEnvelope, fork *ethpb.Fork, genesisRoot []byte, slot primitives.Slot) [96]byte {
t.Helper()
epoch := slots.ToEpoch(slot)
domain, err := signing.Domain(fork, epoch, params.BeaconConfig().DomainBeaconBuilder, genesisRoot)
require.NoError(t, err)
root, err := signing.ComputeSigningRoot(env, domain)
require.NoError(t, err)
sig := sk.Sign(root[:]).Marshal()
var out [96]byte
copy(out[:], sig)
return out
}

View File

@@ -12,6 +12,7 @@ import (
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
payloadattestation "github.com/OffchainLabs/prysm/v7/consensus-types/payload-attestation"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
@@ -97,6 +98,14 @@ func (ini *Initializer) NewPayloadAttestationMsgVerifier(pa payloadattestation.R
}
}
// NewPayloadEnvelopeVerifier creates a SignedExecutionPayloadEnvelopeVerifier for a single signed execution payload envelope with the given set of requirements.
func (ini *Initializer) NewPayloadEnvelopeVerifier(ee interfaces.ROSignedExecutionPayloadEnvelope, reqs []Requirement) *EnvelopeVerifier {
return &EnvelopeVerifier{
results: newResults(reqs...),
e: ee,
}
}
// InitializerWaiter provides an Initializer once all dependent resources are ready
// via the WaitForInitializer method.
type InitializerWaiter struct {

View File

@@ -24,4 +24,11 @@ const (
RequireBlockRootSeen
RequireBlockRootValid
RequireSignatureValid
// Execution payload envelope specific.
RequireBuilderValid
RequirePayloadHashValid
RequireEnvelopeSlotAboveFinalized
RequireEnvelopeSlotMatchesBlock
RequireBuilderSignatureValid
)

View File

@@ -45,6 +45,16 @@ func (r Requirement) String() string {
return "RequireBlockRootValid"
case RequireSignatureValid:
return "RequireSignatureValid"
case RequireBuilderValid:
return "RequireBuilderValid"
case RequirePayloadHashValid:
return "RequirePayloadHashValid"
case RequireEnvelopeSlotAboveFinalized:
return "RequireEnvelopeSlotAboveFinalized"
case RequireEnvelopeSlotMatchesBlock:
return "RequireEnvelopeSlotMatchesBlock"
case RequireBuilderSignatureValid:
return "RequireBuilderSignatureValid"
default:
return unknownRequirementName
}

View File

@@ -0,0 +1,3 @@
### Added
- Add Gloas execution payload envelope gossip validation