mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 21:38:05 -05:00
Use a cache of one entry to build attestation (#13300)
* Use a cache of one entry to build attestation * Gazelle * Enforce on RPC side * Rm unused var * Potuz feedback, dont use pointer * Fix tests * Init fetcher * Add in-progress * Add back missing lock * Potuz feedback * Update beacon-chain/rpc/prysm/v1alpha1/validator/attester_test.go Co-authored-by: Potuz <potuz@prysmaticlabs.com> --------- Co-authored-by: Potuz <potuz@prysmaticlabs.com>
This commit is contained in:
3
beacon-chain/cache/BUILD.bazel
vendored
3
beacon-chain/cache/BUILD.bazel
vendored
@@ -33,6 +33,7 @@ go_library(
|
||||
"//tools:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//beacon-chain/forkchoice/types:go_default_library",
|
||||
"//beacon-chain/state:go_default_library",
|
||||
"//cache/lru:go_default_library",
|
||||
"//config/fieldparams:go_default_library",
|
||||
@@ -78,6 +79,7 @@ go_test(
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//beacon-chain/forkchoice/types:go_default_library",
|
||||
"//beacon-chain/state:go_default_library",
|
||||
"//beacon-chain/state/state-native:go_default_library",
|
||||
"//config/fieldparams:go_default_library",
|
||||
@@ -90,6 +92,7 @@ go_test(
|
||||
"//testing/util:go_default_library",
|
||||
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
|
||||
"@com_github_google_gofuzz//:go_default_library",
|
||||
"@com_github_stretchr_testify//require:go_default_library",
|
||||
"@org_golang_google_protobuf//proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
172
beacon-chain/cache/attestation_data.go
vendored
172
beacon-chain/cache/attestation_data.go
vendored
@@ -1,171 +1,41 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
forkchoicetypes "github.com/prysmaticlabs/prysm/v4/beacon-chain/forkchoice/types"
|
||||
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
|
||||
)
|
||||
|
||||
var (
|
||||
// Delay parameters
|
||||
minDelay = float64(10) // 10 nanoseconds
|
||||
maxDelay = float64(100000000) // 0.1 second
|
||||
delayFactor = 1.1
|
||||
type AttestationConsensusData struct {
|
||||
Slot primitives.Slot
|
||||
HeadRoot []byte
|
||||
Target forkchoicetypes.Checkpoint
|
||||
Source forkchoicetypes.Checkpoint
|
||||
}
|
||||
|
||||
// Metrics
|
||||
attestationCacheMiss = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "attestation_cache_miss",
|
||||
Help: "The number of attestation data requests that aren't present in the cache.",
|
||||
})
|
||||
attestationCacheHit = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "attestation_cache_hit",
|
||||
Help: "The number of attestation data requests that are present in the cache.",
|
||||
})
|
||||
attestationCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "attestation_cache_size",
|
||||
Help: "The number of attestation data in the attestations cache",
|
||||
})
|
||||
)
|
||||
|
||||
// ErrAlreadyInProgress appears when attempting to mark a cache as in progress while it is
|
||||
// already in progress. The client should handle this error and wait for the in progress
|
||||
// data to resolve via Get.
|
||||
var ErrAlreadyInProgress = errors.New("already in progress")
|
||||
|
||||
// AttestationCache is used to store the cached results of an AttestationData request.
|
||||
// AttestationCache stores cached results of AttestationData requests.
|
||||
type AttestationCache struct {
|
||||
cache *cache.FIFO
|
||||
lock sync.RWMutex
|
||||
inProgress map[string]bool
|
||||
a *AttestationConsensusData
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAttestationCache initializes the map and underlying cache.
|
||||
// NewAttestationCache creates a new instance of AttestationCache.
|
||||
func NewAttestationCache() *AttestationCache {
|
||||
return &AttestationCache{
|
||||
cache: cache.NewFIFO(wrapperToKey),
|
||||
inProgress: make(map[string]bool),
|
||||
}
|
||||
return &AttestationCache{}
|
||||
}
|
||||
|
||||
// Get waits for any in progress calculation to complete before returning a
|
||||
// cached response, if any.
|
||||
func (c *AttestationCache) Get(ctx context.Context, req *ethpb.AttestationDataRequest) (*ethpb.AttestationData, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("nil attestation data request")
|
||||
}
|
||||
|
||||
s, e := reqToKey(req)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
delay := minDelay
|
||||
|
||||
// Another identical request may be in progress already. Let's wait until
|
||||
// any in progress request resolves or our timeout is exceeded.
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
if !c.inProgress[s] {
|
||||
c.lock.RUnlock()
|
||||
break
|
||||
}
|
||||
c.lock.RUnlock()
|
||||
|
||||
// This increasing backoff is to decrease the CPU cycles while waiting
|
||||
// for the in progress boolean to flip to false.
|
||||
time.Sleep(time.Duration(delay) * time.Nanosecond)
|
||||
delay *= delayFactor
|
||||
delay = math.Min(delay, maxDelay)
|
||||
}
|
||||
|
||||
item, exists, err := c.cache.GetByKey(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists && item != nil && item.(*attestationReqResWrapper).res != nil {
|
||||
attestationCacheHit.Inc()
|
||||
return ethpb.CopyAttestationData(item.(*attestationReqResWrapper).res), nil
|
||||
}
|
||||
attestationCacheMiss.Inc()
|
||||
return nil, nil
|
||||
// Get retrieves cached attestation data, recording a cache hit or miss. This method is lock free.
|
||||
func (c *AttestationCache) Get() *AttestationConsensusData {
|
||||
return c.a
|
||||
}
|
||||
|
||||
// MarkInProgress a request so that any other similar requests will block on
|
||||
// Get until MarkNotInProgress is called.
|
||||
func (c *AttestationCache) MarkInProgress(req *ethpb.AttestationDataRequest) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
s, e := reqToKey(req)
|
||||
if e != nil {
|
||||
return e
|
||||
// Put adds a response to the cache. This method is lock free.
|
||||
func (c *AttestationCache) Put(a *AttestationConsensusData) error {
|
||||
if a == nil {
|
||||
return errors.New("attestation cannot be nil")
|
||||
}
|
||||
if c.inProgress[s] {
|
||||
return ErrAlreadyInProgress
|
||||
}
|
||||
c.inProgress[s] = true
|
||||
c.a = a
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkNotInProgress will release the lock on a given request. This should be
|
||||
// called after put.
|
||||
func (c *AttestationCache) MarkNotInProgress(req *ethpb.AttestationDataRequest) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
s, e := reqToKey(req)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
delete(c.inProgress, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put the response in the cache.
|
||||
func (c *AttestationCache) Put(_ context.Context, req *ethpb.AttestationDataRequest, res *ethpb.AttestationData) error {
|
||||
data := &attestationReqResWrapper{
|
||||
req,
|
||||
res,
|
||||
}
|
||||
if err := c.cache.AddIfNotPresent(data); err != nil {
|
||||
return err
|
||||
}
|
||||
trim(c.cache, maxCacheSize)
|
||||
|
||||
attestationCacheSize.Set(float64(len(c.cache.List())))
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapperToKey(i interface{}) (string, error) {
|
||||
w, ok := i.(*attestationReqResWrapper)
|
||||
if !ok {
|
||||
return "", errors.New("key is not of type *attestationReqResWrapper")
|
||||
}
|
||||
if w == nil {
|
||||
return "", errors.New("nil wrapper")
|
||||
}
|
||||
if w.req == nil {
|
||||
return "", errors.New("nil wrapper.request")
|
||||
}
|
||||
return reqToKey(w.req)
|
||||
}
|
||||
|
||||
func reqToKey(req *ethpb.AttestationDataRequest) (string, error) {
|
||||
return fmt.Sprintf("%d", req.Slot), nil
|
||||
}
|
||||
|
||||
type attestationReqResWrapper struct {
|
||||
req *ethpb.AttestationDataRequest
|
||||
res *ethpb.AttestationData
|
||||
}
|
||||
|
||||
64
beacon-chain/cache/attestation_data_test.go
vendored
64
beacon-chain/cache/attestation_data_test.go
vendored
@@ -1,41 +1,53 @@
|
||||
package cache_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/v4/beacon-chain/cache"
|
||||
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/v4/testing/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
forkchoicetypes "github.com/prysmaticlabs/prysm/v4/beacon-chain/forkchoice/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAttestationCache_RoundTrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c := cache.NewAttestationCache()
|
||||
|
||||
req := ðpb.AttestationDataRequest{
|
||||
CommitteeIndex: 0,
|
||||
Slot: 1,
|
||||
a := c.Get()
|
||||
require.Nil(t, a)
|
||||
|
||||
insert := &cache.AttestationConsensusData{
|
||||
Slot: 1,
|
||||
HeadRoot: []byte{1},
|
||||
Target: forkchoicetypes.Checkpoint{
|
||||
Epoch: 2,
|
||||
Root: [32]byte{3},
|
||||
},
|
||||
Source: forkchoicetypes.Checkpoint{
|
||||
Epoch: 4,
|
||||
Root: [32]byte{5},
|
||||
},
|
||||
}
|
||||
err := c.Put(insert)
|
||||
require.NoError(t, err)
|
||||
|
||||
a = c.Get()
|
||||
require.Equal(t, insert, a)
|
||||
|
||||
insert = &cache.AttestationConsensusData{
|
||||
Slot: 6,
|
||||
HeadRoot: []byte{7},
|
||||
Target: forkchoicetypes.Checkpoint{
|
||||
Epoch: 8,
|
||||
Root: [32]byte{9},
|
||||
},
|
||||
Source: forkchoicetypes.Checkpoint{
|
||||
Epoch: 10,
|
||||
Root: [32]byte{11},
|
||||
},
|
||||
}
|
||||
|
||||
response, err := c.Get(ctx, req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, (*ethpb.AttestationData)(nil), response)
|
||||
err = c.Put(insert)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoError(t, c.MarkInProgress(req))
|
||||
|
||||
res := ðpb.AttestationData{
|
||||
Target: ðpb.Checkpoint{Epoch: 5, Root: make([]byte, 32)},
|
||||
}
|
||||
|
||||
assert.NoError(t, c.Put(ctx, req, res))
|
||||
assert.NoError(t, c.MarkNotInProgress(req))
|
||||
|
||||
response, err = c.Get(ctx, req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !proto.Equal(response, res) {
|
||||
t.Error("Expected equal protos to return from cache")
|
||||
}
|
||||
a = c.Get()
|
||||
require.Equal(t, insert, a)
|
||||
}
|
||||
|
||||
7
beacon-chain/cache/common.go
vendored
7
beacon-chain/cache/common.go
vendored
@@ -1,16 +1,9 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/prysmaticlabs/prysm/v4/config/params"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
// maxCacheSize is 4x of the epoch length for additional cache padding.
|
||||
// Requests should be only accessing committees within defined epoch length.
|
||||
maxCacheSize = uint64(4 * params.BeaconConfig().SlotsPerEpoch)
|
||||
)
|
||||
|
||||
// trim the FIFO queue to the maxSize.
|
||||
func trim(queue *cache.FIFO, maxSize uint64) {
|
||||
for s := uint64(len(queue.ListKeys())); s > maxSize; s-- {
|
||||
|
||||
5
beacon-chain/cache/error.go
vendored
5
beacon-chain/cache/error.go
vendored
@@ -14,4 +14,9 @@ var (
|
||||
errNotSyncCommitteeIndexPosition = errors.New("not syncCommitteeIndexPosition struct")
|
||||
// ErrNotFoundRegistration when validator registration does not exist in cache.
|
||||
ErrNotFoundRegistration = errors.Wrap(ErrNotFound, "no validator registered")
|
||||
|
||||
// ErrAlreadyInProgress appears when attempting to mark a cache as in progress while it is
|
||||
// already in progress. The client should handle this error and wait for the in progress
|
||||
// data to resolve via Get.
|
||||
ErrAlreadyInProgress = errors.New("already in progress")
|
||||
)
|
||||
|
||||
5
beacon-chain/cache/skip_slot_cache.go
vendored
5
beacon-chain/cache/skip_slot_cache.go
vendored
@@ -15,6 +15,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Delay parameters
|
||||
minDelay = float64(10) // 10 nanoseconds
|
||||
maxDelay = float64(100000000) // 0.1 second
|
||||
delayFactor = 1.1
|
||||
|
||||
// Metrics
|
||||
skipSlotCacheHit = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "skip_slot_cache_hit",
|
||||
|
||||
Reference in New Issue
Block a user