-
Notifications
You must be signed in to change notification settings - Fork 202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add operator state cache to IndexedChainState #983
base: master
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package indexer_test | ||
|
||
import ( | ||
"context" | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/Layr-Labs/eigenda/core" | ||
"github.com/Layr-Labs/eigenda/core/indexer" | ||
coremock "github.com/Layr-Labs/eigenda/core/mock" | ||
indexermock "github.com/Layr-Labs/eigenda/indexer/mock" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
type testComponents struct { | ||
ChainState *coremock.ChainDataMock | ||
Indexer *indexermock.MockIndexer | ||
IndexedChainState *indexer.IndexedChainState | ||
} | ||
|
||
func TestIndexedOperatorStateCache(t *testing.T) { | ||
c := createTestComponents(t) | ||
pubKeys := &indexer.OperatorPubKeys{} | ||
c.Indexer.On("GetObject", mock.Anything, 0).Return(pubKeys, nil) | ||
sockets := indexer.OperatorSockets{ | ||
core.OperatorID{0, 1}: "socket1", | ||
} | ||
c.Indexer.On("GetObject", mock.Anything, 1).Return(sockets, nil) | ||
|
||
operatorState := &core.OperatorState{ | ||
Operators: map[core.QuorumID]map[core.OperatorID]*core.OperatorInfo{ | ||
0: { | ||
core.OperatorID{0}: { | ||
Stake: big.NewInt(100), | ||
Index: 0, | ||
}, | ||
}, | ||
}, | ||
} | ||
c.ChainState.On("GetOperatorState", mock.Anything, uint(100), []core.QuorumID{0}).Return(operatorState, nil) | ||
c.ChainState.On("GetOperatorState", mock.Anything, uint(100), []core.QuorumID{1}).Return(operatorState, nil) | ||
c.ChainState.On("GetOperatorState", mock.Anything, uint(101), []core.QuorumID{0, 1}).Return(operatorState, nil) | ||
|
||
ctx := context.Background() | ||
// Get the operator state for block 100 and quorum 0 | ||
_, err := c.IndexedChainState.GetIndexedOperatorState(ctx, uint(100), []core.QuorumID{0}) | ||
assert.NoError(t, err) | ||
c.ChainState.AssertNumberOfCalls(t, "GetOperatorState", 1) | ||
|
||
// Get the operator state for block 100 and quorum 0 again | ||
_, err = c.IndexedChainState.GetIndexedOperatorState(ctx, uint(100), []core.QuorumID{0}) | ||
assert.NoError(t, err) | ||
c.ChainState.AssertNumberOfCalls(t, "GetOperatorState", 1) | ||
|
||
// Get the operator state for block 100 and quorum 1 | ||
_, err = c.IndexedChainState.GetIndexedOperatorState(ctx, uint(100), []core.QuorumID{1}) | ||
assert.NoError(t, err) | ||
c.ChainState.AssertNumberOfCalls(t, "GetOperatorState", 2) | ||
|
||
// Get the operator state for block 101 and quorum 0 & 1 | ||
_, err = c.IndexedChainState.GetIndexedOperatorState(ctx, uint(101), []core.QuorumID{0, 1}) | ||
assert.NoError(t, err) | ||
c.ChainState.AssertNumberOfCalls(t, "GetOperatorState", 3) | ||
|
||
// Get the operator state for block 101 and quorum 0 & 1 again | ||
_, err = c.IndexedChainState.GetIndexedOperatorState(ctx, uint(101), []core.QuorumID{0, 1}) | ||
assert.NoError(t, err) | ||
c.ChainState.AssertNumberOfCalls(t, "GetOperatorState", 3) | ||
} | ||
|
||
func createTestComponents(t *testing.T) *testComponents { | ||
chainState := &coremock.ChainDataMock{} | ||
idx := &indexermock.MockIndexer{} | ||
ics, err := indexer.NewIndexedChainState(chainState, idx, 1) | ||
assert.NoError(t, err) | ||
return &testComponents{ | ||
ChainState: chainState, | ||
Indexer: idx, | ||
IndexedChainState: ics, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package thegraph | |
|
||
import ( | ||
"context" | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"math" | ||
|
@@ -10,6 +11,7 @@ import ( | |
"github.com/Layr-Labs/eigenda/core" | ||
"github.com/Layr-Labs/eigensdk-go/logging" | ||
"github.com/consensys/gnark-crypto/ecc/bn254" | ||
lru "github.com/hashicorp/golang-lru/v2" | ||
"github.com/shurcooL/graphql" | ||
) | ||
|
||
|
@@ -79,29 +81,41 @@ type ( | |
core.ChainState | ||
querier GraphQLQuerier | ||
|
||
logger logging.Logger | ||
logger logging.Logger | ||
operatorStateCache *lru.Cache[string, *core.IndexedOperatorState] | ||
} | ||
) | ||
|
||
var _ IndexedChainState = (*indexedChainState)(nil) | ||
|
||
func MakeIndexedChainState(config Config, cs core.ChainState, logger logging.Logger) *indexedChainState { | ||
|
||
func MakeIndexedChainState(config Config, cs core.ChainState, logger logging.Logger) (*indexedChainState, error) { | ||
logger.Info("Using graph node") | ||
querier := graphql.NewClient(config.Endpoint, nil) | ||
|
||
// RetryQuerier is a wrapper around the GraphQLQuerier that retries queries on failure | ||
retryQuerier := NewRetryQuerier(querier, config.PullInterval, config.MaxRetries) | ||
|
||
return NewIndexedChainState(cs, retryQuerier, logger) | ||
return NewIndexedChainState(cs, retryQuerier, logger, config.OperatorStateCacheSize) | ||
} | ||
|
||
func NewIndexedChainState(cs core.ChainState, querier GraphQLQuerier, logger logging.Logger) *indexedChainState { | ||
func NewIndexedChainState(cs core.ChainState, querier GraphQLQuerier, logger logging.Logger, cacheSize int) (*indexedChainState, error) { | ||
var operatorStateCache *lru.Cache[string, *core.IndexedOperatorState] | ||
var err error | ||
|
||
if cacheSize > 0 { | ||
operatorStateCache, err = lru.New[string, *core.IndexedOperatorState](cacheSize) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return &indexedChainState{ | ||
ChainState: cs, | ||
querier: querier, | ||
logger: logger.With("component", "IndexedChainState"), | ||
} | ||
|
||
querier: querier, | ||
logger: logger.With("component", "IndexedChainState"), | ||
operatorStateCache: operatorStateCache, | ||
}, nil | ||
} | ||
|
||
func (ics *indexedChainState) Start(ctx context.Context) error { | ||
|
@@ -124,6 +138,14 @@ func (ics *indexedChainState) Start(ctx context.Context) error { | |
} | ||
|
||
func (ics *indexedChainState) GetIndexedOperatorState(ctx context.Context, blockNumber uint, quorums []core.QuorumID) (*core.IndexedOperatorState, error) { | ||
// Check if the indexed operator state has been cached | ||
cacheKey := computeCacheKey(blockNumber, quorums) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are using cache then we need to assume that the blockNumber has been finalized already. I believe all users of this function would satisfy that assumption since they're passing in a reference block number. |
||
if ics.operatorStateCache != nil { | ||
if val, ok := ics.operatorStateCache.Get(cacheKey); ok { | ||
return val, nil | ||
} | ||
} | ||
|
||
operatorState, err := ics.ChainState.GetOperatorState(ctx, blockNumber, quorums) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -173,6 +195,11 @@ func (ics *indexedChainState) GetIndexedOperatorState(ctx context.Context, block | |
IndexedOperators: indexedOperators, | ||
AggKeys: aggKeys, | ||
} | ||
|
||
if ics.operatorStateCache != nil { | ||
ics.operatorStateCache.Add(cacheKey, state) | ||
} | ||
|
||
return state, nil | ||
} | ||
|
||
|
@@ -365,3 +392,13 @@ func convertIndexedOperatorInfoGqlToIndexedOperatorInfo(operator *IndexedOperato | |
Socket: string(operator.SocketUpdates[0].Socket), | ||
}, nil | ||
} | ||
|
||
// Computes a cache key for the operator state cache. The cache key is a | ||
// combination of the block number and the quorum IDs. Note: the order of the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if this is a problem but noticed that the cache key is dependent on the order you input the quorum ids. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's a unordered set, so you may eliminate the ordering effect by sorting the them There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good I'll add sorting. |
||
// quorum IDs matters. | ||
func computeCacheKey(blockNumber uint, quorumIDs []uint8) string { | ||
bytes := make([]byte, 8+len(quorumIDs)) | ||
binary.LittleEndian.PutUint64(bytes, uint64(blockNumber)) | ||
copy(bytes[8:], quorumIDs) | ||
return string(bytes) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should default be 0 or 32?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the estimated size bytes for 32 entries and how many instances of this struct is expected to be created? If it's small enough it should be fine to enable by default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depends on the amount of operators but let's assume 200. The order of magnitude is ~1-5mb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it seems this cache is not really useful:
Where does the cache help?