Skip to content

Commit 18c1baf

Browse files
abi87Copilot
andauthored
chore(stateDB): renamed statedb.Copy (#2958)
Signed-off-by: Alberto Benegiamo <alberto.benegiamo@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1cd58b7 commit 18c1baf

File tree

5 files changed

+129
-15
lines changed

5 files changed

+129
-15
lines changed

beacon/blockchain/process_proposal.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,9 @@ func (s *Service) VerifyIncomingBlock(
262262
)
263263

264264
if shouldBuildNextBlock {
265-
// state copy makes sure that preFetchBuildData does not affect state
266-
copiedState := state.Copy(ctx)
267-
nextBlockData, errFetch = s.preFetchBuildData(copiedState, blk.GetConsensusTime())
265+
// makes sure that preFetchBuildData does not affect state
266+
ephemeralState := state.Protect(ctx)
267+
nextBlockData, errFetch = s.preFetchBuildData(ephemeralState, blk.GetConsensusTime())
268268
if errFetch != nil {
269269
// We don't return with err if pre-fetch fails. Instead we log the issue
270270
// and still move to process the current block. Next block can always be
@@ -303,9 +303,9 @@ func (s *Service) VerifyIncomingBlock(
303303
)
304304

305305
if shouldBuildNextBlock {
306-
// state copy makes sure that preFetchBuildDataForSuccess does not affect state
307-
copiedState := state.Copy(ctx)
308-
nextBlockData, errFetch = s.preFetchBuildData(copiedState, blk.GetConsensusTime())
306+
// makes sure that preFetchBuildDataForSuccess does not affect state
307+
ephemeralState := state.Protect(ctx)
308+
nextBlockData, errFetch = s.preFetchBuildData(ephemeralState, blk.GetConsensusTime())
309309
if errFetch != nil {
310310
// We don't mark the block as rejected if it is valid but pre-fetch fails.
311311
// Instead we log the issue and move to process the current block.

node-api/backend/getters.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ func (b *Backend) StateAndSlotFromHeight(height int64) (ReadOnlyBeaconState, mat
6363
// Copy the state to ensure clients potential changes won't pollute the state
6464
// Also we make sure to create the copy in a thread-safe way via the muCms mutex.
6565
ms := b.cms.CacheMultiStore()
66-
copyCtx := sdk.NewContext(ms, true, log.NewNopLogger())
67-
copyGenesisState := b.genesisState.Copy(copyCtx)
68-
return copyGenesisState, 0, nil
66+
ctx := sdk.NewContext(ms, true, log.NewNopLogger())
67+
ephemeralGenesisState := b.genesisState.Protect(ctx)
68+
return ephemeralGenesisState, 0, nil
6969
}
7070

7171
height = max(0, height) // CreateQueryContext uses 0 to pick latest height.

state-transition/core/state/statedb.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ func NewBeaconStateFromDB(
5757
}
5858
}
5959

60-
// Copy returns a copy of the beacon state.
61-
func (s *StateDB) Copy(ctx context.Context) *StateDB {
60+
// Protect returns an almost copy of stateDB. Specifically Protect guarantees that:
61+
// - No changes done on the returned state will affect the original state
62+
// - However, changes done on the original state will be carried over to the returned state.
63+
// The behaviour is probably best understood by considering the context hosts a stack of cache layers.
64+
// So write operations to the top cache won't be flushed to the lower layer but read operations will walk
65+
// through the cache stack, so bubbling up changes from the lower layers to the top ones.
66+
func (s *StateDB) Protect(ctx context.Context) *StateDB {
6267
return NewBeaconStateFromDB(s.KVStore.Copy(ctx), s.cs, s.logger, s.telemetrySink)
6368
}
6469

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
//
3+
// Copyright (C) 2025, Berachain Foundation. All rights reserved.
4+
// Use of this software is governed by the Business Source License included
5+
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
6+
//
7+
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
8+
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
9+
// VERSIONS OF THE LICENSED WORK.
10+
//
11+
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
12+
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
13+
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
14+
//
15+
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
16+
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
17+
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
18+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
19+
// TITLE.
20+
21+
package state_test
22+
23+
import (
24+
"testing"
25+
26+
"cosmossdk.io/collections"
27+
"cosmossdk.io/log"
28+
"cosmossdk.io/store"
29+
sdkmetrics "cosmossdk.io/store/metrics"
30+
storetypes "cosmossdk.io/store/types"
31+
"github.com/berachain/beacon-kit/config/spec"
32+
ctypes "github.com/berachain/beacon-kit/consensus-types/types"
33+
"github.com/berachain/beacon-kit/node-core/components/metrics"
34+
"github.com/berachain/beacon-kit/primitives/common"
35+
"github.com/berachain/beacon-kit/primitives/math"
36+
"github.com/berachain/beacon-kit/state-transition/core/state"
37+
"github.com/berachain/beacon-kit/storage"
38+
"github.com/berachain/beacon-kit/storage/beacondb"
39+
"github.com/berachain/beacon-kit/storage/db"
40+
dbm "github.com/cosmos/cosmos-db"
41+
sdk "github.com/cosmos/cosmos-sdk/types"
42+
"github.com/stretchr/testify/require"
43+
)
44+
45+
func TestStateProtect(t *testing.T) {
46+
t.Parallel()
47+
48+
db, err := db.OpenDB("", dbm.MemDBBackend)
49+
require.NoError(t, err)
50+
51+
cs, errSpec := spec.MainnetChainSpec()
52+
require.NoError(t, errSpec)
53+
54+
var (
55+
nopLog = log.NewNopLogger()
56+
nopMetrics = sdkmetrics.NewNoOpMetrics()
57+
)
58+
59+
cms := store.NewCommitMultiStore(db, nopLog, nopMetrics)
60+
cms.MountStoreWithDB(testStoreKey, storetypes.StoreTypeIAVL, nil)
61+
require.NoError(t, cms.LoadLatestVersion())
62+
63+
backendStoreService := &storage.KVStoreService{Key: testStoreKey}
64+
kvStore := beacondb.New(backendStoreService)
65+
66+
ms := cms.CacheMultiStore()
67+
sdkCtx := sdk.NewContext(ms, true, nopLog)
68+
originalState := state.NewBeaconStateFromDB(
69+
kvStore.WithContext(sdkCtx),
70+
cs,
71+
sdkCtx.Logger(),
72+
metrics.NewNoOpTelemetrySink(),
73+
)
74+
75+
protectingState := originalState.Protect(sdkCtx)
76+
77+
// 1- set an attribute in the original state and show
78+
// that value is carried over the protecting state
79+
wantSlot := math.Slot(1234)
80+
require.NoError(t, originalState.SetSlot(wantSlot))
81+
82+
gotSlot, err := protectingState.GetSlot()
83+
require.NoError(t, err)
84+
require.Equal(t, wantSlot, gotSlot)
85+
86+
// 2- Show that modifying the protecting state
87+
// does not affect the original state
88+
wantFork := &ctypes.Fork{
89+
PreviousVersion: common.Version{0x11, 0x22, 0x33, 0x44},
90+
CurrentVersion: common.Version{0xff, 0xff, 0xff, 0xff},
91+
Epoch: math.Epoch(1234),
92+
}
93+
require.NoError(t, protectingState.SetFork(wantFork))
94+
95+
_, err = originalState.GetFork()
96+
require.ErrorIs(t, err, collections.ErrNotFound)
97+
98+
// 3- Show that changes made to original state after protection
99+
// are carried over the protecting state
100+
wantEthIdx := uint64(1987)
101+
require.NoError(t, originalState.SetEth1DepositIndex(wantEthIdx))
102+
103+
gotEthIdx, err := protectingState.GetEth1DepositIndex()
104+
require.NoError(t, err)
105+
require.Equal(t, wantEthIdx, gotEthIdx)
106+
}
107+
108+
var testStoreKey = storetypes.NewKVStoreKey("test-stateDB")

testing/simulated/utils.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ func ComputeAndSetStateRoot(
326326
block *ctypes.BeaconBlock,
327327
) (*ctypes.BeaconBlock, error) {
328328

329-
// Copy the current state from the storage backend.
330-
stateDBCopy := storageBackend.StateFromContext(queryCtx).Copy(queryCtx)
329+
// Create an ephemeral state out of storage backend to avoid polluting
330+
// the original state
331+
ephemeralState := storageBackend.StateFromContext(queryCtx).Protect(queryCtx)
331332

332333
// Create a transition context with the provided consensus time and proposer address.
333334
txCtx := transition.NewTransitionCtx(
@@ -340,13 +341,13 @@ func ComputeAndSetStateRoot(
340341
WithMeterGas(false)
341342

342343
// Run the state transition.
343-
_, err := stateProcessor.Transition(txCtx, stateDBCopy, block)
344+
_, err := stateProcessor.Transition(txCtx, ephemeralState, block)
344345
if err != nil {
345346
return nil, fmt.Errorf("state transition failed: %w", err)
346347
}
347348

348349
// Compute the new state root from the updated state.
349-
newStateRoot := stateDBCopy.HashTreeRoot()
350+
newStateRoot := ephemeralState.HashTreeRoot()
350351
block.SetStateRoot(newStateRoot)
351352
return block, nil
352353
}

0 commit comments

Comments
 (0)