Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 32 additions & 14 deletions giga/evmonly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ The `evmonly` package currently provides:
- go-ethereum `core.ApplyMessage` execution against an SDK-free `vm.StateDB`
- key-addressable state reads for balance, nonce, code, and storage
- deterministic post-block `StateChangeSet` construction
- direct snapshot reads and ordered state commits through `giga.Store`
- optional executor-internal Block-STM-style execution for optimistic parallel
transaction execution with granular validation and reruns
- Ethereum receipt construction with logs, bloom, gas, tx hash, block metadata,
contract address, and effective gas price
- a map-backed `MemoryState` for tests and early integration
- a versioned `MemoryStore` giga implementation over an immutable `StateReader`
for tests and load generation
- fail-closed custom precompile placeholders
- a standalone load harness at `giga/evmonly/cmd/evmonly-loadtest` that feeds
generated transfer blocks into the executor with mock state and receipt sinks
generated transfer blocks through the in-memory giga store

The executor accepts config for nonce checks, gas-price checks, minimum gas
price, chain config, parse workers, OCC workers, result pooling, and the custom
Expand Down Expand Up @@ -68,18 +70,34 @@ prepare then execute in one call. `PreparedBlock` is trusted executor-produced
data: callers should pass the result of `PrepareBlock` unchanged, because
`ExecutePreparedBlock` does not recover senders again.

The executor should be commit-neutral. It executes an ordered EVM block and
returns the state writes and receipts produced by that block. The caller owns
durable persistence, state commitment, block indexing, and receipt publication.
The concrete `Executor` accepts a `StateReader` backend through `WithState(...)`;
callers can persist the returned `ChangeSet` with a matching `StateWriter`.
Every `StateReader` method must be safe for concurrent calls because speculative
transactions and overlapping block executions may read the backend at the same
time. Values returned by `GetBalance` and `GetCode` must remain stable while
being read; the executor treats them as immutable and copies them into
transaction-local state. The executor intentionally does not detect or
serialize non-concurrent backends; violating this contract is a data race.
Call `Close()` to disable future OCC execution on an executor.
The executor is always store-backed. `WithStore(...)` selects the `giga.Store`
implementation and its `NamedChangeSetEncoder`; execution fails closed if
either is missing. For each block the executor opens a current
`giga.StateSnapshot`, executes against its EVM-native read methods, converts the
resulting `StateChangeSet`, and calls `CommitStateChanges`. Execution and commit
on an executor are serialized so blocks cannot share a stale snapshot or
overlap commits; callers must still submit block heights in order. The snapshot
stays open through the commit and is always closed afterward. An empty block
still commits an encoded empty changeset so the store can advance its height.
Stateless preparation can continue concurrently with store-backed execution.

The encoder is explicit because `giga.Store` defines the protobuf commit
transport but does not define an on-disk key layout. In particular, an encoder
must preserve `StorageClears` as prefix clears rather than silently dropping
persisted slots that were not read during execution. Encoding or commit failures
release the block result and return an error without invoking `ResultSink`.
`ResultSink` runs after the state commit succeeds; a sink error does not roll
back that commit.

`MemoryStore` is the non-persistent implementation used by tests and the load
harness. It wraps an immutable `StateReader`, encodes changes directly into
typed `NamedChangeSet` key/value pairs, and retains committed values in
versioned overlays so current and historical snapshots stay stable without
copying the complete base state per block. It is not the production SC/SS
implementation. Every base
`StateReader` method must be safe for concurrent calls, and returned balances
and code must remain immutable while read. Call `Close()` to disable future OCC
execution on an executor.

A non-nil `error` means block validation failed and the caller must not commit a
partial output. EVM call failures inside an otherwise valid transaction are
Expand Down
29 changes: 14 additions & 15 deletions giga/evmonly/cmd/evmonly-loadtest/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# evmonly-loadtest

`evmonly-loadtest` is a standalone executable for feeding synthetic blocks to
the EVM-only executor without Cosmos SDK state, mempool, RPC, or chain
persistence.
the EVM-only executor through an in-memory `giga.Store`, without Cosmos SDK
state, mempool, RPC, or production SC/SS persistence.

The synthetic workload defaults to local EVM chain ID `1337`; override it with
`--chain-id` when testing another signing domain.
Expand Down Expand Up @@ -123,11 +123,8 @@ Useful knobs:

