mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-05-02 03:02:54 -04:00
**What type of PR is this?** Bug fix **What does this PR do? Why is it needed?** This PR evicts finalized states in checkpoint states cache. States are efficiently stored in caches, especially thanks to multi value slices. If 1 state takes 300 MB and 2 states that are really similar are stored in a cache, then these 2 states could only need let's say 310MB of cache memory (instead of 300 MB x 2 = 600 MB). **Before the commit creating the memory issue**, new states were regularly stored in the `CheckpointStateCache`. This cache has 10 slots. After this cache is full, oldest values (not quite exactly because it's a LRU cache) are pruned. **After the commit creating the memory issue**, new states are quite rarely inserted into this cache. For example, on a run, almost 5H (!) were needed before the first value was evicted from this cache. This mean this cache contains multiple states that do not share a lot of values with other states in all other caches/head. ==> A lot of fields stay in memory that are exclusively needed for the (old) states only present in this cache. The beacon node now evicts finalized states from the cache. **Which issues(s) does this PR fix?** - https://github.com/OffchainLabs/prysm/issues/16376 <img width="1022" height="914" alt="image" src="https://github.com/user-attachments/assets/98886364-001a-48fc-a952-5c6a7e80bf88" /> **Acknowledgements** - [x] I have read [CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md). - [x] I have included a uniquely named [changelog fragment file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd). - [x] I have added a description with sufficient context for reviewers to understand this PR. - [x] I have tested that my changes work as expected and I added a testing plan to the PR description (if applicable).
148 lines
5.2 KiB
Go
148 lines
5.2 KiB
Go
package cache_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
|
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
|
state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native"
|
|
"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"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
func TestCheckpointStateCache_StateByCheckpoint(t *testing.T) {
|
|
cache := cache.NewCheckpointStateCache()
|
|
|
|
cp1 := ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte{'A'}, 32)}
|
|
st, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{
|
|
GenesisValidatorsRoot: params.BeaconConfig().ZeroHash[:],
|
|
Slot: 64,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
s, err := cache.StateByCheckpoint(cp1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, state.BeaconState(nil), s, "Expected state not to exist in empty cache")
|
|
|
|
require.NoError(t, cache.AddCheckpointState(cp1, st))
|
|
|
|
s, err = cache.StateByCheckpoint(cp1)
|
|
require.NoError(t, err)
|
|
|
|
pbState1, err := state_native.ProtobufBeaconStatePhase0(s.ToProtoUnsafe())
|
|
require.NoError(t, err)
|
|
pbstate, err := state_native.ProtobufBeaconStatePhase0(st.ToProtoUnsafe())
|
|
require.NoError(t, err)
|
|
if !proto.Equal(pbState1, pbstate) {
|
|
t.Error("incorrectly cached state")
|
|
}
|
|
|
|
cp2 := ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte{'B'}, 32)}
|
|
st2, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{
|
|
Slot: 128,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, cache.AddCheckpointState(cp2, st2))
|
|
|
|
s, err = cache.StateByCheckpoint(cp2)
|
|
require.NoError(t, err)
|
|
assert.DeepEqual(t, st2.ToProto(), s.ToProto(), "incorrectly cached state")
|
|
|
|
s, err = cache.StateByCheckpoint(cp1)
|
|
require.NoError(t, err)
|
|
assert.DeepEqual(t, st.ToProto(), s.ToProto(), "incorrectly cached state")
|
|
}
|
|
|
|
func TestCheckpointStateCache_MaxSize(t *testing.T) {
|
|
c := cache.NewCheckpointStateCache()
|
|
st, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{
|
|
Slot: 0,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
for i := uint64(0); i < uint64(cache.MaxCheckpointStateSize()+100); i++ {
|
|
require.NoError(t, st.SetSlot(primitives.Slot(i)))
|
|
require.NoError(t, c.AddCheckpointState(ðpb.Checkpoint{Epoch: primitives.Epoch(i), Root: make([]byte, 32)}, st))
|
|
}
|
|
|
|
assert.Equal(t, cache.MaxCheckpointStateSize(), len(c.Cache().Keys()))
|
|
}
|
|
|
|
func TestCheckpointStateCache_EvictFinalized_FinalizedEntry(t *testing.T) {
|
|
c := cache.NewCheckpointStateCache()
|
|
|
|
cp := ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte{'A'}, 32)}
|
|
st, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{Slot: 32})
|
|
require.NoError(t, err)
|
|
require.NoError(t, c.AddCheckpointState(cp, st))
|
|
|
|
evicted := c.EvictUpTo(1)
|
|
assert.Equal(t, 1, evicted, "expected finalized entry to be evicted")
|
|
|
|
s, err := c.StateByCheckpoint(cp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, state.BeaconState(nil), s, "expected cache to be empty after eviction")
|
|
}
|
|
|
|
func TestCheckpointStateCache_EvictFinalized_NotFinalizedEntry(t *testing.T) {
|
|
c := cache.NewCheckpointStateCache()
|
|
|
|
cp := ðpb.Checkpoint{Epoch: 5, Root: bytesutil.PadTo([]byte{'A'}, 32)}
|
|
st, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{Slot: 160})
|
|
require.NoError(t, err)
|
|
require.NoError(t, c.AddCheckpointState(cp, st))
|
|
|
|
evicted := c.EvictUpTo(3)
|
|
assert.Equal(t, 0, evicted, "expected non-finalized entry NOT to be evicted")
|
|
|
|
s, err := c.StateByCheckpoint(cp)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, s, "expected entry to still be in cache")
|
|
}
|
|
|
|
func TestCheckpointStateCache_EvictFinalized_Mixed(t *testing.T) {
|
|
c := cache.NewCheckpointStateCache()
|
|
|
|
cp1 := ðpb.Checkpoint{Epoch: 1, Root: bytesutil.PadTo([]byte{'A'}, 32)}
|
|
st1, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{Slot: 32})
|
|
require.NoError(t, err)
|
|
|
|
cp2 := ðpb.Checkpoint{Epoch: 2, Root: bytesutil.PadTo([]byte{'B'}, 32)}
|
|
st2, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{Slot: 64})
|
|
require.NoError(t, err)
|
|
|
|
cp5 := ðpb.Checkpoint{Epoch: 5, Root: bytesutil.PadTo([]byte{'C'}, 32)}
|
|
st5, err := state_native.InitializeFromProtoPhase0(ðpb.BeaconState{Slot: 160})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, c.AddCheckpointState(cp1, st1))
|
|
require.NoError(t, c.AddCheckpointState(cp2, st2))
|
|
require.NoError(t, c.AddCheckpointState(cp5, st5))
|
|
|
|
evicted := c.EvictUpTo(3)
|
|
assert.Equal(t, 2, evicted, "expected epochs 1 and 2 to be evicted")
|
|
|
|
s, err := c.StateByCheckpoint(cp1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, state.BeaconState(nil), s, "expected cp1 to be evicted")
|
|
|
|
s, err = c.StateByCheckpoint(cp2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, state.BeaconState(nil), s, "expected cp2 to be evicted")
|
|
|
|
s, err = c.StateByCheckpoint(cp5)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, s, "expected cp5 to still be in cache")
|
|
}
|
|
|
|
func TestCheckpointStateCache_EvictFinalized_EmptyCache(t *testing.T) {
|
|
c := cache.NewCheckpointStateCache()
|
|
evicted := c.EvictUpTo(0)
|
|
assert.Equal(t, 0, evicted, "expected no eviction from empty cache")
|
|
}
|