Compare commits

...

7 Commits

Author SHA1 Message Date
james-prysm
9085a0c4c4 Merge branch 'develop' into proposer-preferences-pool 2026-04-06 14:23:31 -07:00
james-prysm
1d03c8146b adding slot guard for preferences 2026-03-25 11:41:26 -05:00
james-prysm
07bacc58d6 self review, updating cache to not have redundant information 2026-03-25 11:33:20 -05:00
james-prysm
9a7bdef5c9 self review 2026-03-25 11:09:11 -05:00
james-prysm
d73fb859db Merge branch 'develop' into proposer-preferences-pool 2026-03-25 08:10:06 -07:00
james-prysm
138948c65d Merge branch 'develop' into proposer-preferences-pool 2026-03-25 07:15:55 -07:00
james-prysm
9e980f3ace wip 2026-03-24 16:25:31 -05:00
26 changed files with 910 additions and 146 deletions

View File

@@ -54,11 +54,14 @@ go_test(
name = "go_default_test",
srcs = [
"conversions_block_execution_test.go",
"conversions_gloas_test.go",
"conversions_test.go",
],
embed = [":go_default_library"],
deps = [
"//consensus-types/blocks:go_default_library",
"//consensus-types/primitives: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",

View File

@@ -546,6 +546,18 @@ type PayloadAttestationMessage struct {
Signature string `json:"signature"`
}
type ProposerPreferences struct {
ProposalSlot string `json:"proposal_slot"`
ValidatorIndex string `json:"validator_index"`
FeeRecipient string `json:"fee_recipient"`
GasLimit string `json:"gas_limit"`
}
type SignedProposerPreferences struct {
Message *ProposerPreferences `json:"message"`
Signature string `json:"signature"`
}
type BeaconBlockBodyGloas struct {
RandaoReveal string `json:"randao_reveal"`
Eth1Data *Eth1Data `json:"eth1_data"`

View File

@@ -2,8 +2,13 @@ package structs
import (
"fmt"
"strconv"
"github.com/OffchainLabs/prysm/v7/api/server"
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/ethereum/go-ethereum/common/hexutil"
)
@@ -38,6 +43,61 @@ func ROExecutionPayloadBidFromConsensus(b interfaces.ROExecutionPayloadBid) *Exe
}
}
func (s *SignedProposerPreferences) ToConsensus() (*ethpb.SignedProposerPreferences, error) {
if s.Message == nil {
return nil, server.NewDecodeError(errNilValue, "Message")
}
msg, err := s.Message.ToConsensus()
if err != nil {
return nil, server.NewDecodeError(err, "Message")
}
sig, err := bytesutil.DecodeHexWithLength(s.Signature, fieldparams.BLSSignatureLength)
if err != nil {
return nil, server.NewDecodeError(err, "Signature")
}
return &ethpb.SignedProposerPreferences{
Message: msg,
Signature: sig,
}, nil
}
func (p *ProposerPreferences) ToConsensus() (*ethpb.ProposerPreferences, error) {
slot, err := strconv.ParseUint(p.ProposalSlot, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "ProposalSlot")
}
valIdx, err := strconv.ParseUint(p.ValidatorIndex, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "ValidatorIndex")
}
feeRecipient, err := bytesutil.DecodeHexWithLength(p.FeeRecipient, fieldparams.FeeRecipientLength)
if err != nil {
return nil, server.NewDecodeError(err, "FeeRecipient")
}
gasLimit, err := strconv.ParseUint(p.GasLimit, 10, 64)
if err != nil {
return nil, server.NewDecodeError(err, "GasLimit")
}
return &ethpb.ProposerPreferences{
ProposalSlot: primitives.Slot(slot),
ValidatorIndex: primitives.ValidatorIndex(valIdx),
FeeRecipient: feeRecipient,
GasLimit: gasLimit,
}, nil
}
func SignedProposerPreferencesFromConsensus(sp *ethpb.SignedProposerPreferences) *SignedProposerPreferences {
return &SignedProposerPreferences{
Message: &ProposerPreferences{
ProposalSlot: fmt.Sprintf("%d", sp.Message.ProposalSlot),
ValidatorIndex: fmt.Sprintf("%d", sp.Message.ValidatorIndex),
FeeRecipient: hexutil.Encode(sp.Message.FeeRecipient),
GasLimit: fmt.Sprintf("%d", sp.Message.GasLimit),
},
Signature: hexutil.Encode(sp.Signature),
}
}
func BuildersFromConsensus(builders []*ethpb.Builder) []*Builder {
newBuilders := make([]*Builder, len(builders))
for i, b := range builders {

View File

@@ -0,0 +1,110 @@
package structs
import (
"testing"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func TestSignedProposerPreferences_ToConsensus_NilMessage(t *testing.T) {
s := &SignedProposerPreferences{Message: nil, Signature: ""}
_, err := s.ToConsensus()
require.ErrorContains(t, errNilValue.Error(), err)
}
func TestSignedProposerPreferences_ToConsensus(t *testing.T) {
feeRecipient := bytesutil.PadTo([]byte{0xaa, 0xbb}, 20)
sig := bytesutil.PadTo([]byte{0xcc}, 96)
s := &SignedProposerPreferences{
Message: &ProposerPreferences{
ProposalSlot: "32",
ValidatorIndex: "5",
FeeRecipient: hexutil.Encode(feeRecipient),
GasLimit: "30000000",
},
Signature: hexutil.Encode(sig),
}
result, err := s.ToConsensus()
require.NoError(t, err)
assert.Equal(t, primitives.Slot(32), result.Message.ProposalSlot)
assert.Equal(t, primitives.ValidatorIndex(5), result.Message.ValidatorIndex)
assert.DeepEqual(t, feeRecipient, result.Message.FeeRecipient)
assert.Equal(t, uint64(30000000), result.Message.GasLimit)
assert.DeepEqual(t, sig, result.Signature)
}
func TestProposerPreferences_ToConsensus_InvalidSlot(t *testing.T) {
p := &ProposerPreferences{
ProposalSlot: "not_a_number",
ValidatorIndex: "5",
FeeRecipient: hexutil.Encode(make([]byte, 20)),
GasLimit: "30000000",
}
_, err := p.ToConsensus()
require.ErrorContains(t, "ProposalSlot", err)
}
func TestProposerPreferences_ToConsensus_InvalidFeeRecipient(t *testing.T) {
p := &ProposerPreferences{
ProposalSlot: "32",
ValidatorIndex: "5",
FeeRecipient: "0xinvalid",
GasLimit: "30000000",
}
_, err := p.ToConsensus()
require.ErrorContains(t, "FeeRecipient", err)
}
func TestSignedProposerPreferencesFromConsensus(t *testing.T) {
feeRecipient := bytesutil.PadTo([]byte{0xaa, 0xbb}, 20)
sig := bytesutil.PadTo([]byte{0xcc}, 96)
sp := &eth.SignedProposerPreferences{
Message: &eth.ProposerPreferences{
ProposalSlot: 32,
ValidatorIndex: 5,
FeeRecipient: feeRecipient,
GasLimit: 30000000,
},
Signature: sig,
}
result := SignedProposerPreferencesFromConsensus(sp)
assert.Equal(t, "32", result.Message.ProposalSlot)
assert.Equal(t, "5", result.Message.ValidatorIndex)
assert.Equal(t, hexutil.Encode(feeRecipient), result.Message.FeeRecipient)
assert.Equal(t, "30000000", result.Message.GasLimit)
assert.Equal(t, hexutil.Encode(sig), result.Signature)
}
func TestSignedProposerPreferences_RoundTrip(t *testing.T) {
feeRecipient := bytesutil.PadTo([]byte{0xaa, 0xbb}, 20)
sig := bytesutil.PadTo([]byte{0xcc}, 96)
original := &eth.SignedProposerPreferences{
Message: &eth.ProposerPreferences{
ProposalSlot: 32,
ValidatorIndex: 5,
FeeRecipient: feeRecipient,
GasLimit: 30000000,
},
Signature: sig,
}
jsonStruct := SignedProposerPreferencesFromConsensus(original)
roundTripped, err := jsonStruct.ToConsensus()
require.NoError(t, err)
assert.Equal(t, original.Message.ProposalSlot, roundTripped.Message.ProposalSlot)
assert.Equal(t, original.Message.ValidatorIndex, roundTripped.Message.ValidatorIndex)
assert.DeepEqual(t, original.Message.FeeRecipient, roundTripped.Message.FeeRecipient)
assert.Equal(t, original.Message.GasLimit, roundTripped.Message.GasLimit)
assert.DeepEqual(t, original.Signature, roundTripped.Signature)
}

View File

@@ -188,6 +188,11 @@ type BLSToExecutionChangesPoolResponse struct {
Data []*SignedBLSToExecutionChange `json:"data"`
}
type ProposerPreferencesPoolResponse struct {
Version string `json:"version"`
Data []*SignedProposerPreferences `json:"data"`
}
type GetAttesterSlashingsResponse struct {
Version string `json:"version,omitempty"`
Data json.RawMessage `json:"data"` // Accepts both `[]*AttesterSlashing` and `[]*AttesterSlashingElectra` types

View File

@@ -4,57 +4,42 @@ import (
"sync"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
)
// ProposerPreference stores the proposer fee recipient and gas limit for a slot.
type ProposerPreference struct {
FeeRecipient []byte
GasLimit uint64
}
// ProposerPreferencesCache stores proposer preferences by slot.
// ProposerPreferencesCache stores signed proposer preferences by slot.
type ProposerPreferencesCache struct {
slotToPreferences map[primitives.Slot]ProposerPreference
slotToPreferences map[primitives.Slot]*ethpb.SignedProposerPreferences
lock sync.RWMutex
}
// NewProposerPreferencesCache initializes a proposer preferences cache.
func NewProposerPreferencesCache() *ProposerPreferencesCache {
return &ProposerPreferencesCache{
slotToPreferences: make(map[primitives.Slot]ProposerPreference),
slotToPreferences: make(map[primitives.Slot]*ethpb.SignedProposerPreferences),
}
}
// Add stores proposer preferences for a slot. If the slot already exists, the
// existing value is kept and false is returned.
func (c *ProposerPreferencesCache) Add(slot primitives.Slot, feeRecipient []byte, gasLimit uint64) bool {
// Add stores signed proposer preferences for a slot. If the slot already
// exists, the existing value is kept and false is returned.
func (c *ProposerPreferencesCache) Add(slot primitives.Slot, signed *ethpb.SignedProposerPreferences) bool {
c.lock.Lock()
defer c.lock.Unlock()
if _, ok := c.slotToPreferences[slot]; ok {
return false
}
// FeeRecipient comes from validated SSZ-decoded proposer preferences, so
// retaining the slice reference here is intentional.
c.slotToPreferences[slot] = ProposerPreference{
FeeRecipient: feeRecipient,
GasLimit: gasLimit,
}
c.slotToPreferences[slot] = signed
return true
}
// Get returns proposer preferences for a slot.
func (c *ProposerPreferencesCache) Get(slot primitives.Slot) (ProposerPreference, bool) {
// Get returns the signed proposer preferences for a slot.
func (c *ProposerPreferencesCache) Get(slot primitives.Slot) (*ethpb.SignedProposerPreferences, bool) {
c.lock.RLock()
defer c.lock.RUnlock()
pref, ok := c.slotToPreferences[slot]
if !ok {
return ProposerPreference{}, false
}
return pref, true
sp, ok := c.slotToPreferences[slot]
return sp, ok
}
// Has returns true if proposer preferences for the slot already exist.
@@ -66,6 +51,25 @@ func (c *ProposerPreferencesCache) Has(slot primitives.Slot) bool {
return ok
}
// Pending returns cached signed proposer preferences not yet included in a
// block. If slot is non-zero, only the entry for that slot is returned.
func (c *ProposerPreferencesCache) Pending(slot primitives.Slot) []*ethpb.SignedProposerPreferences {
c.lock.RLock()
defer c.lock.RUnlock()
if slot != 0 {
if sp, ok := c.slotToPreferences[slot]; ok {
return []*ethpb.SignedProposerPreferences{sp}
}
return nil
}
result := make([]*ethpb.SignedProposerPreferences, 0, len(c.slotToPreferences))
for _, sp := range c.slotToPreferences {
result = append(result, sp)
}
return result
}
// PruneBefore removes all proposer preferences for slots before the provided slot.
func (c *ProposerPreferencesCache) PruneBefore(slot primitives.Slot) {
c.lock.Lock()
@@ -83,5 +87,5 @@ func (c *ProposerPreferencesCache) Clear() {
c.lock.Lock()
defer c.lock.Unlock()
c.slotToPreferences = make(map[primitives.Slot]ProposerPreference)
c.slotToPreferences = make(map[primitives.Slot]*ethpb.SignedProposerPreferences)
}

View File

@@ -4,43 +4,54 @@ import (
"testing"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/require"
)
func testSigned(slot primitives.Slot, feeRecipient []byte, gasLimit uint64) *ethpb.SignedProposerPreferences {
return &ethpb.SignedProposerPreferences{
Message: &ethpb.ProposerPreferences{
ProposalSlot: slot,
FeeRecipient: feeRecipient,
GasLimit: gasLimit,
},
}
}
func TestProposerPreferencesCache_AddGetHas(t *testing.T) {
c := NewProposerPreferencesCache()
slot := primitives.Slot(123)
feeRecipient := []byte{1, 2, 3, 4}
require.Equal(t, false, c.Has(slot))
added := c.Add(slot, feeRecipient, 42)
added := c.Add(slot, testSigned(slot, feeRecipient, 42))
require.Equal(t, true, added)
require.Equal(t, true, c.Has(slot))
pref, ok := c.Get(slot)
sp, ok := c.Get(slot)
require.Equal(t, true, ok)
require.DeepEqual(t, feeRecipient, pref.FeeRecipient)
require.Equal(t, uint64(42), pref.GasLimit)
require.DeepEqual(t, feeRecipient, sp.Message.FeeRecipient)
require.Equal(t, uint64(42), sp.Message.GasLimit)
}
func TestProposerPreferencesCache_AddDuplicateSlot(t *testing.T) {
c := NewProposerPreferencesCache()
slot := primitives.Slot(456)
require.Equal(t, true, c.Add(slot, []byte{1}, 10))
require.Equal(t, false, c.Add(slot, []byte{2}, 20))
require.Equal(t, true, c.Add(slot, testSigned(slot, []byte{1}, 10)))
require.Equal(t, false, c.Add(slot, testSigned(slot, []byte{2}, 20)))
pref, ok := c.Get(slot)
sp, ok := c.Get(slot)
require.Equal(t, true, ok)
require.DeepEqual(t, []byte{1}, pref.FeeRecipient)
require.Equal(t, uint64(10), pref.GasLimit)
require.DeepEqual(t, []byte{1}, sp.Message.FeeRecipient)
require.Equal(t, uint64(10), sp.Message.GasLimit)
}
func TestProposerPreferencesCache_Clear(t *testing.T) {
c := NewProposerPreferencesCache()
slot := primitives.Slot(789)
require.Equal(t, true, c.Add(slot, []byte{1}, 10))
require.Equal(t, true, c.Add(slot, testSigned(slot, []byte{1}, 10)))
c.Clear()
require.Equal(t, false, c.Has(slot))
@@ -51,9 +62,9 @@ func TestProposerPreferencesCache_Clear(t *testing.T) {
func TestProposerPreferencesCache_PruneBefore(t *testing.T) {
c := NewProposerPreferencesCache()
require.Equal(t, true, c.Add(10, []byte{1}, 10))
require.Equal(t, true, c.Add(11, []byte{2}, 11))
require.Equal(t, true, c.Add(12, []byte{3}, 12))
require.Equal(t, true, c.Add(10, testSigned(10, []byte{1}, 10)))
require.Equal(t, true, c.Add(11, testSigned(11, []byte{2}, 11)))
require.Equal(t, true, c.Add(12, testSigned(12, []byte{3}, 12)))
c.PruneBefore(11)
@@ -61,3 +72,21 @@ func TestProposerPreferencesCache_PruneBefore(t *testing.T) {
require.Equal(t, true, c.Has(11))
require.Equal(t, true, c.Has(12))
}
func TestProposerPreferencesCache_Pending(t *testing.T) {
c := NewProposerPreferencesCache()
c.Add(10, testSigned(10, []byte{1}, 10))
c.Add(11, testSigned(11, []byte{2}, 11))
c.Add(12, testSigned(12, []byte{3}, 12))
all := c.Pending(0)
require.Equal(t, 3, len(all))
bySlot := c.Pending(11)
require.Equal(t, 1, len(bySlot))
require.Equal(t, primitives.Slot(11), bySlot[0].Message.ProposalSlot)
empty := c.Pending(999)
require.Equal(t, 0, len(empty))
}

View File

@@ -49,6 +49,9 @@ const (
// PayloadAttestationMessageReceived is sent after a payload attestation message is received from gossip or rpc.
PayloadAttestationMessageReceived = 13
// ProposerPreferencesReceived is sent after a signed proposer preferences message is received from gossip or rpc.
ProposerPreferencesReceived = 14
)
// UnAggregatedAttReceivedData is the data sent with UnaggregatedAttReceived events.
@@ -122,3 +125,8 @@ type DataColumnReceivedData struct {
type PayloadAttestationMessageReceivedData struct {
Message *ethpb.PayloadAttestationMessage
}
// ProposerPreferencesReceivedData is the data sent with ProposerPreferencesReceived events.
type ProposerPreferencesReceivedData struct {
Preferences *ethpb.SignedProposerPreferences
}

View File

@@ -200,25 +200,26 @@ func (s *Service) validatorEndpoints(
rewardFetcher rewards.BlockRewardsFetcher,
) []endpoint {
server := &validator.Server{
HeadFetcher: s.cfg.HeadFetcher,
TimeFetcher: s.cfg.GenesisTimeFetcher,
SyncChecker: s.cfg.SyncService,
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
AttestationCache: s.cfg.AttestationCache,
AttestationsPool: s.cfg.AttestationsPool,
PeerManager: s.cfg.PeerManager,
Broadcaster: s.cfg.Broadcaster,
V1Alpha1Server: validatorServer,
Stater: stater,
SyncCommitteePool: s.cfg.SyncCommitteeObjectPool,
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
BeaconDB: s.cfg.BeaconDB,
BlockBuilder: s.cfg.BlockBuilder,
OperationNotifier: s.cfg.OperationNotifier,
TrackedValidatorsCache: s.cfg.TrackedValidatorsCache,
PayloadIDCache: s.cfg.PayloadIDCache,
CoreService: coreService,
BlockRewardFetcher: rewardFetcher,
HeadFetcher: s.cfg.HeadFetcher,
TimeFetcher: s.cfg.GenesisTimeFetcher,
SyncChecker: s.cfg.SyncService,
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
AttestationCache: s.cfg.AttestationCache,
AttestationsPool: s.cfg.AttestationsPool,
PeerManager: s.cfg.PeerManager,
Broadcaster: s.cfg.Broadcaster,
V1Alpha1Server: validatorServer,
Stater: stater,
SyncCommitteePool: s.cfg.SyncCommitteeObjectPool,
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
BeaconDB: s.cfg.BeaconDB,
BlockBuilder: s.cfg.BlockBuilder,
OperationNotifier: s.cfg.OperationNotifier,
TrackedValidatorsCache: s.cfg.TrackedValidatorsCache,
PayloadIDCache: s.cfg.PayloadIDCache,
CoreService: coreService,
BlockRewardFetcher: rewardFetcher,
ProposerPreferencesCache: s.cfg.ProposerPreferencesCache,
}
const namespace = "validator"
@@ -411,6 +412,17 @@ func (s *Service) validatorEndpoints(
handler: server.SyncCommitteeSelections,
methods: []string{http.MethodPost},
},
{
template: "/eth/v1/validator/proposer_preferences",
name: namespace + ".SubmitProposerPreferences",
middleware: []middleware.Middleware{
middleware.ContentTypeHandler([]string{api.JsonMediaType}),
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
middleware.AcceptEncodingHeaderHandler(),
},
handler: server.SubmitProposerPreferences,
methods: []string{http.MethodPost},
},
}
}
@@ -522,32 +534,33 @@ func (s *Service) beaconEndpoints(
coreService *core.Service,
) []endpoint {
server := &beacon.Server{
CanonicalHistory: ch,
BeaconDB: s.cfg.BeaconDB,
AttestationCache: s.cfg.AttestationCache,
AttestationsPool: s.cfg.AttestationsPool,
SlashingsPool: s.cfg.SlashingsPool,
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
GenesisTimeFetcher: s.cfg.GenesisTimeFetcher,
BlockNotifier: s.cfg.BlockNotifier,
OperationNotifier: s.cfg.OperationNotifier,
Broadcaster: s.cfg.Broadcaster,
BlockReceiver: s.cfg.BlockReceiver,
StateGenService: s.cfg.StateGen,
Stater: stater,
Blocker: blocker,
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
HeadFetcher: s.cfg.HeadFetcher,
TimeFetcher: s.cfg.GenesisTimeFetcher,
VoluntaryExitsPool: s.cfg.ExitPool,
V1Alpha1ValidatorServer: validatorServer,
SyncChecker: s.cfg.SyncService,
ExecutionReconstructor: s.cfg.ExecutionReconstructor,
BLSChangesPool: s.cfg.BLSChangesPool,
FinalizationFetcher: s.cfg.FinalizationFetcher,
ForkchoiceFetcher: s.cfg.ForkchoiceFetcher,
CoreService: coreService,
AttestationStateFetcher: s.cfg.AttestationReceiver,
CanonicalHistory: ch,
BeaconDB: s.cfg.BeaconDB,
AttestationCache: s.cfg.AttestationCache,
AttestationsPool: s.cfg.AttestationsPool,
SlashingsPool: s.cfg.SlashingsPool,
ChainInfoFetcher: s.cfg.ChainInfoFetcher,
GenesisTimeFetcher: s.cfg.GenesisTimeFetcher,
BlockNotifier: s.cfg.BlockNotifier,
OperationNotifier: s.cfg.OperationNotifier,
Broadcaster: s.cfg.Broadcaster,
BlockReceiver: s.cfg.BlockReceiver,
StateGenService: s.cfg.StateGen,
Stater: stater,
Blocker: blocker,
OptimisticModeFetcher: s.cfg.OptimisticModeFetcher,
HeadFetcher: s.cfg.HeadFetcher,
TimeFetcher: s.cfg.GenesisTimeFetcher,
VoluntaryExitsPool: s.cfg.ExitPool,
V1Alpha1ValidatorServer: validatorServer,
SyncChecker: s.cfg.SyncService,
ExecutionReconstructor: s.cfg.ExecutionReconstructor,
BLSChangesPool: s.cfg.BLSChangesPool,
FinalizationFetcher: s.cfg.FinalizationFetcher,
ForkchoiceFetcher: s.cfg.ForkchoiceFetcher,
CoreService: coreService,
AttestationStateFetcher: s.cfg.AttestationReceiver,
ProposerPreferencesCache: s.cfg.ProposerPreferencesCache,
}
const namespace = "beacon"
@@ -759,6 +772,16 @@ func (s *Service) beaconEndpoints(
handler: server.SubmitAttesterSlashingsV2,
methods: []string{http.MethodPost},
},
{
template: "/eth/v1/beacon/pool/proposer_preferences",
name: namespace + ".ListProposerPreferences",
middleware: []middleware.Middleware{
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
middleware.AcceptEncodingHeaderHandler(),
},
handler: server.ListProposerPreferences,
methods: []string{http.MethodGet},
},
{
template: "/eth/v1/beacon/pool/proposer_slashings",
name: namespace + ".GetProposerSlashings",

View File

@@ -47,6 +47,7 @@ func Test_endpoints(t *testing.T) {
"/eth/v2/beacon/pool/attestations": {http.MethodGet, http.MethodPost},
"/eth/v2/beacon/pool/attester_slashings": {http.MethodGet, http.MethodPost},
"/eth/v1/beacon/pool/proposer_slashings": {http.MethodGet, http.MethodPost},
"/eth/v1/beacon/pool/proposer_preferences": {http.MethodGet},
"/eth/v1/beacon/pool/sync_committees": {http.MethodPost},
"/eth/v1/beacon/pool/voluntary_exits": {http.MethodGet, http.MethodPost},
"/eth/v1/beacon/pool/bls_to_execution_changes": {http.MethodGet, http.MethodPost},
@@ -112,6 +113,7 @@ func Test_endpoints(t *testing.T) {
"/eth/v1/validator/prepare_beacon_proposer": {http.MethodPost},
"/eth/v1/validator/register_validator": {http.MethodPost},
"/eth/v1/validator/liveness/{epoch}": {http.MethodPost},
"/eth/v1/validator/proposer_preferences": {http.MethodPost},
}
prysmBeaconRoutes := map[string][]string{

View File

@@ -88,6 +88,7 @@ go_test(
"//api/server/structs:go_default_library",
"//beacon-chain/blockchain/kzg:go_default_library",
"//beacon-chain/blockchain/testing:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/time:go_default_library",
"//beacon-chain/core/transition:go_default_library",

View File

@@ -941,3 +941,35 @@ func (s *Server) SubmitProposerSlashing(w http.ResponseWriter, r *http.Request)
}
}
}
// ListProposerPreferences retrieves proposer preferences known by the node but not necessarily incorporated into any block.
func (s *Server) ListProposerPreferences(w http.ResponseWriter, r *http.Request) {
_, span := trace.StartSpan(r.Context(), "beacon.ListProposerPreferences")
defer span.End()
if slots.ToEpoch(s.TimeFetcher.CurrentSlot()) < params.BeaconConfig().GloasForkEpoch {
httputil.HandleError(w, "Proposer preferences are not supported before the gloas fork", http.StatusBadRequest)
return
}
_, slot, ok := shared.UintFromQuery(w, r, "slot", false)
if !ok {
return
}
if slot != 0 && slots.ToEpoch(primitives.Slot(slot)) < params.BeaconConfig().GloasForkEpoch {
httputil.HandleError(w, "Requested slot is before the gloas fork", http.StatusBadRequest)
return
}
pending := s.ProposerPreferencesCache.Pending(primitives.Slot(slot))
data := make([]*structs.SignedProposerPreferences, 0, len(pending))
for _, sp := range pending {
data = append(data, structs.SignedProposerPreferencesFromConsensus(sp))
}
w.Header().Set(api.VersionHeader, version.String(version.Gloas))
httputil.WriteJson(w, &structs.ProposerPreferencesPoolResponse{
Version: version.String(version.Gloas),
Data: data,
})
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/OffchainLabs/prysm/v7/api/server"
"github.com/OffchainLabs/prysm/v7/api/server/structs"
blockchainmock "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
prysmtime "github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
@@ -2296,6 +2297,118 @@ func TestSubmitProposerSlashing_InvalidSlashing(t *testing.T) {
assert.StringContains(t, "Invalid proposer slashing", e.Message)
}
func TestListProposerPreferences(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
c := cache.NewProposerPreferencesCache()
sp1 := &ethpbv1alpha1.SignedProposerPreferences{
Message: &ethpbv1alpha1.ProposerPreferences{
ProposalSlot: 32,
ValidatorIndex: 1,
FeeRecipient: bytesutil.PadTo([]byte("fee1"), 20),
GasLimit: 30000000,
},
Signature: bytesutil.PadTo([]byte("sig1"), 96),
}
sp2 := &ethpbv1alpha1.SignedProposerPreferences{
Message: &ethpbv1alpha1.ProposerPreferences{
ProposalSlot: 33,
ValidatorIndex: 2,
FeeRecipient: bytesutil.PadTo([]byte("fee2"), 20),
GasLimit: 30000001,
},
Signature: bytesutil.PadTo([]byte("sig2"), 96),
}
c.Add(32, sp1)
c.Add(33, sp2)
s := &Server{
ProposerPreferencesCache: c,
TimeFetcher: &blockchainmock.ChainService{},
}
t.Run("all", func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/proposer_preferences", nil)
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.ListProposerPreferences(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
assert.Equal(t, "gloas", writer.Header().Get(api.VersionHeader))
resp := &structs.ProposerPreferencesPoolResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
require.Equal(t, 2, len(resp.Data))
assert.Equal(t, "gloas", resp.Version)
})
t.Run("filter by slot", func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/proposer_preferences?slot=32", nil)
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.ListProposerPreferences(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.ProposerPreferencesPoolResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
require.Equal(t, 1, len(resp.Data))
assert.Equal(t, "32", resp.Data[0].Message.ProposalSlot)
})
t.Run("empty result", func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/proposer_preferences?slot=999", nil)
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.ListProposerPreferences(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.ProposerPreferencesPoolResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
require.Equal(t, 0, len(resp.Data))
})
}
func TestListProposerPreferences_PreFork(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 100
params.OverrideBeaconConfig(cfg)
currentSlot := primitives.Slot(0)
s := &Server{
ProposerPreferencesCache: cache.NewProposerPreferencesCache(),
TimeFetcher: &blockchainmock.ChainService{Slot: &currentSlot},
}
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/proposer_preferences", nil)
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.ListProposerPreferences(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
}
func TestListProposerPreferences_PreForkSlot(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 1
params.OverrideBeaconConfig(cfg)
currentSlot := primitives.Slot(32)
s := &Server{
ProposerPreferencesCache: cache.NewProposerPreferencesCache(),
TimeFetcher: &blockchainmock.ChainService{Slot: &currentSlot},
}
request := httptest.NewRequest(http.MethodGet, "http://example.com/eth/v1/beacon/pool/proposer_preferences?slot=5", nil)
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.ListProposerPreferences(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
}
var (
singleAtt = `[
{

View File

@@ -25,30 +25,31 @@ import (
// Server defines a server implementation of the gRPC Beacon Chain service,
// providing RPC endpoints to access data relevant to the Ethereum Beacon Chain.
type Server struct {
BeaconDB db.ReadOnlyDatabase
ChainInfoFetcher blockchain.ChainInfoFetcher
GenesisTimeFetcher blockchain.TimeFetcher
BlockReceiver blockchain.BlockReceiver
BlockNotifier blockfeed.Notifier
OperationNotifier operation.Notifier
Broadcaster p2p.Broadcaster
AttestationCache *cache.AttestationCache
AttestationsPool attestations.Pool
SlashingsPool slashings.PoolManager
VoluntaryExitsPool voluntaryexits.PoolManager
StateGenService stategen.StateManager
Stater lookup.Stater
Blocker lookup.Blocker
HeadFetcher blockchain.HeadFetcher
TimeFetcher blockchain.TimeFetcher
OptimisticModeFetcher blockchain.OptimisticModeFetcher
V1Alpha1ValidatorServer eth.BeaconNodeValidatorServer
SyncChecker sync.Checker
CanonicalHistory *stategen.CanonicalHistory
ExecutionReconstructor execution.Reconstructor
FinalizationFetcher blockchain.FinalizationFetcher
BLSChangesPool blstoexec.PoolManager
ForkchoiceFetcher blockchain.ForkchoiceFetcher
CoreService *core.Service
AttestationStateFetcher blockchain.AttestationStateFetcher
BeaconDB db.ReadOnlyDatabase
ChainInfoFetcher blockchain.ChainInfoFetcher
GenesisTimeFetcher blockchain.TimeFetcher
BlockReceiver blockchain.BlockReceiver
BlockNotifier blockfeed.Notifier
OperationNotifier operation.Notifier
Broadcaster p2p.Broadcaster
AttestationCache *cache.AttestationCache
AttestationsPool attestations.Pool
SlashingsPool slashings.PoolManager
VoluntaryExitsPool voluntaryexits.PoolManager
StateGenService stategen.StateManager
Stater lookup.Stater
Blocker lookup.Blocker
HeadFetcher blockchain.HeadFetcher
TimeFetcher blockchain.TimeFetcher
OptimisticModeFetcher blockchain.OptimisticModeFetcher
V1Alpha1ValidatorServer eth.BeaconNodeValidatorServer
SyncChecker sync.Checker
CanonicalHistory *stategen.CanonicalHistory
ExecutionReconstructor execution.Reconstructor
FinalizationFetcher blockchain.FinalizationFetcher
BLSChangesPool blstoexec.PoolManager
ForkchoiceFetcher blockchain.ForkchoiceFetcher
CoreService *core.Service
AttestationStateFetcher blockchain.AttestationStateFetcher
ProposerPreferencesCache *cache.ProposerPreferencesCache
}

View File

@@ -81,6 +81,8 @@ const (
ExecutionPayloadBidTopic = "execution_payload_bid"
// PayloadAttestationMessageTopic represents a new payload attestation message event topic.
PayloadAttestationMessageTopic = "payload_attestation_message"
// ProposerPreferencesTopic represents a new proposer preferences event topic.
ProposerPreferencesTopic = "proposer_preferences"
)
var (
@@ -116,6 +118,7 @@ var opsFeedEventTopics = map[feed.EventType]string{
operation.BlockGossipReceived: BlockGossipTopic,
operation.DataColumnReceived: DataColumnTopic,
operation.PayloadAttestationMessageReceived: PayloadAttestationMessageTopic,
operation.ProposerPreferencesReceived: ProposerPreferencesTopic,
}
var stateFeedEventTopics = map[feed.EventType]string{
@@ -481,6 +484,8 @@ func topicForEvent(event *feed.Event) string {
return DataColumnTopic
case *operation.PayloadAttestationMessageReceivedData:
return PayloadAttestationMessageTopic
case *operation.ProposerPreferencesReceivedData:
return ProposerPreferencesTopic
case *statefeed.PayloadProcessedData:
return ExecutionPayloadTopic
default:
@@ -659,6 +664,10 @@ func (s *Server) lazyReaderForEvent(ctx context.Context, event *feed.Event, topi
return func() io.Reader {
return jsonMarshalReader(eventName, structs.PayloadAttestationMessageFromConsensus(v.Message))
}, nil
case *operation.ProposerPreferencesReceivedData:
return func() io.Reader {
return jsonMarshalReader(eventName, structs.SignedProposerPreferencesFromConsensus(v.Preferences))
}, nil
case *statefeed.PayloadProcessedData:
return func() io.Reader {
return jsonMarshalReader(eventName, &structs.PayloadEvent{

View File

@@ -124,6 +124,7 @@ func operationEventsFixtures(t *testing.T) (*topicRequest, []*feed.Event) {
BlockGossipTopic,
DataColumnTopic,
PayloadAttestationMessageTopic,
ProposerPreferencesTopic,
})
require.NoError(t, err)
ro, err := blocks.NewROBlob(util.HydrateBlobSidecar(&eth.BlobSidecar{}))
@@ -328,6 +329,20 @@ func operationEventsFixtures(t *testing.T) (*topicRequest, []*feed.Event) {
},
},
},
{
Type: operation.ProposerPreferencesReceived,
Data: &operation.ProposerPreferencesReceivedData{
Preferences: &eth.SignedProposerPreferences{
Message: &eth.ProposerPreferences{
ProposalSlot: 32,
ValidatorIndex: 1,
FeeRecipient: make([]byte, 20),
GasLimit: 30000000,
},
Signature: make([]byte, fieldparams.BLSSignatureLength),
},
},
},
}
}

View File

@@ -18,6 +18,7 @@ go_library(
"//beacon-chain/blockchain:go_default_library",
"//beacon-chain/builder:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/feed:go_default_library",
"//beacon-chain/core/feed/operation:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/db:go_default_library",
@@ -57,11 +58,13 @@ go_test(
name = "go_default_test",
srcs = [
"handlers_block_test.go",
"handlers_gloas_test.go",
"handlers_test.go",
],
embed = [":go_default_library"],
deps = [
"//api:go_default_library",
"//api/server:go_default_library",
"//api/server/structs:go_default_library",
"//beacon-chain/blockchain/testing:go_default_library",
"//beacon-chain/builder/testing:go_default_library",

View File

@@ -1,9 +1,21 @@
package validator
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/OffchainLabs/prysm/v7/api"
"github.com/OffchainLabs/prysm/v7/api/server"
"github.com/OffchainLabs/prysm/v7/api/server/structs"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
"github.com/OffchainLabs/prysm/v7/network/httputil"
"github.com/OffchainLabs/prysm/v7/time/slots"
"github.com/pkg/errors"
)
// ProduceBlockV4 requests a beacon node to produce a valid Gloas block.
@@ -29,3 +41,86 @@ func (s *Server) ExecutionPayloadEnvelope(w http.ResponseWriter, r *http.Request
func (s *Server) PublishExecutionPayloadEnvelope(w http.ResponseWriter, r *http.Request) {
httputil.HandleError(w, "PublishExecutionPayloadEnvelope not yet implemented", http.StatusNotImplemented)
}
// SubmitProposerPreferences submits signed proposer preferences to the node's pool.
func (s *Server) SubmitProposerPreferences(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.SubmitProposerPreferences")
defer span.End()
if slots.ToEpoch(s.TimeFetcher.CurrentSlot()) < params.BeaconConfig().GloasForkEpoch {
httputil.HandleError(w, "Proposer preferences are not supported before the gloas fork", http.StatusBadRequest)
return
}
versionHeader := r.Header.Get(api.VersionHeader)
if versionHeader == "" {
httputil.HandleError(w, api.VersionHeader+" header is required", http.StatusBadRequest)
return
}
var req []*structs.SignedProposerPreferences
err := json.NewDecoder(r.Body).Decode(&req)
switch {
case errors.Is(err, io.EOF):
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
if len(req) == 0 {
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
}
currentEpoch := slots.ToEpoch(s.TimeFetcher.CurrentSlot())
var failures []*server.IndexedError
for i, sp := range req {
consensus, err := sp.ToConsensus()
if err != nil {
failures = append(failures, &server.IndexedError{
Index: i,
Message: "Unable to decode SignedProposerPreferences: " + err.Error(),
})
continue
}
if consensus.Message == nil {
failures = append(failures, &server.IndexedError{
Index: i,
Message: "Message is nil",
})
continue
}
proposalSlot := consensus.Message.ProposalSlot
if slots.ToEpoch(proposalSlot) != currentEpoch+1 {
failures = append(failures, &server.IndexedError{
Index: i,
Message: fmt.Sprintf("proposal_slot must be in the next epoch: slot %d currentEpoch %d", proposalSlot, currentEpoch),
})
continue
}
if s.ProposerPreferencesCache.Has(proposalSlot) {
continue
}
s.OperationNotifier.OperationFeed().Send(&feed.Event{
Type: operation.ProposerPreferencesReceived,
Data: &operation.ProposerPreferencesReceivedData{
Preferences: consensus,
},
})
s.ProposerPreferencesCache.Add(proposalSlot, consensus)
if err := s.Broadcaster.Broadcast(ctx, consensus); err != nil {
log.WithError(err).Error("Could not broadcast signed proposer preferences")
}
}
if len(failures) > 0 {
failuresErr := &server.IndexedErrorContainer{
Code: http.StatusBadRequest,
Message: server.ErrIndexedValidationFail,
Failures: failures,
}
httputil.WriteError(w, failuresErr)
}
}

View File

@@ -0,0 +1,208 @@
package validator
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/OffchainLabs/prysm/v7/api"
"github.com/OffchainLabs/prysm/v7/api/server"
blockchainmock "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
p2pMock "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/testing"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func TestSubmitProposerPreferences_PreFork(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 100
params.OverrideBeaconConfig(cfg)
s := &Server{TimeFetcher: &blockchainmock.ChainService{}}
request := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", strings.NewReader("[]"))
request.Header.Set(api.VersionHeader, "gloas")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitProposerPreferences(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
}
func TestSubmitProposerPreferences_OK(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
c := cache.NewProposerPreferencesCache()
currentSlot := primitives.Slot(31)
broadcaster := &p2pMock.MockBroadcaster{}
s := &Server{
ProposerPreferencesCache: c,
TimeFetcher: &blockchainmock.ChainService{Slot: &currentSlot},
OperationNotifier: &blockchainmock.MockOperationNotifier{},
Broadcaster: broadcaster,
}
feeRecipient := bytesutil.PadTo([]byte{0xaa}, 20)
sig := bytesutil.PadTo([]byte{0xcc}, 96)
body := fmt.Sprintf(`[{
"message": {
"proposal_slot": "32",
"validator_index": "5",
"fee_recipient": "%s",
"gas_limit": "30000000"
},
"signature": "%s"
}]`, hexutil.Encode(feeRecipient), hexutil.Encode(sig))
request := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", strings.NewReader(body))
request.Header.Set(api.VersionHeader, "gloas")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitProposerPreferences(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
require.Equal(t, true, c.Has(32))
pref, ok := c.Get(32)
require.Equal(t, true, ok)
assert.DeepEqual(t, feeRecipient, pref.Message.FeeRecipient)
assert.Equal(t, uint64(30000000), pref.Message.GasLimit)
}
func TestSubmitProposerPreferences_MissingVersionHeader(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
s := &Server{TimeFetcher: &blockchainmock.ChainService{}}
request := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", strings.NewReader("[]"))
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitProposerPreferences(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
}
func TestSubmitProposerPreferences_EmptyBody(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
s := &Server{TimeFetcher: &blockchainmock.ChainService{}}
request := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", nil)
request.Header.Set(api.VersionHeader, "gloas")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitProposerPreferences(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
}
func TestSubmitProposerPreferences_WrongEpoch(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
c := cache.NewProposerPreferencesCache()
currentSlot := primitives.Slot(31)
s := &Server{
ProposerPreferencesCache: c,
TimeFetcher: &blockchainmock.ChainService{Slot: &currentSlot},
}
feeRecipient := bytesutil.PadTo([]byte{0xaa}, 20)
sig := bytesutil.PadTo([]byte{0xcc}, 96)
body := fmt.Sprintf(`[{
"message": {
"proposal_slot": "0",
"validator_index": "5",
"fee_recipient": "%s",
"gas_limit": "30000000"
},
"signature": "%s"
}]`, hexutil.Encode(feeRecipient), hexutil.Encode(sig))
request := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", strings.NewReader(body))
request.Header.Set(api.VersionHeader, "gloas")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitProposerPreferences(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
e := &server.IndexedErrorContainer{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
require.Equal(t, 1, len(e.Failures))
assert.StringContains(t, "next epoch", e.Failures[0].Message)
}
func TestSubmitProposerPreferences_Duplicate(t *testing.T) {
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig().Copy()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
c := cache.NewProposerPreferencesCache()
currentSlot := primitives.Slot(31)
broadcaster := &p2pMock.MockBroadcaster{}
sp := &ethpb.SignedProposerPreferences{
Message: &ethpb.ProposerPreferences{
ProposalSlot: 32,
ValidatorIndex: 5,
FeeRecipient: bytesutil.PadTo([]byte{0xaa}, 20),
GasLimit: 30000000,
},
Signature: bytesutil.PadTo([]byte{0xcc}, 96),
}
c.Add(32, sp)
s := &Server{
ProposerPreferencesCache: c,
TimeFetcher: &blockchainmock.ChainService{Slot: &currentSlot},
OperationNotifier: &blockchainmock.MockOperationNotifier{},
Broadcaster: broadcaster,
}
feeRecipient := bytesutil.PadTo([]byte{0xbb}, 20)
sig := bytesutil.PadTo([]byte{0xdd}, 96)
body := fmt.Sprintf(`[{
"message": {
"proposal_slot": "32",
"validator_index": "6",
"fee_recipient": "%s",
"gas_limit": "30000001"
},
"signature": "%s"
}]`, hexutil.Encode(feeRecipient), hexutil.Encode(sig))
request := httptest.NewRequest(http.MethodPost, "http://example.com/eth/v1/validator/proposer_preferences", strings.NewReader(body))
request.Header.Set(api.VersionHeader, "gloas")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.SubmitProposerPreferences(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
pref, ok := c.Get(32)
require.Equal(t, true, ok)
assert.DeepEqual(t, bytesutil.PadTo([]byte{0xaa}, 20), pref.Message.FeeRecipient)
}

View File

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

View File

@@ -3,6 +3,8 @@ package validator
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed"
opfeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
@@ -69,7 +71,13 @@ func (vs *Server) SubmitSignedProposerPreferences(
broadcast, len(req.SignedProposerPreferences), err)
}
vs.ProposerPreferencesCache.Add(proposalSlot, msg.Message.FeeRecipient, msg.Message.GasLimit)
vs.ProposerPreferencesCache.Add(proposalSlot, msg)
vs.OperationNotifier.OperationFeed().Send(&feed.Event{
Type: opfeed.ProposerPreferencesReceived,
Data: &opfeed.ProposerPreferencesReceivedData{
Preferences: msg,
},
})
broadcast++
}

View File

@@ -53,8 +53,8 @@ func TestSubmitSignedProposerPreferences_OK(t *testing.T) {
assert.Equal(t, true, p2p.BroadcastCalled.Load())
pref, ok := cache.Get(proposalSlot)
require.Equal(t, true, ok)
require.DeepEqual(t, req.SignedProposerPreferences[0].Message.FeeRecipient, pref.FeeRecipient)
require.Equal(t, req.SignedProposerPreferences[0].Message.GasLimit, pref.GasLimit)
require.DeepEqual(t, req.SignedProposerPreferences[0].Message.FeeRecipient, pref.Message.FeeRecipient)
require.Equal(t, req.SignedProposerPreferences[0].Message.GasLimit, pref.Message.GasLimit)
}
func TestSubmitSignedProposerPreferences_Multiple(t *testing.T) {
@@ -105,7 +105,7 @@ func TestSubmitSignedProposerPreferences_Multiple(t *testing.T) {
require.Equal(t, true, ok)
pref2, ok := c.Get(currentSlot + 2)
require.Equal(t, true, ok)
require.Equal(t, uint64(25_000_000), pref2.GasLimit)
require.Equal(t, uint64(25_000_000), pref2.Message.GasLimit)
}
func TestSubmitSignedProposerPreferences_DuplicateSlot(t *testing.T) {
@@ -119,7 +119,9 @@ func TestSubmitSignedProposerPreferences_DuplicateSlot(t *testing.T) {
chain := &chainMock.ChainService{Slot: &currentSlot}
p2p := &p2pmock.MockBroadcaster{}
c := cache.NewProposerPreferencesCache()
c.Add(proposalSlot, make([]byte, 20), 30_000_000)
c.Add(proposalSlot, &ethpb.SignedProposerPreferences{
Message: &ethpb.ProposerPreferences{ProposalSlot: proposalSlot, FeeRecipient: make([]byte, 20), GasLimit: 30_000_000},
})
vs := &Server{
SyncChecker: &mockSync.Sync{IsSyncing: false},
TimeFetcher: chain,

View File

@@ -75,11 +75,11 @@ func (s *Service) validateExecutionPayloadBidGossip(ctx context.Context, pid pee
return pubsub.ValidationReject, err
}
// [REJECT] bid.fee_recipient matches the fee_recipient from the proposer's SignedProposerPreferences associated with bid.slot.
if err := v.VerifyFeeRecipientMatches(pref.FeeRecipient); err != nil {
if err := v.VerifyFeeRecipientMatches(pref.Message.FeeRecipient); err != nil {
return pubsub.ValidationReject, err
}
// [REJECT] bid.gas_limit matches the gas_limit from the proposer's SignedProposerPreferences associated with bid.slot.
if err := v.VerifyGasLimitMatches(pref.GasLimit); err != nil {
if err := v.VerifyGasLimitMatches(pref.Message.GasLimit); err != nil {
return pubsub.ValidationReject, err
}
// The spec lists signature validation later, but the "first signed bid seen

View File

@@ -363,7 +363,13 @@ func setupExecutionPayloadBidService(t *testing.T) (*Service, *pubsub.Message, *
}
signedBid := util.GenerateTestSignedExecutionPayloadBid(1)
signedBid.Message.BuilderIndex = 1
require.Equal(t, true, s.proposerPreferencesCache.Add(signedBid.Message.Slot, signedBid.Message.FeeRecipient, signedBid.Message.GasLimit))
require.Equal(t, true, s.proposerPreferencesCache.Add(signedBid.Message.Slot, &ethpb.SignedProposerPreferences{
Message: &ethpb.ProposerPreferences{
ProposalSlot: signedBid.Message.Slot,
FeeRecipient: signedBid.Message.FeeRecipient,
GasLimit: signedBid.Message.GasLimit,
},
}))
msg := executionPayloadBidToPubsub(t, s, p, signedBid)
return s, msg, signedBid
}

View File

@@ -3,6 +3,8 @@ package sync
import (
"context"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed"
opfeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/operation"
"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
@@ -68,15 +70,21 @@ func (s *Service) validateSignedProposerPreferencesGossip(ctx context.Context, p
return pubsub.ValidationReject, err
}
s.proposerPreferencesCache.Add(slot, signedPreferences.Message.FeeRecipient, signedPreferences.Message.GasLimit)
s.proposerPreferencesCache.Add(slot, signedPreferences)
msg.ValidatorData = signedPreferences
return pubsub.ValidationAccept, nil
}
func (s *Service) signedProposerPreferencesSubscriber(_ context.Context, msg proto.Message) error {
_, ok := msg.(*ethpb.SignedProposerPreferences)
sp, ok := msg.(*ethpb.SignedProposerPreferences)
if !ok {
return errWrongMessage
}
s.cfg.operationNotifier.OperationFeed().Send(&feed.Event{
Type: opfeed.ProposerPreferencesReceived,
Data: &opfeed.ProposerPreferencesReceivedData{
Preferences: sp,
},
})
return nil
}

View File

@@ -96,7 +96,9 @@ func TestValidateSignedProposerPreferencesGossip_AlreadySeenSlot(t *testing.T) {
s, msg, signedPreferences := setupSignedProposerPreferencesService(t)
s.newSignedProposerPreferencesVerifier = testNewSignedProposerPreferencesVerifier(mockSignedProposerPreferencesVerifier{})
require.Equal(t, true, s.proposerPreferencesCache.Add(signedPreferences.Message.ProposalSlot, []byte{0x01}, 10))
require.Equal(t, true, s.proposerPreferencesCache.Add(signedPreferences.Message.ProposalSlot, &ethpb.SignedProposerPreferences{
Message: &ethpb.ProposerPreferences{ProposalSlot: signedPreferences.Message.ProposalSlot, FeeRecipient: []byte{0x01}, GasLimit: 10},
}))
result, err := s.validateSignedProposerPreferencesGossip(ctx, "", msg)
require.NoError(t, err)
require.Equal(t, pubsub.ValidationIgnore, result)
@@ -114,21 +116,25 @@ func TestValidateSignedProposerPreferencesGossip_HappyPath(t *testing.T) {
got, ok := s.proposerPreferencesCache.Get(signedPreferences.Message.ProposalSlot)
require.Equal(t, true, ok)
require.DeepEqual(t, signedPreferences.Message.FeeRecipient, got.FeeRecipient)
require.Equal(t, signedPreferences.Message.GasLimit, got.GasLimit)
require.DeepEqual(t, signedPreferences.Message.FeeRecipient, got.Message.FeeRecipient)
require.Equal(t, signedPreferences.Message.GasLimit, got.Message.GasLimit)
validatorData, ok := msg.ValidatorData.(*ethpb.SignedProposerPreferences)
require.Equal(t, true, ok)
require.DeepEqual(t, signedPreferences, validatorData)
}
func TestSignedProposerPreferencesSubscriber_WrongMessage(t *testing.T) {
s := &Service{}
s := &Service{
cfg: &config{operationNotifier: &mock.MockOperationNotifier{}},
}
err := s.signedProposerPreferencesSubscriber(context.Background(), &ethpb.BeaconBlock{})
require.ErrorIs(t, errWrongMessage, err)
}
func TestSignedProposerPreferencesSubscriber_HappyPath(t *testing.T) {
s := &Service{}
s := &Service{
cfg: &config{operationNotifier: &mock.MockOperationNotifier{}},
}
err := s.signedProposerPreferencesSubscriber(context.Background(), &ethpb.SignedProposerPreferences{})
require.NoError(t, err)
}