- `--blocks`: number of blocks to prebuild and execute. This is required and
must be greater than `0`.
- `--workers`: parallel executor workers. The default is `1`. Prepared blocks
are forwarded to workers in block-number order, but `--workers > 1` can still
finish execution out of order; this is safe for the harness because generated
state is frozen for prebuilt runs and executor changesets are not applied back
into the input state.
- `--workers`: ordered block executor workers. This must be `1` because each
block reads the snapshot produced by the previous `CommitStateChanges` call.
- `--executor-workers`: parallel OCC workers inside each executor. The default
is `min(12, GOMAXPROCS)`, following the `sei-v3` OCC worker default.
- `--prepare-workers`: parallel stateless preparation workers used for
Expand Down Expand Up @@ -179,16 +176,18 @@ The command reports these saturation signals on stdout and at `/metrics`:
and write time
- result-pool capacity, available slots, and overflow allocations

The default executor output path intentionally discards results through mocks:
Every run uses the Giga executor lifecycle:

- `generatedState` implements `evmonly.StateReader` and supplies generated
genesis balances, nonces, code, and storage.
- `discardResultSink` applies the executor `StateChangeSet` to
`discardStateWriter` and discards Ethereum receipts.
- `generatedState` implements `evmonly.StateReader` and supplies immutable
generated genesis balances, nonces, code, and storage.
- `evmonly.MemoryStore` opens versioned snapshots over that genesis state and
applies the executor's encoded output through `CommitStateChanges`.
- `discardResultSink` discards the already-committed block result and receipts;
it is not responsible for state persistence.

