Skip to content

Commit ca73b30

Browse files
committed
feat: add TestStatePressure benchmark for state trie stress test
1 parent a44ae05 commit ca73b30

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//go:build evm
2+
3+
package benchmark
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"time"
9+
10+
"github.com/celestiaorg/tastora/framework/docker/evstack/spamoor"
11+
)
12+
13+
// TestStatePressure measures throughput under maximum storage write pressure.
14+
// Each tx maximizes SSTORE operations, creating rapid state growth that stresses
15+
// the state trie and disk I/O.
16+
//
17+
// Shares system configuration with TestERC20Throughput (100ms blocks, 100M gas,
18+
// 25ms scrape) so results are directly comparable. The gap between
19+
// TestEVMComputeCeiling and this test isolates state root + storage I/O cost.
20+
//
21+
// Primary metrics: MGas/s.
22+
// Diagnostic metrics: Engine.NewPayload latency, ev-node overhead %.
23+
func (s *SpamoorSuite) TestStatePressure() {
24+
const (
25+
numSpammers = 4
26+
countPerSpammer = 5000
27+
totalCount = numSpammers * countPerSpammer
28+
waitTimeout = 10 * time.Minute
29+
)
30+
31+
cfg := newBenchConfig("ev-node-state-pressure")
32+
33+
t := s.T()
34+
ctx := t.Context()
35+
w := newResultWriter(t, "StatePressure")
36+
defer w.flush()
37+
38+
e := s.setupEnv(cfg)
39+
40+
storageSpamConfig := map[string]any{
41+
"throughput": 20, // 20 tx per 100ms slot = 200 tx/s per spammer, 800 tx/s total
42+
"total_count": countPerSpammer,
43+
"gas_units_to_burn": 2000000, // 2M gas per tx via SSTORE. 100M / 2M = 50 txs to fill block
44+
"max_pending": 50000,
45+
"max_wallets": 100,
46+
"base_fee": 20,
47+
"tip_fee": 2,
48+
"refill_amount": "5000000000000000000", // 5 ETH
49+
"refill_balance": "2000000000000000000", // 2 ETH
50+
"refill_interval": 600,
51+
}
52+
53+
s.Require().NoError(deleteAllSpammers(e.spamoorAPI), "failed to delete stale spammers")
54+
55+
var spammerIDs []int
56+
for i := range numSpammers {
57+
name := fmt.Sprintf("bench-storage-%d", i)
58+
id, err := e.spamoorAPI.CreateSpammer(name, spamoor.ScenarioStorageSpam, storageSpamConfig, true)
59+
s.Require().NoError(err, "failed to create spammer %s", name)
60+
spammerIDs = append(spammerIDs, id)
61+
t.Cleanup(func() { _ = e.spamoorAPI.DeleteSpammer(id) })
62+
}
63+
64+
time.Sleep(3 * time.Second)
65+
assertSpammersRunning(t, e.spamoorAPI, spammerIDs)
66+
67+
// wait for wallet funding to finish before recording start block
68+
pollSentTotal := func() (float64, error) {
69+
metrics, mErr := e.spamoorAPI.GetMetrics()
70+
if mErr != nil {
71+
return 0, mErr
72+
}
73+
return sumCounter(metrics["spamoor_transactions_sent_total"]), nil
74+
}
75+
waitForMetricTarget(t, "spamoor_transactions_sent_total (warmup)", pollSentTotal, float64(cfg.WarmupTxs), cfg.WaitTimeout)
76+
77+
// reset trace window to exclude warmup spans
78+
e.traces.resetStartTime()
79+
80+
startHeader, err := e.ethClient.HeaderByNumber(ctx, nil)
81+
s.Require().NoError(err, "failed to get start block header")
82+
startBlock := startHeader.Number.Uint64()
83+
loadStart := time.Now()
84+
t.Logf("start block: %d (after warmup)", startBlock)
85+
86+
// wait for all transactions to be sent
87+
waitForMetricTarget(t, "spamoor_transactions_sent_total", pollSentTotal, float64(totalCount), cfg.WaitTimeout)
88+
89+
// wait for pending txs to drain
90+
drainCtx, drainCancel := context.WithTimeout(ctx, 30*time.Second)
91+
defer drainCancel()
92+
if err := waitForDrain(drainCtx, t.Logf, e.ethClient, 10); err != nil {
93+
t.Logf("warning: %v", err)
94+
}
95+
wallClock := time.Since(loadStart)
96+
97+
endHeader, err := e.ethClient.HeaderByNumber(ctx, nil)
98+
s.Require().NoError(err, "failed to get end block header")
99+
endBlock := endHeader.Number.Uint64()
100+
t.Logf("end block: %d (range %d blocks)", endBlock, endBlock-startBlock)
101+
102+
// collect block-level gas/tx metrics
103+
bm, err := collectBlockMetrics(ctx, e.ethClient, startBlock, endBlock)
104+
s.Require().NoError(err, "failed to collect block metrics")
105+
106+
traces := s.collectTraces(e, cfg.ServiceName)
107+
108+
result := newBenchmarkResult("StatePressure", bm, traces)
109+
s.Require().Greater(result.summary.SteadyState, time.Duration(0), "expected non-zero steady-state duration")
110+
result.log(t, wallClock)
111+
w.addEntries(result.entries())
112+
}

0 commit comments

Comments
 (0)