mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-04-19 03:01:06 -04:00
Compare commits
5 Commits
defer-prot
...
optimizati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2db697da2 | ||
|
|
8a8373899a | ||
|
|
5815d494c7 | ||
|
|
670eda5746 | ||
|
|
0d4892afae |
@@ -327,6 +327,8 @@ func (s *Service) executePostFinalizationTasks(ctx context.Context, finalizedSta
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go s.checkpointStateCache.EvictUpTo(finalized.Epoch)
|
||||
}
|
||||
|
||||
// ReceiveBlockBatch processes the whole block batch at once, assuming the block batch is linear ,transitioning
|
||||
|
||||
1
beacon-chain/cache/BUILD.bazel
vendored
1
beacon-chain/cache/BUILD.bazel
vendored
@@ -53,6 +53,7 @@ go_library(
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//proto/prysm/v1alpha1/attestation:go_default_library",
|
||||
"//runtime/version:go_default_library",
|
||||
"//time/slots:go_default_library",
|
||||
"@com_github_ethereum_go_ethereum//common:go_default_library",
|
||||
"@com_github_hashicorp_golang_lru//:go_default_library",
|
||||
"@com_github_patrickmn_go_cache//:go_default_library",
|
||||
|
||||
51
beacon-chain/cache/checkpoint_state.go
vendored
51
beacon-chain/cache/checkpoint_state.go
vendored
@@ -3,8 +3,10 @@ package cache
|
||||
import (
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
|
||||
lruwrpr "github.com/OffchainLabs/prysm/v7/cache/lru"
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
"github.com/OffchainLabs/prysm/v7/crypto/hash"
|
||||
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
||||
"github.com/OffchainLabs/prysm/v7/time/slots"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
@@ -25,6 +27,14 @@ var (
|
||||
Name: "check_point_state_cache_hit",
|
||||
Help: "The number of check point state requests that are present in the cache.",
|
||||
})
|
||||
checkpointStateSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "check_point_state_cache_size",
|
||||
Help: "The number of entries in the check point state cache.",
|
||||
})
|
||||
checkpointStateEvicted = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "check_point_state_cache_evicted_total",
|
||||
Help: "The number of entries evicted from the check point state cache.",
|
||||
})
|
||||
)
|
||||
|
||||
// CheckpointStateCache is a struct with 1 queue for looking up state by checkpoint.
|
||||
@@ -49,14 +59,14 @@ func (c *CheckpointStateCache) StateByCheckpoint(cp *ethpb.Checkpoint) (state.Be
|
||||
|
||||
item, exists := c.cache.Get(h)
|
||||
|
||||
if exists && item != nil {
|
||||
checkpointStateHit.Inc()
|
||||
// Copy here is unnecessary since the return will only be used to verify attestation signature.
|
||||
return item.(state.BeaconState), nil
|
||||
if !exists || item == nil {
|
||||
checkpointStateMiss.Inc()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
checkpointStateMiss.Inc()
|
||||
return nil, nil
|
||||
checkpointStateHit.Inc()
|
||||
// Copy here is unnecessary since the return will only be used to verify attestation signature.
|
||||
return item.(state.BeaconState), nil
|
||||
}
|
||||
|
||||
// AddCheckpointState adds CheckpointState object to the cache. This method also trims the least
|
||||
@@ -66,6 +76,35 @@ func (c *CheckpointStateCache) AddCheckpointState(cp *ethpb.Checkpoint, s state.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.cache.Add(h, s)
|
||||
checkpointStateSize.Set(float64(c.cache.Len()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// EvictUpTo removes all entries from the cache whose state epoch is at
|
||||
// or before the given epoch. Returns the number of evicted entries.
|
||||
func (c *CheckpointStateCache) EvictUpTo(epoch primitives.Epoch) int {
|
||||
evicted := 0
|
||||
for _, key := range c.cache.Keys() {
|
||||
// Peek is used here to avoid updating the recency of the entry,
|
||||
// as we are only checking for eviction.
|
||||
v, ok := c.cache.Peek(key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
st := v.(state.ReadOnlyBeaconState)
|
||||
if slots.ToEpoch(st.Slot()) <= epoch {
|
||||
c.cache.Remove(key)
|
||||
evicted++
|
||||
}
|
||||
}
|
||||
|
||||
if evicted > 0 {
|
||||
checkpointStateSize.Set(float64(c.cache.Len()))
|
||||
checkpointStateEvicted.Add(float64(evicted))
|
||||
}
|
||||
|
||||
return evicted
|
||||
}
|
||||
|
||||
73
beacon-chain/cache/checkpoint_state_test.go
vendored
73
beacon-chain/cache/checkpoint_state_test.go
vendored
@@ -72,3 +72,76 @@ func TestCheckpointStateCache_MaxSize(t *testing.T) {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ go_library(
|
||||
"//math:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus:go_default_library",
|
||||
"@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -9,9 +9,21 @@ import (
|
||||
multi_value_slice "github.com/OffchainLabs/prysm/v7/container/multi-value-slice"
|
||||
pmath "github.com/OffchainLabs/prysm/v7/math"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
fieldTrieLazyCopyCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "field_trie_lazy_copy_total",
|
||||
Help: "Total number of CopyTrie calls that produced a lazy copy.",
|
||||
})
|
||||
|
||||
fieldTrieMaterializeCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "field_trie_materialize_total",
|
||||
Help: "Total number of lazy copies that were materialized due to mutation.",
|
||||
})
|
||||
|
||||
ErrInvalidFieldTrie = errors.New("invalid field trie")
|
||||
ErrEmptyFieldTrie = errors.New("empty field trie")
|
||||
)
|
||||
@@ -30,6 +42,7 @@ type FieldTrie struct {
|
||||
*sync.RWMutex
|
||||
reference *stateutil.Reference
|
||||
fieldLayers [][]*[32]byte
|
||||
sharedLayers [][]*[32]byte
|
||||
field types.FieldIndex
|
||||
dataType types.DataType
|
||||
length uint64
|
||||
@@ -107,6 +120,9 @@ func (f *FieldTrie) RecomputeTrie(indices []uint64, elements any) ([32]byte, err
|
||||
return f.TrieRoot()
|
||||
}
|
||||
|
||||
// Materialize from shared layers if this is a lazy copy.
|
||||
f.materialize()
|
||||
|
||||
fieldRoots, err := fieldConverters(f.field, indices, elements, false)
|
||||
if err != nil {
|
||||
return [32]byte{}, err
|
||||
@@ -166,26 +182,20 @@ func (f *FieldTrie) RecomputeTrie(indices []uint64, elements any) ([32]byte, err
|
||||
}
|
||||
}
|
||||
|
||||
// CopyTrie copies the references to the elements the trie
|
||||
// is built on.
|
||||
// CopyTrie creates a lazy copy of the trie. The underlying layers are not
|
||||
// copied immediately, they are shared via sharedLayers. A full copy is
|
||||
// deferred until RecomputeTrie needs to mutate the layers. If the trie is
|
||||
// only read with TrieRoot, the copy cost is avoided entirely.
|
||||
func (f *FieldTrie) CopyTrie() *FieldTrie {
|
||||
if f.fieldLayers == nil {
|
||||
return &FieldTrie{
|
||||
field: f.field,
|
||||
dataType: f.dataType,
|
||||
reference: stateutil.NewRef(1),
|
||||
RWMutex: new(sync.RWMutex),
|
||||
length: f.length,
|
||||
numOfElems: f.numOfElems,
|
||||
}
|
||||
}
|
||||
dstFieldTrie := make([][]*[32]byte, len(f.fieldLayers))
|
||||
for i, layer := range f.fieldLayers {
|
||||
dstFieldTrie[i] = make([]*[32]byte, len(layer))
|
||||
copy(dstFieldTrie[i], layer)
|
||||
if f.fieldLayers != nil {
|
||||
f.sharedLayers = f.fieldLayers
|
||||
f.fieldLayers = nil
|
||||
}
|
||||
|
||||
fieldTrieLazyCopyCount.Inc()
|
||||
|
||||
return &FieldTrie{
|
||||
fieldLayers: dstFieldTrie,
|
||||
sharedLayers: f.sharedLayers,
|
||||
field: f.field,
|
||||
dataType: f.dataType,
|
||||
reference: stateutil.NewRef(1),
|
||||
@@ -208,7 +218,7 @@ func (f *FieldTrie) Length() uint64 {
|
||||
// us save on a copy. Any caller of this method will need
|
||||
// to take care that this isn't called on an empty trie.
|
||||
func (f *FieldTrie) TransferTrie() *FieldTrie {
|
||||
if f.fieldLayers == nil {
|
||||
if f.fieldLayers == nil && f.sharedLayers == nil {
|
||||
return &FieldTrie{
|
||||
field: f.field,
|
||||
dataType: f.dataType,
|
||||
@@ -220,13 +230,14 @@ func (f *FieldTrie) TransferTrie() *FieldTrie {
|
||||
}
|
||||
f.isTransferred = true
|
||||
nTrie := &FieldTrie{
|
||||
fieldLayers: f.fieldLayers,
|
||||
field: f.field,
|
||||
dataType: f.dataType,
|
||||
reference: stateutil.NewRef(1),
|
||||
RWMutex: new(sync.RWMutex),
|
||||
length: f.length,
|
||||
numOfElems: f.numOfElems,
|
||||
fieldLayers: f.fieldLayers,
|
||||
sharedLayers: f.sharedLayers,
|
||||
field: f.field,
|
||||
dataType: f.dataType,
|
||||
reference: stateutil.NewRef(1),
|
||||
RWMutex: new(sync.RWMutex),
|
||||
length: f.length,
|
||||
numOfElems: f.numOfElems,
|
||||
}
|
||||
// Zero out field layers here.
|
||||
f.fieldLayers = nil
|
||||
@@ -238,17 +249,23 @@ func (f *FieldTrie) TrieRoot() ([32]byte, error) {
|
||||
if f.Empty() {
|
||||
return [32]byte{}, ErrEmptyFieldTrie
|
||||
}
|
||||
if len(f.fieldLayers[len(f.fieldLayers)-1]) == 0 {
|
||||
|
||||
layers := f.sharedLayers
|
||||
if f.fieldLayers != nil {
|
||||
layers = f.fieldLayers
|
||||
}
|
||||
|
||||
if len(layers[len(layers)-1]) == 0 {
|
||||
return [32]byte{}, ErrInvalidFieldTrie
|
||||
}
|
||||
switch f.dataType {
|
||||
case types.BasicArray:
|
||||
return *f.fieldLayers[len(f.fieldLayers)-1][0], nil
|
||||
return *layers[len(layers)-1][0], nil
|
||||
case types.CompositeArray:
|
||||
trieRoot := *f.fieldLayers[len(f.fieldLayers)-1][0]
|
||||
return stateutil.AddInMixin(trieRoot, uint64(len(f.fieldLayers[0])))
|
||||
trieRoot := *layers[len(layers)-1][0]
|
||||
return stateutil.AddInMixin(trieRoot, uint64(len(layers[0])))
|
||||
case types.CompressedArray:
|
||||
trieRoot := *f.fieldLayers[len(f.fieldLayers)-1][0]
|
||||
trieRoot := *layers[len(layers)-1][0]
|
||||
return stateutil.AddInMixin(trieRoot, uint64(f.numOfElems))
|
||||
default:
|
||||
return [32]byte{}, errors.Errorf("unrecognized data type in field map: %v", reflect.TypeFor[types.DataType]().Name())
|
||||
@@ -264,7 +281,19 @@ func (f *FieldTrie) FieldReference() *stateutil.Reference {
|
||||
// Empty checks whether the underlying field trie is
|
||||
// empty or not.
|
||||
func (f *FieldTrie) Empty() bool {
|
||||
return f == nil || len(f.fieldLayers) == 0 || f.isTransferred
|
||||
if f == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(f.fieldLayers) == 0 && len(f.sharedLayers) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if f.isTransferred {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// InsertFieldLayer manually inserts a field layer. This method
|
||||
@@ -273,3 +302,24 @@ func (f *FieldTrie) Empty() bool {
|
||||
func (f *FieldTrie) InsertFieldLayer(layer [][]*[32]byte) {
|
||||
f.fieldLayers = layer
|
||||
}
|
||||
|
||||
// materialize performs the deferred deep copy from sharedLayers into
|
||||
// fieldLayers. This must be called before any mutation of the trie layers.
|
||||
// Caller must hold the write lock.
|
||||
func (f *FieldTrie) materialize() {
|
||||
if f.fieldLayers != nil || f.sharedLayers == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fieldTrieMaterializeCount.Inc()
|
||||
|
||||
dst := make([][]*[32]byte, 0, len(f.sharedLayers))
|
||||
for _, layer := range f.sharedLayers {
|
||||
copiedLayer := make([]*[32]byte, len(layer))
|
||||
copy(copiedLayer, layer)
|
||||
dst = append(dst, copiedLayer)
|
||||
}
|
||||
|
||||
f.fieldLayers = dst
|
||||
f.sharedLayers = nil
|
||||
}
|
||||
|
||||
@@ -74,11 +74,11 @@ func fieldConverters(field types.FieldIndex, indices []uint64, elements any, con
|
||||
func convertRoots(indices []uint64, elements any, convertAll bool) ([][32]byte, error) {
|
||||
switch castedType := elements.(type) {
|
||||
case customtypes.BlockRoots:
|
||||
return handle32ByteMVslice(multi_value_slice.BuildEmptyCompositeSlice[[32]byte](castedType), indices, convertAll)
|
||||
return handle32ByteMVslice(multi_value_slice.BuildEmptyCompositeSlice(castedType), indices, convertAll)
|
||||
case customtypes.StateRoots:
|
||||
return handle32ByteMVslice(multi_value_slice.BuildEmptyCompositeSlice[[32]byte](castedType), indices, convertAll)
|
||||
return handle32ByteMVslice(multi_value_slice.BuildEmptyCompositeSlice(castedType), indices, convertAll)
|
||||
case customtypes.RandaoMixes:
|
||||
return handle32ByteMVslice(multi_value_slice.BuildEmptyCompositeSlice[[32]byte](castedType), indices, convertAll)
|
||||
return handle32ByteMVslice(multi_value_slice.BuildEmptyCompositeSlice(castedType), indices, convertAll)
|
||||
case multi_value_slice.MultiValueSliceComposite[[32]byte]:
|
||||
return handle32ByteMVslice(castedType, indices, convertAll)
|
||||
default:
|
||||
@@ -97,7 +97,7 @@ func convertEth1DataVotes(indices []uint64, elements any, convertAll bool) ([][3
|
||||
func convertValidators(indices []uint64, elements any, convertAll bool) ([][32]byte, error) {
|
||||
switch casted := elements.(type) {
|
||||
case []*ethpb.Validator:
|
||||
return handleValidatorMVSlice(multi_value_slice.BuildEmptyCompositeSlice[*ethpb.Validator](casted), indices, convertAll)
|
||||
return handleValidatorMVSlice(multi_value_slice.BuildEmptyCompositeSlice(casted), indices, convertAll)
|
||||
case multi_value_slice.MultiValueSliceComposite[*ethpb.Validator]:
|
||||
return handleValidatorMVSlice(casted, indices, convertAll)
|
||||
default:
|
||||
@@ -116,7 +116,7 @@ func convertAttestations(indices []uint64, elements any, convertAll bool) ([][32
|
||||
func convertBalances(indices []uint64, elements any, convertAll bool) ([][32]byte, error) {
|
||||
switch casted := elements.(type) {
|
||||
case []uint64:
|
||||
return handleBalanceMVSlice(multi_value_slice.BuildEmptyCompositeSlice[uint64](casted), indices, convertAll)
|
||||
return handleBalanceMVSlice(multi_value_slice.BuildEmptyCompositeSlice(casted), indices, convertAll)
|
||||
case multi_value_slice.MultiValueSliceComposite[uint64]:
|
||||
return handleBalanceMVSlice(casted, indices, convertAll)
|
||||
default:
|
||||
|
||||
@@ -131,6 +131,9 @@ func TestNewFieldTrie_UnknownType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFieldTrie_CopyTrieImmutable(t *testing.T) {
|
||||
// CopyTrie uses lazy copy-on-write. The production contract is:
|
||||
// after CopyTrie, the COPY may be mutated while the ORIGINAL stays
|
||||
// unchanged. This test verifies mutating the copy does not affect the original.
|
||||
newState, _ := util.DeterministicGenesisState(t, 32)
|
||||
mixes := newState.RandaoMixes()
|
||||
randaoMixes := make([][32]byte, len(mixes))
|
||||
@@ -138,28 +141,84 @@ func TestFieldTrie_CopyTrieImmutable(t *testing.T) {
|
||||
randaoMixes[i] = [32]byte(r)
|
||||
}
|
||||
|
||||
trie, err := NewFieldTrie(types.RandaoMixes, types.BasicArray, customtypes.RandaoMixes(randaoMixes), uint64(params.BeaconConfig().EpochsPerHistoricalVector))
|
||||
originalTrie, err := NewFieldTrie(types.RandaoMixes, types.BasicArray, customtypes.RandaoMixes(randaoMixes), uint64(params.BeaconConfig().EpochsPerHistoricalVector))
|
||||
require.NoError(t, err)
|
||||
originalRoot, err := originalTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
|
||||
newTrie := trie.CopyTrie()
|
||||
copiedTrie := originalTrie.CopyTrie()
|
||||
|
||||
// Verify the copy initially has the same root.
|
||||
copyRoot, err := copiedTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, originalRoot, copyRoot, "copy should initially have same root")
|
||||
|
||||
// Mutate the COPY (the production pattern).
|
||||
changedIdx := []uint64{2, 29}
|
||||
|
||||
changedVals := [][32]byte{{'A', 'B'}, {'C', 'D'}}
|
||||
require.NoError(t, newState.UpdateRandaoMixesAtIndex(changedIdx[0], changedVals[0]))
|
||||
require.NoError(t, newState.UpdateRandaoMixesAtIndex(changedIdx[1], changedVals[1]))
|
||||
|
||||
mixes = newState.RandaoMixes()
|
||||
randaoMixes = make([][32]byte, len(mixes))
|
||||
for i, r := range mixes {
|
||||
randaoMixes[i] = [32]byte(r)
|
||||
}
|
||||
root, err := trie.RecomputeTrie(changedIdx, customtypes.RandaoMixes(randaoMixes))
|
||||
mutatedRoot, err := copiedTrie.RecomputeTrie(changedIdx, customtypes.RandaoMixes(randaoMixes))
|
||||
require.NoError(t, err)
|
||||
newRoot, err := newTrie.TrieRoot()
|
||||
|
||||
// The original should be unchanged.
|
||||
rootAfter, err := originalTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
if root == newRoot {
|
||||
t.Errorf("Wanted roots to be different, but they are the same: %#x", root)
|
||||
require.Equal(t, originalRoot, rootAfter, "original should be unchanged after copy mutation")
|
||||
|
||||
// The mutated copy should have a different root.
|
||||
if mutatedRoot == originalRoot {
|
||||
t.Errorf("Wanted roots to be different, but they are the same: %#x", mutatedRoot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldTrie_LazyCopyOriginalMutationDoesNotAffectCopy(t *testing.T) {
|
||||
// After LazyCopy, mutating the ORIGINAL must not affect the COPY.
|
||||
newState, _ := util.DeterministicGenesisState(t, 32)
|
||||
mixes := newState.RandaoMixes()
|
||||
randaoMixes := make([][32]byte, len(mixes))
|
||||
for i, r := range mixes {
|
||||
randaoMixes[i] = [32]byte(r)
|
||||
}
|
||||
|
||||
originalTrie, err := NewFieldTrie(types.RandaoMixes, types.BasicArray, customtypes.RandaoMixes(randaoMixes), uint64(params.BeaconConfig().EpochsPerHistoricalVector))
|
||||
require.NoError(t, err)
|
||||
originalRoot, err := originalTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
|
||||
copiedTrie := originalTrie.CopyTrie()
|
||||
|
||||
// Verify the copy initially has the same root.
|
||||
copyRoot, err := copiedTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, originalRoot, copyRoot, "copy should initially have same root")
|
||||
|
||||
// Mutate the ORIGINAL (the reverse of TestFieldTrie_CopyTrieImmutable).
|
||||
changedIdx := []uint64{2, 29}
|
||||
changedVals := [][32]byte{{'A', 'B'}, {'C', 'D'}}
|
||||
require.NoError(t, newState.UpdateRandaoMixesAtIndex(changedIdx[0], changedVals[0]))
|
||||
require.NoError(t, newState.UpdateRandaoMixesAtIndex(changedIdx[1], changedVals[1]))
|
||||
mixes = newState.RandaoMixes()
|
||||
randaoMixes = make([][32]byte, len(mixes))
|
||||
for i, r := range mixes {
|
||||
randaoMixes[i] = [32]byte(r)
|
||||
}
|
||||
mutatedRoot, err := originalTrie.RecomputeTrie(changedIdx, customtypes.RandaoMixes(randaoMixes))
|
||||
require.NoError(t, err)
|
||||
|
||||
// The copy should be unchanged.
|
||||
rootAfter, err := copiedTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, copyRoot, rootAfter, "copy should be unchanged after original mutation")
|
||||
|
||||
// The mutated original should have a different root.
|
||||
if mutatedRoot == copyRoot {
|
||||
t.Errorf("Wanted roots to be different, but they are the same: %#x", mutatedRoot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,25 +227,6 @@ func TestFieldTrie_CopyAndTransferEmpty(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.DeepEqual(t, trie, trie.CopyTrie())
|
||||
require.DeepEqual(t, trie, trie.TransferTrie())
|
||||
}
|
||||
|
||||
func TestFieldTrie_TransferTrie(t *testing.T) {
|
||||
newState, _ := util.DeterministicGenesisState(t, 32)
|
||||
maxLength := (params.BeaconConfig().ValidatorRegistryLimit*8 + 31) / 32
|
||||
trie, err := NewFieldTrie(types.Balances, types.CompressedArray, newState.Balances(), maxLength)
|
||||
require.NoError(t, err)
|
||||
oldRoot, err := trie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
|
||||
newTrie := trie.TransferTrie()
|
||||
root, err := trie.TrieRoot()
|
||||
require.ErrorIs(t, err, ErrEmptyFieldTrie)
|
||||
require.Equal(t, root, [32]byte{})
|
||||
require.NotNil(t, newTrie)
|
||||
newRoot, err := newTrie.TrieRoot()
|
||||
require.NoError(t, err)
|
||||
require.DeepEqual(t, oldRoot, newRoot)
|
||||
}
|
||||
|
||||
func FuzzFieldTrie(f *testing.F) {
|
||||
|
||||
@@ -36,13 +36,13 @@ func Test_handleEth1DataSlice_OutOfRange(t *testing.T) {
|
||||
func Test_handleValidatorSlice_OutOfRange(t *testing.T) {
|
||||
vals := make([]*ethpb.Validator, 1)
|
||||
indices := []uint64{3}
|
||||
_, err := handleValidatorMVSlice(mvslice.BuildEmptyCompositeSlice[*ethpb.Validator](vals), indices, false)
|
||||
_, err := handleValidatorMVSlice(mvslice.BuildEmptyCompositeSlice(vals), indices, false)
|
||||
assert.ErrorContains(t, "index 3 greater than number of validators 1", err)
|
||||
}
|
||||
|
||||
func TestBalancesSlice_CorrectRoots_All(t *testing.T) {
|
||||
balances := []uint64{5, 2929, 34, 1291, 354305}
|
||||
roots, err := handleBalanceMVSlice(mvslice.BuildEmptyCompositeSlice[uint64](balances), []uint64{}, true)
|
||||
roots, err := handleBalanceMVSlice(mvslice.BuildEmptyCompositeSlice(balances), []uint64{}, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var root1 [32]byte
|
||||
@@ -59,7 +59,7 @@ func TestBalancesSlice_CorrectRoots_All(t *testing.T) {
|
||||
|
||||
func TestBalancesSlice_CorrectRoots_Some(t *testing.T) {
|
||||
balances := []uint64{5, 2929, 34, 1291, 354305}
|
||||
roots, err := handleBalanceMVSlice(mvslice.BuildEmptyCompositeSlice[uint64](balances), []uint64{2, 3}, false)
|
||||
roots, err := handleBalanceMVSlice(mvslice.BuildEmptyCompositeSlice(balances), []uint64{2, 3}, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var root1 [32]byte
|
||||
|
||||
@@ -25,6 +25,7 @@ go_library(
|
||||
"getters_withdrawal.go",
|
||||
"gloas.go",
|
||||
"hasher.go",
|
||||
"merkle_layers.go",
|
||||
"multi_value_slices.go",
|
||||
"proofs.go",
|
||||
"readonly_validator.go",
|
||||
|
||||
@@ -85,7 +85,7 @@ type BeaconState struct {
|
||||
stateFieldLeaves map[types.FieldIndex]*fieldtrie.FieldTrie
|
||||
rebuildTrie map[types.FieldIndex]bool
|
||||
valMapHandler *stateutil.ValidatorMapHandler
|
||||
merkleLayers [][][]byte
|
||||
merkle *sharedMerkleLayers
|
||||
sharedFieldReferences map[types.FieldIndex]*stateutil.Reference
|
||||
}
|
||||
|
||||
|
||||
66
beacon-chain/state/state-native/merkle_layers.go
Normal file
66
beacon-chain/state/state-native/merkle_layers.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package state_native
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/state/stateutil"
|
||||
)
|
||||
|
||||
// sharedMerkleLayers wraps the beacon state's top-level Merkle tree layers with
|
||||
// reference counting so that Copy can share them instead of deep-copying.
|
||||
// All access is protected by the owning BeaconState's lock. This struct does
|
||||
// not carry its own mutex.
|
||||
type sharedMerkleLayers struct {
|
||||
layers [][][]byte
|
||||
ref *stateutil.Reference
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// newSharedMerkleLayers wraps existing layers in a ref-counted container.
|
||||
func newSharedMerkleLayers(layers [][][]byte) *sharedMerkleLayers {
|
||||
return &sharedMerkleLayers{
|
||||
layers: layers,
|
||||
ref: stateutil.NewRef(1),
|
||||
}
|
||||
}
|
||||
|
||||
// copy increments the reference count and returns the same pointer, making
|
||||
// BeaconState.Copy O(1) for this field. The caller must call ensureUnique
|
||||
// before mutating the layers.
|
||||
func (s *sharedMerkleLayers) copy() *sharedMerkleLayers {
|
||||
s.ref.AddRef()
|
||||
return s
|
||||
}
|
||||
|
||||
// ensureUnique deep-copies the layers if this instance is shared (refs > 1)
|
||||
// and returns the (possibly new) sharedMerkleLayers to use. The caller must
|
||||
// replace its field with the returned value:
|
||||
//
|
||||
// b.merkle = b.merkle.ensureUnique()
|
||||
func (s *sharedMerkleLayers) ensureUnique() *sharedMerkleLayers {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.ref.Refs() == 1 {
|
||||
return s
|
||||
}
|
||||
|
||||
// Shared. Deep-copy and detach.
|
||||
s.ref.MinusRef()
|
||||
|
||||
newLayers := make([][][]byte, len(s.layers))
|
||||
for i, layer := range s.layers {
|
||||
newLayers[i] = make([][]byte, len(layer))
|
||||
for j, content := range layer {
|
||||
newLayers[i][j] = make([]byte, len(content))
|
||||
copy(newLayers[i][j], content)
|
||||
}
|
||||
}
|
||||
|
||||
return newSharedMerkleLayers(newLayers)
|
||||
}
|
||||
|
||||
// release decrements the reference count. Called during finalizer cleanup.
|
||||
func (s *sharedMerkleLayers) release() {
|
||||
s.ref.MinusRef()
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (b *BeaconState) CurrentSyncCommitteeProof(ctx context.Context) ([][]byte,
|
||||
if err := b.recomputeDirtyFields(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return trie.ProofFromMerkleLayers(b.merkleLayers, types.CurrentSyncCommittee.RealPosition()), nil
|
||||
return trie.ProofFromMerkleLayers(b.merkle.layers, types.CurrentSyncCommittee.RealPosition()), nil
|
||||
}
|
||||
|
||||
// NextSyncCommitteeProof from the state's Merkle trie representation.
|
||||
@@ -74,7 +74,7 @@ func (b *BeaconState) NextSyncCommitteeProof(ctx context.Context) ([][]byte, err
|
||||
if err := b.recomputeDirtyFields(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return trie.ProofFromMerkleLayers(b.merkleLayers, types.NextSyncCommittee.RealPosition()), nil
|
||||
return trie.ProofFromMerkleLayers(b.merkle.layers, types.NextSyncCommittee.RealPosition()), nil
|
||||
}
|
||||
|
||||
// FinalizedRootProof crafts a Merkle proof for the finalized root
|
||||
@@ -102,7 +102,7 @@ func (b *BeaconState) FinalizedRootProof(ctx context.Context) ([][]byte, error)
|
||||
epochRoot := bytesutil.ToBytes32(epochBuf)
|
||||
proof := make([][]byte, 0)
|
||||
proof = append(proof, epochRoot[:])
|
||||
branch := trie.ProofFromMerkleLayers(b.merkleLayers, types.FinalizedCheckpoint.RealPosition())
|
||||
branch := trie.ProofFromMerkleLayers(b.merkle.layers, types.FinalizedCheckpoint.RealPosition())
|
||||
proof = append(proof, branch...)
|
||||
return proof, nil
|
||||
}
|
||||
|
||||
@@ -160,13 +160,13 @@ func (b *BeaconState) AppendHistoricalSummaries(summary *ethpb.HistoricalSummary
|
||||
// hold the lock before calling this method.
|
||||
func (b *BeaconState) recomputeRoot(idx int) {
|
||||
hashFunc := hash.CustomSHA256Hasher()
|
||||
layers := b.merkleLayers
|
||||
layers := b.merkle.layers
|
||||
// The merkle tree structure looks as follows:
|
||||
// [[r1, r2, r3, r4], [parent1, parent2], [root]]
|
||||
// Using information about the index which changed, idx, we recompute
|
||||
// only its branch up the tree.
|
||||
currentIndex := idx
|
||||
root := b.merkleLayers[0][idx]
|
||||
root := layers[0][idx]
|
||||
for i := 0; i < len(layers)-1; i++ {
|
||||
isLeft := currentIndex%2 == 0
|
||||
neighborIdx := currentIndex ^ 1
|
||||
@@ -187,7 +187,6 @@ func (b *BeaconState) recomputeRoot(idx int) {
|
||||
layers[i+1][parentIdx] = root
|
||||
currentIndex = parentIdx
|
||||
}
|
||||
b.merkleLayers = layers
|
||||
}
|
||||
|
||||
func (b *BeaconState) markFieldAsDirty(field types.FieldIndex) {
|
||||
|
||||
@@ -307,7 +307,7 @@ func TestBeaconState_AppendBalanceWithTrie(t *testing.T) {
|
||||
}
|
||||
_, err = st.HashTreeRoot(t.Context())
|
||||
assert.NoError(t, err)
|
||||
newRt := bytesutil.ToBytes32(st.merkleLayers[0][types.Balances])
|
||||
newRt := bytesutil.ToBytes32(st.merkle.layers[0][types.Balances])
|
||||
wantedRt, err := stateutil.Uint64ListRootWithRegistryLimit(st.Balances())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, wantedRt, newRt, "state roots are unequal")
|
||||
|
||||
@@ -1024,18 +1024,12 @@ func (b *BeaconState) Copy() state.BeaconState {
|
||||
}
|
||||
}
|
||||
|
||||
if b.merkleLayers != nil {
|
||||
dst.merkleLayers = make([][][]byte, len(b.merkleLayers))
|
||||
for i, layer := range b.merkleLayers {
|
||||
dst.merkleLayers[i] = make([][]byte, len(layer))
|
||||
for j, content := range layer {
|
||||
dst.merkleLayers[i][j] = make([]byte, len(content))
|
||||
copy(dst.merkleLayers[i][j], content)
|
||||
}
|
||||
}
|
||||
if b.merkle != nil {
|
||||
dst.merkle = b.merkle.copy()
|
||||
}
|
||||
|
||||
state.Count.Inc()
|
||||
|
||||
// Finalizer runs when dst is being destroyed in garbage collection.
|
||||
runtime.SetFinalizer(dst, finalizerCleanup)
|
||||
return dst
|
||||
@@ -1055,14 +1049,14 @@ func (b *BeaconState) HashTreeRoot(ctx context.Context) ([32]byte, error) {
|
||||
if err := b.recomputeDirtyFields(ctx); err != nil {
|
||||
return [32]byte{}, err
|
||||
}
|
||||
return bytesutil.ToBytes32(b.merkleLayers[len(b.merkleLayers)-1][0]), nil
|
||||
return bytesutil.ToBytes32(b.merkle.layers[len(b.merkle.layers)-1][0]), nil
|
||||
}
|
||||
|
||||
// Initializes the Merkle layers for the beacon state if they are empty.
|
||||
//
|
||||
// WARNING: Caller must acquire the mutex before using.
|
||||
func (b *BeaconState) initializeMerkleLayers(ctx context.Context) error {
|
||||
if len(b.merkleLayers) > 0 {
|
||||
if b.merkle != nil && len(b.merkle.layers) > 0 {
|
||||
return nil
|
||||
}
|
||||
fieldRoots, err := ComputeFieldRootsWithHasher(ctx, b)
|
||||
@@ -1070,7 +1064,7 @@ func (b *BeaconState) initializeMerkleLayers(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
layers := stateutil.Merkleize(fieldRoots)
|
||||
b.merkleLayers = layers
|
||||
b.merkle = newSharedMerkleLayers(layers)
|
||||
switch b.version {
|
||||
case version.Phase0:
|
||||
b.dirtyFields = make(map[types.FieldIndex]bool, params.BeaconConfig().BeaconStateFieldCount)
|
||||
@@ -1099,13 +1093,17 @@ func (b *BeaconState) initializeMerkleLayers(ctx context.Context) error {
|
||||
//
|
||||
// WARNING: Caller must acquire the mutex before using.
|
||||
func (b *BeaconState) recomputeDirtyFields(ctx context.Context) error {
|
||||
if len(b.dirtyFields) > 0 {
|
||||
b.merkle = b.merkle.ensureUnique()
|
||||
}
|
||||
|
||||
for field := range b.dirtyFields {
|
||||
root, err := b.rootSelector(ctx, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idx := field.RealPosition()
|
||||
b.merkleLayers[0][idx] = root[:]
|
||||
b.merkle.layers[0][idx] = root[:]
|
||||
b.recomputeRoot(idx)
|
||||
delete(b.dirtyFields, field)
|
||||
}
|
||||
@@ -1462,6 +1460,9 @@ func finalizerCleanup(b *BeaconState) {
|
||||
if b.validatorsMultiValue != nil {
|
||||
b.validatorsMultiValue.Detach(b)
|
||||
}
|
||||
if b.merkle != nil {
|
||||
b.merkle.release()
|
||||
}
|
||||
|
||||
state.Count.Sub(1)
|
||||
}
|
||||
|
||||
3
changelog/manu-checkpoint-state-cache.md
Normal file
3
changelog/manu-checkpoint-state-cache.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Implement finalization-based eviction for `CheckpointStateCache`.
|
||||
Reference in New Issue
Block a user