Compare commits

..

8 Commits

Author SHA1 Message Date
Preston Van Loon
9db91f5366 hdiff restart-support: validate on startup by default 2026-02-10 10:40:26 -06:00
Preston Van Loon
ab748a2f4c hdiff restart-support: validate cache coherency 2026-02-10 10:40:26 -06:00
Preston Van Loon
3c3fb85e4e hdiff restart-support: add error types and startup handling 2026-02-10 10:40:26 -06:00
Preston Van Loon
5daf272764 hdiff restart-support: rehydrate cache on restart 2026-02-10 10:40:26 -06:00
Preston Van Loon
99c3fca00a hdiff restart-support: populate cache from DB 2026-02-10 10:40:26 -06:00
Preston Van Loon
5351671d08 hdiff restart-support: add offset loader 2026-02-10 10:40:26 -06:00
Preston Van Loon
28910ef558 hdiff restart-support: persist and validate exponents metadata 2026-02-10 10:40:26 -06:00
Preston Van Loon
da53492588 hdiff restart-support: add exponents encoding helpers 2026-02-10 10:40:26 -06:00
21 changed files with 597 additions and 599 deletions

View File

@@ -74,18 +74,6 @@ type SyncCommitteeDuty struct {
ValidatorSyncCommitteeIndices []string `json:"validator_sync_committee_indices"`
}
type GetPTCDutiesResponse struct {
DependentRoot string `json:"dependent_root"`
ExecutionOptimistic bool `json:"execution_optimistic"`
Data []*PTCDuty `json:"data"`
}
type PTCDuty struct {
Pubkey string `json:"pubkey"`
ValidatorIndex string `json:"validator_index"`
Slot string `json:"slot"`
}
// ProduceBlockV3Response is a wrapper json object for the returned block from the ProduceBlockV3 endpoint
type ProduceBlockV3Response struct {
Version string `json:"version"`

View File

@@ -156,65 +156,6 @@ func payloadCommittee(ctx context.Context, st state.ReadOnlyBeaconState, slot pr
return selected, nil
}
// PTCDuty represents a validator's PTC assignment for a slot.
type PTCDuty struct {
ValidatorIndex primitives.ValidatorIndex
Slot primitives.Slot
}
// PTCDuties returns PTC slot assignments for the requested validators in the given epoch.
// It's optimized for batch lookups with early exit once all validators are found.
// Validators not in any PTC for the epoch will not appear in the result.
func PTCDuties(
ctx context.Context,
st state.ReadOnlyBeaconState,
epoch primitives.Epoch,
validators map[primitives.ValidatorIndex]struct{},
) ([]PTCDuty, error) {
if len(validators) == 0 {
return nil, nil
}
startSlot, err := slots.EpochStart(epoch)
if err != nil {
return nil, err
}
// Track remaining validators to find.
remaining := make(map[primitives.ValidatorIndex]struct{}, len(validators))
for v := range validators {
remaining[v] = struct{}{}
}
var duties []PTCDuty
endSlot := startSlot + params.BeaconConfig().SlotsPerEpoch
for slot := startSlot; slot < endSlot && len(remaining) > 0; slot++ {
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Compute PTC for this slot.
ptc, err := payloadCommittee(ctx, st, slot)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PTC for slot %d", slot)
}
// Check which remaining validators are in this PTC.
for _, valIdx := range ptc {
if _, ok := remaining[valIdx]; ok {
duties = append(duties, PTCDuty{
ValidatorIndex: valIdx,
Slot: slot,
})
delete(remaining, valIdx)
}
}
}
return duties, nil
}
// ptcSeed computes the seed for the payload timeliness committee.
func ptcSeed(st state.ReadOnlyBeaconState, epoch primitives.Epoch, slot primitives.Slot) ([32]byte, error) {
seed, err := helpers.Seed(st, epoch, params.BeaconConfig().DomainPTCAttester)

View File

@@ -291,95 +291,6 @@ func signAttestation(t *testing.T, st state.ReadOnlyBeaconState, data *eth.Paylo
return agg.Marshal()
}
func TestPTCDuties(t *testing.T) {
helpers.ClearCache()
setupTestConfig(t)
// Create state with enough validators.
numVals := 100
vals := make([]*eth.Validator, numVals)
for i := range numVals {
_, pk := newKey(t)
vals[i] = activeValidator(pk)
}
st := newTestState(t, vals, 0)
t.Run("returns duties for validators in PTC", func(t *testing.T) {
// Request duties for all validators.
requested := make(map[primitives.ValidatorIndex]struct{})
for i := range numVals {
requested[primitives.ValidatorIndex(i)] = struct{}{}
}
duties, err := gloas.PTCDuties(t.Context(), st, 0, requested)
require.NoError(t, err)
require.NotEmpty(t, duties, "Should return some duties")
// Verify all returned duties are for requested validators.
for _, duty := range duties {
_, ok := requested[duty.ValidatorIndex]
require.Equal(t, true, ok, "Returned validator should be in requested set")
}
})
t.Run("returns empty for empty request", func(t *testing.T) {
requested := make(map[primitives.ValidatorIndex]struct{})
duties, err := gloas.PTCDuties(t.Context(), st, 0, requested)
require.NoError(t, err)
require.Equal(t, 0, len(duties), "Should return no duties for empty request")
})
t.Run("returns empty for validators not in any PTC", func(t *testing.T) {
// Request duties for validators that don't exist.
requested := make(map[primitives.ValidatorIndex]struct{})
for i := 1000000; i < 1000010; i++ {
requested[primitives.ValidatorIndex(i)] = struct{}{}
}
duties, err := gloas.PTCDuties(t.Context(), st, 0, requested)
require.NoError(t, err)
require.Equal(t, 0, len(duties), "Non-existent validators should have no duties")
})
t.Run("each validator has at most one duty per epoch", func(t *testing.T) {
requested := make(map[primitives.ValidatorIndex]struct{})
for i := range numVals {
requested[primitives.ValidatorIndex(i)] = struct{}{}
}
duties, err := gloas.PTCDuties(t.Context(), st, 0, requested)
require.NoError(t, err)
// Check for duplicates.
seen := make(map[primitives.ValidatorIndex]bool)
for _, duty := range duties {
if seen[duty.ValidatorIndex] {
t.Errorf("Validator %d appears in duties multiple times", duty.ValidatorIndex)
}
seen[duty.ValidatorIndex] = true
}
})
t.Run("duties are deterministic", func(t *testing.T) {
requested := make(map[primitives.ValidatorIndex]struct{})
for i := range 50 {
requested[primitives.ValidatorIndex(i)] = struct{}{}
}
duties1, err := gloas.PTCDuties(t.Context(), st, 0, requested)
require.NoError(t, err)
duties2, err := gloas.PTCDuties(t.Context(), st, 0, requested)
require.NoError(t, err)
require.Equal(t, len(duties1), len(duties2), "Should return same number of duties")
for i := range duties1 {
require.Equal(t, duties1[i].ValidatorIndex, duties2[i].ValidatorIndex)
require.Equal(t, duties1[i].Slot, duties2[i].Slot)
}
})
}
type validatorLookupErrState struct {
state.BeaconState
errIndex primitives.ValidatorIndex

View File

@@ -26,6 +26,15 @@ var ErrNotFoundMetadataSeqNum = errors.Wrap(ErrNotFound, "metadata sequence numb
// but the database was created without state-diff support.
var ErrStateDiffIncompatible = errors.New("state-diff feature enabled but database was created without state-diff support")
// ErrStateDiffCorrupted is returned when state-diff metadata or data is missing or invalid.
var ErrStateDiffCorrupted = errors.New("state-diff database corrupted")
// ErrStateDiffExponentMismatch is returned when configured exponents differ from stored metadata.
var ErrStateDiffExponentMismatch = errors.New("state-diff exponents mismatch")
// ErrStateDiffMissingSnapshot is returned when the offset snapshot is missing.
var ErrStateDiffMissingSnapshot = errors.New("state-diff offset snapshot missing")
var errEmptyBlockSlice = errors.New("[]blocks.ROBlock is empty")
var errIncorrectBlockParent = errors.New("unexpected missing or forked blocks in a []ROBlock")
var errFinalizedChildNotFound = errors.New("unable to find finalized root descending from backfill batch")

View File

@@ -7,9 +7,11 @@ import (
"fmt"
"os"
"path"
"slices"
"time"
"github.com/OffchainLabs/prysm/v7/beacon-chain/db/iface"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/features"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
@@ -21,6 +23,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
prombolt "github.com/prysmaticlabs/prombbolt"
logrus "github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
@@ -223,8 +226,42 @@ func (kv *Store) startStateDiff(ctx context.Context) error {
}
if hasOffset {
// Existing state-diff database - restarts not yet supported.
return errors.New("restarting with existing state-diff database not yet supported")
storedExponents, err := kv.loadStateDiffExponents()
if err != nil {
return fmt.Errorf("%w: state-diff metadata missing or invalid; re-sync required: %v", ErrStateDiffCorrupted, err)
}
currentExponents := flags.Get().StateDiffExponents
if !slices.Equal(storedExponents, currentExponents) {
return errors.Wrapf(
ErrStateDiffExponentMismatch,
"state-diff exponents changed; database incompatible. "+
"Database was initialized with: %v. "+
"Current configuration: %v. "+
"Options: use original exponents (--state-diff-exponents=%s) or delete database and re-sync from genesis/checkpoint.",
storedExponents,
currentExponents,
formatStateDiffExponents(storedExponents),
)
}
offset, err := kv.loadOffset()
if err != nil {
return err
}
cache, err := populateStateDiffCacheFromDB(kv, offset)
if err != nil {
return err
}
kv.stateDiffCache = cache
if flags.Get().StateDiffValidateOnStartup {
if err := validateStateDiffCache(ctx, kv, cache); err != nil {
return err
}
}
log.WithFields(logrus.Fields{
"offset": offset,
"exponents": storedExponents,
}).Info("State-diff cache initialized from existing database")
return nil
}
// Check if this is a new database (no head block).

View File

@@ -3,12 +3,15 @@ package kv
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"testing"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/features"
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/runtime/version"
"github.com/OffchainLabs/prysm/v7/testing/require"
"github.com/OffchainLabs/prysm/v7/testing/util"
bolt "go.etcd.io/bbolt"
@@ -27,6 +30,108 @@ func setupDB(t testing.TB) *Store {
return db
}
func TestStartStateDiff_ExponentMismatch(t *testing.T) {
resetCfg := features.InitWithReset(&features.Flags{EnableStateDiff: true})
defer resetCfg()
setDefaultStateDiffExponents()
store := setupDB(t)
require.NoError(t, store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
offsetBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(offsetBytes, 0)
if err := bucket.Put(offsetKey, offsetBytes); err != nil {
return err
}
encoded, err := encodeStateDiffExponents([]int{20, 10})
if err != nil {
return err
}
return bucket.Put(exponentsKey, encoded)
}))
ctx := t.Context()
err := store.startStateDiff(ctx)
require.ErrorContains(t, "state-diff exponents changed", err)
}
func TestStartStateDiff_MissingOffsetSnapshot(t *testing.T) {
resetCfg := features.InitWithReset(&features.Flags{EnableStateDiff: true})
defer resetCfg()
setDefaultStateDiffExponents()
store := setupDB(t)
require.NoError(t, store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
offsetBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(offsetBytes, 0)
if err := bucket.Put(offsetKey, offsetBytes); err != nil {
return err
}
encoded, err := encodeStateDiffExponents(flags.Get().StateDiffExponents)
if err != nil {
return err
}
return bucket.Put(exponentsKey, encoded)
}))
ctx := t.Context()
err := store.startStateDiff(ctx)
require.ErrorContains(t, "missing offset snapshot", err)
}
func TestStartStateDiff_ValidateOnStartup(t *testing.T) {
resetCfg := features.InitWithReset(&features.Flags{EnableStateDiff: true})
defer resetCfg()
setDefaultStateDiffExponents()
globalFlags := flags.GlobalFlags{
StateDiffExponents: flags.Get().StateDiffExponents,
StateDiffValidateOnStartup: true,
}
flags.Init(&globalFlags)
store := setupDB(t)
require.NoError(t, store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bolt.ErrBucketNotFound
}
st, _ := createState(t, 0, version.Phase0)
stateBytes, err := st.MarshalSSZ()
if err != nil {
return err
}
enc, err := addKey(st.Version(), stateBytes)
if err != nil {
return err
}
offsetBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(offsetBytes, 0)
if err := bucket.Put(offsetKey, offsetBytes); err != nil {
return err
}
encoded, err := encodeStateDiffExponents(flags.Get().StateDiffExponents)
if err != nil {
return err
}
if err := bucket.Put(exponentsKey, encoded); err != nil {
return err
}
key := makeKeyForStateDiffTree(0, 0)
return bucket.Put(key, enc)
}))
err := store.startStateDiff(t.Context())
require.NoError(t, err)
}
func Test_setupBlockStorageType(t *testing.T) {
ctx := t.Context()
t.Run("fresh database with feature enabled to store full blocks should store full blocks", func(t *testing.T) {

View File

@@ -132,6 +132,9 @@ func (s *Store) saveHdiff(lvl int, anchor, st state.ReadOnlyBeaconState) error {
return err
}
}
if err := s.stateDiffCache.setLevelHasData(lvl); err != nil {
return err
}
return nil
}
@@ -171,6 +174,9 @@ func (s *Store) saveFullSnapshot(st state.ReadOnlyBeaconState) error {
if err != nil {
return err
}
if err := s.stateDiffCache.setLevelHasData(0); err != nil {
return err
}
return nil
}

View File

@@ -1,19 +1,131 @@
package kv
import (
"context"
"encoding/binary"
"errors"
"sync"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
pkgerrors "github.com/pkg/errors"
"go.etcd.io/bbolt"
)
type stateDiffCache struct {
sync.RWMutex
anchors []state.ReadOnlyBeaconState
offset uint64
anchors []state.ReadOnlyBeaconState
levelsWithData []bool
offset uint64
}
func populateStateDiffCacheFromDB(s *Store, offset uint64) (*stateDiffCache, error) {
cache := &stateDiffCache{
anchors: make([]state.ReadOnlyBeaconState, len(flags.Get().StateDiffExponents)-1),
levelsWithData: make([]bool, len(flags.Get().StateDiffExponents)),
offset: offset,
}
if err := s.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
for level := range cache.levelsWithData {
if level == 0 {
if bucket.Get(makeKeyForStateDiffTree(0, offset)) != nil {
cache.levelsWithData[level] = true
}
continue
}
cursor := bucket.Cursor()
prefix := []byte{byte(level)}
key, _ := cursor.Seek(prefix)
if key != nil && key[0] == byte(level) {
slot, ok := slotFromStateDiffKey(key)
if !ok {
return ErrStateDiffCorrupted
}
if slot < offset {
return ErrStateDiffCorrupted
}
if level == 0 && slot != offset {
return ErrStateDiffCorrupted
}
if computeLevel(offset, primitives.Slot(slot)) != level {
return ErrStateDiffCorrupted
}
cache.levelsWithData[level] = true
}
}
return nil
}); err != nil {
return nil, err
}
anchor0, err := s.getFullSnapshot(offset)
if err != nil {
return nil, pkgerrors.Wrapf(ErrStateDiffMissingSnapshot, "state diff cache: missing offset snapshot at %d", offset)
}
cache.anchors[0] = anchor0
cache.levelsWithData[0] = true
return cache, nil
}
func validateStateDiffCache(ctx context.Context, s *Store, cache *stateDiffCache) error {
for level, hasData := range cache.levelsWithData {
if !hasData || level == 0 {
continue
}
maxSlot, err := latestSlotForLevel(s, level)
if err != nil {
return err
}
if _, err := s.stateByDiff(ctx, primitives.Slot(maxSlot)); err != nil {
return pkgerrors.Wrapf(ErrStateDiffCorrupted, "state diff validation failed for level %d slot %d: %v", level, maxSlot, err)
}
}
return nil
}
func latestSlotForLevel(s *Store, level int) (uint64, error) {
var maxSlot uint64
found := false
err := s.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
cursor := bucket.Cursor()
prefix := []byte{byte(level)}
for key, _ := cursor.Seek(prefix); key != nil && key[0] == byte(level); key, _ = cursor.Next() {
slot, ok := slotFromStateDiffKey(key)
if !ok {
return ErrStateDiffCorrupted
}
if !found || slot > maxSlot {
maxSlot = slot
found = true
}
}
return nil
})
if err != nil {
return 0, err
}
if !found {
return 0, ErrStateDiffCorrupted
}
return maxSlot, nil
}
func slotFromStateDiffKey(key []byte) (uint64, bool) {
if len(key) < 9 {
return 0, false
}
return binary.LittleEndian.Uint64(key[1:9]), true
}
func newStateDiffCache(s *Store) (*stateDiffCache, error) {
@@ -37,8 +149,9 @@ func newStateDiffCache(s *Store) (*stateDiffCache, error) {
}
return &stateDiffCache{
anchors: make([]state.ReadOnlyBeaconState, len(flags.Get().StateDiffExponents)-1), // -1 because last level doesn't need to be cached
offset: offset,
anchors: make([]state.ReadOnlyBeaconState, len(flags.Get().StateDiffExponents)-1), // -1 because last level doesn't need to be cached
levelsWithData: make([]bool, len(flags.Get().StateDiffExponents)),
offset: offset,
}, nil
}
@@ -58,6 +171,25 @@ func (c *stateDiffCache) setAnchor(level int, anchor state.ReadOnlyBeaconState)
return nil
}
func (c *stateDiffCache) levelHasData(level int) bool {
c.RLock()
defer c.RUnlock()
if level < 0 || level >= len(c.levelsWithData) {
return false
}
return c.levelsWithData[level]
}
func (c *stateDiffCache) setLevelHasData(level int) error {
c.Lock()
defer c.Unlock()
if level < 0 || level >= len(c.levelsWithData) {
return errors.New("state diff cache: level data index out of range")
}
c.levelsWithData[level] = true
return nil
}
func (c *stateDiffCache) getOffset() uint64 {
c.RLock()
defer c.RUnlock()

View File

@@ -5,6 +5,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"strings"
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
statenative "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
@@ -21,9 +22,78 @@ import (
var (
offsetKey = []byte("offset")
exponentsKey = []byte("exponents")
ErrSlotBeforeOffset = errors.New("slot is before state-diff root offset")
)
func encodeStateDiffExponents(exponents []int) ([]byte, error) {
if len(exponents) == 0 {
return nil, errors.New("state diff exponents cannot be empty")
}
if len(exponents) > 255 {
return nil, fmt.Errorf("state diff exponents length %d exceeds max 255", len(exponents))
}
encoded := make([]byte, len(exponents)+1)
encoded[0] = byte(len(exponents))
for i, exp := range exponents {
if exp < 2 || exp > flags.MaxStateDiffExponent {
return nil, fmt.Errorf("state diff exponent %d out of range for encoding", exp)
}
encoded[i+1] = byte(exp)
}
return encoded, nil
}
func decodeStateDiffExponents(encoded []byte) ([]int, error) {
if len(encoded) == 0 {
return nil, errors.New("state diff exponents missing length prefix")
}
count := int(encoded[0])
if count == 0 {
return nil, errors.New("state diff exponents length cannot be zero")
}
if len(encoded) != count+1 {
return nil, fmt.Errorf("state diff exponents length mismatch: expected %d got %d", count, len(encoded)-1)
}
exponents := make([]int, count)
for i := range count {
exponents[i] = int(encoded[i+1])
}
return exponents, nil
}
func formatStateDiffExponents(exponents []int) string {
if len(exponents) == 0 {
return ""
}
parts := make([]string, len(exponents))
for i, exp := range exponents {
parts[i] = fmt.Sprintf("%d", exp)
}
return strings.Join(parts, ",")
}
func (s *Store) loadStateDiffExponents() ([]int, error) {
var encoded []byte
err := s.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
value := bucket.Get(exponentsKey)
if value == nil {
return errors.New("state diff exponents not found")
}
encoded = make([]byte, len(value))
copy(encoded, value)
return nil
})
if err != nil {
return nil, err
}
return decodeStateDiffExponents(encoded)
}
func makeKeyForStateDiffTree(level int, slot uint64) []byte {
buf := make([]byte, 16)
buf[0] = byte(level)
@@ -124,6 +194,29 @@ func (s *Store) getOffset() uint64 {
return s.stateDiffCache.getOffset()
}
func (s *Store) loadOffset() (uint64, error) {
var offset uint64
err := s.db.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
offsetBytes := bucket.Get(offsetKey)
if offsetBytes == nil {
return errors.New("state diff offset not found")
}
if len(offsetBytes) != 8 {
return fmt.Errorf("state diff offset has invalid length %d", len(offsetBytes))
}
offset = binary.LittleEndian.Uint64(offsetBytes)
return nil
})
if err != nil {
return 0, err
}
return offset, nil
}
// hasStateDiffOffset checks if the state-diff offset has been set in the database.
// This is used to detect if an existing database has state-diff enabled.
func (s *Store) hasStateDiffOffset() (bool, error) {
@@ -153,8 +246,13 @@ func (s *Store) initializeStateDiff(slot primitives.Slot, initialState state.Rea
return nil
}
}
// Write offset directly to the database (without using cache which doesn't exist yet).
err := s.db.Update(func(tx *bbolt.Tx) error {
exponentsBytes, err := encodeStateDiffExponents(flags.Get().StateDiffExponents)
if err != nil {
return pkgerrors.Wrap(err, "failed to encode state diff exponents")
}
// Write metadata directly to the database (without using cache which doesn't exist yet).
err = s.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
@@ -162,7 +260,10 @@ func (s *Store) initializeStateDiff(slot primitives.Slot, initialState state.Rea
offsetBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(offsetBytes, uint64(slot))
return bucket.Put(offsetKey, offsetBytes)
if err := bucket.Put(offsetKey, offsetBytes); err != nil {
return err
}
return bucket.Put(exponentsKey, exponentsBytes)
})
if err != nil {
return pkgerrors.Wrap(err, "failed to set offset")
@@ -293,7 +394,12 @@ func (s *Store) getBaseAndDiffChain(offset uint64, slot primitives.Slot) (state.
if diffSlot == lastSeenAnchorSlot {
continue
}
diffChainItems = append(diffChainItems, diffItem{level: i + 1, slot: diffSlot + offset})
level := i + 1
if s.stateDiffCache != nil && !s.stateDiffCache.levelHasData(level) {
lastSeenAnchorSlot = diffSlot
continue
}
diffChainItems = append(diffChainItems, diffItem{level: level, slot: diffSlot + offset})
lastSeenAnchorSlot = diffSlot
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v7/config/features"
"github.com/OffchainLabs/prysm/v7/config/params"
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v7/math"
@@ -34,6 +35,81 @@ func TestStateDiff_LoadOrInitOffset(t *testing.T) {
require.Equal(t, uint64(10), offset)
}
func TestStateDiff_LoadOffset(t *testing.T) {
setDefaultStateDiffExponents()
db := setupDB(t)
_, err := db.loadOffset()
require.ErrorContains(t, "offset not found", err)
err = setOffsetInDB(db, 10)
require.NoError(t, err)
offset, err := db.loadOffset()
require.NoError(t, err)
require.Equal(t, uint64(10), offset)
}
func TestStateDiff_EncodeDecodeExponents(t *testing.T) {
t.Run("roundtrip", func(t *testing.T) {
exponents := []int{21, 18, 16, 13}
encoded, err := encodeStateDiffExponents(exponents)
require.NoError(t, err)
decoded, err := decodeStateDiffExponents(encoded)
require.NoError(t, err)
require.DeepEqual(t, exponents, decoded)
})
t.Run("encode-empty", func(t *testing.T) {
_, err := encodeStateDiffExponents(nil)
require.ErrorContains(t, "cannot be empty", err)
})
t.Run("encode-negative", func(t *testing.T) {
_, err := encodeStateDiffExponents([]int{21, -1})
require.ErrorContains(t, "out of range", err)
})
t.Run("encode-too-large", func(t *testing.T) {
_, err := encodeStateDiffExponents([]int{flags.MaxStateDiffExponent + 1})
require.ErrorContains(t, "out of range", err)
})
t.Run("decode-empty", func(t *testing.T) {
_, err := decodeStateDiffExponents(nil)
require.ErrorContains(t, "missing length prefix", err)
})
t.Run("decode-zero-length", func(t *testing.T) {
_, err := decodeStateDiffExponents([]byte{0})
require.ErrorContains(t, "length cannot be zero", err)
})
t.Run("decode-length-mismatch", func(t *testing.T) {
_, err := decodeStateDiffExponents([]byte{2, 10})
require.ErrorContains(t, "length mismatch", err)
})
}
func TestStateDiff_InitializeStoresExponents(t *testing.T) {
setDefaultStateDiffExponents()
resetCfg := features.InitWithReset(&features.Flags{EnableStateDiff: true})
defer resetCfg()
db := setupDB(t)
st, _ := createState(t, 0, version.Phase0)
require.NoError(t, db.initializeStateDiff(0, st))
stored, err := db.loadStateDiffExponents()
require.NoError(t, err)
require.DeepEqual(t, flags.Get().StateDiffExponents, stored)
}
func TestStateDiff_LoadExponentsMissing(t *testing.T) {
db := setupDB(t)
_, err := db.loadStateDiffExponents()
require.ErrorContains(t, "exponents not found", err)
}
func TestStateDiff_ComputeLevel(t *testing.T) {
db := setupDB(t)
setDefaultStateDiffExponents()
@@ -154,6 +230,94 @@ func TestStateDiff_SaveFullSnapshot(t *testing.T) {
}
}
func TestStateDiff_PopulateStateDiffCacheFromDB(t *testing.T) {
setDefaultStateDiffExponents()
db := setupDB(t)
_, err := populateStateDiffCacheFromDB(db, 0)
require.ErrorContains(t, "missing offset snapshot", err)
st, _ := createState(t, 0, version.Phase0)
require.NoError(t, setOffsetInDB(db, 0))
require.NoError(t, db.saveStateByDiff(context.Background(), st))
err = db.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
key := makeKeyForStateDiffTree(2, math.PowerOf2(16))
return bucket.Put(append(key, stateSuffix...), []byte{1})
})
require.NoError(t, err)
cache, err := populateStateDiffCacheFromDB(db, 0)
require.NoError(t, err)
require.NotNil(t, cache)
require.Equal(t, uint64(0), cache.getOffset())
require.NotNil(t, cache.getAnchor(0))
require.Equal(t, true, cache.levelHasData(0))
require.Equal(t, false, cache.levelHasData(1))
require.Equal(t, true, cache.levelHasData(2))
}
func TestStateDiff_PopulateStateDiffCacheFromDB_InvalidLevelKey(t *testing.T) {
setDefaultStateDiffExponents()
db := setupDB(t)
st, _ := createState(t, 0, version.Phase0)
require.NoError(t, setOffsetInDB(db, 0))
require.NoError(t, db.saveStateByDiff(context.Background(), st))
require.NoError(t, db.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
key := makeKeyForStateDiffTree(2, 1)
return bucket.Put(append(key, stateSuffix...), []byte{1})
}))
_, err := populateStateDiffCacheFromDB(db, 0)
require.ErrorIs(t, ErrStateDiffCorrupted, err)
}
func TestStateDiff_GetBaseAndDiffChainSkipsEmptyLevels(t *testing.T) {
setDefaultStateDiffExponents()
db := setupDB(t)
require.NoError(t, setOffsetInDB(db, 0))
st, _ := createState(t, 0, version.Phase0)
require.NoError(t, db.saveFullSnapshot(st))
cache, err := populateStateDiffCacheFromDB(db, 0)
require.NoError(t, err)
cache.levelsWithData[0] = true
cache.levelsWithData[1] = false
cache.levelsWithData[2] = true
db.stateDiffCache = cache
slot := primitives.Slot(math.PowerOf2(18) + math.PowerOf2(16))
key := makeKeyForStateDiffTree(2, uint64(slot))
require.NoError(t, db.db.Update(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(stateDiffBucket)
if bucket == nil {
return bbolt.ErrBucketNotFound
}
if err := bucket.Put(append(key, stateSuffix...), []byte{1}); err != nil {
return err
}
if err := bucket.Put(append(key, validatorSuffix...), []byte{2}); err != nil {
return err
}
return bucket.Put(append(key, balancesSuffix...), []byte{3})
}))
_, diffChain, err := db.getBaseAndDiffChain(0, slot)
require.NoError(t, err)
require.Equal(t, 1, len(diffChain))
}
func TestStateDiff_SaveAndReadFullSnapshot(t *testing.T) {
setDefaultStateDiffExponents()

View File

@@ -550,6 +550,12 @@ func openDB(ctx context.Context, dbPath string, clearer *dbClearer) (*kv.Store,
cfg := features.Get()
cfg.EnableStateDiff = false
features.Init(cfg)
} else if errors.Is(err, kv.ErrStateDiffExponentMismatch) {
log.WithError(err).Error("State-diff configuration mismatch; restart aborted. Use the stored exponents or re-sync the database.")
return nil, err
} else if errors.Is(err, kv.ErrStateDiffMissingSnapshot) || errors.Is(err, kv.ErrStateDiffCorrupted) {
log.WithError(err).Error("State-diff database corrupted; restart aborted. Delete database and re-sync from genesis/checkpoint.")
return nil, err
} else if err != nil {
return nil, errors.Wrapf(err, "could not create database at %s", dbPath)
}

View File

@@ -340,17 +340,6 @@ func (s *Service) validatorEndpoints(
handler: server.GetSyncCommitteeDuties,
methods: []string{http.MethodPost},
},
{
template: "/eth/v1/validator/duties/ptc/{epoch}",
name: namespace + ".GetPTCDuties",
middleware: []middleware.Middleware{
middleware.ContentTypeHandler([]string{api.JsonMediaType}),
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
middleware.AcceptEncodingHeaderHandler(),
},
handler: server.GetPTCDuties,
methods: []string{http.MethodPost},
},
{
template: "/eth/v1/validator/prepare_beacon_proposer",
name: namespace + ".PrepareBeaconProposer",

View File

@@ -94,7 +94,6 @@ func Test_endpoints(t *testing.T) {
"/eth/v1/validator/duties/attester/{epoch}": {http.MethodPost},
"/eth/v1/validator/duties/proposer/{epoch}": {http.MethodGet},
"/eth/v1/validator/duties/sync/{epoch}": {http.MethodPost},
"/eth/v1/validator/duties/ptc/{epoch}": {http.MethodPost},
"/eth/v3/validator/blocks/{slot}": {http.MethodGet},
"/eth/v1/validator/attestation_data": {http.MethodGet},
"/eth/v2/validator/aggregate_attestation": {http.MethodGet},

View File

@@ -18,7 +18,6 @@ go_library(
"//beacon-chain/builder:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/feed/operation:go_default_library",
"//beacon-chain/core/gloas:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/operations/attestations:go_default_library",
@@ -68,7 +67,6 @@ go_test(
"//beacon-chain/blockchain/testing:go_default_library",
"//beacon-chain/builder/testing:go_default_library",
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/gloas:go_default_library",
"//beacon-chain/core/helpers:go_default_library",
"//beacon-chain/core/transition:go_default_library",
"//beacon-chain/db/testing:go_default_library",

View File

@@ -18,7 +18,6 @@ import (
"github.com/OffchainLabs/prysm/v7/api/server/structs"
"github.com/OffchainLabs/prysm/v7/beacon-chain/builder"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core"
rpchelpers "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/helpers"
@@ -1213,162 +1212,6 @@ func (s *Server) GetSyncCommitteeDuties(w http.ResponseWriter, r *http.Request)
httputil.WriteJson(w, resp)
}
// GetPTCDuties retrieves the payload timeliness committee (PTC) duties for the requested epoch.
// The PTC is responsible for attesting to payload timeliness in ePBS (Gloas fork and later).
func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.GetPTCDuties")
defer span.End()
if shared.IsSyncing(ctx, w, s.SyncChecker, s.HeadFetcher, s.TimeFetcher, s.OptimisticModeFetcher) {
return
}
_, requestedEpochUint, ok := shared.UintFromRoute(w, r, "epoch")
if !ok {
return
}
requestedEpoch := primitives.Epoch(requestedEpochUint)
// PTC duties are only available from Gloas fork onwards.
if requestedEpoch < params.BeaconConfig().GloasForkEpoch {
httputil.HandleError(w, "PTC duties are not available before Gloas fork", http.StatusBadRequest)
return
}
var indices []string
err := json.NewDecoder(r.Body).Decode(&indices)
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(indices) == 0 {
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
}
requestedValIndices := make([]primitives.ValidatorIndex, len(indices))
for i, ix := range indices {
valIx, valid := shared.ValidateUint(w, fmt.Sprintf("ValidatorIndices[%d]", i), ix)
if !valid {
return
}
requestedValIndices[i] = primitives.ValidatorIndex(valIx)
}
// Limit how far in the future we can query (current + 1 epoch).
cs := s.TimeFetcher.CurrentSlot()
currentEpoch := slots.ToEpoch(cs)
nextEpoch := currentEpoch + 1
if requestedEpoch > nextEpoch {
httputil.HandleError(w,
fmt.Sprintf("Request epoch %d can not be greater than next epoch %d", requestedEpoch, nextEpoch),
http.StatusBadRequest)
return
}
// For next epoch requests, we use the current epoch's state since PTC
// assignments for next epoch can be computed from current epoch's state.
// This mirrors the spec's get_ptc_assignment which asserts epoch <= next_epoch
// and uses the current state to compute assignments.
epochForState := requestedEpoch
if requestedEpoch == nextEpoch {
epochForState = currentEpoch
}
st, err := s.Stater.StateByEpoch(ctx, epochForState)
if err != nil {
shared.WriteStateFetchError(w, err)
return
}
// Build a set of requested validators (also deduplicates per spec's uniqueItems requirement).
// Validate that each index exists in the validator registry.
requestedSet := make(map[primitives.ValidatorIndex]struct{}, len(requestedValIndices))
var zeroPubkey [fieldparams.BLSPubkeyLength]byte
for _, idx := range requestedValIndices {
// Skip duplicates.
if _, exists := requestedSet[idx]; exists {
continue
}
// Validate index exists.
pubkey := st.PubkeyAtIndex(idx)
if bytes.Equal(pubkey[:], zeroPubkey[:]) {
httputil.HandleError(w, fmt.Sprintf("Invalid validator index %d", idx), http.StatusBadRequest)
return
}
requestedSet[idx] = struct{}{}
}
// Compute PTC duties using the optimized batch function.
// This exits early once all requested validators are found.
ptcDuties, err := gloas.PTCDuties(ctx, st, requestedEpoch, requestedSet)
if err != nil {
httputil.HandleError(w, "Could not compute PTC duties: "+err.Error(), http.StatusInternalServerError)
return
}
// Convert to response format.
duties := make([]*structs.PTCDuty, 0, len(ptcDuties))
for _, duty := range ptcDuties {
pubkey := st.PubkeyAtIndex(duty.ValidatorIndex)
duties = append(duties, &structs.PTCDuty{
Pubkey: hexutil.Encode(pubkey[:]),
ValidatorIndex: strconv.FormatUint(uint64(duty.ValidatorIndex), 10),
Slot: strconv.FormatUint(uint64(duty.Slot), 10),
})
}
// Get dependent root. Per the spec, dependent_root is:
// get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)
// or the genesis block root in the case of underflow.
var dependentRoot []byte
if requestedEpoch <= 1 {
r, err := s.BeaconDB.GenesisBlockRoot(ctx)
if err != nil {
httputil.HandleError(w, "Could not get genesis block root: "+err.Error(), http.StatusInternalServerError)
return
}
dependentRoot = r[:]
} else {
dependentRoot, err = ptcDependentRoot(st, requestedEpoch)
if err != nil {
httputil.HandleError(w, "Could not get dependent root: "+err.Error(), http.StatusInternalServerError)
return
}
}
isOptimistic, err := s.OptimisticModeFetcher.IsOptimistic(ctx)
if err != nil {
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
return
}
resp := &structs.GetPTCDutiesResponse{
DependentRoot: hexutil.Encode(dependentRoot),
ExecutionOptimistic: isOptimistic,
Data: duties,
}
httputil.WriteJson(w, resp)
}
// ptcDependentRoot returns the block root that PTC assignments depend on.
// Per the spec: get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)
// This should only be called for epoch > 1 (caller handles epoch <= 1 with genesis root).
func ptcDependentRoot(st state.BeaconState, epoch primitives.Epoch) ([]byte, error) {
prevEpochStartSlot, err := slots.EpochStart(epoch.Sub(1))
if err != nil {
return nil, errors.Wrap(err, "could not get previous epoch start slot")
}
root, err := helpers.BlockRootAtSlot(st, prevEpochStartSlot-1)
if err != nil {
return nil, errors.Wrap(err, "could not get block root")
}
return root, nil
}
// GetLiveness requests the beacon node to indicate if a validator has been observed to be live in a given epoch.
// The beacon node might detect liveness by observing messages from the validator on the network,
// in the beacon chain, from its API or from any other source.

View File

@@ -17,7 +17,6 @@ import (
mockChain "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
builderTest "github.com/OffchainLabs/prysm/v7/beacon-chain/builder/testing"
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
dbutil "github.com/OffchainLabs/prysm/v7/beacon-chain/db/testing"
@@ -2960,250 +2959,6 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
})
}
func TestGetPTCDuties(t *testing.T) {
helpers.ClearCache()
params.SetupTestConfigCleanup(t)
cfg := params.BeaconConfig()
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
// Use fixed slot 0 for deterministic tests.
slot := primitives.Slot(0)
genesisTime := time.Now()
// Need enough validators for PTC selection (PTC_SIZE is 512 on mainnet, 2 on minimal)
numVals := uint64(fieldparams.PTCSize * 2)
st, _ := util.DeterministicGenesisStateFulu(t, numVals)
require.NoError(t, st.SetGenesisTime(genesisTime))
// Initialize the committee cache for epoch 0.
require.NoError(t, helpers.UpdateCommitteeCache(t.Context(), st, 0))
// Set up a genesis block root for dependent_root calculation.
genesisRoot := [32]byte{1, 2, 3}
db := dbutil.SetupDB(t)
require.NoError(t, db.SaveGenesisBlockRoot(t.Context(), genesisRoot))
mockChainService := &mockChain.ChainService{Genesis: genesisTime, State: st, Slot: &slot}
s := &Server{
Stater: &testutil.MockStater{BeaconState: st},
SyncChecker: &mockSync.Sync{IsSyncing: false},
TimeFetcher: mockChainService,
HeadFetcher: mockChainService,
OptimisticModeFetcher: mockChainService,
BeaconDB: db,
}
t.Run("single validator in PTC", func(t *testing.T) {
// Request duties for validator index 0
var body bytes.Buffer
_, err := body.WriteString("[\"0\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetPTCDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
assert.NotEmpty(t, resp.DependentRoot)
})
t.Run("verifies PTC duties correctness", func(t *testing.T) {
// Request duties for a range of validators.
// Some will be in the PTC, some won't.
var indices []string
requestedSet := make(map[primitives.ValidatorIndex]struct{})
for i := range 100 {
indices = append(indices, strconv.Itoa(i))
requestedSet[primitives.ValidatorIndex(i)] = struct{}{}
}
// Test PTCDuties directly.
directDuties, err := gloas.PTCDuties(t.Context(), st, 0, requestedSet)
require.NoError(t, err)
// Should return some duties (not necessarily all 100, depends on PTC selection).
require.NotEmpty(t, directDuties, "Should return at least some duties")
// All returned duties should be for slots within epoch 0.
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
for _, duty := range directDuties {
if uint64(duty.Slot) >= uint64(slotsPerEpoch) {
t.Errorf("Duty slot %d should be within epoch 0 (< %d)", duty.Slot, slotsPerEpoch)
}
// Verify returned validator was in the request.
_, ok := requestedSet[duty.ValidatorIndex]
if !ok {
t.Errorf("Returned validator %d should be in requested set", duty.ValidatorIndex)
}
}
// Test via HTTP - should return same count.
indicesJSON, err := json.Marshal(indices)
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", bytes.NewReader(indicesJSON))
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetPTCDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
// HTTP response should match direct call.
assert.Equal(t, len(directDuties), len(resp.Data), "HTTP response should match direct PTCDuties call")
})
t.Run("multiple validators", func(t *testing.T) {
var body bytes.Buffer
_, err := body.WriteString("[\"0\",\"1\",\"2\",\"3\",\"4\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetPTCDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
assert.NotEmpty(t, resp.DependentRoot)
// Verify any returned duties have correct structure
for _, duty := range resp.Data {
assert.NotEmpty(t, duty.Pubkey)
assert.NotEmpty(t, duty.ValidatorIndex)
assert.NotEmpty(t, duty.Slot)
}
})
t.Run("duplicate validator indices are deduplicated", func(t *testing.T) {
// Request the same validator multiple times - should be deduplicated.
var body bytes.Buffer
_, err := body.WriteString("[\"0\",\"0\",\"0\",\"1\",\"1\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusOK, writer.Code)
resp := &structs.GetPTCDutiesResponse{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
// Each validator should appear at most once in the response.
seen := make(map[string]bool)
for _, duty := range resp.Data {
if seen[duty.ValidatorIndex] {
t.Errorf("Validator %s appears multiple times in response", duty.ValidatorIndex)
}
seen[duty.ValidatorIndex] = true
}
})
t.Run("pre-Gloas epoch returns error", func(t *testing.T) {
// Temporarily set GloasForkEpoch to 10
cfg := params.BeaconConfig()
cfg.GloasForkEpoch = 10
params.OverrideBeaconConfig(cfg)
defer func() {
cfg.GloasForkEpoch = 0
params.OverrideBeaconConfig(cfg)
}()
var body bytes.Buffer
_, err := body.WriteString("[\"0\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.StringContains(t, "PTC duties are not available before Gloas fork", e.Message)
})
t.Run("no body", func(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", nil)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.StringContains(t, "No data submitted", e.Message)
})
t.Run("empty body", func(t *testing.T) {
var body bytes.Buffer
_, err := body.WriteString("[]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.StringContains(t, "No data submitted", e.Message)
})
t.Run("invalid validator index string", func(t *testing.T) {
var body bytes.Buffer
_, err := body.WriteString("[\"foo\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
})
t.Run("out of bounds validator index", func(t *testing.T) {
// Request a validator index that's way beyond the number of validators.
var body bytes.Buffer
_, err := body.WriteString("[\"999999999\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "0")
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
// Invalid validator index should return 400, matching attester duties behavior.
assert.Equal(t, http.StatusBadRequest, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.StringContains(t, "Invalid validator index", e.Message)
})
t.Run("epoch too far in future", func(t *testing.T) {
var body bytes.Buffer
_, err := body.WriteString("[\"0\"]")
require.NoError(t, err)
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
request.SetPathValue("epoch", "100") // Far future epoch
writer := httptest.NewRecorder()
writer.Body = &bytes.Buffer{}
s.GetPTCDuties(writer, request)
assert.Equal(t, http.StatusBadRequest, writer.Code)
e := &httputil.DefaultJsonError{}
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
assert.StringContains(t, "can not be greater than next epoch", e.Message)
})
}
func TestPrepareBeaconProposer(t *testing.T) {
tests := []struct {
name string

View File

@@ -1,3 +0,0 @@
### Added
- PTC (Payload Timeliness Committee) duties endpoint: POST /eth/v1/validator/duties/ptc/{epoch} for ePBS (Gloas fork).

View File

@@ -356,6 +356,11 @@ var (
Usage: "A comma-separated list of exponents (of 2) in decreasing order, defining the state diff hierarchy levels. The last exponent must be greater than or equal to 5.",
Value: cli.NewIntSlice(21, 18, 16, 13, 11, 9, 5),
}
// StateDiffValidateOnStartup validates state diff data on startup.
StateDiffValidateOnStartup = &cli.BoolFlag{
Name: "disable-hdiff-validate-on-startup",
Usage: "Disables state-diff validation on startup (enabled by default).",
}
// DisableEphemeralLogFile disables the 24 hour debug log file.
DisableEphemeralLogFile = &cli.BoolFlag{
Name: "disable-ephemeral-log-file",

View File

@@ -9,24 +9,25 @@ import (
"github.com/urfave/cli/v2"
)
const maxStateDiffExponents = 30
const MaxStateDiffExponent = 30
// GlobalFlags specifies all the global flags for the
// beacon node.
type GlobalFlags struct {
SubscribeToAllSubnets bool
StateDiffValidateOnStartup bool
Supernode bool
SemiSupernode bool
DisableGetBlobsV2 bool
MinimumSyncPeers int
MinimumPeersPerSubnet int
MaxConcurrentDials int
BlockBatchLimit int
BlockBatchLimitBurstFactor int
BlobBatchLimit int
SemiSupernode bool
SubscribeToAllSubnets bool
BlobBatchLimitBurstFactor int
DataColumnBatchLimit int
BlockBatchLimit int
MaxConcurrentDials int
MinimumPeersPerSubnet int
MinimumSyncPeers int
DataColumnBatchLimitBurstFactor int
BlockBatchLimitBurstFactor int
BlobBatchLimit int
StateDiffExponents []int
}
@@ -80,6 +81,7 @@ func ConfigureGlobalFlags(ctx *cli.Context) error {
// State-diff-exponents
cfg.StateDiffExponents = ctx.IntSlice(StateDiffExponents.Name)
cfg.StateDiffValidateOnStartup = !ctx.Bool(StateDiffValidateOnStartup.Name)
if features.Get().EnableStateDiff {
if err := validateStateDiffExponents(cfg.StateDiffExponents); err != nil {
return err
@@ -88,6 +90,9 @@ func ConfigureGlobalFlags(ctx *cli.Context) error {
if ctx.IsSet(StateDiffExponents.Name) {
log.Warn("--state-diff-exponents is set but --enable-state-diff is not; the value will be ignored.")
}
if ctx.IsSet(StateDiffValidateOnStartup.Name) {
log.Warn("--disable-hdiff-validate-on-startup is set but --enable-state-diff is not; the value will be ignored.")
}
}
cfg.BlockBatchLimit = ctx.Int(BlockBatchLimit.Name)
@@ -132,7 +137,7 @@ func validateStateDiffExponents(exponents []int) error {
if exponents[length-1] < 5 {
return errors.New("the last state diff exponent must be at least 5")
}
prev := maxStateDiffExponents + 1
prev := MaxStateDiffExponent + 1
for _, exp := range exponents {
if exp >= prev {
return errors.New("state diff exponents must be in strictly decreasing order, and each exponent must be <= 30")

View File

@@ -161,6 +161,7 @@ var appFlags = []cli.Flag{
dasFlags.BlobRetentionEpochFlag,
flags.BatchVerifierLimit,
flags.StateDiffExponents,
flags.StateDiffValidateOnStartup,
flags.DisableEphemeralLogFile,
}

View File

@@ -75,6 +75,7 @@ var appHelpFlagGroups = []flagGroup{
flags.RPCPort,
flags.BatchVerifierLimit,
flags.StateDiffExponents,
flags.StateDiffValidateOnStartup,
},
},
{