With `--result-sink=file`, the loadtest harness hands pooled
`evmonly.BlockResult` values to an async writer through the executor's
`evmonly.ResultSink` interface. The writer appends changesets to
With `--result-sink=file`, after the in-memory Giga commit succeeds the loadtest
harness hands pooled `evmonly.BlockResult` values to an async writer through the
executor's `evmonly.ResultSink` interface. The writer appends changesets to
`changesets.rlp` and receipts to `receipts.rlp` under `--persist-dir`; each
record is framed as an 8-byte big-endian block height, an 8-byte big-endian RLP
payload length, and the RLP payload. The files are temporary calibration
Expand Down
5 changes: 4 additions & 1 deletion giga/evmonly/cmd/evmonly-loadtest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func parseConfig(args []string) (config, error) {
fs.IntVar(&cfg.builders, "builders", runtime.GOMAXPROCS(0), "parallel block builder goroutines")
fs.IntVar(&cfg.prepareWorkers, "prepare-workers", defaultPrepareWorkers(), "parallel block preparation workers for transaction decode and sender recovery")
fs.IntVar(&cfg.parseWorkers, "parse-workers", 0, "parallel transaction decode/sender recovery workers inside each prepared block; 0 defaults to 1 when prepare-workers > 1, otherwise GOMAXPROCS")
fs.IntVar(&cfg.workers, "workers", defaultWorkerCount, "parallel executor workers")
fs.IntVar(&cfg.workers, "workers", defaultWorkerCount, "ordered block executor workers; must be 1 for giga store commits")
fs.IntVar(&cfg.executorWorkers, "executor-workers", defaultExecutorWorkers(), "parallel OCC workers inside each executor")
fs.DurationVar(&cfg.reportInterval, "report-interval", defaultReportInterval, "stdout and rate-gauge reporting interval; 0 disables periodic reports")
fs.StringVar(&cfg.metricsAddr, "metrics-addr", defaultMetricsAddr, "Prometheus listen address; empty disables HTTP metrics")
Expand Down Expand Up @@ -238,6 +238,9 @@ func parseConfig(args []string) (config, error) {
if cfg.workers <= 0 {
return config{}, fmt.Errorf("workers must be positive")
}
if cfg.workers != 1 {
return config{}, fmt.Errorf("workers must be 1 for ordered giga store commits")
}
if cfg.executorWorkers <= 0 {
return config{}, fmt.Errorf("executor-workers must be positive")
}
Expand Down
43 changes: 35 additions & 8 deletions giga/evmonly/cmd/evmonly-loadtest/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,27 @@ import (

"github.com/sei-protocol/sei-chain/giga/evmonly"
"github.com/sei-protocol/sei-chain/giga/evmonly/cmd/evmonly-loadtest/scenarios"
"github.com/sei-protocol/sei-chain/sei-db/proto"
)

func withGeneratedState(state evmonly.StateReader) evmonly.Option {
store := evmonly.NewMemoryStore(state)
return evmonly.WithStore(store, store.EncodeChangeSet)
}

type readOnlyGeneratedStore struct {
*evmonly.MemoryStore
}

func (*readOnlyGeneratedStore) CommitStateChanges(int64, []*proto.NamedChangeSet) error {
return nil
}

func withReadOnlyGeneratedState(state evmonly.StateReader) evmonly.Option {
store := &readOnlyGeneratedStore{MemoryStore: evmonly.NewMemoryStore(state)}
return evmonly.WithStore(store, store.EncodeChangeSet)
}

func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) {
cfg, err := parseConfig([]string{
"--metrics-addr=",
Expand All @@ -41,7 +60,7 @@ func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) {

executor := evmonly.NewExecutor(evmonly.Config{
MinGasPrice: cfg.minGasPrice,
}, evmonly.WithState(state))
}, withGeneratedState(state))
result, err := executor.ExecuteBlock(t.Context(), request)
require.NoError(t, err)

Expand All @@ -54,7 +73,7 @@ func TestTransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) {
}

var released atomic.Bool
require.NoError(t, discardResultSink{writer: &discardStateWriter{}}.StoreBlockResult(t.Context(), request.Context.Number, result, func() {
require.NoError(t, discardResultSink{}.StoreBlockResult(t.Context(), request.Context.Number, result, func() {
released.Store(true)
}))
require.True(t, released.Load())
Expand Down Expand Up @@ -127,7 +146,7 @@ func TestTransferWorkloadOCCScenarios(t *testing.T) {
executor := evmonly.NewExecutor(evmonly.Config{
MinGasPrice: cfg.minGasPrice,
OCCWorkers: 4,
}, evmonly.WithState(state))
}, withGeneratedState(state))
result, err := executor.ExecuteBlock(t.Context(), request)
require.NoError(t, err)
require.True(t, result.OCCStats.Attempted)
Expand Down Expand Up @@ -179,7 +198,7 @@ func TestERC20TransferWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) {
executor := evmonly.NewExecutor(evmonly.Config{
MinGasPrice: cfg.minGasPrice,
OCCWorkers: cfg.executorWorkers,
}, evmonly.WithState(state))
}, withGeneratedState(state))
result, err := executor.ExecuteBlock(t.Context(), request)
require.NoError(t, err)

Expand Down Expand Up @@ -231,7 +250,7 @@ func TestSnapshotRevertWorkloadExecutesAgainstEVMOnlyExecutor(t *testing.T) {
executor := evmonly.NewExecutor(evmonly.Config{
MinGasPrice: cfg.minGasPrice,
OCCWorkers: 4,
}, evmonly.WithState(state))
}, withGeneratedState(state))
result, err := executor.ExecuteBlock(t.Context(), request)
require.NoError(t, err)

Expand Down Expand Up @@ -283,7 +302,7 @@ func TestTransferWorkloadRecipientConflictRate(t *testing.T) {
executor := evmonly.NewExecutor(evmonly.Config{
MinGasPrice: cfg.minGasPrice,
OCCWorkers: 4,
}, evmonly.WithState(state))
}, withGeneratedState(state))
result, err := executor.ExecuteBlock(t.Context(), request)
require.NoError(t, err)
require.True(t, result.OCCStats.Attempted)
Expand Down Expand Up @@ -426,6 +445,14 @@ func TestParseWorkersConfig(t *testing.T) {
require.ErrorContains(t, err, "parse-workers must be non-negative")
}

func TestBlockExecutorWorkersMustRemainOrdered(t *testing.T) {
_, err := parseConfig([]string{
"--blocks=1",
"--workers=2",
})
require.ErrorContains(t, err, "workers must be 1 for ordered giga store commits")
}

