From 25b3ee468fc37395f8a85f2bfcd8bb88a0c3bc82 Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Wed, 4 Feb 2026 15:15:06 -0600 Subject: [PATCH] hdiff restart-support: persist and validate exponents metadata --- beacon-chain/db/kv/kv.go | 18 +++++++++ beacon-chain/db/kv/state_diff_helpers.go | 47 ++++++++++++++++++++++-- beacon-chain/db/kv/state_diff_test.go | 21 +++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/beacon-chain/db/kv/kv.go b/beacon-chain/db/kv/kv.go index b4d55d677b..ca2769f22b 100644 --- a/beacon-chain/db/kv/kv.go +++ b/beacon-chain/db/kv/kv.go @@ -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" @@ -223,6 +225,22 @@ func (kv *Store) startStateDiff(ctx context.Context) error { } if hasOffset { + storedExponents, err := kv.loadStateDiffExponents() + if err != nil { + return errors.Wrap(err, "state-diff metadata missing or invalid; re-sync required") + } + currentExponents := flags.Get().StateDiffExponents + if !slices.Equal(storedExponents, currentExponents) { + return fmt.Errorf( + "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), + ) + } // Existing state-diff database - restarts not yet supported. return errors.New("restarting with existing state-diff database not yet supported") } diff --git a/beacon-chain/db/kv/state_diff_helpers.go b/beacon-chain/db/kv/state_diff_helpers.go index 124683f447..cd5d4e16fa 100644 --- a/beacon-chain/db/kv/state_diff_helpers.go +++ b/beacon-chain/db/kv/state_diff_helpers.go @@ -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" @@ -61,6 +62,38 @@ func decodeStateDiffExponents(encoded []byte) ([]int, error) { 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) @@ -190,8 +223,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 @@ -199,7 +237,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") diff --git a/beacon-chain/db/kv/state_diff_test.go b/beacon-chain/db/kv/state_diff_test.go index 5e9b2846d1..0944adec35 100644 --- a/beacon-chain/db/kv/state_diff_test.go +++ b/beacon-chain/db/kv/state_diff_test.go @@ -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" @@ -75,6 +76,26 @@ func TestStateDiff_EncodeDecodeExponents(t *testing.T) { }) } +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()