From c13bace8ef360e585d6654c7de78b9903070cfd1 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 25 Nov 2025 15:00:15 -0300 Subject: [PATCH] Migrate to cold with state diffs This PR adds the logic to migrate to cold when the database has the hdiff feature. The main difference is that the boundary states have to have the right slot therefore they need to be advanced and aren't necessarily the post-state of a given beacon block root. --- beacon-chain/db/iface/interface.go | 2 + beacon-chain/db/kv/state_diff.go | 19 ++-- beacon-chain/state/stategen/BUILD.bazel | 1 + beacon-chain/state/stategen/migrate.go | 121 +++++++++++++++++++--- changelog/potuz_hdiff_migrate_to_cold.md | 3 + tools/analyzers/recursivelock/analyzer.go | 3 + 6 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 changelog/potuz_hdiff_migrate_to_cold.md diff --git a/beacon-chain/db/iface/interface.go b/beacon-chain/db/iface/interface.go index 179e9b46f287..67850f9110ad 100644 --- a/beacon-chain/db/iface/interface.go +++ b/beacon-chain/db/iface/interface.go @@ -89,6 +89,7 @@ type NoHeadAccessDatabase interface { SaveBlocks(ctx context.Context, blocks []interfaces.ReadOnlySignedBeaconBlock) error SaveROBlocks(ctx context.Context, blks []blocks.ROBlock, cache bool) error SaveGenesisBlockRoot(ctx context.Context, blockRoot [32]byte) error + SlotByBlockRoot(context.Context, [32]byte) (primitives.Slot, error) // State related methods. SaveState(ctx context.Context, state state.ReadOnlyBeaconState, blockRoot [32]byte) error SaveStates(ctx context.Context, states []state.ReadOnlyBeaconState, blockRoots [][32]byte) error @@ -96,6 +97,7 @@ type NoHeadAccessDatabase interface { DeleteStates(ctx context.Context, blockRoots [][32]byte) error SaveStateSummary(ctx context.Context, summary *ethpb.StateSummary) error SaveStateSummaries(ctx context.Context, summaries []*ethpb.StateSummary) error + SlotInDiffTree(primitives.Slot) (uint64, int, error) // Checkpoint operations. SaveJustifiedCheckpoint(ctx context.Context, checkpoint *ethpb.Checkpoint) error SaveFinalizedCheckpoint(ctx context.Context, checkpoint *ethpb.Checkpoint) error diff --git a/beacon-chain/db/kv/state_diff.go b/beacon-chain/db/kv/state_diff.go index a12fe2e87d62..c008e8dd44b0 100644 --- a/beacon-chain/db/kv/state_diff.go +++ b/beacon-chain/db/kv/state_diff.go @@ -23,6 +23,16 @@ const ( The data at level 0 is saved every 2**exponent[0] slots and always contains a full state snapshot that is used as a base for the delta saved at other levels. */ +// SlotInDiffTree returns whether the given slot is a saving point in the diff tree. +// It it is, it also returns the offset and level in the tree. +func (s *Store) SlotInDiffTree(slot primitives.Slot) (uint64, int, error) { + offset := s.getOffset() + if uint64(slot) < offset { + return 0, -1, ErrSlotBeforeOffset + } + return offset, computeLevel(offset, slot), nil +} + // saveStateByDiff takes a state and decides between saving a full state snapshot or a diff. func (s *Store) saveStateByDiff(ctx context.Context, st state.ReadOnlyBeaconState) error { _, span := trace.StartSpan(ctx, "BeaconDB.saveStateByDiff") @@ -33,13 +43,10 @@ func (s *Store) saveStateByDiff(ctx context.Context, st state.ReadOnlyBeaconStat } slot := st.Slot() - offset := s.getOffset() - if uint64(slot) < offset { - return ErrSlotBeforeOffset + offset, lvl, err := s.SlotInDiffTree(slot) + if err != nil { + return errors.Wrap(err, "could not determine if slot is in diff tree") } - - // Find the level to save the state. - lvl := computeLevel(offset, slot) if lvl == -1 { return nil } diff --git a/beacon-chain/state/stategen/BUILD.bazel b/beacon-chain/state/stategen/BUILD.bazel index 751789100039..95309a5380ed 100644 --- a/beacon-chain/state/stategen/BUILD.bazel +++ b/beacon-chain/state/stategen/BUILD.bazel @@ -29,6 +29,7 @@ go_library( "//beacon-chain/state:go_default_library", "//beacon-chain/sync/backfill/coverage:go_default_library", "//cache/lru:go_default_library", + "//config/features:go_default_library", "//config/params:go_default_library", "//consensus-types/blocks:go_default_library", "//consensus-types/interfaces:go_default_library", diff --git a/beacon-chain/state/stategen/migrate.go b/beacon-chain/state/stategen/migrate.go index 25407bc9693f..fd80426f2d98 100644 --- a/beacon-chain/state/stategen/migrate.go +++ b/beacon-chain/state/stategen/migrate.go @@ -5,9 +5,12 @@ import ( "encoding/hex" "fmt" + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition" "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + "github.com/OffchainLabs/prysm/v7/config/features" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -24,6 +27,10 @@ func (s *State) MigrateToCold(ctx context.Context, fRoot [32]byte) error { s.migrationLock.Lock() defer s.migrationLock.Unlock() + if features.Get().EnableStateDiff { + return s.migrateToColdHdiff(ctx, fRoot) + } + s.finalizedInfo.lock.RLock() oldFSlot := s.finalizedInfo.slot s.finalizedInfo.lock.RUnlock() @@ -82,19 +89,7 @@ func (s *State) MigrateToCold(ctx context.Context, fRoot [32]byte) error { } if s.beaconDB.HasState(ctx, aRoot) { - // If you are migrating a state and its already part of the hot state cache saved to the db, - // you can just remove it from the hot state cache as it becomes redundant. - s.saveHotStateDB.lock.Lock() - roots := s.saveHotStateDB.blockRootsOfSavedStates - for i := range roots { - if aRoot == roots[i] { - s.saveHotStateDB.blockRootsOfSavedStates = append(roots[:i], roots[i+1:]...) - // There shouldn't be duplicated roots in `blockRootsOfSavedStates`. - // Break here is ok. - break - } - } - s.saveHotStateDB.lock.Unlock() + s.migrateHotToCold(aRoot) continue } @@ -120,3 +115,103 @@ func (s *State) MigrateToCold(ctx context.Context, fRoot [32]byte) error { return nil } + +// migrateToColdHdiff saves the state-diffs for slots that are in the state diff tree after finalization +func (s *State) migrateToColdHdiff(ctx context.Context, fRoot [32]byte) error { + s.finalizedInfo.lock.RLock() + oldFSlot := s.finalizedInfo.slot + s.finalizedInfo.lock.RUnlock() + fSlot, err := s.beaconDB.SlotByBlockRoot(ctx, fRoot) + if err != nil { + return errors.Wrap(err, "could not get slot by block root") + } + for slot := oldFSlot; slot < fSlot; slot++ { + if ctx.Err() != nil { + return ctx.Err() + } + _, lvl, err := s.beaconDB.SlotInDiffTree(slot) + if err != nil { + log.WithError(err).Errorf("could not determine if slot %d is in diff tree", slot) + continue + } + if lvl == -1 { + continue + } + // The state needs to be saved. + // Try the epoch boundary cache first. + cached, exists, err := s.epochBoundaryStateCache.getBySlot(slot) + if err != nil { + log.WithError(err).Errorf("could not get epoch boundary state for slot %d", slot) + cached = nil + exists = false + } + var aRoot [32]byte + var aState state.BeaconState + if exists { + aRoot = cached.root + aState = cached.state + } else { + _, roots, err := s.beaconDB.HighestRootsBelowSlot(ctx, slot) + if err != nil { + return err + } + // Given the block has been finalized, the db should not have more than one block in a given slot. + // We should error out when this happens. + if len(roots) != 1 { + return errUnknownBlock + } + aRoot = roots[0] + // Different than the legacy MigrateToCold, we need to always get the state even if + // the state exists in DB as part of the hot state db, because we need to process slots + // to the state diff tree slots. + aState, err = s.StateByRoot(ctx, aRoot) + if err != nil { + return err + } + } + if s.beaconDB.HasState(ctx, aRoot) { + s.migrateHotToCold(aRoot) + continue + } + // advance slots to the target slot + if aState.Slot() < slot { + aState, err = transition.ProcessSlots(ctx, aState, slot) + if err != nil { + return errors.Wrapf(err, "could not process slots to slot %d", slot) + } + } + if err := s.beaconDB.SaveState(ctx, aState, aRoot); err != nil { + return err + } + log.WithFields( + logrus.Fields{ + "slot": aState.Slot(), + "root": fmt.Sprintf("%#x", aRoot), + }).Info("Saved state in DB") + } + // Update finalized info in memory. + fInfo, ok, err := s.epochBoundaryStateCache.getByBlockRoot(fRoot) + if err != nil { + return err + } + if ok { + s.SaveFinalizedState(fSlot, fRoot, fInfo.state) + } + return nil +} + +func (s *State) migrateHotToCold(aRoot [32]byte) { + // If you are migrating a state and its already part of the hot state cache saved to the db, + // you can just remove it from the hot state cache as it becomes redundant. + s.saveHotStateDB.lock.Lock() + roots := s.saveHotStateDB.blockRootsOfSavedStates + for i := range roots { + if aRoot == roots[i] { + s.saveHotStateDB.blockRootsOfSavedStates = append(roots[:i], roots[i+1:]...) + // There shouldn't be duplicated roots in `blockRootsOfSavedStates`. + // Break here is ok. + break + } + } + s.saveHotStateDB.lock.Unlock() +} diff --git a/changelog/potuz_hdiff_migrate_to_cold.md b/changelog/potuz_hdiff_migrate_to_cold.md new file mode 100644 index 000000000000..e6d9da5d9842 --- /dev/null +++ b/changelog/potuz_hdiff_migrate_to_cold.md @@ -0,0 +1,3 @@ +### Added + +- Migrate to cold with the hdiff feature. diff --git a/tools/analyzers/recursivelock/analyzer.go b/tools/analyzers/recursivelock/analyzer.go index 7dca9dc1b2db..ac110f8cbf4d 100644 --- a/tools/analyzers/recursivelock/analyzer.go +++ b/tools/analyzers/recursivelock/analyzer.go @@ -525,6 +525,9 @@ func hasNestedlock(fullRLockSelector *selIdentList, goPos token.Pos, compareMap if node == (*ast.FuncDecl)(nil) { return "" } else if castedNode, ok := node.(*ast.FuncDecl); ok && castedNode.Recv != nil { + if len(castedNode.Recv.List) == 0 || len(castedNode.Recv.List[0].Names) == 0 { + return "" + } recv = castedNode.Recv.List[0].Names[0] rLockSelector.changeRoot(recv, pass.TypesInfo.ObjectOf(recv)) }