func TestRunPrebuiltBlocks(t *testing.T) {
cfg, err := parseConfig([]string{
"--metrics-addr=",
Expand Down Expand Up @@ -736,7 +763,7 @@ func TestExecutorResultPoolReusesSlotsWithFileSink(t *testing.T) {
defer func() {
require.NoError(t, sinks.Close())
}()
executor := evmonly.NewExecutor(executorConfig(cfg), evmonly.WithState(state), evmonly.WithResultSink(sinks))
executor := evmonly.NewExecutor(executorConfig(cfg), withGeneratedState(state), evmonly.WithResultSink(sinks))
defer executor.Close()

for blockNumber := uint64(1); blockNumber <= 20; blockNumber++ {
Expand Down Expand Up @@ -864,7 +891,7 @@ func BenchmarkExecuteTransferBlock(b *testing.B) {
executor := evmonly.NewExecutor(evmonly.Config{
MinGasPrice: cfg.minGasPrice,
OCCWorkers: cfg.executorWorkers,
}, evmonly.WithState(state))
}, withReadOnlyGeneratedState(state))

b.ReportAllocs()
b.SetBytes(int64(cfg.txsPerBlock))
Expand Down
7 changes: 6 additions & 1 deletion giga/evmonly/cmd/evmonly-loadtest/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ func runPrebuilt(ctx context.Context, cfg config, state *generatedState, workloa

startedAt := time.Now()
group, groupCtx := errgroup.WithContext(ctx)
executor := evmonly.NewExecutor(executorConfig(cfg), evmonly.WithState(state), evmonly.WithResultSink(sinks))
store := evmonly.NewMemoryStore(state)
executor := evmonly.NewExecutor(
executorConfig(cfg),
evmonly.WithStore(store, store.EncodeChangeSet),
evmonly.WithResultSink(sinks),
)
defer executor.Close()
metrics.recordResultPoolStats(executor.ResultPoolStats())
group.Go(func() error {
Expand Down
15 changes: 3 additions & 12 deletions giga/evmonly/cmd/evmonly-loadtest/sinks.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ import (
"github.com/sei-protocol/sei-chain/giga/evmonly"
)

type discardStateWriter struct{}

var _ evmonly.StateWriter = (*discardStateWriter)(nil)

func (*discardStateWriter) ApplyChangeSet(evmonly.StateChangeSet) {}

type resultSinks struct {
sink evmonly.ResultSink
close func() error
Expand All @@ -33,7 +27,7 @@ func newResultSinks(cfg config, metrics *loadMetrics) (*resultSinks, error) {
switch cfg.resultSink {
case resultSinkDiscard:
return &resultSinks{
sink: discardResultSink{writer: &discardStateWriter{}},
sink: discardResultSink{},
}, nil
case resultSinkFile:
return newFileResultSinks(cfg, metrics)
Expand Down Expand Up @@ -63,13 +57,10 @@ func (s *resultSinks) Cleanup() error {
return s.cleanup()
}

type discardResultSink struct {
writer evmonly.StateWriter
}
type discardResultSink struct{}

func (s discardResultSink) StoreBlockResult(_ context.Context, _ uint64, result *evmonly.BlockResult, release func()) error {
func (discardResultSink) StoreBlockResult(_ context.Context, _ uint64, _ *evmonly.BlockResult, release func()) error {
defer release()
s.writer.ApplyChangeSet(result.ChangeSet)
return nil
}

Expand Down
29 changes: 29 additions & 0 deletions giga/evmonly/cmd/evmonly-loadtest/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,35 @@ func (s *generatedState) Freeze() {
s.frozen.Store(true)
}

func (s *generatedState) AccountExists(addr common.Address) bool {
if s.frozen.Load() {
if _, ok := s.balances[addr]; ok {
return true
}
if _, ok := s.nonces[addr]; ok {
return true
}
if _, ok := s.code[addr]; ok {
return true
}
_, ok := s.storage[addr]
return ok
}
s.mu.RLock()
defer s.mu.RUnlock()
if _, ok := s.balances[addr]; ok {
return true
}
if _, ok := s.nonces[addr]; ok {
return true
}
if _, ok := s.code[addr]; ok {
return true
}
_, ok := s.storage[addr]
return ok
}

func (s *generatedState) GetBalance(addr common.Address) *big.Int {
if s.frozen.Load() {
// Frozen reads return shared, non-owned pointers; StateReader consumers
Expand Down
Loading
Loading