diff --git a/docker/monitornode/dashboards/cryptosim-dashboard.json b/docker/monitornode/dashboards/cryptosim-dashboard.json index 12d6542a09..288035db07 100644 --- a/docker/monitornode/dashboards/cryptosim-dashboard.json +++ b/docker/monitornode/dashboards/cryptosim-dashboard.json @@ -2587,6 +2587,442 @@ "x": 0, "y": 37 }, + "id": 277, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 278, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "histogram_quantile(0.99, rate(cryptosim_receipt_block_write_duration_seconds_bucket[$__rate_interval]))", + "legendFormat": "p99", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, rate(cryptosim_receipt_block_write_duration_seconds_bucket[$__rate_interval]))", + "instant": false, + "legendFormat": "p95", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.50, rate(cryptosim_receipt_block_write_duration_seconds_bucket[$__rate_interval]))", + "instant": false, + "legendFormat": "p50", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rate(cryptosim_receipt_block_write_duration_seconds_sum[$__rate_interval]) / rate(cryptosim_receipt_block_write_duration_seconds_count[$__rate_interval])", + "instant": false, + "legendFormat": "average", + "range": true, + "refId": "D" + } + ], + "title": "Receipt Write Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 279, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rate(cryptosim_receipts_written_total[$__rate_interval])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Receipts Written/sec", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "locale" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 280, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "editorMode": "code", + "expr": "cryptosim_receipt_channel_depth", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Receipt Channel Depth", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 281, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "rate(cryptosim_receipt_errors_total[$__rate_interval])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Receipt Errors", + "type": "timeseries" + } + ], + "title": "Receipts", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, "id": 29, "panels": [ { @@ -2884,7 +3320,7 @@ "h": 1, "w": 24, "x": 0, - "y": 38 + "y": 39 }, "id": 35, "panels": [ @@ -3182,7 +3618,7 @@ "h": 1, "w": 24, "x": 0, - "y": 39 + "y": 40 }, "id": 37, "panels": [ @@ -3671,7 +4107,7 @@ "h": 1, "w": 24, "x": 0, - "y": 40 + "y": 41 }, "id": 44, "panels": [ @@ -3780,7 +4216,7 @@ "h": 1, "w": 24, "x": 0, - "y": 41 + "y": 42 }, "id": 117, "panels": [ @@ -4364,7 +4800,7 @@ "h": 1, "w": 24, "x": 0, - "y": 42 + "y": 43 }, "id": 191, "panels": [ @@ -5041,7 +5477,7 @@ "h": 1, "w": 24, "x": 0, - "y": 43 + "y": 44 }, "id": 118, "panels": [ @@ -6378,7 +6814,7 @@ "h": 1, "w": 24, "x": 0, - "y": 44 + "y": 45 }, "id": 115, "panels": [ @@ -7337,7 +7773,7 @@ "h": 1, "w": 24, "x": 0, - "y": 45 + "y": 46 }, "id": 193, "panels": [ @@ -8667,7 +9103,7 @@ "h": 1, "w": 24, "x": 0, - "y": 46 + "y": 47 }, "id": 192, "panels": [ @@ -9437,7 +9873,7 @@ "h": 1, "w": 24, "x": 0, - "y": 47 + "y": 48 }, "id": 194, "panels": [ @@ -11345,7 +11781,7 @@ "h": 1, "w": 24, "x": 0, - "y": 48 + "y": 49 }, "id": 195, "panels": [ @@ -12020,7 +12456,7 @@ "h": 1, "w": 24, "x": 0, - "y": 49 + "y": 50 }, "id": 210, "panels": [ @@ -12794,7 +13230,7 @@ "h": 1, "w": 24, "x": 0, - "y": 50 + "y": 51 }, "id": 230, "panels": [ @@ -13186,7 +13622,7 @@ "h": 1, "w": 24, "x": 0, - "y": 51 + "y": 52 }, "id": 250, "panels": [ @@ -13675,7 +14111,7 @@ "h": 1, "w": 24, "x": 0, - "y": 52 + "y": 53 }, "id": 100, "panels": [ @@ -15308,6 +15744,6 @@ "timezone": "browser", "title": "CryptoSim", "uid": "adnqfm4", - "version": 29, + "version": 30, "weekStart": "" -} \ No newline at end of file +} diff --git a/sei-db/ledger_db/receipt/parquet_store.go b/sei-db/ledger_db/receipt/parquet_store.go index db24aff47e..bdf88b2fd0 100644 --- a/sei-db/ledger_db/receipt/parquet_store.go +++ b/sei-db/ledger_db/receipt/parquet_store.go @@ -177,7 +177,7 @@ func (s *parquetReceiptStore) SetReceipts(ctx sdk.Context, receipts []ReceiptRec BlockNumber: blockNumber, ReceiptBytes: parquet.CopyBytesOrEmpty(receiptBytes), }, - Logs: buildParquetLogRecords(txLogs, blockHash), + Logs: BuildParquetLogRecords(txLogs, blockHash), ReceiptBytes: parquet.CopyBytesOrEmpty(receiptBytes), }) } @@ -308,7 +308,7 @@ func (s *parquetReceiptStore) replayWAL() error { BlockNumber: blockNumber, ReceiptBytes: parquet.CopyBytesOrEmpty(receiptBytes), }, - Logs: buildParquetLogRecords(txLogs, blockHash), + Logs: BuildParquetLogRecords(txLogs, blockHash), } if err := s.store.ApplyReceiptFromReplay(input); err != nil { @@ -348,14 +348,14 @@ func truncateReplayWAL(w interface{ TruncateBefore(offset uint64) error }, dropO return nil } -func buildParquetLogRecords(logs []*ethtypes.Log, blockHash common.Hash) []parquet.LogRecord { +func BuildParquetLogRecords(logs []*ethtypes.Log, blockHash common.Hash) []parquet.LogRecord { if len(logs) == 0 { return nil } records := make([]parquet.LogRecord, 0, len(logs)) for _, lg := range logs { - topic0, topic1, topic2, topic3 := extractLogTopics(lg.Topics) + topic0, topic1, topic2, topic3 := ExtractLogTopics(lg.Topics) rec := parquet.LogRecord{ BlockNumber: lg.BlockNumber, TxHash: lg.TxHash[:], @@ -393,7 +393,7 @@ func buildTopicsFromParquetLogResult(lr parquet.LogResult) []common.Hash { return topicList } -func extractLogTopics(topics []common.Hash) ([]byte, []byte, []byte, []byte) { +func ExtractLogTopics(topics []common.Hash) ([]byte, []byte, []byte, []byte) { t0 := make([]byte, 0) t1 := make([]byte, 0) t2 := make([]byte, 0) diff --git a/sei-db/state_db/bench/cryptosim/block.go b/sei-db/state_db/bench/cryptosim/block.go index a7833f957d..9c935935ae 100644 --- a/sei-db/state_db/bench/cryptosim/block.go +++ b/sei-db/state_db/bench/cryptosim/block.go @@ -1,6 +1,10 @@ package cryptosim -import "iter" +import ( + "iter" + + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" +) // A simulated block of transactions. type block struct { @@ -9,6 +13,9 @@ type block struct { // The transactions in the block. transactions []*transaction + // If receipt generation is enabled, this will contain the receipts for each transaction in the block. + reciepts []*evmtypes.Receipt + // The block number. This is not currently preserved across benchmark restarts, but otherwise monotonically // increases as you'd expect. blockNumber int64 @@ -32,11 +39,18 @@ func NewBlock( blockNumber int64, capacity int, ) *block { + + var reciepts []*evmtypes.Receipt + if config.GenerateReceipts { + reciepts = make([]*evmtypes.Receipt, 0, capacity) + } + return &block{ config: config, blockNumber: blockNumber, transactions: make([]*transaction, 0, capacity), metrics: metrics, + reciepts: reciepts, } } @@ -56,6 +70,11 @@ func (b *block) AddTransaction(txn *transaction) { b.transactions = append(b.transactions, txn) } +// Adds a receipt to the block. +func (b *block) AddReceipt(receipt *evmtypes.Receipt) { + b.reciepts = append(b.reciepts, receipt) +} + // Returns the block number. func (b *block) BlockNumber() int64 { return b.blockNumber diff --git a/sei-db/state_db/bench/cryptosim/block_builder.go b/sei-db/state_db/bench/cryptosim/block_builder.go index eb36b5d281..66d45e66d5 100644 --- a/sei-db/state_db/bench/cryptosim/block_builder.go +++ b/sei-db/state_db/bench/cryptosim/block_builder.go @@ -69,6 +69,21 @@ func (b *blockBuilder) buildBlock() *block { continue } blk.AddTransaction(txn) + + if b.config.GenerateReceipts { + receipt, err := BuildERC20TransferReceiptFromTxn( + b.dataGenerator.Rand(), + b.dataGenerator.FeeCollectionAddress(), + uint64(blk.BlockNumber()), //nolint:gosec + uint32(i), //nolint:gosec + txn, + ) + if err != nil { + fmt.Printf("failed to build receipt: %v\n", err) + continue + } + blk.AddReceipt(receipt) + } } blk.SetBlockAccountStats( diff --git a/sei-db/state_db/bench/cryptosim/config/reciept-store.json b/sei-db/state_db/bench/cryptosim/config/reciept-store.json new file mode 100644 index 0000000000..dbb621e8ae --- /dev/null +++ b/sei-db/state_db/bench/cryptosim/config/reciept-store.json @@ -0,0 +1,8 @@ +{ + "Comment": "For testing with the state store and reciept store both enabled.", + "DataDir": "data", + "MinimumNumberOfColdAccounts": 1000000, + "MinimumNumberOfDormantAccounts": 1000000, + "GenerateReceipts": true +} + diff --git a/sei-db/state_db/bench/cryptosim/cryptosim.go b/sei-db/state_db/bench/cryptosim/cryptosim.go index 0f1899fc14..2b1b593250 100644 --- a/sei-db/state_db/bench/cryptosim/cryptosim.go +++ b/sei-db/state_db/bench/cryptosim/cryptosim.go @@ -7,6 +7,7 @@ import ( "time" "github.com/sei-protocol/sei-chain/sei-db/state_db/bench/wrappers" + "golang.org/x/time/rate" ) const ( @@ -17,8 +18,9 @@ const ( // EVM key sizes (matches sei-db/common/evm). const ( - AddressLen = 20 // EVM address length - SlotLen = 32 // EVM storage slot length + AddressLen = 20 // EVM address length + SlotLen = 32 // EVM storage slot length + StorageKeyLen = AddressLen + SlotLen ) // The test runner for the cryptosim benchmark. @@ -74,6 +76,12 @@ type CryptoSim struct { // This is fixed after initial setup is complete, since we don't currently simulate // the creation of new ERC20 contracts during the benchmark. nextERC20ContractID int64 + + // The channel that holds blocks sent to the receipt store. + recieptsChan chan *block + + // Enforces a maximum transaction rate (if enabled). + rateLimiter *rate.Limiter } // Creates a new cryptosim benchmark runner. @@ -137,7 +145,7 @@ func NewCryptoSim( start := time.Now() - database := NewDatabase(config, db, metrics) + database := NewDatabase(config, db, metrics, 0) dataGenerator, err := NewDataGenerator(config, database, rand, metrics) if err != nil { @@ -147,6 +155,7 @@ func NewCryptoSim( } return nil, fmt.Errorf("failed to create data generator: %w", err) } + database.nextBlockNumber = dataGenerator.InitialNextBlockNumber() threadCount := int(config.ThreadsPerCore)*runtime.NumCPU() + config.ConstantThreadCount if threadCount < 1 { threadCount = 1 @@ -156,7 +165,23 @@ func NewCryptoSim( executors := make([]*TransactionExecutor, threadCount) for i := 0; i < threadCount; i++ { executors[i] = NewTransactionExecutor( - ctx, cancel, database, dataGenerator.FeeCollectionAddress(), config.ExecutorQueueSize, metrics) + ctx, cancel, config, database, dataGenerator.FeeCollectionAddress(), config.ExecutorQueueSize, metrics) + } + + var recieptsChan chan *block + if config.GenerateReceipts { + recieptsChan = make(chan *block, config.RecieptChannelCapacity) + _, err := NewRecieptStoreSimulator(ctx, config, recieptsChan, metrics) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to create receipt store simulator: %w", err) + } + metrics.startReceiptChannelDepthSampling(recieptsChan, config.BackgroundMetricsScrapeInterval) + } + + var rateLimiter *rate.Limiter + if config.MaxTPS > 0 { + rateLimiter = rate.NewLimiter(rate.Limit(config.MaxTPS), config.TransactionsPerBlock) } blockBuilder := NewBlockBuilder(ctx, config, metrics, dataGenerator) @@ -175,6 +200,8 @@ func NewCryptoSim( executors: executors, metrics: metrics, suspendChan: make(chan bool, 1), + recieptsChan: recieptsChan, + rateLimiter: rateLimiter, } database.SetFlushFunc(c.flushExecutors) @@ -233,7 +260,7 @@ func (c *CryptoSim) setupAccounts() error { if err != nil { return fmt.Errorf("failed to create new account: %w", err) } - c.database.IncrementTransactionCount(1) + c.database.IncrementTransactionCount() finalized, err := c.database.MaybeFinalizeBlock( c.dataGenerator.NextAccountID(), c.dataGenerator.NextErc20ContractID()) if err != nil { @@ -255,7 +282,8 @@ func (c *CryptoSim) setupAccounts() error { fmt.Printf("Created %s of %s accounts. \n", int64Commas(c.dataGenerator.NextAccountID()), int64Commas(int64(requiredNumberOfAccounts))) - err := c.database.FinalizeBlock(c.dataGenerator.NextAccountID(), c.dataGenerator.NextErc20ContractID(), true) + err := c.database.FinalizeBlock( + c.dataGenerator.NextAccountID(), c.dataGenerator.NextErc20ContractID(), true) if err != nil { return fmt.Errorf("failed to finalize block: %w", err) } @@ -290,7 +318,7 @@ func (c *CryptoSim) setupErc20Contracts() error { break } - c.database.IncrementTransactionCount(1) + c.database.IncrementTransactionCount() _, _, err := c.dataGenerator.CreateNewErc20Contract(c.config.Erc20ContractSize, true) if err != nil { @@ -320,7 +348,10 @@ func (c *CryptoSim) setupErc20Contracts() error { fmt.Printf("Created %s of %s simulated ERC20 contracts. \n", int64Commas(c.dataGenerator.NextErc20ContractID()), int64Commas(int64(c.config.MinimumNumberOfErc20Contracts))) - err := c.database.FinalizeBlock(c.dataGenerator.NextAccountID(), c.dataGenerator.NextErc20ContractID(), true) + err := c.database.FinalizeBlock( + c.dataGenerator.NextAccountID(), + c.dataGenerator.NextErc20ContractID(), + true) if err != nil { return fmt.Errorf("failed to finalize block: %w", err) } @@ -365,6 +396,7 @@ func (c *CryptoSim) run() { c.cancel() return case blk := <-c.blockBuilder.blocksChan: + c.maybeThrottle() c.handleNextBlock(blk) } @@ -372,12 +404,30 @@ func (c *CryptoSim) run() { } } +// Potentially block for a while if we are throttling the transaction rate. +func (c *CryptoSim) maybeThrottle() { + if c.config.MaxTPS == 0 { + // Throttling is disabled. + return + } + + c.metrics.SetMainThreadPhase("throttling") + + if err := c.rateLimiter.WaitN(c.ctx, c.config.TransactionsPerBlock); err != nil { + fmt.Printf("failed to wait for rate limit: %v\n", err) + c.cancel() + return + } +} + // Execute and finalize the next block. func (c *CryptoSim) handleNextBlock(blk *block) { c.mostRecentBlock = blk c.metrics.SetMainThreadPhase("send_to_executors") - c.database.IncrementTransactionCount(blk.TransactionCount()) + for i := int64(0); i < blk.TransactionCount(); i++ { + c.database.IncrementTransactionCount() + } for txn := range blk.Iterator() { c.executors[c.nextExecutorIndex].ScheduleForExecution(txn) @@ -389,6 +439,15 @@ func (c *CryptoSim) handleNextBlock(blk *block) { c.cancel() return } + + if c.config.GenerateReceipts { + select { + case <-c.ctx.Done(): + return + case c.recieptsChan <- blk: + } + } + blk.ReportBlockMetrics() } diff --git a/sei-db/state_db/bench/cryptosim/cryptosim_config.go b/sei-db/state_db/bench/cryptosim/cryptosim_config.go index 73d6c604d0..53b5f28e65 100644 --- a/sei-db/state_db/bench/cryptosim/cryptosim_config.go +++ b/sei-db/state_db/bench/cryptosim/cryptosim_config.go @@ -140,6 +140,30 @@ type CryptoSimConfig struct { // The capacity of the channel that holds blocks awaiting execution. BlockChannelCapacity int + // If true, the benchmark will generate receipts for each transaction in each block and + // feed those receipts into the receipt store. + GenerateReceipts bool + + // The capacity of the channel that holds blocks sent to the receipt store. + RecieptChannelCapacity int + + // If true, disables simulation of transaction execution, and writes very little to the database. This is + // potentially useful when benchmarking things other than state storage (e.g. the receipt store). + // + // Note that switching execution on after previously running with execution disabled may result in buggy behavior, + // as the benchmark will not be properly maintaining DB state when transaction execution is disabled. In order + // to switch transaction execution back on, it is necessary to delete the on-disk database and start over. + DisableTransactionExecution bool + + // If greater than 0, the benchmark will throttle the transaction rate to this value, in hertz. + MaxTPS float64 + + // Number of recent blocks to keep before pruning parquet files. 0 disables pruning. + ReceiptKeepRecent int64 + + // Interval in seconds between prune checks. 0 disables pruning. + ReceiptPruneIntervalSeconds int64 + // Directory for seilog output files. Independent of DataDir so logs and data // live in separate trees. Supports ~ expansion and relative paths (resolved // from cwd). Must be set, there is no default. @@ -187,6 +211,12 @@ func DefaultCryptoSimConfig() *CryptoSimConfig { BackgroundMetricsScrapeInterval: 60, EnableSuspension: true, BlockChannelCapacity: 8, + GenerateReceipts: false, + RecieptChannelCapacity: 32, + DisableTransactionExecution: false, + MaxTPS: 0, + ReceiptKeepRecent: 100_000, + ReceiptPruneIntervalSeconds: 600, LogLevel: "info", } } @@ -266,6 +296,12 @@ func (c *CryptoSimConfig) Validate() error { if c.BlockChannelCapacity < 1 { return fmt.Errorf("BlockChannelCapacity must be at least 1 (got %d)", c.BlockChannelCapacity) } + if c.RecieptChannelCapacity < 1 { + return fmt.Errorf("RecieptChannelCapacity must be at least 1 (got %d)", c.RecieptChannelCapacity) + } + if c.MaxTPS < 0 { + return fmt.Errorf("MaxTPS must be non-negative (got %f)", c.MaxTPS) + } switch strings.ToLower(c.LogLevel) { case "debug", "info", "warn", "error": default: diff --git a/sei-db/state_db/bench/cryptosim/cryptosim_metrics.go b/sei-db/state_db/bench/cryptosim/cryptosim_metrics.go index e7b47be491..050a8f6772 100644 --- a/sei-db/state_db/bench/cryptosim/cryptosim_metrics.go +++ b/sei-db/state_db/bench/cryptosim/cryptosim_metrics.go @@ -19,6 +19,12 @@ import ( const cryptosimMeterName = "cryptosim" +var receiptWriteLatencyBuckets = []float64{ + 0.001, 0.0025, 0.005, 0.0075, 0.01, + 0.015, 0.02, 0.03, 0.05, 0.075, + 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, +} + // CryptosimMetrics holds OpenTelemetry metrics for the cryptosim benchmark. // Metrics are exported via whatever exporter is configured on the global OTel // MeterProvider (e.g., Prometheus, OTLP). This package does not import Prometheus. @@ -42,6 +48,12 @@ type CryptosimMetrics struct { processWriteCountTotal metric.Int64Counter uptimeSeconds metric.Float64Gauge + // Receipt metrics + receiptBlockWriteDuration metric.Float64Histogram + receiptChannelDepth metric.Int64Gauge + receiptsWrittenTotal metric.Int64Counter + receiptErrorsTotal metric.Int64Counter + mainThreadPhase *metrics.PhaseTimer transactionPhaseTimerFactory *metrics.PhaseTimerFactory } @@ -146,6 +158,28 @@ func NewCryptosimMetrics( metric.WithUnit("s"), ) + receiptBlockWriteDuration, _ := meter.Float64Histogram( + "cryptosim_receipt_block_write_duration_seconds", + metric.WithDescription("Time to write a block of receipts to the parquet store"), + metric.WithExplicitBucketBoundaries(receiptWriteLatencyBuckets...), + metric.WithUnit("s"), + ) + receiptChannelDepth, _ := meter.Int64Gauge( + "cryptosim_receipt_channel_depth", + metric.WithDescription("Current number of blocks queued for receipt writing"), + metric.WithUnit("{count}"), + ) + receiptsWrittenTotal, _ := meter.Int64Counter( + "cryptosim_receipts_written_total", + metric.WithDescription("Total number of receipts written to the parquet store"), + metric.WithUnit("{count}"), + ) + receiptErrorsTotal, _ := meter.Int64Counter( + "cryptosim_receipt_errors_total", + metric.WithDescription("Total receipt processing errors (marshal or write failures)"), + metric.WithUnit("{count}"), + ) + mainThreadPhase := dbPhaseTimer if mainThreadPhase == nil { mainThreadPhase = metrics.NewPhaseTimer(meter, "seidb_main_thread") @@ -171,6 +205,10 @@ func NewCryptosimMetrics( processReadCountTotal: processReadCountTotal, processWriteCountTotal: processWriteCountTotal, uptimeSeconds: uptimeSeconds, + receiptBlockWriteDuration: receiptBlockWriteDuration, + receiptChannelDepth: receiptChannelDepth, + receiptsWrittenTotal: receiptsWrittenTotal, + receiptErrorsTotal: receiptErrorsTotal, mainThreadPhase: mainThreadPhase, transactionPhaseTimerFactory: transactionPhaseTimerFactory, } @@ -424,3 +462,45 @@ func (m *CryptosimMetrics) SetMainThreadPhase(phase string) { } m.mainThreadPhase.SetPhase(phase) } + +func (m *CryptosimMetrics) RecordReceiptBlockWriteDuration(latency time.Duration) { + if m == nil || m.receiptBlockWriteDuration == nil { + return + } + m.receiptBlockWriteDuration.Record(context.Background(), latency.Seconds()) +} + +func (m *CryptosimMetrics) ReportReceiptsWritten(count int64) { + if m == nil || m.receiptsWrittenTotal == nil { + return + } + m.receiptsWrittenTotal.Add(context.Background(), count) +} + +func (m *CryptosimMetrics) ReportReceiptError() { + if m == nil || m.receiptErrorsTotal == nil { + return + } + m.receiptErrorsTotal.Add(context.Background(), 1) +} + +// startReceiptChannelDepthSampling periodically records the depth of the receipt channel. +func (m *CryptosimMetrics) startReceiptChannelDepthSampling(ch <-chan *block, intervalSeconds int) { + if m == nil || m.receiptChannelDepth == nil || intervalSeconds <= 0 || ch == nil { + return + } + interval := time.Duration(intervalSeconds) * time.Second + ctx := context.Background() + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + m.receiptChannelDepth.Record(ctx, int64(len(ch))) + } + } + }() +} diff --git a/sei-db/state_db/bench/cryptosim/data_generator.go b/sei-db/state_db/bench/cryptosim/data_generator.go index eecd1bee77..09ead76273 100644 --- a/sei-db/state_db/bench/cryptosim/data_generator.go +++ b/sei-db/state_db/bench/cryptosim/data_generator.go @@ -12,6 +12,8 @@ const ( accountIdCounterKey = "accountIdCounterKey" // Used to store the next ERC20 contract ID in the database. erc20IdCounterKey = "erc20IdCounterKey" + // Used to store the next block number in the database. + blockNumberCounterKey = "blockNumberCounterKey" // Use the code hash as a proxy. There is currently no mechanism to force FlatKV to update the account balance // field, and code hash keys will cause the account DB to get updated, which is the important part for this @@ -29,6 +31,10 @@ type DataGenerator struct { // The next ERC20 contract ID to be used when creating a new ERC20 contract. nextErc20ContractID int64 + // The next block number at startup time. Not updated after initialization; + // the block builder tracks the ongoing value. + initialNextBlockNumber uint64 + // The random number generator. rand *CannedRandom @@ -90,6 +96,17 @@ func NewDataGenerator( fmt.Printf("There are currently %s ERC20 contracts in the database.\n", int64Commas(nextErc20ContractID)) metrics.SetTotalNumberOfERC20Contracts(nextErc20ContractID) + nextBlockNumberBinary, found, err := database.Get(BlockNumberCounterKey()) + if err != nil { + return nil, fmt.Errorf("failed to read block number counter: %w", err) + } + var nextBlockNumber uint64 + if found { + nextBlockNumber = binary.BigEndian.Uint64(nextBlockNumberBinary) + } + + fmt.Printf("Next block number: %s.\n", int64Commas(int64(nextBlockNumber))) //nolint:gosec + feeCollectionAddress := evm.BuildMemIAVLEVMKey( accountKeyPrefix, rand.Address(accountPrefix, 0, AddressLen), @@ -99,6 +116,7 @@ func NewDataGenerator( config: config, nextAccountID: nextAccountID, nextErc20ContractID: nextErc20ContractID, + initialNextBlockNumber: nextBlockNumber, rand: rand, feeCollectionAddress: feeCollectionAddress, database: database, @@ -130,6 +148,11 @@ func (d *DataGenerator) NextErc20ContractID() int64 { return d.nextErc20ContractID } +// Get the next block number as it was at startup time. +func (d *DataGenerator) InitialNextBlockNumber() uint64 { + return d.initialNextBlockNumber +} + // Creates a new account and optionally writes it to the database. Returns the address of the new // account and whether it is a cold account (vs dormant). func (d *DataGenerator) CreateNewAccount( @@ -247,7 +270,7 @@ func (d *DataGenerator) randomAccountSlot(accountID int64) ([]byte, error) { slotNumber := d.rand.Int64Range(0, int64(d.config.Erc20InteractionsPerAccount)) slotID := accountID*int64(d.config.Erc20InteractionsPerAccount) + slotNumber - storageKeyBytes := d.rand.Address(ethStoragePrefix, slotID, AddressLen) + storageKeyBytes := d.rand.Address(ethStoragePrefix, slotID, StorageKeyLen) return evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, storageKeyBytes), nil } @@ -297,3 +320,9 @@ func (d *DataGenerator) FeeCollectionAddress() []byte { func (d *DataGenerator) ReportEndOfBlock() { d.highestSafeAccountIDInBlock = d.nextAccountID - 1 } + +// Get the random number generator. Note that the random number generator is not thread safe, and +// so the caller is responsible for ensuring that it is not used concurrently with other calls to the data generator. +func (d *DataGenerator) Rand() *CannedRandom { + return d.rand +} diff --git a/sei-db/state_db/bench/cryptosim/database.go b/sei-db/state_db/bench/cryptosim/database.go index 21d9a48937..189bc0adb0 100644 --- a/sei-db/state_db/bench/cryptosim/database.go +++ b/sei-db/state_db/bench/cryptosim/database.go @@ -26,6 +26,9 @@ type Database struct { // The number of blocks that have been executed since the last commit. uncommittedBlockCount int64 + // The next block number to be persisted. Tracked internally and incremented after each finalized block. + nextBlockNumber uint64 + // The current batch of key-value pairs waiting to be committed. Represents changes we are accumulating // as part of a simulated "block". Stored as value []byte; converted to NamedChangeSet when applied to the DB. batch *SyncMap[string, []byte] @@ -42,12 +45,14 @@ func NewDatabase( config *CryptoSimConfig, db wrappers.DBWrapper, metrics *CryptosimMetrics, + initialNextBlockNumber uint64, ) *Database { return &Database{ - config: config, - db: db, - batch: NewSyncMap[string, []byte](), - metrics: metrics, + config: config, + db: db, + batch: NewSyncMap[string, []byte](), + metrics: metrics, + nextBlockNumber: initialNextBlockNumber, } } @@ -80,10 +85,10 @@ func (d *Database) Get(key []byte) ([]byte, bool, error) { return nil, false, nil } -// Signal that transactions have been added to the current block. -func (d *Database) IncrementTransactionCount(count int64) { - d.transactionCount += count - d.transactionsInCurrentBlock += count +// Signal that a transaction has been added to the current block. +func (d *Database) IncrementTransactionCount() { + d.transactionCount++ + d.transactionsInCurrentBlock++ } // Reset the transaction count. Useful for when changing test phases. @@ -133,7 +138,7 @@ func (d *Database) FinalizeBlock( d.metrics.SetMainThreadPhase("finalizing") - changeSets := make([]*proto.NamedChangeSet, 0, d.transactionsInCurrentBlock+2) + changeSets := make([]*proto.NamedChangeSet, 0, d.transactionsInCurrentBlock+3) for key, value := range d.batch.Iterator() { changeSets = append(changeSets, &proto.NamedChangeSet{ Name: wrappers.EVMStoreName, @@ -164,6 +169,17 @@ func (d *Database) FinalizeBlock( }}, }) + // Persist the block number counter in every batch. + blockNumberValue := make([]byte, 8) + binary.BigEndian.PutUint64(blockNumberValue, d.nextBlockNumber) + changeSets = append(changeSets, &proto.NamedChangeSet{ + Name: wrappers.EVMStoreName, + Changeset: iavl.ChangeSet{Pairs: []*iavl.KVPair{ + {Key: BlockNumberCounterKey(), Value: blockNumberValue}, + }}, + }) + d.nextBlockNumber++ + err := d.db.ApplyChangeSets(changeSets) if err != nil { return fmt.Errorf("failed to apply change sets: %w", err) diff --git a/sei-db/state_db/bench/cryptosim/receipt.go b/sei-db/state_db/bench/cryptosim/receipt.go new file mode 100644 index 0000000000..4ab348aa2c --- /dev/null +++ b/sei-db/state_db/bench/cryptosim/receipt.go @@ -0,0 +1,204 @@ +package cryptosim + +import ( + "encoding/binary" + "errors" + "fmt" + "hash" + + ethtypes "github.com/ethereum/go-ethereum/core/types" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" + "golang.org/x/crypto/sha3" +) + +const ( + erc20TransferEventSignatureHex = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + + // These mirror immutable memiavl EVM key prefixes and are duplicated here to keep the hot path minimal. + evmCodeKeyPrefixByte = 0x07 + evmCodeHashKeyPrefixByte = 0x08 + evmStorageKeyPrefixByte = 0x03 + + hashLen = 32 + indexedAddressBase = hashLen - AddressLen + + syntheticReceiptMinBlockNumber uint64 = 1_000_000 + + syntheticReceiptGasUsedBase uint64 = 52_000 + syntheticReceiptGasUsedSpan uint64 = 18_000 + syntheticReceiptPreviousGasBase uint64 = 21_000 + syntheticReceiptPreviousGasSpan uint64 = 35_000 + syntheticReceiptGasPriceBase uint64 = 1_000_000_000 + syntheticReceiptGasPriceSpan uint64 = 9_000_000_000 + syntheticReceiptTransferBase uint64 = 1_000_000 + syntheticReceiptTransferSpan uint64 = 10_000_000_000 +) + +var erc20TransferEventSignatureBytes = [hashLen]byte{ + 0xdd, 0xf2, 0x52, 0xad, 0x1b, 0xe2, 0xc8, 0x9b, + 0x69, 0xc2, 0xb0, 0x68, 0xfc, 0x37, 0x8d, 0xaa, + 0x95, 0x2b, 0xa7, 0xf1, 0x63, 0xc4, 0xa1, 0x16, + 0x28, 0xf5, 0x5a, 0x4d, 0xf5, 0x23, 0xb3, 0xef, +} + +// BuildERC20TransferReceiptFromTxn produces a plausible successful ERC20 transfer receipt from a transaction. +func BuildERC20TransferReceiptFromTxn( + crand *CannedRandom, + feeCollectionAccount []byte, + blockNumber uint64, + txIndex uint32, + txn *transaction, +) (*evmtypes.Receipt, error) { + return BuildERC20TransferReceipt( + crand, + feeCollectionAccount, + txn.srcAccount, + txn.dstAccount, + txn.srcAccountSlot, + txn.dstAccountSlot, + txn.erc20Contract, + blockNumber, + txIndex) +} + +// BuildERC20TransferReceipt produces a plausible successful ERC20 transfer receipt. +// +// The sender and receiver are derived from the address portion of the supplied storage keys, since cryptosim tracks +// ERC20 balances as storage slots rather than separate account references. The caller supplies the block number and tx +// index so the resulting receipt can line up with the simulated block being benchmarked. +func BuildERC20TransferReceipt( + crand *CannedRandom, + feeCollectionAccount []byte, + srcAccount []byte, + dstAccount []byte, + senderSlot []byte, + receiverSlot []byte, + erc20ContractCode []byte, + blockNumber uint64, + txIndex uint32, +) (*evmtypes.Receipt, error) { + if crand == nil { + return nil, errors.New("canned random is required") + } + + if err := validateAccountKey("fee collection account", feeCollectionAccount); err != nil { + return nil, err + } + srcAddressBytes, err := extractAccountKeyBytes("src account", srcAccount) + if err != nil { + return nil, err + } + if err := validateAccountKey("dst account", dstAccount); err != nil { + return nil, err + } + senderAddressBytes, err := extractStorageKeyAddressBytes("sender slot", senderSlot) + if err != nil { + return nil, err + } + receiverAddressBytes, err := extractStorageKeyAddressBytes("receiver slot", receiverSlot) + if err != nil { + return nil, err + } + contractAddressBytes, err := extractCodeKeyBytes("erc20 contract code", erc20ContractCode) + if err != nil { + return nil, err + } + txType := uint32(ethtypes.DynamicFeeTxType) + if crand.Int64Range(0, 5) == 0 { + txType = uint32(ethtypes.LegacyTxType) + } + + gasUsed := syntheticReceiptGasUsedBase + + uint64(crand.Int64Range(0, int64(syntheticReceiptGasUsedSpan))) //nolint:gosec // constants fit in int64 + previousGas := syntheticReceiptPreviousGasBase + + uint64(crand.Int64Range(0, int64(syntheticReceiptPreviousGasSpan))) //nolint:gosec // constants fit in int64 + cumulativeGasUsed := gasUsed + uint64(txIndex)*previousGas + effectiveGasPrice := syntheticReceiptGasPriceBase + + uint64(crand.Int64Range(0, int64(syntheticReceiptGasPriceSpan))) //nolint:gosec // constants fit in int64 + transferAmount := syntheticReceiptTransferBase + + uint64(crand.Int64Range(0, int64(syntheticReceiptTransferSpan))) //nolint:gosec // constants fit in int64 + + var senderTopic [hashLen]byte + copy(senderTopic[indexedAddressBase:], senderAddressBytes) + var receiverTopic [hashLen]byte + copy(receiverTopic[indexedAddressBase:], receiverAddressBytes) + + contractAddressHex := BytesToHex(contractAddressBytes) + amountData := encodeUint256FromUint64(transferAmount) + var bloom ethtypes.Bloom + hasher := sha3.NewLegacyKeccak256() + var bloomDigest [hashLen]byte + addToBloom(hasher, &bloomDigest, &bloom, contractAddressBytes) + addToBloom(hasher, &bloomDigest, &bloom, erc20TransferEventSignatureBytes[:]) + addToBloom(hasher, &bloomDigest, &bloom, senderTopic[:]) + addToBloom(hasher, &bloomDigest, &bloom, receiverTopic[:]) + + return &evmtypes.Receipt{ + TxType: txType, + CumulativeGasUsed: cumulativeGasUsed, + ContractAddress: contractAddressHex, + TxHashHex: BytesToHex(crand.Bytes(hashLen)), + GasUsed: gasUsed, + EffectiveGasPrice: effectiveGasPrice, + BlockNumber: blockNumber, + TransactionIndex: txIndex, + Status: uint32(ethtypes.ReceiptStatusSuccessful), + From: BytesToHex(srcAddressBytes), + To: contractAddressHex, + Logs: []*evmtypes.Log{{ + Address: contractAddressHex, + Topics: []string{ + erc20TransferEventSignatureHex, + BytesToHex(senderTopic[:]), + BytesToHex(receiverTopic[:]), + }, + Data: amountData, + Index: 0, + }}, + LogsBloom: bloom[:], + }, nil +} + +func validateAccountKey(name string, key []byte) error { + _, err := extractAccountKeyBytes(name, key) + return err +} + +// extractAccountKeyBytes accepts keys with either EVMKeyCode (0x07) or EVMKeyCodeHash (0x08) prefix, +// since cryptosim uses EVMKeyCodeHash for accounts while ERC20 contracts use EVMKeyCode. +func extractAccountKeyBytes(name string, key []byte) ([]byte, error) { + if len(key) != 1+AddressLen || (key[0] != evmCodeKeyPrefixByte && key[0] != evmCodeHashKeyPrefixByte) { + return nil, fmt.Errorf("%s must be an EVM code key with %d address bytes", name, AddressLen) + } + return key[1:], nil +} + +func extractCodeKeyBytes(name string, key []byte) ([]byte, error) { + if len(key) != 1+AddressLen || key[0] != evmCodeKeyPrefixByte { + return nil, fmt.Errorf("%s must be an EVM code key with %d address bytes", name, AddressLen) + } + return key[1:], nil +} + +func extractStorageKeyAddressBytes(name string, key []byte) ([]byte, error) { + if len(key) != 1+StorageKeyLen || key[0] != evmStorageKeyPrefixByte { + return nil, fmt.Errorf("%s must be an EVM storage key with %d address+slot bytes", name, StorageKeyLen) + } + return key[1 : 1+AddressLen], nil +} + +func addToBloom(hasher hash.Hash, digest *[hashLen]byte, bloom *ethtypes.Bloom, value []byte) { + hasher.Reset() + _, _ = hasher.Write(value) + hash := hasher.Sum(digest[:0]) + for i := 0; i < 6; i += 2 { + bit := (uint(hash[i])<<8)&2047 + uint(hash[i+1]) + bloom[ethtypes.BloomByteLength-1-bit/8] |= byte(1 << (bit % 8)) + } +} + +func encodeUint256FromUint64(value uint64) []byte { + encoded := make([]byte, hashLen) + binary.BigEndian.PutUint64(encoded[hashLen-8:], value) + return encoded +} diff --git a/sei-db/state_db/bench/cryptosim/receipt_test.go b/sei-db/state_db/bench/cryptosim/receipt_test.go new file mode 100644 index 0000000000..f1e61109fb --- /dev/null +++ b/sei-db/state_db/bench/cryptosim/receipt_test.go @@ -0,0 +1,166 @@ +package cryptosim + +import ( + "testing" + + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/sei-protocol/sei-chain/sei-db/common/evm" +) + +func makeTestKeys(t *testing.T) (feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract []byte) { + t.Helper() + keyRand := NewCannedRandom(4096, 1) + + feeAccount = evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, keyRand.Address(accountPrefix, 0, AddressLen)) + srcAddr := keyRand.Address(accountPrefix, 1, AddressLen) + srcAccount = evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, srcAddr) + dstAddr := keyRand.Address(accountPrefix, 2, AddressLen) + dstAccount = evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, dstAddr) + + senderSlotBytes := make([]byte, StorageKeyLen) + copy(senderSlotBytes[:AddressLen], srcAddr) + copy(senderSlotBytes[AddressLen:], keyRand.SeededBytes(SlotLen, 11)) + senderSlot = evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, senderSlotBytes) + + receiverSlotBytes := make([]byte, StorageKeyLen) + copy(receiverSlotBytes[:AddressLen], dstAddr) + copy(receiverSlotBytes[AddressLen:], keyRand.SeededBytes(SlotLen, 12)) + receiverSlot = evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, receiverSlotBytes) + + erc20Contract = evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, keyRand.Address(contractPrefix, 0, AddressLen)) + return +} + +func TestBuildERC20TransferReceipt(t *testing.T) { + crand := NewCannedRandom(1<<20, 42) + feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract := makeTestKeys(t) + + receipt, err := BuildERC20TransferReceipt( + crand, feeAccount, srcAccount, dstAccount, + senderSlot, receiverSlot, erc20Contract, + 1_000_000, 0, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if receipt.Status != uint32(ethtypes.ReceiptStatusSuccessful) { + t.Errorf("expected successful status, got %d", receipt.Status) + } + if receipt.BlockNumber != 1_000_000 { + t.Errorf("expected block number 1000000, got %d", receipt.BlockNumber) + } + if len(receipt.Logs) != 1 { + t.Fatalf("expected 1 log, got %d", len(receipt.Logs)) + } + if receipt.Logs[0].Topics[0] != erc20TransferEventSignatureHex { + t.Error("first log topic should be ERC20 Transfer event signature") + } + + // Receipt must be marshallable (used by the write path). + data, err := receipt.Marshal() + if err != nil { + t.Fatalf("failed to marshal receipt: %v", err) + } + if len(data) == 0 { + t.Fatal("marshalled receipt is empty") + } +} + +func TestBuildERC20TransferReceipt_InvalidInputs(t *testing.T) { + crand := NewCannedRandom(1<<20, 42) + feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract := makeTestKeys(t) + + if _, err := BuildERC20TransferReceipt(nil, feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract, 1_000_000, 0); err == nil { + t.Error("expected error for nil CannedRandom") + } + if _, err := BuildERC20TransferReceipt(crand, []byte("bad"), srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract, 1_000_000, 0); err == nil { + t.Error("expected error for invalid fee account key") + } + if _, err := BuildERC20TransferReceipt(crand, feeAccount, srcAccount, dstAccount, []byte("bad"), receiverSlot, erc20Contract, 1_000_000, 0); err == nil { + t.Error("expected error for invalid sender slot key") + } +} + +// Regression test: account keys with EVMKeyCode prefix must still be accepted. +func TestBuildERC20TransferReceipt_EVMKeyCodeAccounts(t *testing.T) { + crand := NewCannedRandom(1<<20, 42) + keyRand := NewCannedRandom(4096, 1) + + feeAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, keyRand.Address(accountPrefix, 0, AddressLen)) + srcAddr := keyRand.Address(accountPrefix, 1, AddressLen) + srcAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, srcAddr) + dstAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, keyRand.Address(accountPrefix, 2, AddressLen)) + + senderSlotBytes := make([]byte, StorageKeyLen) + copy(senderSlotBytes[:AddressLen], srcAddr) + copy(senderSlotBytes[AddressLen:], keyRand.SeededBytes(SlotLen, 11)) + senderSlot := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, senderSlotBytes) + + receiverSlotBytes := make([]byte, StorageKeyLen) + copy(receiverSlotBytes[:AddressLen], keyRand.Address(accountPrefix, 2, AddressLen)) + copy(receiverSlotBytes[AddressLen:], keyRand.SeededBytes(SlotLen, 12)) + receiverSlot := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, receiverSlotBytes) + + erc20Contract := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, keyRand.Address(contractPrefix, 0, AddressLen)) + + _, err := BuildERC20TransferReceipt(crand, feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract, 1_000_000, 0) + if err != nil { + t.Fatalf("EVMKeyCode accounts should be accepted: %v", err) + } +} + +// Regression test: uses the exact key formats produced by data_generator.go +// (EVMKeyCodeHash for accounts, EVMKeyStorage with full StorageKeyLen payload). +func TestBuildERC20TransferReceipt_DataGeneratorKeyFormats(t *testing.T) { + crand := NewCannedRandom(1<<20, 42) + keyRand := NewCannedRandom(4096, 1) + + feeAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, keyRand.Address(accountPrefix, 0, AddressLen)) + srcAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, keyRand.Address(accountPrefix, 1, AddressLen)) + dstAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, keyRand.Address(accountPrefix, 2, AddressLen)) + + senderSlot := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, keyRand.Address(ethStoragePrefix, 10, StorageKeyLen)) + receiverSlot := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, keyRand.Address(ethStoragePrefix, 20, StorageKeyLen)) + erc20Contract := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, keyRand.Address(contractPrefix, 0, AddressLen)) + + receipt, err := BuildERC20TransferReceipt(crand, feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract, 1_000_000, 0) + if err != nil { + t.Fatalf("data_generator key formats should be accepted: %v", err) + } + if receipt.Status != uint32(ethtypes.ReceiptStatusSuccessful) { + t.Errorf("expected successful status, got %d", receipt.Status) + } +} + +func BenchmarkBuildERC20TransferReceipt(b *testing.B) { + keyRand := NewCannedRandom(4096, 1) + receiptRand := NewCannedRandom(1<<20, 2) + + feeAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, keyRand.Address(accountPrefix, 0, AddressLen)) + srcAddr := keyRand.Address(accountPrefix, 1, AddressLen) + srcAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, srcAddr) + dstAddr := keyRand.Address(accountPrefix, 2, AddressLen) + dstAccount := evm.BuildMemIAVLEVMKey(evm.EVMKeyCodeHash, dstAddr) + + senderSlotBytes := make([]byte, StorageKeyLen) + copy(senderSlotBytes[:AddressLen], srcAddr) + copy(senderSlotBytes[AddressLen:], keyRand.SeededBytes(SlotLen, 11)) + senderSlot := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, senderSlotBytes) + + receiverSlotBytes := make([]byte, StorageKeyLen) + copy(receiverSlotBytes[:AddressLen], dstAddr) + copy(receiverSlotBytes[AddressLen:], keyRand.SeededBytes(SlotLen, 12)) + receiverSlot := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, receiverSlotBytes) + + erc20Contract := evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, keyRand.Address(contractPrefix, 0, AddressLen)) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := BuildERC20TransferReceipt(receiptRand, feeAccount, srcAccount, dstAccount, senderSlot, receiverSlot, erc20Contract, syntheticReceiptMinBlockNumber, 0) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/sei-db/state_db/bench/cryptosim/reciept_store_simulator.go b/sei-db/state_db/bench/cryptosim/reciept_store_simulator.go new file mode 100644 index 0000000000..309c03667e --- /dev/null +++ b/sei-db/state_db/bench/cryptosim/reciept_store_simulator.go @@ -0,0 +1,167 @@ +package cryptosim + +import ( + "context" + "fmt" + "path/filepath" + "time" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + dbconfig "github.com/sei-protocol/sei-chain/sei-db/config" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt" + tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" + evmtypes "github.com/sei-protocol/sei-chain/x/evm/types" +) + +// A simulated receipt store using the real production receipt.ReceiptStore +// (cached parquet backend with WAL, flush, rotation, and pruning). +type RecieptStoreSimulator struct { + ctx context.Context + cancel context.CancelFunc + + config *CryptoSimConfig + + recieptsChan chan *block + + store receipt.ReceiptStore + metrics *CryptosimMetrics +} + +// Creates a new receipt store simulator backed by the production ReceiptStore +// (parquet backend + ledger cache), matching the real node write path. +func NewRecieptStoreSimulator( + ctx context.Context, + config *CryptoSimConfig, + recieptsChan chan *block, + metrics *CryptosimMetrics, +) (*RecieptStoreSimulator, error) { + derivedCtx, cancel := context.WithCancel(ctx) + + storeCfg := dbconfig.ReceiptStoreConfig{ + DBDirectory: filepath.Join(config.DataDir, "receipts"), + Backend: "parquet", + KeepRecent: int(config.ReceiptKeepRecent), + PruneIntervalSeconds: int(config.ReceiptPruneIntervalSeconds), + } + + // nil StoreKey is safe: the parquet write path never touches the legacy KV store. + store, err := receipt.NewReceiptStore(storeCfg, nil) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to create receipt store: %w", err) + } + + r := &RecieptStoreSimulator{ + ctx: derivedCtx, + cancel: cancel, + config: config, + recieptsChan: recieptsChan, + store: store, + metrics: metrics, + } + go r.mainLoop() + return r, nil +} + +func (r *RecieptStoreSimulator) mainLoop() { + defer func() { + if err := r.store.Close(); err != nil { + fmt.Printf("failed to close receipt store: %v\n", err) + } + }() + for { + select { + case <-r.ctx.Done(): + return + case blk := <-r.recieptsChan: + r.processBlock(blk) + } + } +} + +// Processes a block of receipts using the production ReceiptStore.SetReceipts path, +// which writes to parquet (WAL + buffer + rotation) and populates the ledger cache. +func (r *RecieptStoreSimulator) processBlock(blk *block) { + blockNumber := uint64(blk.BlockNumber()) //nolint:gosec + + records := make([]receipt.ReceiptRecord, 0, len(blk.reciepts)) + var marshalErrors int64 + + for _, rcpt := range blk.reciepts { + if rcpt == nil { + continue + } + + receiptBytes, err := rcpt.Marshal() + if err != nil { + fmt.Printf("failed to marshal receipt: %v\n", err) + marshalErrors++ + continue + } + + txHash := common.HexToHash(rcpt.TxHashHex) + records = append(records, receipt.ReceiptRecord{ + TxHash: txHash, + Receipt: rcpt, + ReceiptBytes: receiptBytes, + }) + } + + for range marshalErrors { + r.metrics.ReportReceiptError() + } + + if len(records) > 0 { + // Build a minimal sdk.Context with the block height set. + // The parquet write path only uses ctx.BlockHeight() and ctx.Context(). + sdkCtx := sdk.NewContext(nil, tmproto.Header{Height: int64(blockNumber)}, false) //nolint:gosec + + start := time.Now() + if err := r.store.SetReceipts(sdkCtx, records); err != nil { + fmt.Printf("failed to write receipts for block %d: %v\n", blockNumber, err) + r.metrics.ReportReceiptError() + return + } + r.metrics.RecordReceiptBlockWriteDuration(time.Since(start)) + r.metrics.ReportReceiptsWritten(int64(len(records))) + } + + if err := r.store.SetLatestVersion(int64(blockNumber)); err != nil { //nolint:gosec + fmt.Printf("failed to update latest version for block %d: %v\n", blockNumber, err) + } +} + +// convertLogsForTx converts evmtypes.Log entries to ethtypes.Log entries. +// Mirrors receipt.getLogsForTx. +func convertLogsForTx(rcpt *evmtypes.Receipt, logStartIndex uint) []*ethtypes.Log { + logs := make([]*ethtypes.Log, 0, len(rcpt.Logs)) + for _, l := range rcpt.Logs { + logs = append(logs, convertLogEntry(l, rcpt, logStartIndex)) + } + return logs +} + +// convertLogEntry converts a single evmtypes.Log to an ethtypes.Log. +// Mirrors receipt.convertLog. +func convertLogEntry(l *evmtypes.Log, rcpt *evmtypes.Receipt, logStartIndex uint) *ethtypes.Log { + return ðtypes.Log{ + Address: common.HexToAddress(l.Address), + Topics: mapTopics(l.Topics), + Data: l.Data, + BlockNumber: rcpt.BlockNumber, + TxHash: common.HexToHash(rcpt.TxHashHex), + TxIndex: uint(rcpt.TransactionIndex), + Index: uint(l.Index) + logStartIndex, + } +} + +// mapTopics converts hex-encoded topic strings to common.Hash values. +func mapTopics(topics []string) []common.Hash { + result := make([]common.Hash, len(topics)) + for i, t := range topics { + result[i] = common.HexToHash(t) + } + return result +} diff --git a/sei-db/state_db/bench/cryptosim/transaction_executor.go b/sei-db/state_db/bench/cryptosim/transaction_executor.go index 40c23c309c..17271e1f1f 100644 --- a/sei-db/state_db/bench/cryptosim/transaction_executor.go +++ b/sei-db/state_db/bench/cryptosim/transaction_executor.go @@ -10,6 +10,7 @@ import ( type TransactionExecutor struct { ctx context.Context cancel context.CancelFunc + config *CryptoSimConfig // The database for the benchmark. database *Database @@ -33,6 +34,7 @@ type flushRequest struct { func NewTransactionExecutor( ctx context.Context, cancel context.CancelFunc, + config *CryptoSimConfig, database *Database, feeCollectionAddress []byte, queueSize int, @@ -41,6 +43,7 @@ func NewTransactionExecutor( e := &TransactionExecutor{ ctx: ctx, cancel: cancel, + config: config, database: database, feeCollectionAddress: feeCollectionAddress, workChan: make(chan any, queueSize), @@ -86,6 +89,10 @@ func (e *TransactionExecutor) mainLoop() { switch request := request.(type) { case *transaction: + if e.config.DisableTransactionExecution { + continue + } + var phaseTimer *metrics.PhaseTimer if request.ShouldCaptureMetrics() { phaseTimer = e.phaseTimer diff --git a/sei-db/state_db/bench/cryptosim/util.go b/sei-db/state_db/bench/cryptosim/util.go index ffee60bdac..508408d61e 100644 --- a/sei-db/state_db/bench/cryptosim/util.go +++ b/sei-db/state_db/bench/cryptosim/util.go @@ -30,6 +30,11 @@ func Erc20IDCounterKey() []byte { return evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, paddedCounterKey(erc20IdCounterKey)) } +// Get the key for the block number counter in the database. +func BlockNumberCounterKey() []byte { + return evm.BuildMemIAVLEVMKey(evm.EVMKeyCode, paddedCounterKey(blockNumberCounterKey)) +} + // paddedCounterKey pads the string to AddressLen bytes for use with EVM key builders. func paddedCounterKey(s string) []byte { b := make([]byte, AddressLen)