Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
086caeb
Show time spent per block.
Mar 4, 2026
1386543
Handle block creation on background goroutine.
Mar 4, 2026
8ecfba5
added missing files
Mar 4, 2026
6fc55ba
Merge branch 'main' into cody-littley/time-per-block
Mar 4, 2026
8938c6d
adjust executor queue size
Mar 4, 2026
a7d0c54
lint
Mar 5, 2026
dc531f4
Merge branch 'main' into cody-littley/time-per-block
Mar 6, 2026
8674330
made suggested changes
Mar 6, 2026
e70db1f
add receipt generation to cryptosim
jewei1997 Mar 10, 2026
9f34024
Merge branch 'cody-littley/time-per-block' into cryptosim-receipt-gen
Mar 10, 2026
c8d278a
persist block number across restarts
Mar 10, 2026
3e89a72
added reciept store simulator
Mar 10, 2026
c12d9ca
Merge branch 'main' into cryptosim-receipt-gen
Mar 11, 2026
61cd8da
fill out RecieptStoreSimulator
jewei1997 Mar 11, 2026
d10374e
add metrics to cryptosim
jewei1997 Mar 16, 2026
2cc5efa
Add new config flags
Mar 16, 2026
e13b1e2
lint
Mar 16, 2026
6752582
Merge branch 'main' into cryptosim-receipt-gen
jewei1997 Mar 17, 2026
a2eca9f
Merge branch 'main' into cryptosim-receipt-gen
jewei1997 Mar 17, 2026
2f2e4f9
fix
jewei1997 Mar 17, 2026
966935f
cryptosim reads
jewei1997 Mar 17, 2026
13912da
use ledger cache
jewei1997 Mar 17, 2026
d329019
fix receipt buckets
jewei1997 Mar 17, 2026
c600f8a
Merge branch 'cryptosim-receipt-gen' into cryptosim-receipt-gen-reads
jewei1997 Mar 17, 2026
33e5ba6
deterministic tx hashes for stateless receipt reads
jewei1997 Mar 17, 2026
200043b
add read metrics
jewei1997 Mar 18, 2026
fe39260
use duration
jewei1997 Mar 18, 2026
7169755
Merge branch 'main' into cryptosim-receipt-gen
jewei1997 Mar 18, 2026
a20def5
fix
jewei1997 Mar 18, 2026
4ebeaa7
use CodeHashKeyPrefix
jewei1997 Mar 18, 2026
00f5e88
fix
jewei1997 Mar 18, 2026
a3ca540
Merge branch 'cryptosim-receipt-gen' into cryptosim-receipt-gen-reads
jewei1997 Mar 18, 2026
f3d006d
grafana dashboard
jewei1997 Mar 18, 2026
09a0391
fix config
jewei1997 Mar 18, 2026
4e13419
fix
jewei1997 Mar 18, 2026
d236aac
fix
jewei1997 Mar 18, 2026
8d3d34b
Merge branch 'main' into cryptosim-receipt-gen-reads
jewei1997 Mar 23, 2026
da3b85a
fix
jewei1997 Mar 23, 2026
e30ae96
make recent queries more frequent for receipts
jewei1997 Mar 23, 2026
184541a
add more metrics for log reading
jewei1997 Mar 23, 2026
8feda4c
log filter cache hits at 90%
jewei1997 Mar 23, 2026
9eaffb3
add metric for num logs returned
jewei1997 Mar 23, 2026
9e9c00d
separate out read goroutine for logs
jewei1997 Mar 23, 2026
dd7b029
fix
jewei1997 Mar 23, 2026
7b8ccba
Merge branch 'main' into cryptosim-receipt-gen-reads
yzang2019 Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
806 changes: 806 additions & 0 deletions docker/monitornode/dashboards/cryptosim-dashboard.json

Large diffs are not rendered by default.

54 changes: 53 additions & 1 deletion sei-db/ledger_db/receipt/cached_receipt_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ type cachedReceiptStore struct {
cacheRotateInterval uint64
cacheNextRotate uint64
cacheMu sync.Mutex
readObserver ReceiptReadObserver
}

func newCachedReceiptStore(backend ReceiptStore) ReceiptStore {
func newCachedReceiptStore(backend ReceiptStore, observer ReceiptReadObserver) ReceiptStore {
if backend == nil {
return nil
}
Expand All @@ -44,13 +45,34 @@ func newCachedReceiptStore(backend ReceiptStore) ReceiptStore {
backend: backend,
cache: newLedgerCache(),
cacheRotateInterval: interval,
readObserver: observer,
}
if provider, ok := backend.(cacheWarmupProvider); ok {
store.cacheReceipts(provider.warmupReceipts())
}
return store
}

// StableReceiptCacheWindowBlocks returns the near-tip block window that is
// guaranteed to stay in the active write chunk until the next rotation.
func StableReceiptCacheWindowBlocks(store ReceiptStore) uint64 {
cached, ok := store.(*cachedReceiptStore)
if !ok || cached.cacheRotateInterval == 0 {
return 0
}
return cached.cacheRotateInterval
}

// EstimatedReceiptCacheWindowBlocks returns the approximate recent block window
// normally served by the in-memory receipt cache (current chunk + previous one).
func EstimatedReceiptCacheWindowBlocks(store ReceiptStore) uint64 {
hotWindow := StableReceiptCacheWindowBlocks(store)
if hotWindow == 0 {
return 0
}
return hotWindow * uint64(numCacheChunks-1)
}

func (s *cachedReceiptStore) LatestVersion() int64 {
return s.backend.LatestVersion()
}
Expand All @@ -65,15 +87,19 @@ func (s *cachedReceiptStore) SetEarliestVersion(version int64) error {

func (s *cachedReceiptStore) GetReceipt(ctx sdk.Context, txHash common.Hash) (*types.Receipt, error) {
if receipt, ok := s.cache.GetReceipt(txHash); ok {
s.reportCacheHit()
return receipt, nil
}
s.reportCacheMiss()
return s.backend.GetReceipt(ctx, txHash)
}

func (s *cachedReceiptStore) GetReceiptFromStore(ctx sdk.Context, txHash common.Hash) (*types.Receipt, error) {
if receipt, ok := s.cache.GetReceipt(txHash); ok {
s.reportCacheHit()
return receipt, nil
}
s.reportCacheMiss()
return s.backend.GetReceiptFromStore(ctx, txHash)
}

Expand All @@ -99,8 +125,10 @@ func (s *cachedReceiptStore) FilterLogs(ctx sdk.Context, fromBlock, toBlock uint

// Merge results, avoiding duplicates by tracking seen (blockNum, txIndex, logIndex)
if len(cacheLogs) == 0 {
s.reportLogFilterCacheMiss()
return backendLogs, nil
}
s.reportLogFilterCacheHit()
if len(backendLogs) == 0 {
sortLogs(cacheLogs)
return cacheLogs, nil
Expand Down Expand Up @@ -220,3 +248,27 @@ func (s *cachedReceiptStore) maybeRotateCacheLocked(blockNumber uint64) {
s.cacheNextRotate += s.cacheRotateInterval
}
}

func (s *cachedReceiptStore) reportCacheHit() {
if s.readObserver != nil {
s.readObserver.ReportReceiptCacheHit()
}
}

func (s *cachedReceiptStore) reportCacheMiss() {
if s.readObserver != nil {
s.readObserver.ReportReceiptCacheMiss()
}
}

func (s *cachedReceiptStore) reportLogFilterCacheHit() {
if s.readObserver != nil {
s.readObserver.ReportLogFilterCacheHit()
}
}

func (s *cachedReceiptStore) reportLogFilterCacheMiss() {
if s.readObserver != nil {
s.readObserver.ReportLogFilterCacheMiss()
}
}
62 changes: 59 additions & 3 deletions sei-db/ledger_db/receipt/cached_receipt_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ type fakeReceiptBackend struct {
filterLogCalls int
}

type fakeReceiptReadObserver struct {
cacheHits int
cacheMisses int
logFilterCacheHits int
logFilterCacheMisses int
}

func (f *fakeReceiptReadObserver) ReportReceiptCacheHit() {
f.cacheHits++
}

func (f *fakeReceiptReadObserver) ReportReceiptCacheMiss() {
f.cacheMisses++
}

func (f *fakeReceiptReadObserver) ReportLogFilterCacheHit() {
f.logFilterCacheHits++
}

func (f *fakeReceiptReadObserver) ReportLogFilterCacheMiss() {
f.logFilterCacheMisses++
}

func newFakeReceiptBackend() *fakeReceiptBackend {
return &fakeReceiptBackend{
receipts: make(map[common.Hash]*types.Receipt),
Expand Down Expand Up @@ -70,7 +93,7 @@ func (f *fakeReceiptBackend) Close() error {
func TestCachedReceiptStoreUsesCacheForReceipt(t *testing.T) {
ctx, _ := newTestContext()
backend := newFakeReceiptBackend()
store := newCachedReceiptStore(backend)
store := newCachedReceiptStore(backend, nil)

txHash := common.HexToHash("0x1")
addr := common.HexToAddress("0x100")
Expand All @@ -89,7 +112,7 @@ func TestCachedReceiptStoreUsesCacheForReceipt(t *testing.T) {
func TestCachedReceiptStoreFilterLogsDelegates(t *testing.T) {
ctx, _ := newTestContext()
backend := newFakeReceiptBackend()
store := newCachedReceiptStore(backend)
store := newCachedReceiptStore(backend, nil)

txHash := common.HexToHash("0x2")
addr := common.HexToAddress("0x200")
Expand Down Expand Up @@ -124,7 +147,7 @@ func TestCachedReceiptStoreFilterLogsReturnsSortedLogs(t *testing.T) {
Index: 0,
},
}
store := newCachedReceiptStore(backend)
store := newCachedReceiptStore(backend, nil)

receiptA := makeTestReceipt(common.HexToHash("0xa"), 11, 1, common.HexToAddress("0x210"), []common.Hash{common.HexToHash("0x1")})
receiptB := makeTestReceipt(common.HexToHash("0xb"), 11, 0, common.HexToAddress("0x220"), []common.Hash{common.HexToHash("0x2")})
Expand All @@ -143,3 +166,36 @@ func TestCachedReceiptStoreFilterLogsReturnsSortedLogs(t *testing.T) {
require.Equal(t, uint(1), logs[2].TxIndex)
require.Equal(t, uint64(12), logs[3].BlockNumber)
}

func TestCachedReceiptStoreReportsCacheHit(t *testing.T) {
ctx, _ := newTestContext()
backend := newFakeReceiptBackend()
observer := &fakeReceiptReadObserver{}
store := newCachedReceiptStore(backend, observer)

txHash := common.HexToHash("0x10")
receipt := makeTestReceipt(txHash, 7, 1, common.HexToAddress("0x100"), nil)

require.NoError(t, store.SetReceipts(ctx, []ReceiptRecord{{TxHash: txHash, Receipt: receipt}}))

backend.getReceiptCalls = 0
got, err := store.GetReceipt(ctx, txHash)
require.NoError(t, err)
require.Equal(t, receipt.TxHashHex, got.TxHashHex)
require.Equal(t, 0, backend.getReceiptCalls)
require.Equal(t, 1, observer.cacheHits)
require.Equal(t, 0, observer.cacheMisses)
}

func TestCachedReceiptStoreReportsCacheMiss(t *testing.T) {
ctx, _ := newTestContext()
backend := newFakeReceiptBackend()
observer := &fakeReceiptReadObserver{}
store := newCachedReceiptStore(backend, observer)

_, err := store.GetReceipt(ctx, common.HexToHash("0x404"))
require.ErrorIs(t, err, ErrNotFound)
require.Equal(t, 1, backend.getReceiptCalls)
require.Equal(t, 0, observer.cacheHits)
require.Equal(t, 1, observer.cacheMisses)
}
3 changes: 3 additions & 0 deletions sei-db/ledger_db/receipt/parquet_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ func (s *parquetReceiptStore) GetReceipt(ctx sdk.Context, txHash common.Hash) (*
return receipt, nil
}

if s.storeKey == nil {
return nil, ErrNotFound
}
store := ctx.KVStore(s.storeKey)
bz := store.Get(types.ReceiptKey(txHash))
if bz == nil {
Expand Down
21 changes: 20 additions & 1 deletion sei-db/ledger_db/receipt/receipt_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ type ReceiptRecord struct {
ReceiptBytes []byte // Optional pre-marshaled receipt (must match Receipt if set)
}

// ReceiptReadObserver receives callbacks when cached receipt lookups either hit
// the in-memory ledger cache or fall through to the backend store.
type ReceiptReadObserver interface {
ReportReceiptCacheHit()
ReportReceiptCacheMiss()
ReportLogFilterCacheHit()
ReportLogFilterCacheMiss()
}

type receiptStore struct {
db seidbtypes.StateStore
storeKey sdk.StoreKey
Expand All @@ -78,11 +87,21 @@ func normalizeReceiptBackend(backend string) string {
}

func NewReceiptStore(config dbconfig.ReceiptStoreConfig, storeKey sdk.StoreKey) (ReceiptStore, error) {
return NewReceiptStoreWithReadObserver(config, storeKey, nil)
}

// NewReceiptStoreWithReadObserver constructs a receipt store and optionally
// reports cache hits/misses for receipt-by-hash reads via observer callbacks.
func NewReceiptStoreWithReadObserver(
config dbconfig.ReceiptStoreConfig,
storeKey sdk.StoreKey,
observer ReceiptReadObserver,
) (ReceiptStore, error) {
backend, err := newReceiptBackend(config, storeKey)
if err != nil {
return nil, err
}
return newCachedReceiptStore(backend), nil
return newCachedReceiptStore(backend, observer), nil
}

// BackendTypeName returns the backend implementation name ("parquet" or "pebble") for testing.
Expand Down
3 changes: 3 additions & 0 deletions sei-db/state_db/bench/cryptosim/canned_random.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ func (cr *CannedRandom) Bytes(count int) []byte {

// Returns a slice of random bytes from a given seed. Bytes are deterministic given the same seed.
//
// Unlike most CannedRandom methods, SeededBytes is safe for concurrent use: it only reads
// from the immutable buffer and does not advance the internal index.
//
// Returned slice is NOT safe to modify. If modification is required, the caller should make a copy of the slice.
func (cr *CannedRandom) SeededBytes(count int, seed int64) []byte {
if count < 0 {
Expand Down
13 changes: 10 additions & 3 deletions sei-db/state_db/bench/cryptosim/config/reciept-store.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{
"Comment": "For testing with the state store and reciept store both enabled.",
"Comment": "For testing with the state store and receipt store both enabled, with concurrent reads, pruning, and cache.",
"DataDir": "data",
"LogDir": "logs",
"LogLevel": "info",
"MinimumNumberOfColdAccounts": 1000000,
"MinimumNumberOfDormantAccounts": 1000000,
"GenerateReceipts": true
"GenerateReceipts": true,
"ReceiptReadConcurrency": 4,
"ReceiptReadsPerSecond": 1000,
"ReceiptColdReadRatio": 0.1,
"LogFilterReadConcurrency": 4,
"LogFilterReadsPerSecond": 200,
"LogFilterColdReadRatio": 0.1
}

62 changes: 61 additions & 1 deletion sei-db/state_db/bench/cryptosim/cryptosim_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,41 @@ type CryptoSimConfig struct {
// If greater than 0, the benchmark will throttle the transaction rate to this value, in hertz.
MaxTPS float64

// Number of concurrent reader goroutines issuing receipt lookups. 0 disables reads.
ReceiptReadConcurrency int

// Target total receipt reads per second across all reader goroutines.
// Reads are distributed evenly across readers.
ReceiptReadsPerSecond int

// Fraction of single-receipt reads that intentionally target blocks older
// than the in-memory receipt-cache window (0.0-1.0).
// These reads should mostly miss cache and fall through to DuckDB.
ReceiptColdReadRatio float64

// Deprecated: ignored when LogFilterReadConcurrency > 0. Previously controlled
// the fraction of shared reads that were log filters vs receipt lookups.
ReceiptLogFilterRatio float64
Comment on lines +173 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this field is deprecated, why add it to the config?


// Number of concurrent goroutines issuing log filter (eth_getLogs) queries. 0 disables log filter reads.
// These goroutines are independent from the receipt reader goroutines.
LogFilterReadConcurrency int

// Target total log filter reads per second across all log filter goroutines.
LogFilterReadsPerSecond int
Comment on lines +181 to +182
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I know from context that these new settings are specific to receipt store benchmarking, based on the names of the settings in isolation this fact isn't immediately obvious.

I suggest one or more of the following strategies:

  • Adding a line to the godocs saying something like "This is configuration for benchmarking the receipt store, and is ignored if receipt simulation is disabled.", or
  • Adding a "Reciept" prefix to the setting name, or
  • Nest receipt store configuration in a struct that just contains receipt store configuration (that struct could live in this file or a new file, not strongly opinionated on where we put it)


// Fraction of log filter reads that intentionally target blocks older than
// the in-memory cache window (0.0-1.0). These cold reads miss the cache
// and fall through to DuckDB on closed parquet files. The remaining
// (1 - LogFilterColdReadRatio) reads target recent blocks likely in cache.
// Default 0.1 yields ~90% cache hit ratio.
LogFilterColdReadRatio float64

// Exponent controlling the recency bias within the chosen hot/cold read range.
// 1.0 = uniform within that range; higher values skew reads toward the newest
// blocks in the selected bucket.
ReceiptReadRecencyExponent float64

// Number of recent blocks to keep before pruning parquet files. 0 disables pruning.
ReceiptKeepRecent int64

Expand Down Expand Up @@ -215,6 +250,14 @@ func DefaultCryptoSimConfig() *CryptoSimConfig {
RecieptChannelCapacity: 32,
DisableTransactionExecution: false,
MaxTPS: 0,
ReceiptReadConcurrency: 0,
ReceiptReadsPerSecond: 100,
ReceiptColdReadRatio: 0.1,
ReceiptLogFilterRatio: 0,
LogFilterReadConcurrency: 0,
LogFilterReadsPerSecond: 100,
LogFilterColdReadRatio: 0.1,
ReceiptReadRecencyExponent: 3.0,
ReceiptKeepRecent: 100_000,
ReceiptPruneIntervalSeconds: 600,
LogLevel: "info",
Expand Down Expand Up @@ -302,12 +345,29 @@ func (c *CryptoSimConfig) Validate() error {
if c.MaxTPS < 0 {
return fmt.Errorf("MaxTPS must be non-negative (got %f)", c.MaxTPS)
}
if c.ReceiptColdReadRatio < 0 || c.ReceiptColdReadRatio > 1 {
return fmt.Errorf("ReceiptColdReadRatio must be in [0, 1] (got %f)", c.ReceiptColdReadRatio)
}
if c.ReceiptLogFilterRatio < 0 || c.ReceiptLogFilterRatio > 1 {
return fmt.Errorf("ReceiptLogFilterRatio must be in [0, 1] (got %f)", c.ReceiptLogFilterRatio)
}
if c.LogFilterReadConcurrency < 0 {
return fmt.Errorf("LogFilterReadConcurrency must be non-negative (got %d)", c.LogFilterReadConcurrency)
}
if c.LogFilterColdReadRatio < 0 || c.LogFilterColdReadRatio > 1 {
return fmt.Errorf("LogFilterColdReadRatio must be in [0, 1] (got %f)", c.LogFilterColdReadRatio)
}
if c.ReceiptReadConcurrency < 0 {
return fmt.Errorf("ReceiptReadConcurrency must be non-negative (got %d)", c.ReceiptReadConcurrency)
}
if c.ReceiptReadRecencyExponent < 1 {
return fmt.Errorf("ReceiptReadRecencyExponent must be >= 1.0 (got %f)", c.ReceiptReadRecencyExponent)
}
switch strings.ToLower(c.LogLevel) {
case "debug", "info", "warn", "error":
default:
return fmt.Errorf("LogLevel must be one of debug, info, warn, error (got %q)", c.LogLevel)
}

return nil
}

Expand Down
Loading
Loading